[譯] iOS App 上一種靈活的路由方式

YinTokey發表於2018-03-19

“Trollstigen”

Rosberry 中我們已經放棄使用除了 Launch Screen 以外的所有 storyboard,當然,所有佈局和跳轉邏輯都在程式碼裡進行配置。如果想要進一步瞭解,請參考我們團隊的這篇文章 沒有 Interface Builder 的生活,我希望你會覺得這篇文章非常實用。

在這篇文章裡,我將會介紹一種在 View Controller 之間的新的路由方式。我們將帶著問題開始,然後一步一步地走向最終結論。享受閱讀吧!


深入挖掘這個問題

讓我們使用一個具體的例子來理解這個問題。例如我們準備做一個 App,它包含了個人主頁、好友列表、聊天視窗等組成部分。很顯然,我們可以注意到在很多 Controller 裡都需要通過頁面跳轉去顯示使用者的個主頁,如果這個邏輯只實現一次,並且能複用的話,那就非常好了。我們記得 DRY! 我們無法使用一些 storyboard 來實現它,你可以想象一下,它在 storyboard 裡面看起像什麼 —— weeeeb 頁面. ?

現在我們使用的是 MVVM + Router 的架構,由 ViewModel 告訴 Router 需要跳轉到一個其他的模組,然後 router 去執行。在我們的例子中,為了避免 view controller(或者View model)臃腫,Router 僅僅攜帶了所有的跳轉邏輯。如果你一開始不是很明白,不用擔心!我將會用一種比較淺顯的方式來解釋這種解決方案,所以它也會很容易地被應用到簡單的 MVC 中去。


解決方案

1. 一開始,新增一個擴充到 ViewController 看起來像是一個毫無異議的解決方案:

extension UIViewController {
    func openProfile(for user: User) {
        let profileViewController = ProfileViewController(user: user)
        present(profileViewController, animated: true, completion: nil)
    }
}
複製程式碼

這就是我們想要的 —— 一次編寫,多次使用。但是當有很多頁面跳轉的時候,它會變得很凌亂。我知道 Xcode 的自動補全不好用,但是有時候會給顯示很多不需要的方法。即使你不想要在這一頁面顯示一個個人主頁,它還是會存在於那裡。所以試著更進一步去優化它。

2. 不要在 ViewControlelr 裡寫一個擴充套件,然後在一個地方寫大量方法,讓我們在一個單獨的協議中實現每一個路由,然後使用 Swift 的一個非常好的特性 —— 協議擴充套件。

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. 但是,理所當然地這裡還有一些改進方式:

  • 如果我們想要從所有地方跳轉到個人主頁,除了一個地方以外(這很罕見,但有可能)呢?
  • 或者更嚴重的情況 —— 如果我改變了跳轉的進入方式,那麼我也應該改變跳轉頁消失的方式( 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

[譯] iOS App 上一種靈活的路由方式

這是編譯ITC過程中的我。

Rosberry 的粗野iOS工程師。Reactive、開源愛好者和迴圈引用檢測家。

感謝 Anton KovalevRosberry


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章