在 Swift 中,如何从异步任务外壳内部更新主线程上的文本视图

Posted

技术标签:

【中文标题】在 Swift 中,如何从异步任务外壳内部更新主线程上的文本视图【英文标题】:In Swift, how to update a Text view on main thread from inside async Task enclosure 【发布时间】:2022-01-21 03:03:42 【问题描述】:

这是目标。我想运行一个长的异步任务,并让它定期向 UI 发送消息,这些消息将显示在旋转轮活动指示器的下方。现在,我不关心活动指示器。我只需要异步任务在其进程的各个阶段发送消息,并在消息进入时向用户显示该消息。

因此,我查看了 GCD 解决方案、可观察对象、演员、委托等。提供的许多解决方案都非常陈旧且无法正常工作,或者它们足够新,但太复杂了,我不理解他们,我对 Swift 还很陌生。

我确实找到了一种工作方法,但它太俗气了,它确实必须是错误的方法。我刚刚为我的主视图创建了一个扩展,这样我就可以将异步函数放在一个单独的文件中(它有点大)。所以我在 UI 中使用的字符串 var 对异步任务是可见的。但只有当我发生“尝试等待”时,它才会真正更新。这必须触发 UI 中的某些内容来刷新字符串并重新显示布局。但这似乎很愚蠢。一定有更好的办法。

无论如何,这是我的解决方法。顺便说一句,这几乎是整个项目,致力于解决这个问题。我已经删除了所有我无法工作的失败替代方案。另外,请原谅任务睡眠扩展。我宁愿在几秒钟内工作而不是纳秒。

import SwiftUI


struct ContentView: View 
    
    @State var progressmsg: String = ""
    @State var loadingViewShowing = false
    
    var body: some View 
        VStack 
            Button("Download Something") 
                loadingViewShowing = true
                Task 
                    print("try 1")
                    progressmsg = "doing first sleep task"
                    try await Task.sleep(seconds: 5)
                    
                    print("try 2")
                    await diggingEvenDeeper()
                    
                    print("try 3")
                    progressmsg = "doing third sleep task"
                    try await Task.sleep(seconds: 5)
                    loadingViewShowing = false
                
            
            if !loadingViewShowing 
                Text("Nothing Happening ...")
             else 
                ProgressView().scaleEffect(1.0, anchor: .center)
                Text("\(progressmsg)").font(.title).fontWeight(.semibold)
            
        
        .frame(width: 400, height: 400)
    
    


extension ContentView 
    
    func diggingEvenDeeper() async 
        //really long process, need a message on stages of progress

        print("trying to start")
        self.progressmsg = "doing second & really long sleep task"
        do 
            try await Task.sleep(seconds: 5)
         catch 
            print(error)
        
    
    


extension Task where Success == Never, Failure == Never 
    static func sleep(seconds: Double) async throws 
        let duration = UInt64(seconds * 1_000_000_000)
        try await Task.sleep(nanoseconds: duration)
    

【问题讨论】:

符合@ObservableObject 的视图模型类比在视图中执行所有控制器 的东西要好得多。 【参考方案1】:

好的,我得到了这个工作。谢谢@vadian 的建议。我又看了一眼@ObservableObject,发现了一些我之前错过的细节。基本上,通过使用 @StateObject 来关注我的 observable 类(结构不起作用),它为我的后台线程提供了一个稳定的位置来引用(如果我说的没错的话)。

我还要指出,在我的实际项目中,我收到了关于不允许从后台线程更新已发布项目的紫色错误消息,因此我还必须将进度消息包装在 DispatchQueue.main.async 中外壳。奇怪的小示例程序没有抱怨。哦,好吧。

无论如何,这是可行的解决方案:

import SwiftUI

class Progress: ObservableObject 
    @Published var message = "Doing something ..."
    @Published var value = 0.0



struct ContentView: View 
    @StateObject var progress: Progress = Progress()
    
    //@State var progressmsg: String = ""
    @State var loadingViewShowing = false
    
    var body: some View 
        VStack 
            Button("Download Something") 
                loadingViewShowing = true
                Task 
                    print("try 1")
                    progress.message = "doing first sleep task"
                    try await Task.sleep(seconds: 5)
                    
                    print("try 2")
                    await diggingEvenDeeper(progress: progress)
                    
                    print("try 3")
                    progress.message = "doing third sleep task"
                    try await Task.sleep(seconds: 5)
                    loadingViewShowing = false
                
            
            if !loadingViewShowing 
                Text("Nothing Happening ...")
             else 
                ProgressView().scaleEffect(1.0, anchor: .center)
                Text("\(progress.message)").font(.title).fontWeight(.semibold)
            
        
        .frame(width: 400, height: 400)
    
    


//extension ContentView 
    
func diggingEvenDeeper(progress: Progress) async 
        //really long process, need a message on stages of progress

    print("trying to start")
    progress.message = "doing second & really long sleep task"
    do 
        try await Task.sleep(seconds: 5)
     catch 
        print(error)
    

    
//

extension Task where Success == Never, Failure == Never 
    static func sleep(seconds: Double) async throws 
        let duration = UInt64(seconds * 1_000_000_000)
        try await Task.sleep(nanoseconds: duration)
    

【讨论】:

以上是关于在 Swift 中,如何从异步任务外壳内部更新主线程上的文本视图的主要内容,如果未能解决你的问题,请参考以下文章

IOS Swift我如何从异步方法中获得价值[重复]

事件循环(event loop)

异步作用域闭包

如何取消 useEffect 清理函数中的所有订阅和异步任务?

Swift之深入解析如何使用并发系统并行运行多个任务

Swift 按顺序执行异步任务