前言
最近公司給了一個新項目,說是要在過年前完成(又是一個不可能的任務...)
說是要從另外一份搬過來,皮換一換就好(更別說公司的 UI 設計的圖有多奇耙,我都不敢承認我是 app rd 了...)
但是,事情從來都不是這麼簡單...
看了看原本的 source code 後,雖然說換皮而已,但是基本上我覺得就是跟重寫差不多
一如往常的 MVC 架構,UI、business logic、API 等等,全部都在 ViewController 裡面啊!
說是要換皮,還真不知道要從哪邊下手。
忽然間我想起 Router
這個東西,可以在幾乎不動原本架構下將頁面跳轉這件事統一到某個地方處理,也比較符合 單一職責原則 Single Responsibility Principle (SRP)
(雖然很想全部重構...)
正文開始
Router
主要的思路會是將頁面跳轉這件事統一到一個地方處理,例如說:Router這個物件身上,
也因為他幾乎是跨頁面存在,所以我這邊會將他設計成 Singleton 物件。
也會在一開始設定一個新的 root view controller (navigation controller) ,去貫穿整個App的走向。
final class Router:NSObject {
static let instance = Router()
override init() {
super.init()
}
private var rootController: UINavigationController?
func setRootViewController(rootController:UINavigationController) {
self.rootController = rootController
}
}
一個簡單的 Router 雛形就出來了。
接下來我會加入一個 enum,這個 enum 裡面有包含所有的 ViewController 的 case,另外會有一個 getViewController 的 function 去將這個 ViewController 的實體回傳。
enum RouterView : Equatable {
case home
case login
case register
case member
case updatePwd
.
.
.
}
private func getViewController(_ view:RouterView) -> UIViewController? {
var controller:UIViewController?
switch view {
case .home:
let storyboard = UIStoryboard(name: "Home", bundle: nil)
let vc = storyboard.instantiateViewController(identifier: "HomeViewController") as! HomeViewController
controller = vc
...
}
return controller
}
通常頁面跳轉會有幾個方式:PUSH & PRESENT
,所以我們就要在 Router 裡面實作如何跳轉頁面的功能。
func show(_ view:RouterView) {
guard let navi = rootController else {
fatalError("call setRootViewController(rootController:) first")
}
guard let controller:UIViewController = getViewController(view) else {
return
}
DispatchQueue.main.async {
controller.hidesBottomBarWhenPushed = true
navi.setNavigationBarHidden(false, animated: true)
navi.show(controller, sender: nil)
}
}
func present(_ view:RouterView) {
guard let navi = rootController else {
fatalError("call setRootViewController(rootController:) first")
}
guard let controller:UIViewController = getViewController(view) else {
return
}
presentView(navi, controller: controller)
}
到這邊為止就把一個基本的 Router 的架構建好,以後如果要新增轉跳的頁面時,就到這個地方加上新的 enum 跟實體的 ViewController。
而且原本的 ViewController 也只要一句話就完成了,是不是超棒der ~
Rounter.instance.show(.home)
或是
Router.instance.present(.updatePwd)
RouterCheck
到這邊,你搞不好會問,這個 Router 只做這樣的功能,感覺沒有很吸引其他工程師做這些改變。
如果是我,我也覺得誘因不大。
但是,我覺得最最重要的是接下來要講的這個東西。
每個 app rd 一定會碰到一個流程就是:當使用者點選某幾個頁面時,要檢查是不是登入狀態,如果沒有就要跳出登入的畫面做登入。
那原本的作法就會是在每個 ViewController 要去轉跳前檢查是不是登入狀態。
那如果有15個地方要檢查就要寫15次,如果有200個地方就要寫200次... (我的人生很寶貴,不想浪費時間在這上面)
而且如果檢查的邏輯有改變呢 ???
所以在這個 Router 裡面,我多加了一個 Check 的 function,把檢查的邏輯統一到這裡面,
以後邏輯有改也只要到這裡面改就可以囉~~
class RouterCheck:NSObject {
lazy var controllers:[Router.RouterView] = {
return [.member, .updatePwd]
}()
override init() {
super.init()
}
func isNeedsLogin(accountStatus:Bool, toView:Router.RouterView) -> Bool {
if controllers.filter({ $0 == toView }).count > 0 && accountStatus != true {
return true
} else {
return false
}
}
}
在 getViewcontroller() 裡面加入這個檢查
if checker.isNeedsLogin(accountStatus: ... , toView: view) {
return nil
}
到這邊就完成了 Router 的實作以及不破壞現有架構進行的重構。
我個人覺得 Router 的好處就是跳轉頁面跟跳轉頁面檢查可以統一在一個地方處理就好,不用撒的到處都是囉。
完整的 code 請看下方。
希望這篇對大家有幫助 ^^
final class Router:NSObject {
enum RouterView : Equatable {
case home
case login
case register
case member
case updatePwd
.
.
.
}
static let instance = Router()
override init() {
super.init()
}
private var rootController: UINavigationController?
func setRootViewController(rootController:UINavigationController) {
self.rootController = rootController
}
func show(_ view:RouterView) {
guard let navi = rootController else {
fatalError("call setRootViewController(rootController:) first")
}
guard let controller:UIViewController = getViewController(view) else {
let login = LoginCheckViewController()
presentView(navi, controller: login)
return
}
DispatchQueue.main.async {
controller.hidesBottomBarWhenPushed = true
navi.setNavigationBarHidden(false, animated: true)
navi.show(controller, sender: nil)
}
}
func present(_ view:RouterView) {
guard let navi = rootController else {
fatalError("call setRootViewController(rootController:) first")
}
guard let controller:UIViewController = getViewController(view) else {
let login = LoginCheckViewController()
presentView(navi, controller: login)
return
}
presentView(navi, controller: controller)
}
private func presentView(_ navi:UINavigationController, controller:UIViewController) {
DispatchQueue.main.async {
controller.hidesBottomBarWhenPushed = true
navi.present(controller, animated: true, completion: nil)
}
}
private func getViewController(_ view:RouterView) -> UIViewController? {
var controller:UIViewController?
if checker.isNeedsLogin(accountStatus: ... , toView: view) {
return nil
}
switch view {
case .home:
let storyboard = UIStoryboard(name: "Home", bundle: nil)
let vc = storyboard.instantiateViewController(identifier: "HomeViewController") as! HomeViewController
controller = vc
...
}
return controller
}
}
class RouterCheck:NSObject {
lazy var controllers:[Router.RouterView] = {
return [.member, .updatePwd]
}()
override init() {
super.init()
}
func isNeedsLogin(accountStatus:Bool, toView:Router.RouterView) -> Bool {
if controllers.filter({ $0 == toView }).count > 0 && accountStatus != true {
return true
}else{
return false
}
}
}