使用来自异步回调的数据推送视图 [英] Push view with data from asynchronous callback

查看:90
本文介绍了使用来自异步回调的数据推送视图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm trying to find a viable way to handle navigation with data that has been returned from an asynchronous callback.

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

Consider the following example. The button in NavigationExampleView is triggering some async method on a separate object, NavigationExampleViewModel in this case. The returned data form the method, should then be pushed on the navigation stack in a UserView. A NavigationLink seems to be the way to archive this, but I can't find a way to get hold of a non-optional value of the data that I need to present.

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, content: Identifiable -> View),如下所示:

A way I can almost archive this is with the view modifier .sheet(item: Binding<Identifiable?>, content: Identifiable -> View), like this:

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

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

How can archive the same for pushing the view onto the navigation stack, instead of presenting it as a sheet?

推荐答案

基于 @pawello2222 回答中的 if let 方法,我创建了这个 ViewModifier 让您将视图推送到导航堆栈.它遵循与 .sheet 相同的模式 修饰符.

Based on the if let method from @pawello2222's answer, I've created this ViewModifier that let's you push a View to the Navigation Stack. It follows the same pattern as the .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 生效.

Be sure to keep your implementing view in a NavigationView for the push to take effect.

编辑

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

I found an issue with this solution. If using a reference type as the binding item, the object will not get deinitialised on dismiss. It seems like the content is holding a reference to the item. The Object will only the deinitialised once a new object is set on the binding (next time a screen is pushed).

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

This is also the case for @pawello2222's answer, but not an issue when using the sheet modifier. Any suggestions on how to solve this would be much welcomed.

编辑 2

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

Adding .navigationViewStyle(StackNavigationViewStyle()) to the parent NavigationView removes the memory leak for some reason. See answer here for more details.

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

Using the if-let-method caused the push view transition to stop working. Instead pass nil to the NavigationLinks destination when the data is not pressent. I've updated ViewModifier to handle this.

这篇关于使用来自异步回调的数据推送视图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