博客 / 詳情

返回

Swift - 界面路由

隨着業務增加,項目中的模塊越來越多,並且這些模塊進行相互的調用,使得它們交纏在一起,增加了維護成本,並且會降低開發效率。此時就需要對整個項目進行模塊劃分,將這些模塊劃分給多個開發人員(組)進行維護,然後在主工程中對這些模塊進行調用。

每個模塊獨立存在,提供接口供其他模塊調用。從而如何有效且解耦的進行模塊間的調用成了重中之重。

那麼如何傳遞參數並且初始化對應模塊的主窗口成為了主要問題。下面會介紹兩種方案來解決這個問題。

方案1

得益於Swift中枚舉可以傳參的特性,我們可以通過枚舉來進行參數傳遞,然後添加方法來映射控制器。

在枚舉中通過參數來確定初始化控制器的數據,外部進行調用時可以很明確的知道需要傳入哪些參數,同時還能傳遞非常規參數(DataUIImage等)

enum Scene {

    case targetA
    case targetB(data: Data)

    func transToViewController() -> UIViewController  {
        switch self {
        case .targetA:
            return ViewControllerA()
        case let .targetB(data):
            return ViewControllerB(data: data)
        }
    }

}

解決了UIViewController映射的問題後,我們接下來解決如何統一跳轉方法。

項目中,基本上都會使用UINavigationController來進行界面的跳轉,主要涉及pushpop操作。此時簡便的做法是擴展UIViewController為其添加統一界面跳轉方法。

extension UIViewController {
    
    func push(to scene: Scene, animated: Bool = true) {
        let scene = scene.transToViewController()
        DispatchQueue.main.async { [weak self] in
            self?.navigationController?.pushViewController(scene, animated: animated)
        }
    }
    
    func pop(toRoot: Bool = false, animated: Bool = true) {
        DispatchQueue.main.async { [weak self] in
            if toRoot {
                self?.navigationController?.popToRootViewController(animated: animated)
            } else {
                self?.navigationController?.popViewController(animated: animated)
            }
        }
    }

最後我們跳轉界面時進行如下調用即可

push(to: .targetA)

push(to: .targetB(data: Data()))

面向協議進行改造

protocol Scene: UIViewController {
    
}

protocol SceneAdpater {
    
    func transToScene() -> Scene
}

protocol Navigable: UIViewController {
    
    func push(to scene: SceneAdpater, animated: Bool)

    func pop(toRoot: Bool, animated: Bool)
}

擴展Navigable為其添加默認的實現。

extension Navigable {
    
    func push(to scene: SceneAdpater, animated: Bool = true) {
        let scene = scene.transToScene()
        DispatchQueue.main.async { [weak self] in
            self?.navigationController?.pushViewController(scene, animated: animated)
        }
    }
    
    func pop(toRoot: Bool = false, animated: Bool = true) {
        DispatchQueue.main.async { [weak self] in
            if toRoot {
                self?.navigationController?.popToRootViewController(animated: animated)
            } else {
                self?.navigationController?.popViewController(animated: animated)
            }
        }
    }
    
}

經過上述的面向協議的改造,我們在使用時的代碼如下:

enum TestScene: SceneAdpater {

    case targetA
    case targetB(data: Data)

    func transToScene() -> Scene {
        switch self {
        case .targetA:
            return ViewControllerA()
        case let .targetB(data):
            return ViewControllerB(data: data)
        }
    }

}

class ViewControllerA: UIViewController, Navigable, Scene {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        push(to: TestScene.targetB(data: Data()))
    }
    
}

class ViewControllerB: UIViewController, Scene {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    init(data: Data) {
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

方案2

初始化UIViewController存在兩種情況:需要傳參、不需要傳參。不需要傳參的比較簡單,直接調用UIKit提供的初始化方法即可。而需要傳參的UIViewController,就涉及到如何確定傳參的類型、傳入哪些參數。此時需要利用協議的關聯類型來確定傳入的參數。

基於上述結論,制定的協議如下:

protocol Scene: UIViewController {
    
}

protocol NeedInputScene: Scene {
    
    associatedtype Input
    
    init(input: Input)
}

protocol NoneInputScene: Scene {
    
}

由此在跳轉方法中需要用到泛型

protocol Navigable: UIViewController {
    
    func push<S: NeedInputScene>(to scene: S.Type, input: S.Input, animated: Bool)
    
    func push<S: NoneInputScene>(to scene: S.Type, animated: Bool)
}

extension Navigable {
    
    func push<S: NeedInputScene>(to scene: S.Type, input: S.Input, animated: Bool = true) {
        let scene = scene.init(input: input)
        DispatchQueue.main.async { [weak self] in
            self?.navigationController?.pushViewController(scene, animated: animated)
        }
    }
    
    func push<S: NoneInputScene>(to scene: S.Type, animated: Bool = true) {
        let scene = scene.init()
        DispatchQueue.main.async { [weak self] in
            self?.navigationController?.pushViewController(scene, animated: animated)
        }
    }
}

使用示例:

class ViewControllerA: UIViewController, Navigable {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        push(to: ViewControllerB.self, input: Data())
        
        push(to: ViewControllerC.self)
    }
    
}

class ViewControllerB: UIViewController, NeedInputScene {
    
    typealias Input = Data

    required init(input: Data) {
        super.init(nibName: nil, bundle: nil)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class ViewControllerC: UIViewController, NoneInputScene {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

方案2相較於方案1最顯著的區別就是不再需要維護映射UIViewController的枚舉類型。

使用枚舉來作為入參,外部在調用時可以很清晰的確定需要傳入參數的意義。而關聯類型則不具備這種優勢,不過這個問題通過使用枚舉作為關聯類型來解決,但是在UIViewController僅需要一個字符串類型時這種做法就顯得有點重。

方案2在模塊需要提供多個入口時,需要暴露出多個控制器的類型,增加了耦合。而方案1則僅需用暴露出枚舉類型即可。

Demo

Demo對方案1進行了演示,也是我目前在項目中使用的方案。

參考連接:

https://github.com/meili/MGJRouter

https://casatwy.com/iOS-Modulization.html

http://blog.cnbang.net/tech/3080/

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.