[译] iOS App 上一种灵活的路由方式

“Trollstigen”前端

Rosberry 中咱们已经放弃使用除了 Launch Screen 之外的全部 storyboard,固然,全部布局和跳转逻辑都在代码里进行配置。若是想要进一步了解,请参考咱们团队的这篇文章 没有 Interface Builder 的生活,我但愿你会以为这篇文章很是实用。android

在这篇文章里,我将会介绍一种在 View Controller 之间的新的路由方式。咱们将带着问题开始,而后一步一步地走向最终结论。享受阅读吧!ios


深刻挖掘这个问题

让咱们使用一个具体的例子来理解这个问题。例如咱们准备作一个 App,它包含了我的主页、好友列表、聊天窗口等组成部分。很显然,咱们能够注意到在不少 Controller 里都须要经过页面跳转去显示用户的个主页,若是这个逻辑只实现一次,而且能复用的话,那就很是好了。咱们记得 DRY! 咱们没法使用一些 storyboard 来实现它,你能够想象一下,它在 storyboard 里面看起像什么 —— weeeeb 页面. 😬git

如今咱们使用的是 MVVM + Router 的架构,由 ViewModel 告诉 Router 须要跳转到一个其余的模块,而后 router 去执行。在咱们的例子中,为了不 view controller(或者View model)臃肿,Router 仅仅携带了全部的跳转逻辑。若是你一开始不是很明白,不用担忧!我将会用一种比较浅显的方式来解释这种解决方案,因此它也会很容易地被应用到简单的 MVC 中去。github


解决方案

1. 一开始,添加一个拓展到 ViewController 看起来像是一个毫无异议的解决方案:swift

extension UIViewController {
    func openProfile(for user: User) {
        let profileViewController = ProfileViewController(user: user)
        present(profileViewController, animated: true, completion: nil)
    }
}
复制代码

这就是咱们想要的 —— 一次编写,屡次使用。可是当有不少页面跳转的时候,它会变得很凌乱。我知道 Xcode 的自动补全很差用,可是有时候会给显示不少不须要的方法。即便你不想要在这一页面显示一个我的主页,它仍是会存在于那里。因此试着更进一步去优化它。后端

2. 不要在 ViewControlelr 里写一个扩展,而后在一个地方写大量方法,让咱们在一个单独的协议中实现每个路由,而后使用 Swift 的一个很是好的特性 —— 协议扩展。bash

protocol ProfileRoute {
    func openProfile(for user: User)
}

extension ProfileRoute where Self: UIViewController {
    func openProfile(for user: User) {
        let profileViewController = ProfileViewController(user: user)
        present(profileViewController, animated: true, completion: nil)
    }
}

final class FriendsViewController: UIViewController, ProfileRoute {}
复制代码

如今这个方法就比较灵活了 —— 咱们能够扩展一个控制器,仅添加那些所须要的路由(避免写大量的方法),只是添加一个路由到控制器的继承体系里。 🎉架构

3. 可是,理所固然地这里还有一些改进方式:app

  • 若是咱们想要从全部地方跳转到我的主页,除了一个地方之外(这很罕见,但有可能)呢?
  • 或者更严重的状况 —— 若是我改变了跳转的进入方式,那么我也应该改变跳转页消失的方式( present / dismiss )。

咱们如今没有机会去配置它,因此如今是时候使用少许的代码去实现一个抽象跳转 —— ModalTransitionPushTransition

protocol Transition: class {
    weak var viewController: UIViewController? { get set }

    func open(_ viewController: UIViewController)
    func close(_ viewController: UIViewController)
}
复制代码

为了排版简化,下面我少写了一些 ModalTransition 的实现逻辑代码。Github 上有完整能用的版本。

class ModalTransition: NSObject {
    var animator: Animator?
    weak var viewController: UIViewController?

    init(animator: Animator? = nil) {
        self.animator = animator
    }
}

extension ModalTransition: Transition {}
extension ModalTransition: UIViewControllerTransitioningDelegate {}
复制代码

下面一样减小了部分 PushTransition 的代码逻辑:

class PushTransition: NSObject {
    var animator: Animator?
    weak var viewController: UIViewController?

    init(animator: Animator? = nil) {
        self.animator = animator
    }
}

extension PushTransition: Transition {}
extension PushTransition: UINavigationControllerDelegate {}
复制代码

你必定注意到了 Animator 这个对象,它是一个简单的用于自定义跳转的协议:

protocol Animator: UIViewControllerAnimatedTransitioning {
    var isPresenting: Bool { get set }
}
复制代码

