Я новичок в Swift и пытаюсь обновить поле Text
, основанное на ходе выполнения асинхронной рабочей функции.
Я часами искал и играл над этим, но не там. Этот пост кажется правильной идеей, но я не могу заставить его работать в моем примере. Какие-либо предложения?
public class ExportEngine : ObservableObject
{
@Published var curFileIdx : Int
@Published var isRunning : Bool
init?()
{
self.curFileIdx = 0
self.isRunning = false
}
@MainActor
public func exportFiles() async
{
isRunning = true;
for i in (0...10)
{
curFileIdx = i
print("i = \(i)")
sleep(5)
}
isRunning = false
}
}
struct ContentView: View
{
@ObservedObject var reloadViewHelper = ReloadViewHelper()
@ObservedObject var exportEngine : ExportEngine
@State private var pctComplete = 0.0
var body: some View
{
VStack
{
Button("Run")
{
print("run")
// do what here?
}
Text("File \(exportEngine.curFileIdx)") // doesn't like this
.refreshable
{
Task
{
await exportEngine.exportFiles()
}
}
}
}
}
Как сказал @lorem ipsum, вам следует переключиться на Task.sleep
, поскольку sleep
приостановит выполнение потока, который является основным потоком.
print("i = \(i)")
try? await Task.sleep(for: .seconds(5) //<- here
Кроме того, вы привязываете Text
к curFileIdx
, чтобы при каждом изменении curFileIdx
обновлялся и Text
. Вы сделали это в exportFiles()
.
curFileIdx = i
Я предполагаю, что это действие должно выполняться при нажатии кнопки Run
.
VStack {
Button("Run") {
Task {
await exportEngine.exportFiles() //<- here
}
}
if exportEngine.isRunning { //<- I added a progress view here to represent running state
ProgressView()
}
Text("File \(exportEngine.curFileIdx)")
}
Это результат:
Однако здесь есть проблема. @MainActor
добавлен к вашему exportFiles()
. В результате весь процесс всегда выполняется в основном потоке. С другой стороны, все, что нужно, чтобы появиться в тексте, — это curFileIdx
. Поэтому лучше выполнять другие действия в разных потоках и обращаться к основному потоку только при необходимости.
@MainActor //<- here
public class ExportEngine: ObservableObject {
...
nonisolated //<- here
public func exportFiles() async {
for i in (0...5) {
await MainActor.run {
print("get \(i) on thread: \(Thread.current)")
curFileIdx = i
}
print("running \(i) on thread: \(Thread.current)")
try? await Task.sleep(for: .seconds(1))
}
}
}
get 0 on thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
running 0 on thread: <NSThread: 0x6000017021c0>{number = 3, name = (null)}
get 1 on thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
running 1 on thread: <NSThread: 0x6000017021c0>{number = 3, name = (null)}
get 2 on thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
running 2 on thread: <NSThread: 0x60000174c980>{number = 7, name = (null)}
get 3 on thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
running 3 on thread: <NSThread: 0x600001754480>{number = 4, name = (null)}
get 4 on thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
running 4 on thread: <NSThread: 0x600001754480>{number = 4, name = (null)}
get 5 on thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
running 5 on thread: <NSThread: 0x600001754480>{number = 4, name = (null)}
Какое совпадение, я добавил ответ одновременно с вами, а также предоставил ProgressView для демонстрации состояния «загрузки». В любом случае, хорошая мысль с @MainActor! Я бы только добавил (как я уже упоминал в своем ответе), что важно учитывать разные сценарии и соответствующим образом отменять незавершенную задачу.
Хаха боже мой, я этого не заметил xD.
Это простое решение для обновления текстового представления с помощью асинхронной функции, запускаемой кнопкой, как в вашем примере.
Однако при работе с параллелизмом необходимо учитывать несколько сценариев, например отмену незавершенных задач, когда пользователь покидает экран или когда он снова нажимает кнопку до завершения текущей задачи.
Все зависит от вашего варианта использования и логики вашего приложения.
import SwiftUI
public class ExportEngine : ObservableObject {
@Published var curFileIdx : Int = 0
@Published var isRunning : Bool = false
@MainActor
public func exportFiles() async
{
isRunning = true
for i in (0...10) {
curFileIdx = i
print("i = \(i)")
// it waits 1 second before the next iteration
try? await Task.sleep(nanoseconds: 1_000_000_000)
}
isRunning = false
}
}
struct ContentView: View {
// Initialize the exportEngine here as the @StateObject or pass the reference from the parent view as the @ObservedObject
@StateObject private var exportEngine = ExportEngine()
var body: some View {
VStack {
Button("Run") {
// This task handles the execution of your async function.
// However, you need to handle the cancelation of this task in certain situations
// (for example cancel the task when the use leaves the screen, etc.)
Task {
await exportEngine.exportFiles()
}
}
// Progress view is visible only when the engine is running
if exportEngine.isRunning {
ProgressView()
}
Text("File \(exportEngine.curFileIdx)")
}
}
}
#Preview {
ContentView()
}
Если вы используете .task вместо Task, вам не нужен StateObject.
Да, хорошая мысль, малхал! Я использую @StateObject в своем ответе, потому что в ответе указан класс ExportEngine
, поэтому я хотел сохранить ссылку на исходный ответ.
@malhal, можешь ли ты объяснить, что ты имеешь в виду, или привести пример? Я парень на C++, который программирует на Swift всего 10 дней. Крутая кривая обучения.