使用来自异步回调的数据推送视图

Push view with data from asynchronous callback

我正在尝试找到一种可行的方法来处理从异步回调返回的数据的导航。

考虑以下示例。 NavigationExampleView 中的按钮在单独的对象上触发一些异步方法,在本例中为 NavigationExampleViewModel。从该方法返回的数据,然后应该 推送到 UserView 中的导航堆栈 上。 NavigationLink 似乎是存档的方法,但我找不到一种方法来获取我需要呈现的数据的非可选值。

struct User: Identifiable {
    let id: String
    let name: String
}

protocol API {
    func getUser() -> AnyPublisher<User, Never>
}

struct NavigationExampleView: View {
    
    @ObservedObject var vm: NavigationExampleViewModel
    
    var body: some View {
        HStack {
            Button("Get User") {
                vm.getUser()
            }
            NavigationLink.init(
                destination: UserView(user: ???),
                isActive: ???,
                label: EmptyView.init
            )
        }
    }
}

class NavigationExampleViewModel: ObservableObject {
    @Published var isLoading = false
    @Published var pushUser: User?
    
    var cancellable: AnyCancellable?
    
    let api: API
    init(api: API) { self.api = api }
    
    func getUser() {
        isLoading = true
        cancellable = api.getUser().sink { user in
            self.pushUser = user
            self.isLoading = false
        }
    }
}

struct UserView: View, Identifiable {
    let id: String
    let user: User
    
    var body: some View {
        Text(user.name)
    }
}

问题:

  1. 如何获取数据以在视图中显示为非可选值?
  2. 我应该使用什么作为绑定来控制演示?

我几乎可以将其存档的一种方法是使用视图修饰符 .sheet(item: Binding<Identifiable?>, content: Identifiable -> View),如下所示:

struct NavigationExampleView: View {
    
    @ObservedObject var vm: NavigationExampleViewModel
    
    var body: some View {
        HStack {
            Button("Get User") {
                vm.getUser()
            }
        }.sheet(item: $vm.pushUser, content: UserView.init)
    }
}

如何将相同的内容存档以将视图推送到导航堆栈,而不是将其呈现为 sheet?

您可以使用 if-let 来解包可选的 vm.pushUser。然后,创建自定义绑定以将 Binding<User?> 映射到 Binding<Bool>:

struct NavigationExampleView: View {
    
    @ObservedObject var vm: NavigationExampleViewModel
    
    var body: some View {
        HStack {
            Button("Get User") {
                vm.getUser()
            }
            if let user = vm.pushUser {
                NavigationLink(
                    destination: UserView(id: "<id>", user: user),
                    isActive: binding,
                    label: EmptyView.init
                )
            }
        }
    }
    
    var binding: Binding<Bool> {
        .init(
            get: { vm.pushUser != nil },
            set: { if ![=10=] { vm.pushUser = nil } }
        )
    }
}

或者,使用它 内联:

if let user = vm.pushUser {
    NavigationLink(
        destination: UserView(id: "<id>", user: user),
        isActive: .init(get: { vm.pushUser != nil }, set: { if ![=11=] { vm.pushUser = nil } }),
        label: EmptyView.init
    )
}

基于 @pawello2222 的回答中的 if let 方法,我创建了这个 ViewModifier 让你将视图推送到导航堆。它遵循与 .sheet modifier.

相同的模式
import SwiftUI

private struct PushPresenter<Item: Identifiable, DestinationView: View>: ViewModifier {
    
    let item: Binding<Item?>
    let destination: (Item) -> DestinationView
    let presentationBinding: Binding<Bool>
    
    init(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: @escaping (Item) -> DestinationView) {
        self.item = item
        self.destination = content
        
        presentationBinding = Binding<Bool> {
            item.wrappedValue != nil
        } set: { isActive in
            if !isActive && item.wrappedValue != nil {
                onDismiss?()
                item.wrappedValue = nil
            }
        }
    }
    
    func body(content: Content) -> some View {
        ZStack {
            content
            NavigationLink(
                destination: item.wrappedValue == nil ? nil : destination(item.wrappedValue!),
                isActive: presentationBinding,
                label: EmptyView.init
            )
        }
    }
}

extension View {
    
    /// Pushed a View onto the navigation stack using the given item as a data source for the View's content.  **Notice**: Make sure to use `.navigationViewStyle(StackNavigationViewStyle())` on the parent `NavigationView` otherwise using this modifier will cause a memory leak.
    /// - Parameters:
    ///   - item: A binding to an optional source of truth for the view's presentation. When `item` is non-nil, the system passes the `item`’s content to the modifier’s closure. This uses a `NavigationLink` under the hood, so make sure to have a `NavigationView` as a parent in the view hierarchy.
    ///   - onDismiss: A closure to execute when poping the view of the navigation stack.
    ///   - content: A closure returning the content of the view.
    func push<Item: Identifiable, Content: View>(
        item: Binding<Item?>,
        onDismiss: (() -> Void)? = nil,
        content: @escaping (Item) -> Content) -> some View
    {
        self.modifier(PushPresenter.init(item: item, onDismiss: onDismiss, content: content))
    }
}

然后像这样使用它:

struct NavigationExampleView: View {
    
    @ObservedObject var vm: NavigationExampleViewModel
    
    var body: some View {
        Button("Get User") {
            vm.getUser()
        }
        .push(item: $vm.pushUser, content: UserView.init)
    }
}

请务必将您的实施视图保持在 NavigationView 中,以便 push 生效。

编辑

我发现此解决方案存在问题。如果使用引用类型作为绑定 item,对象将不会在关闭时被取消初始化。似乎 content 持有对 item 的引用。一旦在绑定上设置了新对象(下次推送屏幕时),对象只会被取消初始化。

@pawello2222 的回答也是如此,但在使用 sheet 修饰符时不是问题。非常欢迎任何有关如何解决此问题的建议。

编辑 2

.navigationViewStyle(StackNavigationViewStyle()) 添加到父 NavigationView 可以消除由于某种原因造成的内存泄漏。有关详细信息,请参阅答案

使用 if-let 方法导致推送视图转换停止工作。当数据不存在时,而是将 nil 传递给 NavigationLinks destination。我已经更新 ViewModifier 来处理这个问题。