🚀 了解如何构建和使用此包:https://www.swiftful-thinking.com/offers/REyNLwwH

SwiftfulRouting 🤙

SwiftfulRouting 是一个原生、声明式的框架,可以在 SwiftUI 应用程序中实现程序化导航。

工作原理

详情(点击展开)
基于程序化代码的路由器不会提前声明视图层级结构,而是在执行时声明。然而,SwiftUI 是声明式的,因此我们必须提前声明视图层级结构。此处的解决方案是通过连接视图修饰符来支持预先的路由,从而将 SwiftUI 的声明式代码转换为程序化代码。

当您跳转到新屏幕时,该框架会将一组视图修饰符添加到目标视图的根目录,这将支持所有潜在的导航路线。 这些修饰符基于通用和/或类型擦除的目标,这保持了声明式视图层级结构,同时允许开发人员仍然在执行时确定目标。

示例项目:https://github.com/SwiftfulThinking/SwiftfulRoutingExample

设置

详情(点击展开)
将该包添加到您的 Xcode 项目。
https://github.com/SwiftfulThinking/SwiftfulRouting.git

导入该包

import SwiftfulRouting

在您的视图层级结构的顶部添加一个 RouterViewRouterView 会将您的视图嵌入到导航层级结构中,并添加修饰符以支持所有潜在的跳转。

struct ContentView: View {
    var body: some View {
        RouterView { _ in
            MyView()
        }
    }
}

所有子视图都可以访问 Environment 中的 Router

@Environment(\.router) var router
    
var body: some View {
     Text("Hello, world!")
          .onTapGesture {
               router.showScreen(.push) { _ in
                    Text("Another screen!")
               }
          }
     }
}

除了依赖 Environment 之外,您还可以将 Router 直接传递到子视图中。 这允许 Router 完全与 View 解耦(对于更复杂的应用程序架构)。

RouterView { router in
     ContentView(router: router)
          .onTapGesture {
               router.showScreen(.push) { router2 in
                    Text("View2")
                         .onTapGesture {
                              router2.showScreen(.push) { router3 in
                                   Text("View3")
                              }
                         }
               }
          }
}

每次 Segue 后,都会创建一个新的 Router 并将其添加到视图层级结构中。 请参阅 AnyRouter.swift 以查看所有可访问的方法。

设置(现有项目)

详情(点击展开)

为了进入框架的视图层级结构,您必须将您的内容包装在 RouterView 中。 默认情况下,您的视图将被包装在导航堆栈中(iOS 16+ 使用 NavigationStack,iOS 15 及以下版本使用 NavigationView)。

RouterView(addNavigationView: false, screens: $existingStack) { router in
   MyView(router: router)
        .navigationBarHidden(true)
        .toolbar {
        }
}

显示屏幕

详情(点击展开)

Router 支持所有原生的 SwiftUI 跳转。

// NavigationLink
router.showScreen(.push) { _ in
     Text("View2")
}

// Sheet
router.showScreen(.sheet) { _ in
     Text("View2")
}

// FullScreenCover
router.showScreen(.fullScreenCover) { _ in
     Text("View2")
}

Segue 方法也接受 AnyRoute 作为便利,这使得在您的代码中传递 Route 变得容易。

let route = AnyRoute(.push, destination: { router in
     Text("Hello, world!")
})
                        
router.showScreen(route)

所有 segues 都有一个 onDismiss 方法。

router.showScreen(.push, onDismiss: {
     // dismiss action
}, destination: { _ in
     Text("Hello, world!")
})
                
let route = AnyRoute(.push, onDismiss: {
     // dismiss action
}, destination: { _ in
     Text("Hello, world!")
})
                
router.showScreen(route)

iOS 16+ 使用 NavigationStack,它支持一次推送多个屏幕。

let route1 = PushRoute(destination: { router in
     Text("View1")
})
let route2 = PushRoute(destination: { router in
     Text("View2")
})
let route3 = PushRoute(destination: { router in
     Text("View3")
})
                        
router.pushScreenStack(destinations: [route1, route2, route3])

iOS 16+ 还支持可调整大小的 sheet。

