英文:
Redirecting after task w/ Await completes
问题
以下是代码的翻译部分:
在一个视图中,我希望等待一系列异步调用完成加载,然后重定向到另一个屏幕。不幸的是,我看到代码在后台运行(JSON 数据已加载),但一旦完成,它不会重定向到新视图。
这是我的视图:
struct LoadingView: View {
@ObservedObject var dataLoader: DataLoader = DataLoader()
@State var isLoaded: Bool = false
var body: some View {
VStack {
Text("加载中 \(isLoaded)")
}
}
.task {
await self.dataLoader.loadJSONData(isLoaded: $isLoaded)
MainScreen()
}
}
...和 DataLoader 类:
@MainActor class DataLoader: NSObject, ObservableObject {
func loadJSONData(isLoaded: Binding<Bool>) {
await doLoadData()
isLoaded.wrappedValue = true
}
func doLoadData() async {
/* 进行数据加载 */
/* 这段代码有效 */
}
}
英文:
In a view, I want to wait for a series of async calls to finish loading, then redirect to another screen. Unfortunately, I see the code running in the back (The JSON data gets loaded) but once it completes it does not redirect to the new view.
Here is my view:
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()
}
}
...and the DataLoader class:
@MainActor DataLoader: NSObject, ObservableObject {
func loadJSONData(isLoaded: Binding<Bool>) {
await doLoadData()
isLoaded.wrappedValue = True
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
答案1
得分: 2
“重定向”在这里没有太多意义。你真的希望用户能够返回到加载屏幕吗?也许你在想象这个像一个网页,但是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("加载中")
case .loaded(let model): content(model)
case .error(let error): Text(verbatim: "错误:\(error)")
}
}
.task {
do {
loadState = .loaded(try await loader())
} catch {
loadState = .error(error)
}
}
}
}
它不需要重定向。它只是在不同的状态下显示不同的内容(显然,Text
视图可以被更有趣的内容替代)。
然后在另一个View中使用它。在我的个人代码中,它包括一个这样的视图:
struct DailyView: View {
var body: some View {
LoadingView() { model in
LoadedDailyView(model: model)
} loader: {
try await DailyModel()
}
}
}
然后LoadedDailyView
是“真正的”视图。它处理一个由DailyModel.init
(一个可抛出的异步初始化方法)创建的完全填充的model
。
英文:
"Redirecting" here doesn't really make sense. Do you really want the user to be able to navigate back to the loading screen? Perhaps you're thinking of this like a web page, but SwiftUI is nothing like that. What you really want to do is display one thing when loading, and a different thing when loaded. That's just if
, not "redirection."
Instead, consider the following pattern. Create this kind of LoadingView (extracted from some personal code of mine):
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)
}
}
}
}
It require no redirection. It just displays different things when in different states (obviously the Text
view can be replaced by something more interesting).
Then to use this, embed it in another View. In my personal code, that includes a view like this:
struct DailyView: View {
var body: some View {
LoadingView() { model in
LoadedDailyView(model: model)
} loader: {
try await DailyModel()
}
}
}
Then LoadedDailyView is the "real" view. It is handled a fully populated model
that is created by DailyModel.init
(a throwing, async init).
答案2
得分: 0
以下是翻译好的内容:
你可以尝试这种方法,使用 `NavigationStack` 和 `NavigationPath` 来在 `Await` 完成后进行重定向。
这是我用来测试我的答案的代码:
struct ContentView: View {
var body: some View {
loadingView()
}
}
@MainActor
class DataLoader: NSObject, ObservableObject {
func loadJSONData() async {
await doLoadData()
// 用于测试,等待 1 秒
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
}
func doLoadData() async {
/* 进行数据加载 */
/* 这段代码有效 */
}
}
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 <--")
}
}
如果您需要支持 iOS 15 或更早版本,可以使用 `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
}
}
}
英文:
You could try this approach, using NavigationStack
and NavigationPath
to Redirecting after task w/ Await completes
.
Here is the code I use to test my answer:
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 <---")
}
}
If you need ios 15 or earlier, then use 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
}
}
}
If your loadingView
has the only purpose of showing the "loading" message, then
display the MainScreen
after the data is loaded, you could use the following approach using a simple swicth:
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
}
}
}
答案3
得分: 0
使用@StateObject
而不是@ObservedObject
。使用@Published
而不是尝试传递绑定到对象(这是一个错误,因为绑定只是一对将在LoadingView
重新初始化时失效的获取和设置闭包),使用Group
和if
条件ally show a 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 {
/* 进行数据加载 */
/* 此代码有效 */
}
}
这种模式在苹果的教程中显示在此处,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)
}
}
}
英文:
Use @StateObject
instead of @ObservedObject
. Use @Published
instead of trying to pass a binding to the object (that is a mistake because a binding is just a pair of get and set closures that will expire if LoadingView
is re-init), use Group
with an if
to conditionally show a View
e.g.
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()
}
}
The DataLoader
should not be @MainActor
because you want it to run on a background thread. Use @MainActor
instead on a sub-task once the async work has finished e.g.
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 */
}
}
This pattern is shown in Apple's tutorial here, PandaCollectionFetcher.swift
copied below:
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)
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论