SwiftUICoordinator

Build Status License: MIT Static Badge

简介

Coordinator 模式是 Swift/iOS 应用中一种广泛使用的设计模式,它有助于管理应用内的导航和视图流程。该模式背后的主要思想是将导航逻辑与视图解耦,从而使应用程序更易于维护和随着时间的推移进行扩展。通过为导航目的提供一个中心联系点,Coordinator 模式封装了导航逻辑,并使视图保持轻量级并专注于自己的职责。

此软件包提供了 Coordinator 模式与 SwiftUI 框架的无缝集成,使您可以轻松地在 SwiftUI 应用程序中实现和管理导航。使用 Coordinator 模式,您可以轻松管理应用内的视图流程,同时保持视图和导航逻辑之间清晰的分离。 这将产生一个更易于维护和扩展的应用程序,并具有干净且易于理解的代码。

💡 问题

尽管使用 SwiftUI 有诸多好处,但在视图之间导航和管理其流程可能变得复杂而繁琐。 使用 NavigationStack,存在一些限制,例如在堆栈中间关闭或替换视图变得具有挑战性。 当您按顺序显示多个视图,并且需要关闭或替换其中一个中间视图时,可能会发生这种情况。

第二个挑战与返回到根视图有关,当您以分层方式呈现多个视图,并且想要返回到根视图时,会出现这个问题。

🏃 实现

workflow

Coordinator

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)
}

CoordinatorAction

此协议定义了协调器可用的操作。 视图应仅通过操作与协调器交互,从而确保单向通信流。

协议声明

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)
}

NavigationRoute

此协议定义了协调器流程中可用的导航路线。

协议声明

@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

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

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 或更高版本

Swift Package Manager

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

通过符合 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 添加到应用

我们将实例化 AppCoordinatorRootCoordinator 的一个子类),将 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。 要禁用此功能,您需要将 NavigationRouteattachCoordinator 属性设置为 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)

模态过渡

自定义模态过渡可以通过提供一种独特的方式来 presentdismiss 视图控制器来增强用户体验。

首先,定义一个符合 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

要提交 pull request

  1. Fork 该仓库。
  2. 为您的更改创建一个新分支。
  3. 进行更改并彻底测试。
  4. 打开一个 pull request,清楚地描述您所做的更改。

感谢您为 SwiftUICoordinator 做出贡献! 🚀

如果您喜欢这个项目,请给它一个 ⭐️,以帮助其他人发现该仓库。