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
最後に
これから開発中に便利になる機能をどんどん加えていく予定です。
デバッグ画面いかがでしたでしょうか。良ければみなさんも導入をご検討ください。