在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
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)")
})
)
}







网友评论