正如我以前所说到的臃肿的 view controller,如今让咱们添加一个包含整个路由逻辑的对象,而后让他做为 controller 的一个属性。这就是咱们所实现的路由 —— 一个将来能够被全部路由继承的基类。 🎉

protocol Closable: class {
    func close()
}

protocol RouterProtocol: class {
    associatedtype V: UIViewController
    weak var viewController: V? { get }
    
    func open(_ viewController: UIViewController, transition: Transition)
}

class Router<U>: RouterProtocol, Closable where U: UIViewController {
    typealias V = U
    
    weak var viewController: V?
    var openTransition: Transition?

    func open(_ viewController: UIViewController, transition: Transition) {
        transition.viewController = self.viewController
        transition.open(viewController)
    }

    func close() {
        guard let openTransition = openTransition else {
            assertionFailure("You should specify an open transition in order to close a module.")
            return
        }
        guard let viewController = viewController else {
            assertionFailure("Nothing to close.")
            return
        }
        openTransition.close(viewController)
    }
}
复制代码

请稍微花点时间去理解上面这些代码,这个类包含两个用于页面的打开和关闭的方法、一个 view controller 的引用和一个 openTransition 对象来让咱们知道如何关闭这个模块。

如今让咱们使用这个新的类来更新咱们的 ProfileRoute

protocol ProfileRoute {
    var profileTransition: Transition { get }
    func openProfile(for user: User)
}

extension ProfileRoute where Self: RouterProtocol {

    var profileTransition: Transition {
        return ModalTransition()
    }

    func openProfile(for user: User) {
        let router = ProfileRouter()
        let profileViewController = ProfileViewController(router: router)
        router.viewController = profileViewController

        let transition = profileTransition // 这是一个已经计算过的属性,为了获取一个实例,我把它存为一个变量
        router.openTransition = transition
        open(profileViewController, transition: transition)
    }
}
复制代码

你能够看到默认的界面的跳转是模态的,在 openProfile 方法中咱们生成一个新的模块,而后打开它(固然若是使用建造者模式或者工厂模式来生成会更好)。同时注意一个变量 transition,为了拥有一个实例,profileTransition 会被保存到这个变量里。

下一步是更新 Friends 模块:

final class FriendsRouter: Router<FriendsViewController>, FriendsRouter.Routes  {
    typealias Routes = ProfileRoute & /* other routes */ 
}

final class FriendsViewController: UIViewController {

    private let router: FriendsRouter.Routes

    init(router: FriendsRouter.Routes) {
        self.router = router
        super.init(nibName: nil, bundle: nil)
    }
  
    func userButtonPressed() {
        router.openProfile(for: /* some user */)
    }
}
复制代码

咱们已经建立了 FriendsRouter ,而且经过 typealias 添加了所须要的路由。这正是魔术发生的地方!咱们使用协议组成(&)去添加更多路由和协议扩展,以此来使用一个默认的路由实现。😎

这篇文章的最后一步是简单友好的实现关闭跳转。若是你从新调用 ProfileRouter,那边咱们实现已经配置好了 openTransition,那么如今就能够利用它。

我建立了一个 Profile 模块,它只有一个路由 —— 关闭,并且当一个用户点击了关闭按钮,咱们使用同样的跳转方式去关闭这个模块。

final class ProfileRouter: Router<ProfileViewController> {
    typealias Routes = Closable
}

final class ProfileViewController: UIViewController {

    private let router: ProfileRouter.Routes

    init(router: ProfileRouter.Routes) {
        self.router = router
        super.init(nibName: nil, bundle: nil)
    }

    func closeButtonPressed() {
        router.close()
    }
}
复制代码

若是须要改变跳转模式,只须要在 ProfileRoute 的协议扩展里去修改,这些代码能够继续运行,不须要改。是否是很好?


结论

最后我想说这个路由方式能够简单地适配 MVCVIPERMVVM 架构,即便你使用 Coordinators,它们能够一块儿运行。我正在尽力去改进这个方案,并且我也很乐意听取你的建议!

对这个方案感兴趣的人,我准备了一个例子,里面包含了少数模块,在它们之间有不一样的跳转方式,来让你更深刻地理解它。去下载和玩一下!


感谢阅读!若是你喜欢上面文章 —— 不要客气,加入咱们的 telegram channel

这是编译ITC过程当中的我。

Rosberry 的粗野iOS工程师。Reactive、开源爱好者和循环引用检测家。

感谢 Anton KovalevRosberry


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索