[譯] 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索