239
213

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RxSwift? いやClean Swiftっしょ

Last updated at Posted at 2016-06-24

#いつもiOS開発で悩むこと
MVCやMVVMで作ってると、最初開発するときは自由度の高くて楽に実装できるのだが、
ViewControllerやModel層は改修を重ねるごとにどんどん肥大化し、複雑化してしまう。

どんどん肥大化するソースは、情熱がない限り結局誰もリファクタリングしないので、
どんどん負の遺産が増える悪循環になってしまう。

ここ数年ではFlux ArchitectureベースのRxSwiftやReactive Cocoaが流行っているのだが。。。。
flux-overview.png

使ったことはないが聞く感じ単純にKVOでしょって感がいなめない。。。
データの画面更新は遷移等考えなくても楽だからすごく魅力的だが、イベント数が増えれば管理が大変だし、イベントは非同期だから予期せぬ画面更新が起こりそうだしっていうので少しマイナスイメージが多い感じ

なんかいいものないかと色々見てみるとQiitaに面白い記事が。
まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて

お! おお!! javaエンジニアとしてはまさにこれこれ!!
Clean Architecture
もちろんiOSだけじゃなくてAndroidでもこの概念は通用するらしい。

#Clean Architectureってなんだよ
Clean ArchitectureはFlux Architectureと同様にデータの流れが一方向性にし、
曖昧なModelという概念をさらに明確に役割分解し、変更機会が多い、UIや外部ソースを一番外側に置き、Entityを一番内側に置いた考え方である。
これならViewControllerがもつロジックも、例えばUse CaseとViewController の間にPresenterを間に入れてそいつにやらせてしまえばいい。

903dd9be-6941-3a04-2e31-1a80429496c2 (1).jpeg

#SwiftでClean Architectureを実現しよう!!

構成はリンク先を参考に以下のような感じ。

Diagram2.png

結局FluxアーキテクチャはデータとUIがObserverによってdata storeと疎結合になるのが利点だけど、
このままClean Architectureを適応すると疎結合にするには難しいよねって感じた。
せっかくロジックを明確に分解できても疎結合にできないとUnit Testを上手くまわせないじゃんって思う。
Javaみたいに簡単にswiftでもDIできて、且つPresenter(ViewControllerのロジック部分)も簡単にUnitテストができるフレームワークがあれば一番嬉しい。

なんかないかなぁっと思ってると
Clean Swift
http://clean-swift.com/clean-swift-ios-architecture/

おぉ、まさにこれこれ。
何かと言うと、先ほどのViewControllerとPresenterからさらにInteractorの層を切り分けて、疎結合にしてしまい、外部クラス(Configrator)からDelegateを使ったRoutingとインスタンスのDIをやってしまおうというもの。

さっきの図をClean Swiftで書き直すとこんな感じ。
Clean Archtecture.png

#試しに実装してみる
ここではClean SwiftのViewController、Interactor、Presenterのコードをここに表記しようと思う。
ポイントはViewController、Interactor、PresenterにInput, Outputプロトコルを定義します。
このとき、ViewControllerのOutputプロトコルはInteractorのInputプロトコルと、
InteractorのOutputプロトコルはPresenterのInputプロトコルと、
PresenterのOutputプロトコルはViewControllerのInputプロトコルと同じメソッドを定義しておく。

それぞれの3つとも自身のOutputプロトコルを型としてインスタンス変数を定義し
ConfigratorはViewController、Interactor、Presenterのoutput先をそれぞれ指定する。
たとえば、ViewControllerのoutputはInteractorなので、

LoginViewConfigrator.swift
  let interactor = LoginInteractor()
  viewController.output = interactor

みたいなイメージ。
以下がざっくりと書いたソースのイメージ。
これでViewControllerのインスタンス生成がされるときに、ConfigratorによってPresenterとInteractorのDIができる。
テストのときはそれぞれoutputにdummyクラスを突っ込んでやるだけでロジック確認できる!!

LoginViewController.swift
import UIKit

protocol LoginViewControllerInput {
    func didLogined()
}

protocol LoginViewControllerOutput {
    func executeLoginFacebook(viewController: UIViewController)
}

protocol LoginViewControllerTransitionDelegate: class {
    func pushViewController()
    func dismissModalViewController()
}

