【问题标题】:Push view with data from asynchronous callback使用来自异步回调的数据推送视图
【发布时间】:2021-03-02 23:24:59
【问题描述】:

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

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

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&lt;Identifiable?&gt;, content: Identifiable -&gt; 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)
    }
}

如何归档相同的视图以将视图推送到导航堆栈,而不是将其呈现为工作表?

【问题讨论】:

  • 我会给 UserView 提供多种状态,例如“未加载”、“正在加载”、“已加载”。然后将 API 发布者直接传递给 userView 和 .onAppear { API.getUser }。所以当 UserView 出现时,它会立即开始加载,并在 api 返回时显示数据。这里解释得很好:swiftbysundell.com/articles/handling-loading-states-in-swiftui
  • 这并不能解决我的问题:/ 如果应该推送的特定视图取决于返回的数据怎么办。例如,如果我想为管理员用户呈现一些特定的视图,而为不是管理员的用户呈现一些完全不同的视图。
  • 制作一个异步用户容器视图,就像我上面的评论一样。然后制作另外两个视图结构(非管理员和管理员)。容器视图将确定用户是否是管理员,并将数据传递并显示到适当的用户视图(管理员或非管理员)
  • 加载完成后我需要推送视图。
  • 如果你想做类似的事情,你必须做一些额外的工作来制作某种自定义绑定以与 sheetCoverView 一起使用。我不认为这是一个好的设计模式。如果用户单击预期的东西并且会发生操作并且在加载数据之前没有任何反应,那么用户将会感到困惑。以“苹果音乐”为例。点击相册时,视图会立即呈现,相册在加载时会显示。

标签: swift swiftui navigation


【解决方案1】:

基于 @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 会出于某种原因消除内存泄漏。更多详情请见答案here

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

【讨论】:

    【解决方案2】:

    您可以使用if-let 来解开可选的vm.pushUser。然后,创建一个自定义绑定,将Binding&lt;User?&gt; 映射到Binding&lt;Bool&gt;

    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 !$0 { vm.pushUser = nil } }
            )
        }
    }
    

    或者,使用它内联

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

    【讨论】:

    • 这对我不起作用:/ 永远不会推送详细视图。我尝试打印绑定的返回值,getter 正在返回true,但仍然没有推送任何视图。你试过了吗?
    • @Wiingaard 是的,它适用于我 - Xcode 12.4,iOS 14.4。你的环境是什么?
    • 对不起,我再次测试了它并且它工作了。我在测试它的应用程序中一定有其他问题。我已经将你的方法包装在一个漂亮的小 ViewModifier 中与sheet 修饰符相同的模式。我现在发布答案.. 谢谢! :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多