Skip to content

Latest commit

 

History

History
198 lines (161 loc) · 8.98 KB

README.ja.md

File metadata and controls

198 lines (161 loc) · 8.98 KB

AppContainer

まるでコンテナを載せ替えるかのように、一つのアプリで複数の環境を作成・管理することのできるライブラリです。

Github issues Github forks Github stars Github top language

コンセプト

通常1つのアプリに対して、1つの環境(ディレクトリ, UserDefaults、Cookie, Cache, …)が存在しています。 Debugのためや複数のアカウントを扱うために複数の環境を用意するには、複数の同一アプリをインストールする必要があります。(bundle idの異なる) Debugにおいては、アカウントのログインとログアウトを繰り返しての確認が必要となるケースもあるかもしれません。
そこで、同一アプリ内に複数の環境を作成し、簡単に切り替えることができないかと考えました。 それで作成したのが、AppContainerというこのライブラリです。

デモ

Default Debug1
Default Debug1
コンテナ選択 コンテナリスト コンテナ情報
Select List Info

原理

ディレクトリ

アプリが書き込み可能な領域は、ホームディレクトリ配下にあります。 UserDefaultsもCoreDataもCookieも、アプリが生成するデータは全てここに保存されています。 このディレクトリをコンテナごとに載せ替えることで複数の環境を作成しています。 コンテナは、Library配下に特別なディレクトリを用意してそこに退避させるように実装しています。

// UserDefaults
Library/Preferences/XXXXX.plist

// CoreData
Library/Application Support/YOU_APP_NAME

// Cookie
Library/Cookies

UserDefaults/CFPreferences

UserDefaultsやその上位実装であるCFPreferencesはsetされたデータを、別プロセスであるcfprefsdというものによってキャッシングをおこなっています。 これらはsetされたデータをplistファイルに保存し永続化をおこなっていますが、上記のキャッシングにより、plist内のデータとUserDefaults/CFPreferencesから取得できるデータは常に等しくなるわけではありません。(非同期で読み書きが行われる。) これはアプリの再起動を行っても同期されるとは限りません。 よってコンテナの有効化処理を行う処理で、同期を行う処理をおこなっています。

HTTPCookieStorage

HTTPCookieStorageもキャッシングされており、非同期でファイル(Library/Cookies)への書き込みが行われています。 予期せぬタイミングで書き込みが行われてしまうと、コンテナ内でデータの不整合が起こってしまいます。 特に同一ドメイン宛のCookieを複数コンテナで扱っている場合には、セッションが引き継げなくなってしまう問題が起きます。 そのため、コンテナの切り替え時に、保存とキャッシュの解放を行なっています。

ドキュメント

AppGroup

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()

通知(Notification)

コンテナ切り替え時に通知を受け取ることができます。 厳密に、切り替え前および切り替え後に行いたい処理を追加する場合は、後述するdelegateを使用してください。

  • containerWillChangeNotification コンテナ切り替え前
  • containerDidChangeNotification コンテナ切り替え後

委譲(Delegate)

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"
]

AppContainerUI

AppContainerを扱うためのUIを提供しています。 SwiftUIおよびUIKitに対応しています。

SwiftUI

import AppContainerUI

// コンテナのリストを表示
ContainerListView(appContainer: .standard, title: String = "Containers")

// コンテナ情報を表示
ContainerInfoView(appContainer: .standard, container: container)

UIKit

import AppContainerUI

// コンテナのリストを表示
ContainerListViewController(appContainer: .standard, title: String = "Containers")

// コンテナ情報を表示
ContainerInfoViewController(appContainer: .standard, container: container)