SDRouting 是一个面向 SwiftUI 的轻量路由层,统一管理:
pushsheetfullScreenCoveralert / confirmationDialog- 自定义
modal
它的目标不是再包一层“语法糖”,而是给 SwiftUI 建一个更稳定的页面流转模型。
- iOS 16+
- macOS 15+
- Swift 6
dependencies: [
.package(url: "https://github.com/sinduke/SDRouting.git", from: "0.0.3")
]然后在目标中引入:
dependencies: [
.product(name: "SDRouting", package: "SDRouting")
]import SwiftUI
import SDRouting
struct RootView: View {
var body: some View {
RouterView { router in
HomeView(router: router)
}
}
}
struct HomeView: View {
let router: RouterProtocol
var body: some View {
VStack(spacing: 16) {
Button("Push") {
router.navigateTo(.push) { router in
DetailView(router: router)
}
}
Button("Sheet") {
router.navigateTo(.sheet) { router in
DetailView(router: router)
}
}
Button("Full Screen") {
router.navigateTo(.fullScreenCover) { router in
DetailView(router: router)
}
}
}
.padding()
}
}
struct DetailView: View {
let router: RouterProtocol
var body: some View {
VStack(spacing: 16) {
Text("Detail")
Button("Dismiss") {
router.dismissScreen()
}
}
.padding()
}
}SDRouting 现在采用两个明确分离的上下文:
负责当前 NavigationStack 的 path。
规则:
push复用当前 navigation context- 同一条 push 链共享同一个
path
负责当前展示层的:
sheetfullScreenCoveralertmodal
规则:
sheet会创建一个新的 presentation contextfullScreenCover也会创建一个新的 presentation context- 当前 context 内只有根节点负责真正挂载 presentation modifier
这意味着:
- pushed page 不再自己挂
.sheet/.fullScreenCover - pushed page 只是当前上下文中的一个 screen
- 真正的展示宿主只有当前 context root
这套规则是整个库稳定性的关键。
之前最容易出问题的结构是:
fullScreenCover -> sheet -> fullScreenCover -> push -> sheet
旧实现里,每个递归的 RouterView 都会自己挂:
.sheet.fullScreenCover.alert.modal
这会导致两个问题:
push页面也会成为 presentation hostfullScreenCover内部再弹sheet时,SwiftUI 会反复重建展示宿主
具体表现就是:
fullScreen.content.appear/disappear反复抖动sheet看起来像直接覆盖全屏- 长按导航栏返回按钮时,系统在读取历史导航项时可能闪退
根因不是业务页面本身,而是 presentation host 分散在整条递归视图树里,导致宿主不稳定。
核心修复不是改某一个 fullScreenCover 调用,而是改路由结构:
- 引入
RouterPresentationCoordinator
它集中管理当前 context 的展示状态:
pathsheetfullScreenCoveralertmodal
- 把
RouterView分成两种角色
hostsPresentation == true当前 context 的根节点,会挂所有 presentation modifierhostsPresentation == false纯 push 页面,不再挂 presentation modifier
- 明确路由规则
push:复用当前 coordinator,且新页面hostsPresentation = falsesheet:创建新的 coordinator,且新页面hostsPresentation = truefullScreenCover:创建新的 coordinator,且新页面hostsPresentation = true
最终效果是:
- 一个 presentation context 只有一个宿主
- push 链不会再把 presentation host 越推越深
sheet/fullScreenCover的宿主位置稳定- 嵌套
full -> sheet -> full -> push -> sheet的行为恢复正常
对外统一入口是 RouterProtocol:
@MainActor
public protocol RouterProtocol {
func navigateTo<T: View>(
_ segue: SegueType,
@ViewBuilder destination: @escaping (RouterProtocol) -> T
)
func dismissScreen()
func showAlert(_ alert: AnyAppAlert)
func dismissAlert()
func showModal<T: View>(
configuration: AppModalConfiguration,
@ViewBuilder content: @escaping () -> T
)
func dismissModal()
}router.navigateTo(.push) { router in
DetailView(router: router)
}router.navigateTo(.sheet) { router in
SettingsView(router: router)
}router.navigateTo(.fullScreenCover) { router in
LoginView(router: router)
}router.showAlert(
.ok(title: "Done", message: "Saved successfully.")
)确认弹窗:
router.showAlert(
.confirm(
title: "Delete",
message: "This action cannot be undone.",
confirmTitle: "Delete"
) {
print("delete confirmed")
}
)router.showModal {
VStack(spacing: 12) {
Text("Custom Modal")
Button("Close") {
router.dismissModal()
}
}
.padding()
}如果需要自定义展示参数:
router.showModal(configuration: .default) {
Text("Modal Content")
}RouterView 会自动把 router 注入环境。
struct ChildView: View {
@Environment(\.router) private var router
var body: some View {
Button("Open") {
router.navigateTo(.push) { router in
Text("Next")
}
}
}
}如果你更重视依赖清晰,也可以继续显式传 RouterProtocol。
库内置了调试输出:
import SDRouting
@main
struct DemoApp: App {
init() {
SDRoutingDebug.isEnabled = true
}
var body: some Scene {
WindowGroup {
RootView()
}
}
}也可以自定义输出方式:
SDRoutingDebug.printer = { message in
print(message)
}当前日志会输出:
routerIDcoordinatorIDhostsPresentationpathsheetfullScreenmodalalert
这些信息足够定位大多数路由链问题。
AnyAppAlert.AppButton 现在可以在包外直接构造:
let button = AnyAppAlert.AppButton(
title: "OK",
role: .cancel
)也可以继续使用便捷方法:
.ok().cancel().destructive(...)
可以把 SDRouting 理解成下面这套规则:
push是“当前导航上下文内继续前进”sheet是“开启一个新的展示上下文”fullScreenCover是“开启一个新的全屏展示上下文”- 每个展示上下文只有一个真正的 presentation host
- pushed page 不承担 presentation host 角色
这也是当前版本能稳定处理复杂嵌套场景的根本原因。