42
44

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.

[iOS] 条件分岐を減らして、処理をシンプルにする

Last updated at Posted at 2016-11-09

はじめに

①ViewControllerから処理の一部を切り出したい  → ViewControllerの肥大化
②ViewControllerで状態(フラグ)を意識したくない → フラグ管理がカオス
③ユニットテストしやすくしたい → テスト容易性

上記の課題を解消する一つの方法をご紹介します。

その答えは、「条件分岐を減らす」です。 

環境

XCode : 8.0
Swift : 3.0

今回ご説明する例

今回は、下記のようなシンプルな例で説明します。

①ログオフ状態の場合、ログインボタンを表示する(フォントカラーは青色)
②ログイン状態の場合、ログアウトボタンを表示する(フォントカラーは赤色)

1. Badケース

まず最初にBadケースをご紹介します。

ViewController内でログイン状態を意識し、
ログインとログオフで処理や表示に違いがある度に、
条件分岐が増えてしまいます。
ユニットテストの実施も難しいですね。

※今回は、UI周り(storyboard関連)は割愛させていただきます。

ViewController.swift
import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var loginButton: UIButton!
    private var isLogined = false
    
    //MARK:- LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupLoginButton()
    }
    
    //MARK:- IBAction
    @IBAction func didTapLogin(_ sender: UIButton) {
        
        isLogined = loadLoginState()
        
        if isLogined {
            setLogoutState()
        } else {
            setLoginState()
        }
        setupLoginButton()
    }
    
    //MAKR:- Private
    fileprivate func setupLoginButton() {
        
        isLogined = loadLoginState()

        if isLogined {
            setLoginButtonView()
        } else {
            setLogoutButtonView()
        }
    }
    
    fileprivate func setLoginState() {
        UserDefaults.standard.set(true, forKey: "isLogined")
        UserDefaults.standard.synchronize()
    }
    
    fileprivate func setLogoutState() {
        UserDefaults.standard.set(false, forKey: "isLogined")
        UserDefaults.standard.synchronize()
    }
    
    fileprivate func loadLoginState() -> Bool{
        return UserDefaults.standard.bool(forKey: "isLogined")
    }
    
    fileprivate func setLogoutButtonView() {
        loginButton.setTitle("ログアウト", for: .normal)
        loginButton.setTitleColor(UIColor.red, for: .normal)
    }
    
    fileprivate func setLoginButtonView() {
        loginButton.setTitle("ログイン", for: .normal)
        loginButton.setTitleColor(UIColor.blue, for: .normal)
    }
}

2. Goodケース

今回の課題を解決するために、
GoFのStrategy パターン or Stateパターンを利用して、条件分岐を減らします。
それでは、やってみます。

2.1. ログイン状態を管理するクラス

ログイン状態は、UserDefaultsにて管理します。

必要な機能は、下記の3点とします。
①ログイン中かどうか?
②ログインする
③ログアウトする

2.1.1. プロトコル

LoginStateRepository.swift
import UIKit

protocol LoginStateProtocol {
    func isLogined() -> Bool
    func setLoginState()
    func setLogoutState()
}

2.1.2. UserDefaultsのキー名

LoginStateRepository.swift
struct LoginStateUDConst {
    static let isLogined = "isLogined"
}

2.1.3. 実装クラス

LoginStateRepository.swift
final class LoginStateRepository: LoginStateProtocol {
    
    func isLogined() -> Bool {
        return UserDefaults.standard.bool(forKey: LoginStateUDConst.isLogined)
    }
    
    func setLoginState() {
        UserDefaults.standard.set(true, forKey: LoginStateUDConst.isLogined)
        UserDefaults.standard.synchronize()
    }
    
    func setLogoutState() {
        UserDefaults.standard.set(false, forKey: LoginStateUDConst.isLogined)
        UserDefaults.standard.synchronize()        
    }
}

2.2. ログイン・ログアウトの実行、ログインボタンの表示切り替えを行うクラス

2.2.1. プロトコル

必要な機能は、下記の3点とします。
①ログインまたは、ログアウトする
②ログインボタンのタイトルを取得する
③ログインボタンのフォントカラーを取得する

LoginUsecase.swift
import UIKit

protocol LoginUsecaseProtocol {
    func loginOrLogout()
    func loginButtonTitle() -> String
    func loginButtonTitleColor() -> UIColor
}

2.2.2. ログインボタンのタイトル

LoginUsecase.swift
struct LoginTitleString {
    static let login = "ログイン"
    static let logout = "ログアウト"
}

2.2.3. ログイン用の実装クラス

ログイン時の処理を記載します

Login.swift
import UIKit

final class Login: LoginUsecaseProtocol {
    
    func loginOrLogout() {
        LoginStateRepository().setLoginState()
    }
    
