swift 等待的任务完成后重定向

ccgok5k5  于 2023-02-21  发布在  Swift
关注(0)|答案(3)|浏览(149)

在一个视图中,我想等待一系列异步调用完成加载,然后重定向到另一个屏幕。不幸的是,我看到代码在后面运行(JSON数据被加载),但一旦完成,它就不重定向到新视图。
以下是我的看法:

struct loadingView: View {
    @ObservedObject var dataLoader: DataLoader = DataLoader()
    @State var isLoaded: Bool = false

    var body: some View {
        VStack {
           Text("Loading \(isLoaded)")
        }
    }
    .task {
        await self.dataloader.loadJSONData(isLoaded: $isLoaded)
        MainScreen()
    }
}

...和DataLoader类:

@MainActor DataLoader: NSObject, ObservableObject {
    func loadJSONData(isLoaded: Binding<Bool>) {
        await doLoadData()
        isLoaded.wrappedValue = True
    }

    func doLoadData() async {
        /* do data load */
        /* This code works */
    }
}
vh0rcniy

vh0rcniy1#

这里的“重定向”并没有什么意义。你真的希望用户能够导航回加载屏幕吗?也许你把它想象成一个网页,但SwiftUI不是这样的。你真正想做的是在加载时显示一个东西,而在加载时显示另一个东西。这只是if,而不是“重定向”。
相反,考虑下面的模式,创建这样的LoadingView(从我的一些个人代码中提取):

struct LoadingView<Content: View, Model>: View {
    enum LoadState {
        case loading
        case loaded(Model)
        case error(Error)
    }

    @ViewBuilder let content: (Model) -> Content
    let loader: () async throws -> Model

    @State var loadState = LoadState.loading

    var body: some View {
        ZStack {
            Color.white
            switch loadState {
            case .loading: Text("Loading")
            case .loaded(let model): content(model)
            case .error(let error): Text(verbatim: "Error: \(error)")
            }
        }
        .task {
            do {
                loadState = .loaded(try await loader())
            } catch {
                loadState = .error(error)
            }
        }
    }
}

它不需要重定向,只是在不同的状态下显示不同的内容(显然,Text视图可以被更有趣的东西取代)。
然后要使用它,将它嵌入到另一个视图中。在我的个人代码中,它包括这样的视图:

struct DailyView: View {
    var body: some View {
        LoadingView() { model in
            LoadedDailyView(model: model)
        } loader: {
            try await DailyModel()
        }
    }
}

那么LoadedDailyView就是“真实的的”视图,它由DailyModel.init创建的完全填充的model处理(抛出,异步初始化)。

oxf4rvwz

oxf4rvwz2#

您可以尝试这种方法,使用NavigationStackNavigationPathRedirecting after task w/ Await completes
下面是我用来测试答案的代码:

struct ContentView: View {
    var body: some View {
        loadingView()
    }
}

@MainActor
class DataLoader: NSObject, ObservableObject {
    
    func loadJSONData() async {
        await doLoadData()
        // for testing, wait for 1 second
        try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
    }

    func doLoadData() async {
        /* do data load */
        /* This code works */
    }
}

struct loadingView: View {
    @StateObject var dataLoader = DataLoader()
    @State private var navPath = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $navPath) {
            VStack (spacing: 44) {
                Text("Loading....")
            }
            .navigationDestination(for: Bool.self) { _ in
                MainScreen()
            }
        }
        .task {
            await dataLoader.loadJSONData()
            navPath.append(true)
        }
    }
}

struct MainScreen: View {
    var body: some View {
        Text("---> MainScreen here <---")
    }
}

如果你需要iOS15或更早版本,那么使用NavigationView

struct loadingView: View {
    @StateObject var dataLoader = DataLoader()
    @State var isLoaded: Bool?
    
    var body: some View {
        NavigationView {
            VStack {
                Text(isLoaded == nil ? "Loading..." : "Finished loading")
                NavigationLink("", destination: MainScreen(), tag: true, selection: $isLoaded)
            }
        }.navigationViewStyle(.stack)
        .task {
            await dataLoader.loadJSONData()
            isLoaded = true
        }
    }
}

如果loadingView的唯一目的是显示“loading”消息,然后在加载数据后显示MainScreen,则可以使用以下方法(使用简单的开关):

struct loadingView: View {
    @StateObject var dataLoader = DataLoader()
    @State private var isLoaded = false
    
    var body: some View {
        VStack {
            if isLoaded {
                MainScreen()
            } else {
                ProgressView("Loading")
            }
        }
        .task {
            await dataLoader.loadJSONData()
            isLoaded = true
        }
    }
}
eufgjt7s

eufgjt7s3#

使用@StateObject而不是@ObservedObject。使用@Published而不是尝试传递绑定到对象(这是一个错误,因为绑定只是一对get和set闭包,如果LoadingView被重新初始化,它们将过期),使用Groupif来有条件地显示View,例如:

struct LoadingView: View {
    @StateObject var dataLoader: DataLoader = DataLoader()

    var body: some View {
        Group {
           if dataLoader.isLoaded {
               LoadedView(data: dataLoader.data)
           } else {
                Text("Loading...")
           }
        }
        .task {
            await dataloader.loadJSONData()
        }
    }

DataLoader不应是@MainActor,因为您希望它在后台线程上运行。一旦异步工作完成,请在子任务上改用@MainActor,例如

class DataLoader: ObservableObject {

    @Published var isLoaded = false
    @Published var data: [Data] = []

    func loadJSONData async {
        let d = await doLoadData()
        Task { @MainActor in
            isLoaded = true
            data = d
        }
    }

    func doLoadData() async {
        /* do data load */
        /* This code works */
    }
}

这个模式在苹果的教程中显示,PandaCollectionFetcher.swift复制如下:

import SwiftUI

class PandaCollectionFetcher: ObservableObject {
    @Published var imageData = PandaCollection(sample: [Panda.defaultPanda])
    @Published var currentPanda = Panda.defaultPanda
    
    let urlString = "http://playgrounds-cdn.apple.com/assets/pandaData.json"
    
    enum FetchError: Error {
        case badRequest
        case badJSON
    }
    
     func fetchData() async 
     throws  {
        guard let url = URL(string: urlString) else { return }

        let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
        guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badRequest }

        Task { @MainActor in
            imageData = try JSONDecoder().decode(PandaCollection.self, from: data)
        }
    }
    
}

相关问题