- 原文地址: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 的生活,我希望你會覺得這篇文章非常實用。
在這篇文章裡,我將會介紹一種在 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 )。
我們現在沒有機會去配置它,所以現在是時候使用少量的程式碼去實現一個抽象跳轉 —— 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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。