从0到1学习SwiftUI

SwiftUI encountered an issue whe

2022-01-17  本文已影响0人  一粒咸瓜子

在SwiftUI中,使用 NavigationLink 时不注意状态共享的问题,很容就会产生数据错乱的bug,并在控制台出现提示:
SwiftUI encountered an issue when pushing aNavigationLink. Please file a bug.

在一般处理像是 cell 这样的会重复多次出现的 View 中的状态时,我们需要牢记,如果使用的状态不属于 cell 本身,那么它们是可能会在多个 cell 之间共享的。

问题分析

// 错误示例:
struct ContentView : View {
    let base = "https://cn.portal-pokemon.com/play/pokedex/00"
    @State var show: Bool = false
    var body: some View {
        NavigationView {
            List(1..<10) { i in
                NavigationLink(
                    destination:
                        SafariView(url: URL(string: base+"\(i)")! , onFinish: {
                            show = false
                        }).navigationBarTitle(Text("\(i)"), displayMode: .inline),
                    isActive: $show) { // 此处 show状态将在所有cell之间共享
                        Text("Cell \(i)")
                    }
            }
        }
    }
}


struct SafariView: UIViewControllerRepresentable {
    typealias UIViewControllerType = SFSafariViewController
    let url: URL
    let onFinish: ()->Void
    
    class Coordinator: NSObject, SFSafariViewControllerDelegate {
        let parent: SafariView
        init(_ parent: SafariView) {
            self.parent = parent
        }
        
        func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
            parent.onFinish()
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> SFSafariViewController {
        let controller = SFSafariViewController(url: url)
        controller.delegate = context.coordinator
        return controller
    }
    
    func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
        
    }
}

究其原因,是因为 show 是定义在 ContentView 中的变量,它被所有的 cell 共享。 当你点击某个 cell 时,show 的值被设置,这导致 body 部分被重新求值。而此时,show 不仅会作用在你点击的 cell 上,也会作用在所有其他 cell 上,也就是同时有多个 NavigationLink 的 isActive 参数接收到了 true。

同样的情况也会发生在 List 的 cell 上使用 .sheet 以 modal 方式弹出的界面中。.sheet 最常见的用法是 sheet(isPresented:content:),它需要 Binding 来决定展示与否,在实际 app 中更容易让人犯错。

面对这类 cell 中需要某个状态的时候,我们需要引入这个状态和当前 cell 的某种关联。另一种常见的解决方式是不要在 列表 cell 上定义导航行为 (包括 NavigationLink 或 sheet),而是将它们放到列表外层,这样就不会出现多个 cell 共用一个状态的情况了。


解决方法

法一:BindingForIndex

解决方式:https://www.5axxw.com/questions/content/jfimjq

struct ContentView : View {
    
    @State var activeIndex: Int?
    let base = "https://cn.portal-pokemon.com/play/pokedex/00"
    var body: some View {
        
        NavigationView {
            List(1..<10) { i in
                NavigationLink(
                    destination:
                        SafariView(url: URL(string: base+"\(i)")! , onFinish: {
                            activeIndex = nil
                        }).navigationBarTitle(Text("\(i)"), displayMode: .inline),
                    isActive: bindingForIndex(i)) {
                        Text("Cell \(i)")
                    }
            }
        }
    }
    
    func bindingForIndex(_ index: Int) -> Binding<Bool> {
        .init {
            self.activeIndex == index
        } set: { newValue in
            self.activeIndex = newValue ? index : nil
        }
    }
}

法二:改为 Button + background

将 NavigationLink 放到列表外层,内部改为 Button,并在action中改变状态,标记点击index

@State var selectedIndex: Int?
@State var isActive = false

NavigationView {
    List(1..<10) { i in
        Button(action: {
            self.selectedIndex = i
            self.isActived = true
        }, label: {
            Text("Cell \(i)")
        })
    }
    .background(
        NavigationLink(
            destination:
                SafariView(url: URL(string: base+"\(self.selectedIndex ?? 0)")! , onFinish: {
                    self.show.toggle()
                }).navigationBarTitle(Text("\(self.selectedIndex ?? 0)"), displayMode: .inline),
            isActive: self.$isActived , label: {
                Text("Cell \(self.selectedIndex ?? 0)")
            })
    )
}
上一篇 下一篇

猜你喜欢

热点阅读