SwiftUICoordinator
简介
协调器模式是Swift/iOS应用程序中广泛使用的设计模式,它有助于管理应用程序内的导航和视图流。这种模式的主要思想是将导航逻辑与视图解耦,从而使应用程序更容易维护和扩展。通过为导航目的提供一个中心联系点,协调器模式封装了导航逻辑,使视图保持轻量级并专注于自身的职责。
这个包提供了协调器模式与SwiftUI框架的无缝集成,使在SwiftUI应用程序中实现和管理导航变得容易。使用协调器模式,您可以轻松管理应用程序中的视图流,同时保持视图和导航逻辑之间的明确分离。这导致了一个更易维护和可扩展的应用程序,代码清晰易懂。
💡 问题
尽管使用SwiftUI有诸多好处,但在视图之间导航和管理其流程可能变得复杂和繁琐。使用NavigationStack
时,在栈中间关闭或替换视图会变得具有挑战性。当您有多个按顺序呈现的视图,并且需要关闭或替换其中一个中间视图时,可能会出现这种情况。
第二个挑战与弹出到根视图有关,当您有几个以层级方式呈现的视图,并且想要返回到根视图时。
🏃 实现
协调器
协调器协议是该模式的核心组件,代表应用程序中每个不同的视图流。
协议声明
@MainActor
public protocol Coordinator: AnyObject {
/// 存储父协调器引用的属性(如果有)。
/// 应该使用弱引用。
var parent: Coordinator? { get }
/// 存储任何子协调器引用的数组。
var childCoordinators: [WeakCoordinator] { get set }
/// 接受action参数并处理`CoordinatorAction`。
func handle(_ action: CoordinatorAction)
/// 将子协调器添加到列表中。
func add(child: Coordinator)
/// 从子协调器列表中移除协调器。
func remove(coordinator: Coordinator)
}
CoordinatorAction
此协议定义了协调器可用的操作。视图应该仅通过操作与协调器交互,确保单向通信流。
协议声明
public protocol CoordinatorAction {}
public enum Action: CoordinatorAction {
/// 表示成功完成,带有关联值。
case done(Any)
/// 表示取消,带有关联值。
case cancel(Any)
}
NavigationRoute
此协议定义了协调器流程中可用的导航路由。
协议声明
@MainActor
public protocol NavigationRoute {
/// 使用此标题在显示路由时设置导航栏标题。
var title: String? { get }
/// 提供路由在导航系统中的外观和样式信息的属性。
var appearance: RouteAppearance? { get }
/// 显示路由时使用的过渡动作。
/// 可以是推送动作、模态呈现或`nil`(用于子协调器)。
var action: TransitionAction? { get }
/// 指示协调器是否应作为EnvironmentObject附加到视图的属性。
var attachCoordinator: Bool { get }
/// 在导航期间隐藏返回按钮的属性
var hidesBackButton: Bool? { get }
/// 隐藏导航栏的属性
var hidesNavigationBar: Bool? { get }
}
Navigator
Navigator协议封装了导航层级内容所需的所有逻辑,包括管理NavigationController
及其子视图。
协议声明
@MainActor
public protocol Navigator: ObservableObject {
associatedtype Route: NavigationRoute
var navigationController: NavigationController { get }
/// 导航器的起始路由。
var startRoute: Route { get }
/// 应调用此方法以启动流程并显示`startRoute`的视图。
func start() throws
/// 为路由创建视图并将其添加到导航栈中。
func show(route: Route) throws
/// 为路由创建视图,并用指定的视图替换导航栈。
func set(routes: [Route], animated: Bool)
/// 为路由创建视图,并将它们附加到导航栈上。
func append(routes: [Route], animated: Bool)
/// 从导航栈中弹出顶部视图。
func pop(animated: Bool)
/// 弹出栈中除根视图外的所有视图。
func popToRoot(animated: Bool)
/// 关闭视图。
func dismiss(animated: Bool)
}
TabBarCoordinator
TabBarCoordinator
协议提供了一种在应用程序中管理标签栏界面的方法。
它定义了处理标签栏导航所需的属性和方法。
协议声明
@MainActor
public protocol TabBarCoordinator: ObservableObject {
associatedtype Route: TabBarNavigationRoute
var navigationController: NavigationController { get }
/// 管理标签栏界面的标签栏控制器。
var tabBarController: UITabBarController { get }
/// 标签栏界面中可用的标签,由`Route`类型表示。
var tabs: [Route] { get }
/// 应调用此方法以显示`tabBarController`。
///
/// - Parameter action: 通过提供`TransitionAction`可以自定义过渡类型。
func start(with action: TransitionAction)
}
💿 安装
要求
iOS 15.0
或更高版本
Swift包管理器
dependencies: [
.package(url: "https://github.com/erikdrobne/SwiftUICoordinator")
]
🔧 使用方法
import SwiftUICoordinator
创建路由
首先创建一个枚举,包含特定协调器流程的所有可用路由。
enum ShapesRoute: NavigationRoute {
case shapes
case simpleShapes
case customShapes
case featuredShape
var title: String? {
switch self {
case .shapes:
return "SwiftUI 形状"
default:
return nil
}
}
var action: TransitionAction? {
switch self {
case .simpleShapes:
// 对于呈现子协调器的路由,我们必须传递nil。
return nil
default:
return .push(animated: true)
}
}
}
创建操作
指定可以从协调对象发送到其父协调器的自定义操作。
enum ShapesAction: CoordinatorAction {
case simpleShapes
case customShapes
case featuredShape(NavigationRoute)
}
创建协调器
协调器必须遵循Routing
协议并实现handle(_ action: CoordinatorAction)
方法,该方法在接收到操作时执行特定流程的逻辑。
class ShapesCoordinator: Routing {
// MARK: - 内部属性
weak var parent: Coordinator?
var childCoordinators = [WeakCoordinator]()
let navigationController: NavigationController
let startRoute: ShapesRoute
let factory: CoordinatorFactory
// MARK: - 初始化
init(
parent: Coordinator?,
navigationController: NavigationController,
startRoute: ShapesRoute = .shapes,
factory: CoordinatorFactory
) {
self.parent = parent
self.navigationController = navigationController
self.startRoute = startRoute
self.factory = factory
}
func handle(_ action: CoordinatorAction) {
switch action {
case ShapesAction.simpleShapes:
let coordinator = factory.makeSimpleShapesCoordinator(parent: self)
try? coordinator.start()
case ShapesAction.customShapes:
let coordinator = factory.makeCustomShapesCoordinator(parent: self)
try? coordinator.start()
case let ShapesAction.featuredShape(route):
switch route {
...
default:
return
}
case Action.done(_):
popToRoot()
childCoordinators.removeAll()
default:
parent?.handle(action)
}
}
}
遵循RouterViewFactory协议
通过遵循RouterViewFactory
协议,我们定义了每个路由应该显示哪个视图。
重要提示:当我们想要显示子协调器时,应该返回一个EmptyView。
extension ShapesCoordinator: RouterViewFactory {
@ViewBuilder
public func view(for route: ShapesRoute) -> some View {
switch route {
case .shapes:
ShapeListView<ShapesCoordinator>()
case .simpleShapes:
EmptyView()
case .customShapes:
CustomShapesView<CustomShapesCoordinator>()
case .featuredShape:
EmptyView()
}
}
}
将RootCoordinator添加到应用程序
我们将实例化AppCoordinator
(RootCoordinator
的子类),将ShapesCoordinator
作为其子协调器传递,然后启动流程。
我们的起始路由将是ShapesRoute.shapes
。
final class SceneDelegate: NSObject, UIWindowSceneDelegate {
var dependencyContainer = DependencyContainer()
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let window = (scene as? UIWindowScene)?.windows.first else {
return
}
let appCoordinator = dependencyContainer.makeAppCoordinator(window: window)
dependencyContainer.set(appCoordinator)
let coordinator = dependencyContainer.makeShapesCoordinator(parent: appCoordinator)
appCoordinator.start(with: coordinator)
}
}
在SwiftUI视图中访问协调器
默认情况下,协调器作为@EnvironmentObject
附加到SwiftUI中。
要禁用此功能,您需要将NavigationRoute
的attachCoordinator
属性设置为false
。
struct ShapeListView<Coordinator: Routing>: View {
@EnvironmentObject var coordinator: Coordinator
@StateObject var viewModel = ViewModel<Coordinator>()
var body: some View {
List {
Button {
viewModel.didTapBuiltIn()
} label: {
Text("Simple")
}
Button {
viewModel.didTapCustom()
} label: {
Text("Custom")
}
Button {
viewModel.didTapFeatured()
} label: {
Text("Featured")
}
}
.onAppear {
viewModel.coordinator = coordinator
}
}
}
自定义转场
SwiftUICoordinator还支持创建自定义转场。
class FadeTransition: NSObject, Transitionable {
func isEligible(
from fromRoute: NavigationRoute,
to toRoute: NavigationRoute,
operation: NavigationOperation
) -> Bool {
return (fromRoute as? CustomShapesRoute == .customShapes && toRoute as? CustomShapesRoute == .star)
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to) else {
transitionContext.completeTransition(false)
return
}
let containerView = transitionContext.containerView
toView.alpha = 0.0
containerView.addSubview(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toView.alpha = 1.0
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
通过创建NavigationControllerDelegateProxy
并将转场作为参数传递来注册转场。
let factory = NavigationControllerFactory()
lazy var delegate = factory.makeNavigationDelegate([FadeTransition()])
lazy var navigationController = factory.makeNavigationController(delegate: delegate)
模态转场
自定义模态转场可以通过提供独特的方式来呈现
和关闭
视图控制器,从而增强用户体验。
首先,定义一个遵循UIViewControllerTransitioningDelegate
协议的转场委托对象。
final class SlideTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideTransition(isPresenting: true)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideTransition(isPresenting: false)
}
}
在这个例子中,SlideTransition
是一个遵循UIViewControllerAnimatedTransitioning
协议的自定义类,负责处理实际的动画逻辑。
将SlideTransitionDelegate
实例传递给你希望应用模态转场的特定操作。
var action: TransitionAction? {
switch self {
case .rect:
return .present(delegate: SlideTransitionDelegate())
default:
return .push(animated: true)
}
}
处理深层链接
在你的应用程序中,你可以通过创建一个遵循DeepLinkHandling
协议的DeepLinkHandler
来处理深层链接。这个处理器将指定URL方案和你的应用程序可以识别的支持的深层链接。
class DeepLinkHandler: DeepLinkHandling {
static let shared = DeepLinkHandler()
let scheme = "coordinatorexample"
let links: Set<DeepLink> = [
DeepLink(action: "custom", route: ShapesRoute.customShapes)
]
private init() {}
}
要在你的应用程序中处理传入的深层链接,你可以在场景委托中实现scene(_:openURLContexts:)
方法。
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard
let url = URLContexts.first?.url,
let deepLink = try? dependencyContainer.deepLinkHandler.link(for: url),
let params = try? dependencyContainer.deepLinkHandler.params(for: url, and: deepLink.params)
else {
return
}
dependencyContainer.appCoordinator?.handle(deepLink, with: params)
}
📒 示例项目
为了更好地理解,我建议你查看位于SwiftUICoordinatorExample
文件夹中的示例项目。
🤝 贡献
欢迎贡献,以帮助改进和发展这个项目!
报告bug
如果你遇到bug,请在GitHub上开一个issue,提供问题的详细描述。 包括以下信息:
- 复现bug的步骤
- 预期行为
- 实际行为
- 环境详情(Swift版本等)
请求功能
对于功能请求,请在GitHub上开一个issue。清楚地描述你想看到的新功能,并提供任何相关的细节或用例。
提交拉取请求
要提交拉取请求:
- Fork存储库。
- 为你的更改创建一个新分支。
- 进行更改并彻底测试。
- 打开一个拉取请求,清楚地描述你所做的更改。
感谢你对SwiftUICoordinator的贡献!🚀
如果你喜欢这个项目,请给它一个⭐️,以帮助其他人发现这个存储库。