Позвольте мне сказать, что я новичок в использовании пользовательского интерфейса в 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?
«асинхронный» вызов в автозакрытии, который не поддерживает параллелизм
Если я поднимусь по стеку, вставив соответствующий 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
Мутация захваченной переменной 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
}
Вызов основного статического метода, изолированного от актера, 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
}
Как и в первой попытке, вы можете распространить 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 Я это понимаю, но не хочу делать дополнительных предположений о том, что делает ОП. В зависимости от того, что еще делает Target
и для чего он используется, вполне может быть, что это необходимо.
Спасибо! Это отлично работает для меня. Раньше я не знал о .task. Я приму во внимание ваши предложения и посмотрю, смогу ли я в будущем отказаться от использования ObservableObject.
Обычно асинхронные функции находятся в структуре, а не в классе. Обычно они принимают параметры и возвращают результаты. В SwiftUI вызовите их из
.task
для правильного времени жизни, после чего вы можете просто удалить Объединить ObservableObject.