Swiftで環境(Stg, Prd等)を管理するFrameworkを作成する

この記事では環境を管理するEnvironment frameworkの作成とEnvironment classを Frameworkで管理した際に、環境のタイプ(.stg, .prodなど)を実行中に変更できてしまう問題への対処を解説する。


### Environment Framework作成の必要性

SwiftプロジェクトでModel, View, Networkingなど**コンポーネントをFramework化**することで強固な構造化を行ったりビルド時間を短縮するなど様々な穏健を受けられる。

さてこのような構造化を行った場合なら、環境を管理するクラスはどのように実装するのが良いだろうか？全くコンポーネントのFramework化を行わない場合はプロジェクト名のTargetへEnvironmentクラスを作成して、if #debugなどの記述でEnvironmentクラスへ
差し込む環境を決め、その環境に応じた設定を取り出せるようにすれば良いだろう。

しかしFrameworkを複数組み込んだプロジェクトの場合は、プロジェクト名のTarget
へEnvironmentクラスを作成してしまうと、他のFrameworkからEnvironmentクラス
を参照できない、もしくは参照できる実装にしてもFramework間で相互参照が起こるなどアーキテクチャ的な問題が発生する。

この問題への一つの対処として、環境を一括で管理する**Environment Frameworkを作成**して、他の全てのFrameworkからEnvironment Frameworkへ一方通行的に参照させる方法が考えられる。


### Environment Framework作成時の問題

しかしこの方法だと、Environment Frameworkからプロジェクト名のTargetのconfigで定義したflag([Active Compilation Conditions 等](https://dev.classmethod.jp/smartphone/iphone/xcode-8-active-compilation-conditions/))を参照できない。この問題のため、**Environmentへの実際の環境のタイプ(.stg, .prod等)の差し込みはプロジェクト名のTargetから行わなければならない。**

ただしこの方法ではEnvironmentのタイプの変更がアプリの起動後にいつでも行えてしまい**危険**であるという問題がある。

この記事ではEnvironment Frameworkの作成および上記の問題への対処を解説する。


## 1. Environment Frameworkの概要
環境を管理するEnvironment Frameworkは独立したFrameworkとして作成し、排他的な環境の差し込み、各環境に対応した設定を外部からより出せるような機能を持たせる。

環境とは例として以下のようなものを想定

- Staging
- Production
- Stub
- Test

もちろんこれら以外の環境を定義しても良い。

設定の代表的な想定は以下

- API URL
- App Name
- 広告SDKのID(Stg, Prodで変えたりするため)

設定はプロジェクトにより多数あると思うので各々必要なものを実装。

最終的なEnvironment Frameworkへ以下のファイルと追加する

- Environment.swift
- EnvironmentType.swift
- SettingsForEnvironment.swift


## 2. Environment Frameworkの追加
プロジェクトへ"EnvironmentAndSettings"という命名のFrameworkを追加する。
通常のFrameworkを追加する処理と同一なので、詳しい人は読み飛ばして良い。
Xcodeのバージョンで操作が変わる可能性がある。

Xcode上でFile -> New -> Target -> Cocoa Touch Framework(Xcode11では命名が変更される)の操作を行い"EnvironmentAndSettings"という命名のFrameworkを追加。

このFrameworkを参照したいFrameworkの"Linked Frameworks and Libraries"へ"EnvironmentAndSettings"を追加。

## 3. EnvironmentTypeの追加
環境を定義するenum EnvironmentTypeを追加する。
以下のcaseは例なので必要に応じて変更のこと。

```EnvironmentType.swift
public enum EnvironmentType {
    case stub
    case local
    case production
    case test
}

```

## 4. SettingsForEnvironmentの追加
設定を管理する"SettingsForEnvironment"というクラスを追加
このクラスへ実際に環境に応じて違う値を返すpropertyを記述する。


```SettingsForEnvironment.swift
/// 環境に応じて設定が変わることを表現するためにこのクラス名を選択
public class SettingsForEnvironment: NSObject {
    
    // MARK: - Environment: ここに差し込まれた環境によってPropertyの値を変化させる
    
    private var currentEnvironment: EnvironmentType {
        return Environment.shared.type
    }
    
    
    
    
    // MARK: - Settings: 参考のPropertyを記述
    
    public var baseUrl: String {
        switch currentEnvironment {
        case .stub      : return ""
        case .local     : return "http://127.0.0.1:8000"
        case .production: return "https://qiita.com" // 本番のURLを差し込み
        }
    }
    
    public var appNmae: String {
        switch currentEnvironment {
        case .stub      : return "MyAppStub"
        case .local     : return "MyAppLocal"
        case .production: return "MyApp"  
        }
    }
    
    public var myAdSdkId: Int {
        switch currentEnvironment {
        case .stub      : return 123
        case .local     : return 456
        case .production: return 789
        }
    }
}

```


## 5. Environment(class)の追加
環境全体を管理する"Environment"クラスを作成する。
現在の環境の保持や、環境に応じた設定の取り出しは、このsharedインスタンス
を通じて行う。

特記すべきこととして、環境の差し込みが一回しか行われないことを保証するコードが書かれている点でである。これの実現方法は、一度でも環境が差し込まれると、environmentTypeHasBeenSetプロパティがtrueに設定され、次に環境を差し込もうとしてもenvironmentTypeHasBeenSetがtrueの時は新たに環境を差し替えないようにしている。


```Environment.swift
public class Environment {

    // MARK: - Private
    
    private var environmentType: EnvironmentType!
    private var environmentTypeHasBeenSet: Bool = false
    
    // 外部からの初期化を禁止するためのコード
    private init () {}
    
    
    
    // MARK: - Internal: EnvironmentAndSettingsから参照するためinternal
    
    var type: EnvironmentType { return self.environmentType }
    
    
    
    // MARK: - Public
    
    public static let shared: Environment = Environment()
    public let settings: SettingsForEnvironment = SettingsForEnvironment()
    
    
    ///  Environment can only be set once
    public func set(environmentType: EnvironmentType) {
        
        // Guard is first time to set Type
        if environmentTypeHasBeenSet == true { return }
        
        // Set Type
        self.environmentType = environmentType
        
        // Assure Type won't be set again
        environmentTypeHasBeenSet = true
    }
}
```


## 6. 初期化&使い方
AppDelegate等でEnvironmentのTypeを差し込み、設定を取り出す場合は、
Environment.shared.settingsから値を取り出す。

### 初期化
環境の初期化は application didFinishLaunchingWithOptionsの一番最初の方で行うのが望ましい。

Configに差し込んだflagを#if DLOCALのような記述で判別して、適切な環境をEnvironmentクラスのsharedインスタンスへ差し込む。安全対策のためenvironmentTypeを２回以上差し込んだい場合、２回目以降の差し込みは反映されないようになっている。


```AppDelegate.swift
import UIKit
import EnvironmentAndSettings // 環境frameworkをimport

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        setupEnvironment() // 環境の初期化を呼び出し
        return true
    }

    /// Flagに応じた環境をEnvironmentへ差し込む
    func setupEnvironment() {
        #if DSTUB
            Environment.shared.set(environmentType: .stub)
        #elseif DLOCAL
            Environment.shared.set(environmentType: .local)
        #elsif DTEST
            Environment.shared.set(environmentType: .test)
        #else
            Environment.shared.set(environmentType: .production)
        #endif
    }
}

```


### 設定の取り出し
Environment.shared.settingsでSettingsForEnvironmentクラスへアクセス可能。

```hoge.swift
// ex.
Environment.shared.settings.baseUrl
```