Получение async/await для работы с ObservableObject, Published, StateObject

Позвольте мне сказать, что я новичок в использовании пользовательского интерфейса в SwiftUI, и поэтому прошу прощения, если на этот вопрос мог быть дан ответ в различных других сценариях, но я не могу найти тот, который подходит конкретно для моей проблемы.

Я создал функцию, которая генерирует CGImage для MacOS. Он прекрасно работает в инструменте командной строки, для которого я изначально его предназначал. Не вдаваясь в подробности, это асинхронный метод, поскольку он использует withTaskGroup() для сокращения времени работы, необходимой для создания изображения.

Я хочу использовать его как переменную @Published внутри класса @ObservableObject. Вот базовый код, который я начал с неасинхронной версии:

struct ContentView: View {
    @StateObject var target = Target()
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Button("Hello, world! \(target.value)")
            {
                target.value += 1
                if (target.value == 3)
                {
                    target.value = 0
                }
                target.update()
            }
            target.image
        }
        .padding()
    }
}

class Target: ObservableObject
{
    @Published var image: Image
    @Published var value: Int
    
    init()
    {
        value = 0
        image = Target.update(input: 0)        
    }
    
    func update()
    {
        image = Target.update(input: value)
    }
    
    static func update(input: Int) -> Image
    {
        var cgimage: CGImage?
        cgimage = drawMyImage(input: input)
        return Image(cgimage!, scale: 1.0, label: Text("target \(input)"))        
    }
}

Я поместил фиктивную функцию drawMyImage() ниже, если кто-то заинтересован в быстром тестировании.

Таким образом, приведенный выше код не является асинхронным. Проблема в том, что когда я начинаю использовать асинхронность, я асинхронизировал функцию drawMyImage(), которая и будет моей настоящей функцией drawImage():

func drawMyImage(input: Int) async -> CGImage?

Попытка 1

«асинхронный» вызов в автозакрытии, который не поддерживает параллелизм

Если я поднимусь по стеку, вставив соответствующий async/await:

    init() async
    {
        value = 0
        image = await Target.update(input: 0)
        
    }
    
    func update() async
    {
        image = await Target.update(input: value)
    }
    
    static func update(input: Int) async -> Image
    {
        var cgimage: CGImage?
        cgimage = await drawMyImage(input: input)

В конце концов я получаю это в ContentView, и мне кажется, что я не могу обойти его:

struct ContentView: View {
    @StateObject var target = Target() //'async' call in an autoclosure that does not support concurrency
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Button("Hello, world! \(target.value)")
            {
                target.value += 1
                if (target.value == 3)
                {
                    target.value = 0
                }
                target.update() //'async' call in an autoclosure that does not support concurrency

Попытка 2

Мутация захваченной переменной myImage в одновременно исполняемом коде

Поэтому я попробовал обернуть в задачу только функцию drawMyImage(), и получилось не очень хорошо:

    static func update(input: Int) -> Image
    {
        var myImage: Image
        Task
        {
            var cgimage: CGImage?
            cgimage = await drawMyImage(input: input)
            myImage = Image(cgimage!, scale: 1.0, label: Text("target \(input)")) //Mutation of captured var 'myImage' in concurrently-executing code
        }
        return myImage
    }

Попытка 3

Вызов основного статического метода, изолированного от актера, update(input:) в синхронном неизолированном контексте.

Итак, я попытался добавить декоратор @MainActor, и это дало мне другую ошибку:

    init()
    {
        value = 0
        image = Target.update(input: 0) //Call to main actor-isolated static method 'update(input:)' in a synchronous nonisolated context
        
    }
    
    func update()
    {
        image = Target.update(input: value) //Call to main actor-isolated static method 'update(input:)' in a synchronous nonisolated context
    }
    
    @MainActor
    static func update(input: Int) -> Image
    {

Я попробовал еще несколько вещей, чтобы прояснить это, но, похоже, это основное, к чему я прихожу снова и снова. Если бы кто-нибудь мог мне помочь, я был бы признателен. Заранее спасибо.

фиктивная функция drawMyImage()

//dummy test function with no real input, no real async
//remove 'async' for how it's supposed to work
func drawMyImage(input: Int) async -> CGImage? {
    let bounds = CGRect(x: 0, y:0, width: 200, height: 200)
    let intWidth = Int(ceil(bounds.width))
    let intHeight = Int(ceil(bounds.height))
    let bitmapContext = CGContext(data: nil,
                                  width: intWidth, height: intHeight,
                                  bitsPerComponent: 8,
                                  bytesPerRow: (((intWidth * 4) + 15) / 16) * 16,
                                  space: CGColorSpace(name: CGColorSpace.sRGB)!,
                                  bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)
    if let cgContext = bitmapContext {
        cgContext.saveGState()
        if input == 0
        {
            cgContext.setFillColor(red: 255, green: 0, blue: 0, alpha: 255)
        }
        else if input == 1
        {
            cgContext.setFillColor(red: 0, green: 255, blue: 0, alpha: 255)
        }
        else
        {
            cgContext.setFillColor(red: 0, green: 0, blue: 255, alpha: 255)
        }
        cgContext.fill(bounds)
        cgContext.restoreGState()

        return cgContext.makeImage()
    }

    return nil
}

Обычно асинхронные функции находятся в структуре, а не в классе. Обычно они принимают параметры и возвращают результаты. В SwiftUI вызовите их из .task для правильного времени жизни, после чего вы можете просто удалить Объединить ObservableObject.

malhal 22.07.2024 07:47
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
1
1
53
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как и в первой попытке, вы можете распространить async вверх по цепочке вызовов, но не раньше init. Поскольку операция является асинхронной, Target не будет доступен сразу после инициализации. Кроме того, как вы узнали в своей попытке, представление требует синхронной инициализации Image.

Вы должны сделать ObservableObject необязательным:

class Target: ObservableObject
{
    @Published var image: Image?
    @Published var value: Int = 0
    
    @MainActor // isolate this to the main actor because @Published vars can only be updated from the main thread
    func update() async
    {
        image = await Target.update(input: value)
    }
    
    static func update(input: Int) async -> Image
    {
        var cgimage: CGImage?
        cgimage = await drawMyImage(input: input)
        return Image(cgimage!, scale: 1.0, label: Text("target \(input)"))
    }
}

На ваш взгляд, это прекрасная возможность использовать Task(id:). При этом выполняется асинхронная операция при первом появлении представления, а также при изменении его аргумента image. Просто передайте id, и SwiftUI выполнит за вас всю отмену задач.

VStack {
    Image(systemName: "globe")
        .imageScale(.large)
        .foregroundStyle(.tint)
    Button("Hello, world! \(target.value)")
    {
        target.value += 1
        if (target.value == 3)
        {
            target.value = 0
        }
    }
    target.image
}
.padding()
.task(id: target.value) {
    await target.update()
}

Цель .task — устранить необходимость в ObservableObject, поскольку он обеспечивает время жизни

malhal 22.07.2024 07:49

@malhal Я это понимаю, но не хочу делать дополнительных предположений о том, что делает ОП. В зависимости от того, что еще делает Target и для чего он используется, вполне может быть, что это необходимо.

Sweeper 22.07.2024 08:22

Спасибо! Это отлично работает для меня. Раньше я не знал о .task. Я приму во внимание ваши предложения и посмотрю, смогу ли я в будущем отказаться от использования ObservableObject.

nilgirian 23.07.2024 03:51

Другие вопросы по теме