router.showResizableSheet(sheetDetents: [.medium, .large], selection: nil, showDragIndicator: true) { _ in
     Text("Hello, world!)
}

其他便利方法

router.showSafari {
     URL(string: "https://www.apple.com")
}

进入屏幕流

详情(点击展开)

屏幕“流”是支持应用程序中动态路由的一种新方法。 当您进入“屏幕流”时,您会将一个 Routes 数组添加到层级结构中。 应用程序将立即跳转到第一个屏幕,然后将剩余的屏幕设置到队列中。

router.enterScreenFlow([
     AnyRoute(.fullScreenCover, destination: screen1),
     AnyRoute(.push, destination: screen2),
     AnyRoute(.push, destination: screen3),
     AnyRoute(.push, destination: screen4),
])

这允许开发人员一次设置多个未来的 segues,而无需每个子视图中的屏幕特定代码。 每个子视图的路由逻辑都像“尝试转到下一个屏幕”一样简单。

do {
     try router.showNextScreen()
} catch {
     // There is no next screen set in the flow
     // Dismiss the flow (see below dismiss methods) or do something else
}

使用“流”的好处

取消屏幕

详情(点击展开)

取消一个屏幕。 您还可以使用原生的 SwiftUI 代码取消屏幕,包括向后滑动或 presentationMode

router.dismissScreen()

取消推送到堆栈上的所有屏幕。 这会取消屏幕导航堆栈上的每个“推送”(NavigationLink)。 这不会取消 sheetfullScreenCover

router.dismissScreenStack()

取消屏幕环境。 这会取消屏幕的根环境(如果有一个要取消),这是调用站点下方最近的“sheet”或 fullScreenCover

router.dismissEnvironment()

例如,如果您输入了以下屏幕流,并且您从任何子视图中调用了 dismissEnvironment,它将取消 fullScreenCover,进而取消显示在该环境中的每个视图。

router.enterScreenFlow([
     AnyRoute(.fullScreenCover, destination: screen1),
     AnyRoute(.push, destination: screen2),
     AnyRoute(.push, destination: screen3),
     AnyRoute(.push, destination: screen4),
])

用于取消“流”的逻辑通常如下所示

do {
     try router.showNextScreen()
} catch {
     router.dismissEnvironment()
}

或者便利方法

router.showNextScreenOrDismissEnvironment()

将此代码复制并粘贴到您的项目中以启用向后滑动的手势。 默认情况下,这不包含在 SwiftUI 框架中,因此不会自动包含在此处。

extension UINavigationController: UIGestureRecognizerDelegate {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }
    
    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
}

警报

详情(点击展开)

Router 支持原生的 SwiftUI 警报。

// Alert
router.showAlert(.alert, title: "Title goes here", subtitle: "Subtitle goes here!") {
     Button("OK") {

     }
     Button("Cancel") {
                        
     }
}

// Confirmation Dialog
router.showAlert(.confirmationDialog, title: "Title goes here", subtitle: "Subtitle goes here!") {
     Button("A") {
                        
     }
     Button("B") {
                        
     }
     Button("C") {
                        
     }
}

取消警报。

router.dismissAlert()

其他便利方法

router.showBasicAlert(text: "Error")

模态框

详情(点击展开)

Router 还支持任何模态转换,该转换显示在当前内容之上。 自定义过渡、动画、背景颜色/模糊等。 有关示例实现,请参见示例项目。

router.showModal(transition: .move(edge: .top), animation: .easeInOut, alignment: .top, backgroundColor: nil, useDeviceBounds: true) {
     Text("Sample")
          .onTapGesture {
               router.dismissModal()
          }
}

您可以同时显示多个模态框。 模态框具有一个可选的 ID 字段,稍后可用于取消模态框。

router.showModal(id: "top1") {
     Text("Sample")
}

// Dismiss top-most modal
router.dismissModal()

// Dismiss modal by ID
router.dismissModal(id: "top1")

// Dismiss all modals
router.dismissAllModals()

其他便利方法

router.showBasicModal {
     Text("Sample")
          .onTapGesture {
               router.dismissModal()
          }
}

贡献 🤓

详情(点击展开)

我们鼓励社区贡献! 请确保您的代码符合项目现有的编码风格和结构。 大多数新功能可能都是现有功能的派生,因此应重用许多现有的 ViewModifier 和 Bindings。

即将推出的功能