- 原文地址:A Flexible Routing Approach in an iOS App
- 原文做者:Nikita Ermolenko
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:YinTokey
- 校對者:ellcyyang, 94haox
「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
咱們如今沒有機會去配置它,因此如今是時候使用少許的代碼去實現一個抽象跳轉 —— ModalTransition 和 PushTransition:
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 的協議擴展裏去修改,這些代碼能夠繼續運行,不須要改。是否是很好?
最後我想說這個路由方式能夠簡單地適配 MVC,VIPER,MVVM 架構,即便你使用 Coordinators,它們能夠一塊兒運行。我正在盡力去改進這個方案,並且我也很樂意聽取你的建議!
對這個方案感興趣的人,我準備了一個例子,裏面包含了少數模塊,在它們之間有不一樣的跳轉方式,來讓你更深刻地理解它。去下載和玩一下!
感謝閱讀!若是你喜歡上面文章 —— 不要客氣,加入咱們的 telegram channel!
這是編譯ITC過程當中的我。
在 Rosberry 的粗野iOS工程師。Reactive、開源愛好者和循環引用檢測家。
感謝 Anton Kovalev 和 Rosberry。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。