【问题标题】:SwiftUI NavigationLink loads destination view immediately, without clickingSwiftUI NavigationLink 立即加载目标视图,无需单击
【发布时间】:2019-12-26 21:10:19
【问题描述】:

使用以下代码:

struct HomeView: View {
    var body: some View {
        NavigationView {
            List(dataTypes) { dataType in
                NavigationLink(destination: AnotherView()) {
                    HomeViewRow(dataType: dataType)
                }
            }
        }
    }
}

奇怪的是,当HomeView 出现时,NavigationLink 立即加载AnotherView。结果,所有AnotherView 依赖项也被加载,即使它在屏幕上还不可见。用户必须单击该行以使其显示。 我的AnotherView 包含一个DataSource,发生各种事情。问题是此时整个 DataSource 已加载,包括一些计时器等。

我做错了什么..?如何以这种方式处理它,一旦用户按下 AnotherView 就会加载 HomeViewRow

【问题讨论】:

  • 没有错。是的,View 是 init,但它的 body func 没有被调用,并且没有 state 或 state 对象是 init,因为这一切都是在 body 被调用之前完成的。

标签: ios swift swiftui


【解决方案1】:

适用于 iOS 14 SwiftUI。

基于 post 的延迟导航目的地加载的非优雅解决方案,使用视图修饰符。

extension View {
    func navigate<Value, Destination: View>(
        item: Binding<Value?>,
        @ViewBuilder content: @escaping (Value) -> Destination
    ) -> some View {
        return self.modifier(Navigator(item: item, content: content))
    }
}

private struct Navigator<Value, Destination: View>: ViewModifier {
    let item: Binding<Value?>
    let content: (Value) -> Destination
    
    public func body(content: Content) -> some View {
        content
            .background(
                NavigationLink(
                    destination: { () -> AnyView in
                        if let value = self.item.wrappedValue {
                            return AnyView(self.content(value))
                        } else {
                            return AnyView(EmptyView())
                        }
                    }(),
                    isActive: Binding<Bool>(
                        get: { self.item.wrappedValue != nil },
                        set: { newValue in
                            if newValue == false {
                                self.item.wrappedValue = nil
                            }
                        }
                    ),
                    label: EmptyView.init
                )
            )
    }
}

这样称呼它:

struct ExampleView: View {
    @State
    private var date: Date? = nil
    
    var body: some View {
        VStack {
            Text("Source view")
            Button("Send", action: {
                self.date = Date()
            })
        }
        .navigate(
            item: self.$date,
            content: {
                VStack {
                    Text("Destination view")
                    Text($0.debugDescription)
                }
            }
        )
    }
}

【讨论】:

  • 谢谢,原来的帖子解决方案导致导航链接转换出现一些奇怪的问题,但您的解决方案完美运行。
【解决方案2】:

在目标视图中,您应该监听事件onAppear 并在那里放置仅在新屏幕出现时才需要执行的所有代码。像这样:

struct DestinationView: View {
    var body: some View {
        Text("Hello world!")
        .onAppear {
            // Do something important here, like fetching data from REST API
            // This code will only be executed when the view appears
        }
    }
}

【讨论】:

  • ViewDidLoad 在视图加载后被调用一次。然而,每次呈现视图时都会调用 viewDidAppear(类似 SwiftUI 中的 .onAppear)。因此,在下载的情况下,您通常希望在 viewDidLoad 上而不是在onAppear 上进行下载。后一种情况可能经常发生。
【解决方案3】:

编辑:请参阅@MwcsMac 的答案以获得更简洁的解决方案,该解决方案将视图创建包装在闭包中,并且仅在视图呈现后才对其进行初始化。

它需要一个自定义的ForEach 来执行您的要求,因为函数构建器确实必须评估表达式

NavigationLink(destination: AnotherView()) {
    HomeViewRow(dataType: dataType)
}

每个可见行都能够显示HomeViewRow(dataType:),在这种情况下AnotherView()也必须初始化。

因此,为避免这种情况,需要自定义 ForEach

import SwiftUI

struct LoadLaterView: View {
    var body: some View {
        HomeView()
    }
}

struct DataType: Identifiable {
    let id = UUID()
    var i: Int
}

struct ForEachLazyNavigationLink<Data: RandomAccessCollection, Content: View, Destination: View>: View where Data.Element: Identifiable {
    var data: Data
    var destination: (Data.Element) -> (Destination)
    var content: (Data.Element) -> (Content)
    
    @State var selected: Data.Element? = nil
    @State var active: Bool = false
    
    var body: some View {
        VStack{
            NavigationLink(destination: {
                VStack{
                    if self.selected != nil {
                        self.destination(self.selected!)
                    } else {
                        EmptyView()
                    }
                }
            }(), isActive: $active){
                Text("Hidden navigation link")
                    .background(Color.orange)
                    .hidden()
            }
            List{
                ForEach(data) { (element: Data.Element) in
                    Button(action: {
                        self.selected = element
                        self.active = true
                    }) { self.content(element) }
                }
            }
        }
    }
}

struct HomeView: View {
    @State var dataTypes: [DataType] = {
        return (0...99).map{
            return DataType(i: $0)
        }
    }()
    
    var body: some View {
        NavigationView{
            ForEachLazyNavigationLink(data: dataTypes, destination: {
                return AnotherView(i: $0.i)
            }, content: {
                return HomeViewRow(dataType: $0)
            })
        }
    }
}

struct HomeViewRow: View {
    var dataType: DataType
    
    var body: some View {
        Text("Home View \(dataType.i)")
    }
}

struct AnotherView: View {
    init(i: Int) {
        print("Init AnotherView \(i.description)")
        self.i = i
    }
    
