深度编程 SwiftUI NavigationView 导航
Posted
技术标签:
【中文标题】深度编程 SwiftUI NavigationView 导航【英文标题】:Deep programmatic SwiftUI NavigationView navigation 【发布时间】:2020-04-11 16:12:19 【问题描述】:我正在尝试按顺序获得深度嵌套的编程导航堆栈。当手动完成导航(即:按下链接)时,以下代码按预期工作。当您按下 Set Nav
按钮时,导航堆栈确实会发生变化 - 但不会像预期的那样 - 您最终会得到一个损坏的堆栈 [start -> b -> bbb]
并在视图之间翻转很多
class NavState: ObservableObject
@Published var firstLevel: String? = nil
@Published var secondLevel: String? = nil
@Published var thirdLevel: String? = nil
struct LandingPageView: View
@ObservedObject var navigationState: NavState
func resetNav()
self.navigationState.firstLevel = "b"
self.navigationState.secondLevel = "ba"
self.navigationState.thirdLevel = "bbb"
var body: some View
return NavigationView
List
NavigationLink(
destination: Place(
text: "a",
childValues: [ ("aa", [ "aaa"]) ],
navigationState: self.navigationState
).navigationBarTitle("a"),
tag: "a",
selection: self.$navigationState.firstLevel
)
Text("a")
NavigationLink(
destination: Place(
text: "b",
childValues: [ ("bb", [ "bbb"]), ("ba", [ "baa", "bbb" ]) ],
navigationState: self.navigationState
).navigationBarTitle("b"),
tag: "b",
selection: self.$navigationState.firstLevel
)
Text("b")
Button(action: self.resetNav)
Text("Set Nav")
.navigationBarTitle("Start")
.navigationViewStyle(StackNavigationViewStyle())
struct Place: View
var text: String
var childValues: [ (String, [String]) ]
@ObservedObject var navigationState: NavState
var body: some View
List(childValues, id: \.self.0) childValue in
NavigationLink(
destination: NextPlace(
text: childValue.0,
childValues: childValue.1,
navigationState: self.navigationState
).navigationBarTitle(childValue.0),
tag: childValue.0,
selection: self.$navigationState.secondLevel
)
Text(childValue.0)
struct NextPlace: View
var text: String
var childValues: [String]
@ObservedObject var navigationState: NavState
var body: some View
List(childValues, id: \.self) childValue in
NavigationLink(
destination: FinalPlace(
text: childValue,
navigationState: self.navigationState
).navigationBarTitle(childValue),
tag: childValue,
selection: self.$navigationState.thirdLevel
)
Text(childValue)
struct FinalPlace: View
var text: String
@ObservedObject var navigationState: NavState
var body: some View
let concat: String = "\(navigationState.firstLevel)/\(navigationState.secondLevel))/\(navigationState.thirdLevel)/"
return VStack
Text(text)
Text(concat)
我最初试图将导航过渡动画作为问题源来解决 - 但How to disable NavigationView push and pop animations 暗示这是不可配置的
那里有 >1 级程序化导航的合理示例吗?
编辑:我希望在这里获得的部分内容也是导航正常工作的初始状态 - 如果我从外部上下文中进入我希望反映的导航状态(即:从带有一些应用程序内上下文的通知,或者从保存到磁盘编码状态开始)然后我希望能够加载顶部View
,导航正确指向正确的子视图。本质上 - 用实际值替换NavState
中的nil
s。 Qt 的 QML 和 ReactRouter 都可以声明式地做到这一点 - SwiftUI 也应该能够做到。
【问题讨论】:
有趣的是,如果您在视图中使用本地 @State 变量,这将完美运行...因此您可以通过初始化传递状态来进行深度链接。 用 DefaultNavigationViewStyle 就是。 这可能最近发生了变化——我为此提交了一份反馈报告,说如果其他框架可以做到这一点——SwiftUI 也应该这样做。他们最近发回了一些东西(我认为是 13.4.5 测试版),但我还没有跟进 这是个好消息。我期待着找出他们是否解决了这个问题。干杯! 【参考方案1】:这是因为动画完成时会形成新的堆栈层级,这就是它在手动点击的情况下起作用的原因。
通过以下修改,它可以工作:
func resetNav()
self.navigationState.firstLevel = "b"
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
self.navigationState.secondLevel = "ba"
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
self.navigationState.thirdLevel = "bbb"
【讨论】:
是的,我也尝试过这种方法 - 我稍微更新了问题以澄清。使用其他框架进行编程导航可以很好地定义初始状态,因此进入视图不应该加载然后执行几个转换 - 它应该直接从数据告诉它的地方开始。我认为这里的问题是苹果对Navigation*
结构行为的僵化【参考方案2】:
最简单的方法是删除后续级别的@Published。
class NavState: ObservableObject
@Published var firstLevel: String? = nil
var secondLevel: String? = nil
var thirdLevel: String? = nil
这也是告诉你如何有条件地改变变量可能会影响导航行为。
【讨论】:
你试过这个吗?这打破了基于触摸的导航。我想我看到了你的目标(一个objectWillChange.send()
点),但我仍然无法做到不做傻事并允许两种导航类型
是的,我试过了。此解决方案直接针对much flipping
问题。换句话说,您可能需要多个视图模型来处理每一层。这样更有意义。
你能提供一个更具体的例子吗?我在上面发布的是我的实际问题的简化版本,其中涉及动态层数和每层的动态元素数 - 如果没有可通用的导航模型,我无法看到如何获得类似于您建议的工作的东西顶层。还更新了问题以反映初始状态对我也很重要
@E.Coms 我尝试了使用导航状态的解决方案并将其与 Asperi 的重置导航功能结合使用,但我看到了相同的反弹效果。它适用于所有情况,除非我在堆栈中处于 1 级。在预期视图上闪烁半秒后,程序化链接将我弹回主视图。【参考方案3】:
在有人遇到NavigationView
似乎无法满足的相同导航要求的情况下,将我自己的解决方案作为一个选项放在这里。在我的书中 - 功能比图形元素和动画更难修复。灵感来自https://medium.com/swlh/swiftui-custom-navigation-view-for-your-applications-7f6effa7dbcf
它摆脱了根级别的单个声明性变量确定导航堆栈的想法 - 当像我上面的演示中从 a
链跳转到 b
链这样的操作时,绑定到单个变量似乎很奇怪,或者需要从特定位置开始
我使用了像 URI 这样的字符串化路径的概念作为变量概念 - 这可能会被一个更具表现力的模型(比如 enum
s 的向量)替换
注意事项 - 它非常粗糙,没有动画,看起来根本不是原生的,使用 AnyView
,你不能多次使用相同的节点名称,只反映 StackNavigationViewStyle
等 -如果我把它变成更漂亮、理智和通用的东西,我会把它放在 Gist 中。
struct NavigationElement
var tag: String
var title: String
var viewBuilder: () -> AnyView
class NavigationState: ObservableObject
let objectWillChange = ObservableObjectPublisher()
var stackPath: [String]
willSet
if rectifyRootElement(path: newValue)
_stackPath = newValue
rectifyStack(path: newValue, elements: _stackElements)
/// Temporary storage for the most current stack path during transition periods
private var _stackPath: [String] = []
@Published var stack: [NavigationElement] = []
var stackElements: [String : NavigationElement] = [:]
willSet
_stackElements = newValue
rectifyStack(path: _stackPath, elements: newValue)
/// Temporary storage for the most current stack elements during transition periods
private var _stackElements: [String : NavigationElement] = [:]
init(initialPath: [String] = [])
self.stackPath = initialPath
rectifyRootElement(path: initialPath)
_stackPath = self.stackPath
@discardableResult func rectifyRootElement(path: [String]) -> Bool
// Rectify root state if set from outside
if path.first != ""
stackPath = [ "" ] + path
return false
return true
private func rectifyStack(path: [String], elements: [String : NavigationElement])
var newStack: [NavigationElement] = []
for tag in path
if let elem = elements[tag]
newStack.append(elem)
else
print("Path has a tag '\(tag)' which cannot be represented - truncating at last usable element")
break
stack = newStack
struct NavigationStack<Content: View>: View
@ObservedObject var navState: NavigationState
@State private var trigger: String = "humperdoo" //HUMPERDOO! Chose something that would not conflict with empty root state - could probably do better with an enum
init(_ navState: NavigationState, title: String, builder: @escaping () -> Content)
self.navState = navState
self.navState.stackElements[""] = NavigationElement(tag: "", title: title, viewBuilder: AnyView(builder()) )
var backButton: some View
Button(action: self.navState.stackPath.removeLast() )
Text("Back")
var navigationHeader: some View
HStack
ViewBuilder.buildIf( navState.stack.count > 1 ? backButton : nil )
Spacer()
ViewBuilder.buildIf( navState.stack.last?.title != nil ? Text(navState.stack.last!.title) : nil )
Spacer()
.frame(height: 40)
.background(Color(.systemGray))
var currentNavElement: some View
return navState.stack.last!.viewBuilder()
var body: some View
VStack
// This is an effectively invisible element which primarily serves to force refreshes of the tree
Text(trigger)
.frame(width: 0, height: 0)
.onReceive(navState.$stack, perform: stack in
self.trigger = stack.reduce("") tag, elem in
return tag + "/" + elem.tag
)
navigationHeader
ViewBuilder.buildBlock(
navState.stack.last != nil
? ViewBuilder.buildEither(first: currentNavElement)
: ViewBuilder.buildEither(second: Text("The navigation stack is empty - this is odd"))
)
struct NavigationDestination<Label: View, Destination: View>: View
@ObservedObject var navState: NavigationState
var tag: String
var label: () -> Label
init(_ navState: NavigationState, tag: String, title: String, destination: @escaping () -> Destination, label: @escaping () -> Label)
self.navState = navState
self.tag = tag
self.label = label
self.navState.stackElements[tag] = NavigationElement(tag: tag, title: title, viewBuilder: AnyView(destination()) )
var body: some View
label()
.onTapGesture
self.navState.stackPath.append(self.tag)
还有一些基本的使用代码
struct LandingPageView: View
@ObservedObject var navState: NavigationState
var destinationA: some View
List
NavigationDestination(self.navState, tag: "aa", title: "AA", destination: Text("AA") )
Text("Go to AA")
NavigationDestination(self.navState, tag: "ab", title: "AB", destination: Text("AB") )
Text("Go to AB")
var destinationB: some View
List
NavigationDestination(self.navState, tag: "ba", title: "BA", destination: Text("BA") )
Text("Go to BA")
Button(action: self.navState.stackPath = [ "a", "ab" ] )
Text("Jump to AB")
var body: some View
NavigationStack(navState, title: "Start")
List
NavigationDestination(self.navState, tag: "a", title: "A", destination: self.destinationA )
Text("Go to A")
NavigationDestination(self.navState, tag: "b", title: "B", destination: self.destinationB )
Text("Go to B")
【讨论】:
以上是关于深度编程 SwiftUI NavigationView 导航的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI之NavigationView的基础使用与进阶实践
为啥我的 SwiftUI 应用程序在 `NavigationView` 中的 `navigationBarItems` 内放置 `NavigationLink` 后向后导航时崩溃?
如何将 CoreSpotlight 与 Swiftui 应用生命周期进行深度链接?
深度解析:为何在 SwiftUI 视图的 init 初始化器里无法更改 @State 的值?