class LoginViewController: UIViewController {
    
    weak var transitionDelegate:LoginViewControllerTransitionDelegate?
    var output: LoginViewControllerOutput?
    
    //MARK: Object Lifecycle
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        setup()
    }
    
    override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        setup()
    }
    
    convenience init() {
        self.init(nibName: nil, bundle: nil)
    }
    
    //MARK: View Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        self.configureOnLoad()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    //MARK: private method
    internal func setup() {
        LoginViewConfigurator.sharedInstance.configure(self)
    }
    
    internal func configureOnLoad() {
        let facebookLoginButton: UIButton = UIButton()
        facebookLoginButton.frame = CGRectMake(0,0,180,40)
        facebookLoginButton.backgroundColor = UIColor.darkGrayColor()
        facebookLoginButton.center = self.view.center
        facebookLoginButton.setTitle("Facebook Login", forState: UIControlState.Normal)
        facebookLoginButton.addTarget(self, action: #selector(LoginViewController.clickFacebookLoginButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
        self.view.addSubview(facebookLoginButton)
    }

    internal func clickFacebookLoginButton(sender: UIButton) {
        self.output?.executeLoginFacebook(self)
    }
}

//MAKR: LoginViewControllerInput
extension LoginViewController:LoginViewControllerInput {
    func didLogined() {
        self.transitionDelegate?.dismissModalViewController()
    }
}
LoginViewInteractor.swift
import Foundation
import UIKit

protocol LoginInteractorInput {
    func executeLoginFacebook(viewController: UIViewController)
}

protocol LoginInteractorOutput {
    func didFacebookLogined()
    func didGoogleLogined()
}

class LoginInteractor {
    var output: LoginInteractorOutput?
    var loginUseCase: LoginUseCase = LoginUseCase()
    
}

//MARK LoginInteractorInput
extension LoginInteractor: LoginInteractorInput {
    
    func executeLoginFacebook(viewController: UIViewController) {
        self.loginUseCase.output = self
        self.loginUseCase.executeLoginFacebook(viewController)
    }
}

//MARK LoginUseCaseOutput
extension LoginInteractor: LoginUseCaseOutput {
    func didAuthorizedFacebook(){
        self.output?.didFacebookLogined()
    }
        
    func didAuthorizedGoogle(){
        self.output?.didGoogleLogined()
    }
    
    func hasLogined(){
        
    }
    
    func notAuthorizedOrNoAccount(){
        
    }
    
}
LoginViewPresenter.swift
import Foundation

protocol LoginPresenterInput {
    func didFacebookLogined()
    func didGoogleLogined()
}

protocol LoginPresenterOutput {
    func didLogined()
}

class LoginPresenter {

    var output: LoginPresenterOutput?
    
}

//MARK: LoginPresenterInput
extension LoginPresenter:LoginPresenterInput {
    func didFacebookLogined() {
        self.output?.didLogined()
    }
    
    func didGoogleLogined(){
        self.output?.didLogined()
    }
}

LoginViewConfigrator.swift
import Foundation

class LoginViewConfigurator {
    
    //MARK: Object lifecycle
    static var sharedInstance = LoginViewConfigurator()
    
    //MARK: public method
    func configure(viewController: LoginViewController) {
        
        let presenter: LoginPresenter = LoginPresenter()
        presenter.output = viewController

        let interactor: LoginInteractor = LoginInteractor()
        interactor.output = presenter
        
        viewController.output = interactor
        
    }
}

extension LoginViewController: LoginPresenterOutput {
    
}

extension LoginPresenter: LoginInteractorOutput {
    
}

extension LoginInteractor: LoginViewControllerOutput {
    
}

総評

メリット

  • データの方向性が一方向になりわかりやすい。
  • ViewControllerから完全にロジックを分離でき、疎結合にできるため単体テストが実施しやすい。
  • DIがやりやすいため、単体テストが書きやすい。

デメリット

  • 冗長的にするのでコード量が多くなる。

Javaエンジニアにはしっくりくる構成なのでうれしい。
上記のソースには記載していないが、UseCaseもRepositoryもConfiguratorクラスでDIしてしまえばいい。

239
213
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
239
213

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?