【FedEx】iOS デバッグ時にAPIの接続先を変更

ZAICOでは、Android・iOS・Rubyエンジニアを絶賛募集中です! 詳しくは、採用ページをご覧ください。

好きな場所で働こう

はじめに

こんにちは。ZAICO開発チームです。

先日弊社で行われた社内ハッカソンにて、iOSアプリでAPIの接続先を変更する機能を作ったのでご紹介します。

背景

アプリの開発中には別の環境に接続したいことが多々あります。

ZAICOでは本番環境の他にステージング環境や開発環境がありますが、テスト配信されたアプリの動作確認中に別の環境に接続したい場合、改めてテスト配信をする必要があります。

もしアプリ起動中に接続先を変更することができれば、この作業自体が不要になるのでは。と思い開発することにしました。

できたもの

設定画面 最下部に追加したデバッグ画面への遷移ボタンを押下 「API接続先変更」を押下 API接続先変更ダイアログから接続先を押下

これで次回起動時に接続先が切り替わります!

リリースされるアプリではAppleの審査に引っかかるリスクがあるため、デバッグ時のみ表示されるデバッグ画面で提供することにしました。

実装について

先日まで環境ごとに独自のカスタムフラグを設定した Build Configuration を使い、コード上では環境ごとのURLを直接返すことで、 Build Configuration を切り替えるだけで接続先変更を実現していました。

var baseURL: URL {
    #if RELEASE
    // 本番環境のBASEURLを返す
    #elseif STAGING
    // ステージング環境のBASEURLを返す
    #elseif DEBUG
    // 開発環境のBASEURLを返す
    #endif
}

しかし、カスタムフラグによる接続先の決定では、アプリの起動中に接続先を変更することができませんので、UserDefaultsを使って、保存した環境のURLを返すようにしてみました。

import Foundation

@objc
enum EnvironmentType: Int, CaseIterable {
    case production
    case staging
    case development
}

// MARK: - API
extension EnvironmentType {
    var zaicoAPIBaseURL: String {
        switch self {
        case .production:
            return "https://production"
        case .staging:
            return "https://staging"
        case .development:
            return "http://development"
        }
    }
}
import Foundation

/*
 * アプリ内のUserDefaultsを管理するクラス
 */
@objc
protocol UserDefaultsServiceProtocol: AnyObject {
    var environmentType: EnvironmentType { get set }
}

@objcMembers
final class UserDefaultsService: NSObject, UserDefaultsServiceProtocol {

    // MARK: - Key
    typealias Key = UserDefaultsKey

    // MARK: - Properties
    let defaults: UserDefaults

    // MARK: - Initialize
    init(defaults: UserDefaults = UserDefaults.standard) {
        self.defaults = defaults
    }

    // MARK: - Values
    var environmentType: EnvironmentType {
        get {
            let defaultEnv: EnvironmentType
            #if RELEASE
            return .production
            #elseif DEBUG
            defaultEnv = .development
            #elseif STAGING
            defaultEnv = .staging
            #endif
            return loadObject(forKey: .environmentType) ?? defaultEnv
        }
        set { setObject(newValue, forKey: .environmentType) }
    }
}

// MARK: - Private Methods
private extension UserDefaultsService {
    func loadObject<T: RawRepresentable>(forKey key: Key) -> T? {
        guard let object = defaults.object(forKey: key.keyName) as? T.RawValue else { return nil }
        return T(rawValue: object)
    }

    func setObject(_ object: Any?, forKey key: Key) {
        defaults.set(object, forKey: key.keyName)
        defaults.synchronize()
    }

    func setObject<T: RawRepresentable>(_ object: T, forKey key: Key) {
        setObject(object.rawValue, forKey: key)
    }
}

@objc
enum UserDefaultsKey: Int {
    case environmentType

    var keyName: String {
        switch self {
        case .environmentType: return "environment_type"
        }
    }
}
var baseURL: URL {
    userDefaultsService.environmentType.zaicoAPIBaseURL
}

最後にデバッグ画面の実装です。

import UIKit
import RxSwift
import RxCocoa

#if DEBUG
@objcMembers
final class DebugViewController: UIViewController {

    // MARK: - Properties
    @IBOutlet private var tableView: UITableView!

    private let disposeBag = DisposeBag()

    // MARK: - View Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "デバッグ"

        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

        bind()
    }
}

private extension DebugViewController {
    func bind() {
        Driver.just(Menu.allCases)
            .drive(tableView.rx.items(cellIdentifier: "Cell")) { row, menu, cell in
                cell.textLabel?.text = menu.title
            }
            .disposed(by: disposeBag)
        tableView.rx.modelSelected(Menu.self).asDriver()
            .drive(with: self, onNext: { owner, menu in
                switch menu {
                case .environment:
                    let message = "接続先を選択してください\n選択後アプリは強制的に終了します。"
                                        let alertController = UIAlertController(title: "API接続先変更", message: message, preferredStyle: .alert)
                                        let onAction: (EnvironmentType) -> Void = { type in
                                                 userDefaultsService.environmentType = type
                                                // 環境切り替え時にログアウトする
                        AccountService.current.logout()
                        // アプリを落とす
                        exit(0)
                    }
                    let alertActions: [UIAlertAction] = [
                        .init(title: "開発", style: .default) { _ in onAction(.development) },
                        .init(title: "ステージング", style: .default) { _ in onAction(.staging) },
                        .init(title: "本番", style: .default) { _ in onAction(.production) }
                    ]
                    alertActions.forEach(alertController.addAction)
                    alertController.addAction(UIAlertAction(title: "キャンセル", style: .cancel))
                    owner.present(alertController, animated: true)
                }
            })
            .disposed(by: disposeBag)
        tableView.rx.itemSelected.asDriver()
            .drive(with: tableView, onNext: { tableView, indexPath in
                tableView.deselectRow(at: indexPath, animated: true)
            })
            .disposed(by: disposeBag)
    }
}

private extension DebugViewController {
    enum Menu: CaseIterable {
        case environment

        var title: String {
            switch self {
            case .environment:
                return "API 接続先変更"
            }
        }
    }
}
#endif

最後に

これから開発中に便利になる機能をどんどん加えていく予定です。

デバッグ画面いかがでしたでしょうか。良ければみなさんも導入をご検討ください。

 

ZAICOでは、新しいテクノロジーの力でモノの状態・流れを把握する仕組みに一緒に取り組む仲間を募集しております。
詳しくは、採用ページをご覧ください。

好きな場所で働こう