Coordinator 模式是 Swift/iOS 应用中一种广泛使用的设计模式,它有助于管理应用内的导航和视图流程。该模式背后的主要思想是将导航逻辑与视图解耦,从而使应用程序更易于维护和随着时间的推移进行扩展。通过为导航目的提供一个中心联系点,Coordinator 模式封装了导航逻辑,并使视图保持轻量级并专注于自己的职责。
此软件包提供了 Coordinator 模式与 SwiftUI 框架的无缝集成,使您可以轻松地在 SwiftUI 应用程序中实现和管理导航。使用 Coordinator 模式,您可以轻松管理应用内的视图流程,同时保持视图和导航逻辑之间清晰的分离。 这将产生一个更易于维护和扩展的应用程序,并具有干净且易于理解的代码。
尽管使用 SwiftUI 有诸多好处,但在视图之间导航和管理其流程可能变得复杂而繁琐。 使用 NavigationStack
,存在一些限制,例如在堆栈中间关闭或替换视图变得具有挑战性。 当您按顺序显示多个视图,并且需要关闭或替换其中一个中间视图时,可能会发生这种情况。
第二个挑战与返回到根视图有关,当您以分层方式呈现多个视图,并且想要返回到根视图时,会出现这个问题。
Coordinator 协议是该模式的核心组件,代表应用程序中每个不同的视图流程。
协议声明
@MainActor
public protocol Coordinator: AnyObject {
/// A property that stores a reference to the parent coordinator, if any.
/// Should be used as a weak reference.
var parent: Coordinator? { get }
/// An array that stores references to any child coordinators.
var childCoordinators: [WeakCoordinator] { get set }
/// Takes action parameter and handles the `CoordinatorAction`.
func handle(_ action: CoordinatorAction)
/// Adds child coordinator to the list.
func add(child: Coordinator)
/// Removes the coordinator from the list of children.
func remove(coordinator: Coordinator)
}
此协议定义了协调器可用的操作。 视图应仅通过操作与协调器交互,从而确保单向通信流。
协议声明
public protocol CoordinatorAction {}
public enum Action: CoordinatorAction {
/// Indicates a successful completion with an associated value.
case done(Any)
/// Indicates cancellation with an associated value.
case cancel(Any)
}
此协议定义了协调器流程中可用的导航路线。
协议声明
@MainActor
public protocol NavigationRoute {
/// Use this title to set the navigation bar title when the route is displayed.
var title: String? { get }
/// A property that provides the info about the appearance and styling of a route in the navigation system.
var appearance: RouteAppearance? { get }
/// Transition action to be used when the route is shown.
/// This can be a push action, a modal presentation, or `nil` (for child coordinators).
var action: TransitionAction? { get }
/// A property that indicates whether the Coordinator should be attached to the View as an EnvironmentObject.
var attachCoordinator: Bool { get }
/// A property that hides the back button during navigation
var hidesBackButton: Bool? { get }
/// A property that hides the navigation bar
var hidesNavigationBar: Bool? { get }
}
Navigator 协议封装了导航分层内容所需的所有逻辑,包括管理 NavigationController
及其子视图。
协议声明
@MainActor
public protocol Navigator: ObservableObject {
associatedtype Route: NavigationRoute
var navigationController: NavigationController { get }
/// The starting route of the navigator.
var startRoute: Route { get }
/// This method should be called to start the flow and to show the view for the `startRoute`.
func start() throws(NavigatorError)
/// It creates a view for the route and adds it to the navigation stack.
func show(route: Route) throws(NavigatorError)
/// Creates views for routes, and replaces the navigation stack with the specified views.
func set(routes: [Route], animated: Bool)
/// Creates views for routes, and appends them on the navigation stack.
func append(routes: [Route], animated: Bool)
/// Pops the top view from the navigation stack.
func pop(animated: Bool)
/// Pops all the views on the stack except the root view.
func popToRoot(animated: Bool)
/// Dismisses the view.
func dismiss(animated: Bool)
}
TabBarCoordinator
协议提供了一种在应用程序中管理选项卡栏界面的方法。 它定义了处理选项卡栏导航所需的属性和方法。
协议声明
@MainActor
public protocol TabBarCoordinator: ObservableObject {
associatedtype Route: TabBarNavigationRoute
associatedtype TabBarController: UITabBarController
var navigationController: NavigationController { get }
/// The tab bar controller that manages the tab bar interface.
var tabBarController: TabBarController { get }
/// The tabs available in the tab bar interface, represented by `Route` types.
var tabs: [Route] { get }
/// This method should be called to show the `tabBarController`.
///
/// - Parameter action:The type of transition can be customized by providing a `TransitionAction`.
func start(with action: TransitionAction)
}
iOS 15.0
或更高版本
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 Shapes"
default:
return nil
}
}
var action: TransitionAction? {
switch self {
case .simpleShapes:
// We have to pass nil for the route presenting a child coordinator.
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: - Internal properties
weak var parent: Coordinator?
var childCoordinators = [WeakCoordinator]()
let navigationController: NavigationController
let startRoute: ShapesRoute
let factory: CoordinatorFactory
// MARK: - Initialization
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
协议,我们定义了每个路由应显示的视图。 重要提示:当我们想要显示子协调器时,我们应该返回一个 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()
}
}
}
我们将实例化 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)
}
}
默认情况下,协调器作为 @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)
自定义模态过渡可以通过提供一种独特的方式来 present
和 dismiss
视图控制器来增强用户体验。
首先,定义一个符合 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
文件夹中的示例项目。
欢迎贡献,以帮助改进和发展此项目!
如果您遇到错误,请在 GitHub 上打开一个 issue,并提供问题的详细描述。 包括以下信息
对于功能请求,请在 GitHub 上打开一个 issue。 清楚地描述您希望看到的新功能,并提供任何相关详细信息或用例。
要提交 pull request
感谢您为 SwiftUICoordinator 做出贡献! 🚀
如果您喜欢这个项目,请给它一个 ⭐️,以帮助其他人发现该仓库。