    func loginButtonTitle() -> String {
        return LoginTitleString.login
    }
    
    func loginButtonTitleColor() -> UIColor {
        return UIColor.blue
    }
}

2.2.4. ログアウト用の実装クラス

ログアウト時の処理を記載します

Logout.swift
import UIKit

final class Logout: LoginUsecaseProtocol {
    
    func loginOrLogout() {
        LoginStateRepository().setLogoutState()
    }
    
    func loginButtonTitle() -> String {
        return LoginTitleString.logout
    }
    
    func loginButtonTitleColor() -> UIColor {
        return UIColor.red
    }
}

2.2.5. ViewControllerからのディスパッチするクラス

ここで初めて、分岐処理が登場します。

ログイン中は、ログアウトインスタンスを、
ログアウト中は、ログインインスタンスを返却します。

それ以外の処理は、ViewControllerの指示の元に、該当インスタンスへ処理を委譲します。(ポリモーフィズム)

LoginUsecase.swift
final class LoginUsecase {
    
    fileprivate var loginState: LoginUsecaseProtocol?
    
    init() {
        loginState = LoginStateRepository().isLogined() ? Logout() : Login()
    }
    
    func loginOrLogout() {
        loginState?.loginOrLogout()
    }
    
    func loginButtonTitle() -> String {
        return loginState?.loginButtonTitle() ?? ""
    }
    
    func loginButtonTitleColor() -> UIColor {
        return loginState?.loginButtonTitleColor() ?? UIColor.black
    }
}

2.3. ViewControllerクラス

初期表示とボタン押下時の処理を実装します
基本的には、ViewController内では、
ログインの状態を管理せずに、ユースケースに処理を委譲しています。

※今回は、UI周り(storyboard関連)は割愛させていただきます。

ViewController.swift
import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var loginButton: UIButton!
    
    //MARK:- LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupLoginButton()
    }
    
    //MARK:- IBAction
    @IBAction func didTapLogin(_ sender: UIButton) {
        login()
        setupLoginButton()
    }
    
    //MAKR:- Private
    fileprivate func setupLoginButton() {
        let loginUsecase = LoginUsecase()
        loginButton.setTitle(loginUsecase.loginButtonTitle(), for: .normal)
        loginButton.setTitleColor(loginUsecase.loginButtonTitleColor(), for: .normal)
    }
    
    fileprivate func login() {
        LoginUsecase().loginOrLogout()
    }
}

まとめ

・ViewControllerから処理の一部を切り出したい
・ViewControllerで状態を意識したくない
・テストしやすくしたい

上記の課題は、一応クリアできたのではないでしょうか。
もっと、良い方法があれば、ご教授頂ければ幸いです。

おい、ユニットテストはどうなったという方のために、おまけ

おまけ

LoginUsecaseTests.swift
import XCTest
@testable import Test

class LoginUsecaseTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
        
        //デフォルトは、false(ログアウト状態)
        UserDefaults.standard.set(false, forKey: LoginStateUDConst.isLogined)
        UserDefaults.standard.synchronize()
    }
    
    override func tearDown() {
        super.tearDown()
        UserDefaults.standard.removeObject(forKey: LoginStateUDConst.isLogined)
    }
    
    func testConfig() {
        
        //UDのキー名は正しいか?
        XCTAssertEqual(LoginStateUDConst.isLogined, "isLogined")
    }
    
    func testInit() {
        
        //初期表示は正しいか?
        XCTAssertEqual(LoginUsecase().loginButtonTitle(), "ログイン")
        XCTAssertEqual(LoginUsecase().loginButtonTitleColor(), UIColor.blue)
    }
    
    func testLogin() {
        
        //ログイン後の表示は正しいか?
        LoginUsecase().loginOrLogout()
        XCTAssertTrue(UserDefaults.standard.bool(forKey: LoginStateUDConst.isLogined))
        
        XCTAssertEqual(LoginUsecase().loginButtonTitle(), "ログアウト")
        XCTAssertEqual(LoginUsecase().loginButtonTitleColor(), UIColor.red)
    }
    
    func testLogout() {
        
        //ログアウト後の表示は正しいか?
        LoginUsecase().loginOrLogout()
        XCTAssertTrue(UserDefaults.standard.bool(forKey: LoginStateUDConst.isLogined))
        
        LoginUsecase().loginOrLogout()
        XCTAssertFalse(UserDefaults.standard.bool(forKey: LoginStateUDConst.isLogined))
        
        XCTAssertEqual(LoginUsecase().loginButtonTitle(), "ログイン")
        XCTAssertEqual(LoginUsecase().loginButtonTitleColor(), UIColor.blue)
    }
}
42
44
0

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
42
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?