まるでコンテナを載せ替えるかのように、一つのアプリで複数の環境を作成・管理することのできるライブラリです。
通常1つのアプリに対して、1つの環境(ディレクトリ, UserDefaults、Cookie, Cache, …)が存在しています。
Debugのためや複数のアカウントを扱うために複数の環境を用意するには、複数の同一アプリをインストールする必要があります。(bundle idの異なる)
Debugにおいては、アカウントのログインとログアウトを繰り返しての確認が必要となるケースもあるかもしれません。
そこで、同一アプリ内に複数の環境を作成し、簡単に切り替えることができないかと考えました。
それで作成したのが、AppContainer
というこのライブラリです。
Default | Debug1 |
---|---|
コンテナ選択 | コンテナリスト | コンテナ情報 |
---|---|---|
アプリが書き込み可能な領域は、ホームディレクトリ配下にあります。 UserDefaultsもCoreDataもCookieも、アプリが生成するデータは全てここに保存されています。 このディレクトリをコンテナごとに載せ替えることで複数の環境を作成しています。 コンテナは、Library配下に特別なディレクトリを用意してそこに退避させるように実装しています。
// UserDefaults
Library/Preferences/XXXXX.plist
// CoreData
Library/Application Support/YOU_APP_NAME
// Cookie
Library/Cookies
UserDefaults
やその上位実装であるCFPreferences
はsetされたデータを、別プロセスであるcfprefsd
というものによってキャッシングをおこなっています。
これらはsetされたデータをplistファイルに保存し永続化をおこなっていますが、上記のキャッシングにより、plist内のデータとUserDefaults
/CFPreferences
から取得できるデータは常に等しくなるわけではありません。(非同期で読み書きが行われる。)
これはアプリの再起動を行っても同期されるとは限りません。
よってコンテナの有効化処理を行う処理で、同期を行う処理をおこなっています。
HTTPCookieStorageもキャッシングされており、非同期でファイル(Library/Cookies)への書き込みが行われています。 予期せぬタイミングで書き込みが行われてしまうと、コンテナ内でデータの不整合が起こってしまいます。 特に同一ドメイン宛のCookieを複数コンテナで扱っている場合には、セッションが引き継げなくなってしまう問題が起きます。 そのため、コンテナの切り替え時に、保存とキャッシュの解放を行なっています。
extension AppContainer {
static let group = .init(groupIdentifier: "YOUR APP GROUP IDENTIFIER")
}
let container = try AppContainer.standard.createNewContainer(name: "Debug1")
元のコンテナはDEFAULT
という名前で、UUIDは00000000-0000-0000-0000-000000000000
となっています。
isDefault
というプロパティで確認できます。
let containers: [Container] = AppContainer.standard.containers
let activeContainer: Container? = AppContainer.standard.activeContainer
このメソッドを呼んだ後は、アプリを再起動することをお勧めします。
try AppContainer.standard.activate(container: container)
try AppContainer.standard.activateContainer(uuid: uuid)
もし削除しようとしているコンテナが使用中の場合、Defaultコンテナを有効化してから削除します。
try AppContainer.standard.delete(container: container)
try AppContainer.standard.deleteContainer(uuid: uuid)
try AppContainer.standard.clean(container: container)
try AppContainer.standard.cleanContainer(uuid: uuid)
このライブラリを使用する前の状態に戻します。 具体的には、DEFAULTコンテナを有効にして、その他のAppContainer関連のファイルは全て削除されます。
try AppContainer.standard.reset()
コンテナ切り替え時に通知を受け取ることができます。 厳密に、切り替え前および切り替え後に行いたい処理を追加する場合は、後述するdelegateを使用してください。
- containerWillChangeNotification コンテナ切り替え前
- containerDidChangeNotification コンテナ切り替え後
Delegateを使用して、コンテナの切り替え時に、任意の処理を追加することができます。 以下の順で処置が行われます。
// `activate`メソッドが呼び出される
// ↓↓↓↓↓↓↓↓↓↓
func appContainer(_ appContainer: AppContainer, willChangeTo toContainer: Container, from fromContainer: Container?) // Delegate(コンテナ切り替え前)
// ↓↓↓↓↓↓↓↓↓↓
// コンテナの切り替え処理(ライブラリ)
// ↓↓↓↓↓↓↓↓↓↓
func appContainer(_ appContainer: AppContainer, didChangeTo toContainer: Container, from fromContainer: Container?) // Delegate(コンテナ切り替え後)
このライブラリでは複数のdelegateを設定できるようになっています。 以下のように追加します。
AppContainer.standard.delegates.add(self) // selfがAppContainerDelegateに準拠している場合
弱参照で保持されており、オブジェクトが解放された場合は自動で解除されます。 もし、delegateの設定を解除したい場合は以下のように書きます。
AppContainer.standard.delegates.remove(self) // selfがAppContainerDelegateに準拠している場合
コンテナ切り替え時には、一部のシステムファイルを除くほぼ全てのファイルが、コンテナディレクトリへ退避そして復元されます。 これらの移動対象から除外するファイルを設定することができます。
例えば、以下はUserDefaultを全てのコンテナで共通で利用したいときの例です。 このファイルは、コンテナ切り替え時に、退避も復元もされません。
appcontainer.customExcludeFiles = [
"Library/Preferences/<Bundle Identifier>.plist"
]
ファイルパスのうち、最後がcustomExcludeFilesの内容に一致するものが全て移動対象から除外されます。
例えば、以下のように設定した場合、全てのディレクトリ配下のXXX.yy
というファイルが移動対象から除外されます。
appcontainer.customExcludeFiles = [
"XXX.yy"
]
AppContainerを扱うためのUIを提供しています。 SwiftUIおよびUIKitに対応しています。
import AppContainerUI
// コンテナのリストを表示
ContainerListView(appContainer: .standard, title: String = "Containers")
// コンテナ情報を表示
ContainerInfoView(appContainer: .standard, container: container)
import AppContainerUI
// コンテナのリストを表示
ContainerListViewController(appContainer: .standard, title: String = "Containers")
// コンテナ情報を表示
ContainerInfoViewController(appContainer: .standard, container: container)