    var i: Int
    var body: some View {
        print("Loading AnotherView \(i.description)")
        return Text("hello \(i.description)").onAppear {
            print("onAppear AnotherView \(self.i.description)")
        }
    }
}

【讨论】:

  • 您是在视图加载时进行测试,而不是在它初始化时进行测试。请将init(i: Int) { self.i = i print("test") } 粘贴到您的另一个视图中,您会看到它在 HomeView 列表单元格加载时被调用。
  • 啊,我明白了,这通常是用.onAppear 完成的。我发布了一个如何初始化AnotherView lazily 的示例。
  • 我会说它应该在实际上打开后进行初始化。从逻辑的角度来看:您有一个包含 1000 个位置的表。在 NavigationLink 上,您可以将视图与地图和视频播放器链接起来。立即启动 18 个项目。现在使用非可选的让很愉快,所以你就这样做了。所以你在那里加载所有的项目。协调器模式也会很奇怪。它应该与 UITableViewCell 一样工作 - 它在显示单元格时初始化单元格,并且仅在按下它时才执行操作。优化。
  • 我明白你的意思。但是请注意,此解决方案既不重用旧视图(据我所知)也不关心多次创建视图,导致每次切换到视图时都会再次构建视图。
  • 你是说 UITableViewCell 吗?它正在使它们出列,因此 100% 重用旧单元。
【解决方案4】:

我最近一直在努力解决这个问题(用于表单的导航行组件),这对我有用:

@State private var shouldShowDestination = false

NavigationLink(destination: DestinationView(), isActive: $shouldShowDestination) {
    Button("More info") {
        self.shouldShowDestination = true
    }
}

只需将ButtonNavigationLink 包装起来,即可通过按钮控制激活。

现在,如果您要在同一个视图中有多个按钮+链接,而不是为每个激活 State 属性,您应该依赖此初始化程序

    /// Creates an instance that presents `destination` when `selection` is set
    /// to `tag`.
    public init<V>(destination: Destination, tag: V, selection: Binding<V?>, @ViewBuilder label: () -> Label) where V : Hashable

https://developer.apple.com/documentation/swiftui/navigationlink/3364637-init

按照这个例子的思路:

struct ContentView: View {
    @State private var selection: String? = nil

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) {
                    Button("Tap to show second") {
                        self.selection = "Second"
                    }
                }
                NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) {
                    Button("Tap to show third") {
                        self.selection = "Third"
                    }
                }
            }
            .navigationBarTitle("Navigation")
        }
    }
}

更多信息(以及上面稍作修改的示例)取自 https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui(在“程序化导航”下)。

或者,创建一个自定义视图组件(嵌入NavigationLink),例如这个

struct FormNavigationRow<Destination: View>: View {

    let title: String
    let destination: Destination

    var body: some View {
        NavigationLink(destination: destination, isActive: $shouldShowDestination) {
            Button(title) {
                self.shouldShowDestination = true
            }
        }
    }

    // MARK: Private

    @State private var shouldShowDestination = false
}

并将其作为Form(或List)的一部分重复使用:

Form {
    FormNavigationRow(title: "One", destination: Text("1"))
    FormNavigationRow(title: "Two", destination: Text("2"))
    FormNavigationRow(title: "Three", destination: Text("3"))
}

【讨论】:

    【解决方案5】:

    我发现解决此问题的最佳方法是使用惰性视图。

    struct NavigationLazyView<Content: View>: View {
        let build: () -> Content
        init(_ build: @autoclosure @escaping () -> Content) {
            self.build = build
        }
        var body: Content {
            build()
        }
    }
    

    然后 NavigationLink 看起来像这样。您可以将要显示的视图放在()

    NavigationLink(destination: NavigationLazyView(DetailView(data: DataModel))) { Text("Item") }
    

    【讨论】:

    • 这解决了我的所有嵌套视图模型(及其存储库的 api 调用)过早初始化的问题。
    • 谢谢!我觉得这不是 NavigationLink 的默认行为真的很奇怪。它给我带来了很多问题!
    • @MwcsMac 没多少次我想亲吻回答这个问题的人。谢谢!但是,我不太明白为什么这不是默认行为,并且想知道是否会出现一种解决方案,这意味着有一天将不再需要这种解包。
    • 很好的解决方案!为了方便起见,我将如何将其包装为LazyNavigationLink?例如。只是打电话给LazyNavigationLink(DetailView(data: ...
    • 这种非延迟加载细节视图看起来很疯狂。非常违反直觉。
    【解决方案6】:

    我遇到了同样的问题,我可能有一个包含 50 个项目的列表,然后为调用 API 的详细视图加载了 50 个视图(这导致下载了 50 个额外的图像)。

    对我来说,答案是使用 .onAppear 来触发当视图出现在屏幕上时需要执行的所有逻辑(例如设置计时器)。

    struct AnotherView: View {
        var body: some View {
            VStack{
                Text("Hello World!")
            }.onAppear {
                print("I only printed when the view appeared")
                // trigger whatever you need to here instead of on init
            }
        }
    }
    

    【讨论】:

    • 这太可怕了,这将对架构产生影响。希望他们能在发布前修复它。
    • 是的,从优化的角度来看并不好,但实际上它可以正常工作。我从 API 端点异步加载数据发生在 onAppear 上,如果我点击一个单元格并加载另一个视图控制器,我会这样做。我会加载视图然后加载所有相关的远程内容。此时唯一的区别是它在用户点击单元格之前加载脚手架。想知道这是他们尚未完成最终解决方案的优化还是意外结果。
    猜你喜欢
    • 1970-01-01
    • 2020-04-27
    • 2020-09-28
    • 2023-04-04
    • 2020-08-04
    • 2021-12-08
    • 2016-09-24
    • 2020-09-27
    • 1970-01-01
    相关资源
    最近更新 更多