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


