Tl;Dr
任意の iOS アプリに組み込んで利用可能なデバッグツール的な OSS です。
以下ツイートの動画を見ていただくのが分かりやすいかと思います。
現時点の機能
Property List でサポートされている型に加え、URL
やImage
、JSONシリアライズされたData
型にも対応しています。
App Groups にも対応しています。
閲覧・検索
閲覧 | 検索 | App Groups |
---|---|---|
![]() |
![]() |
![]() |
編集・削除
JSONでの編集 | 削除 | Dateの編集 |
---|---|---|
![]() |
![]() |
![]() |
エクスポート・コピー
Xcode コンソールへの出力。
UserDefaults を一括で出力。(ほぼ Swift コード)
導入
README にも記載していますが、Swift Package Manager で提供しているので、Xcode か Package.swift
で追加し、ランチャー用のアイコンをセットアップするコードを追加すれば完了です。
import UserDefaultsBrowser
@main
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
UserDefaultsBrowserContainer { // ✅ ルート View を囲む
ContentView()
}
}
}
}
import UserDefaultsBrowser
class ViewController: UIViewController { // 💡 ルート ViewController
override func viewDidLoad() {
super.viewDidLoad()
UserDefaultsBrowser.setupUserDefaultsBrowserLauncher() // ✅ 追加
}
}
画面左下に起動用のアイコンがセットアップされるので、あとはそれをタップすれば画面を開けます。
開発時の常駐ツールとして利用する際は、#if DEBUG
などを併用するとよいかと思います。
App Groups の ID やアイコンの見た目をカスタマイズするオプションも含まれています。
UserDefaultsBrowserContainer(
suiteNames: ["group.xxx"], // App Groups の ID を配列で指定
excludeKeys: { $0.hasPrefix("not-display-key") }, // 表示から除外したいキーをクロージャで指定
accentColor: .orange, // アイコンの表示色
imageName: "wrench.and.screwdriver", // アイコンの画像名(SF Symbols で指定)
displayStyle: .fullScreen // シート表示またはフルスクリーン表示(`.sheet` or `.fullScreen`)
)
開発のキッカケ
最近、SwiftUI でアプリ開発をしている際に UserDefaults に保存されている値を確認したいケースがありました。
調べてみるとどうやらシミュレータであれば、特定のディレクトリに.plist
として保存されていることが分かりました。
そんなわけで無事に.plist
で値を確認できたのですが、
あまり見やすくない、と感じました。
JSONシリアライズされたData
型については確認することもできませんし、実機ビルドした際にすばやく確認する方法は見つけられませんでした(社内で実機テストをしていて不具合が見つかった際に、ちょっと確認できたら便利なケースは多いはずです)。
そんなわけで、アプリ上で UserDefaults の内容を確認できるツールを作ることに決めました。
開発の流れ
私は SwiftUI 向けに SwiftUI-Simulator というツールを開発していたので、この OSS のマイルストーンに組み込むことにしました。
SwiftUI-Simulator としてもっと開発に注力すべき機能も多かったのですが、UserDefaults の閲覧くらいであれば対して時間もかからないだろうと予想し、キリの良いタイミングで気分転換も兼ねて開発に着手しました。
そして、閲覧機能が完成したので 1.5.0 のリリースに含めました。
これで個人的には満足したのですが、ふと「編集機能の需要ってあるだろうか?」と気になり、Twitter で初のアンケートを取ってみることにしました。
アンケートの結果、値の変更をしたいと思ったことがある人がそれなりにいることが分かり、せっかくなので開発してしまうことに決めました。
そして、App Groups のサポートも含めた編集機能を 1.6.0 としてリリースしました。
さて、SwiftUI Simulator は SwiftUI に限定された OSS ですが、この UserDefaults の閲覧・編集機能は UIKit でも問題なく利用できるはずでした。
そこで、外部の Swift Package として切り出し、UIKit 向けの API を追加した OSS を UserDefaults-Browser 1.0.0 としてリリースしました。
仕組みについて
UserDefaults に用意された dictionaryRepresentation() というメソッドで、すべてのキーと値を参照できたので、それを利用してすべての値を取得して、適切な形式で表示するようにしています。
ただ、これだとシステム側で用意されたキー・値も取得されてしまうため、それについてはコード側で判定を入れて振り分けています。
private let userDefaultsSystemKeys: [String] = [
"AddingEmojiKeybordHandled",
"CarCapabilities",
...
]
private let userDefaultsSystemKeyPrefixes: [String] = [
"Apple",
"com.apple.",
...
]
struct UserDefaultsContainer: Identifiable {
var allKeys: [String] {
Array(
defaults.dictionaryRepresentation().keys.exclude {
isOSSKey($0) || excludeKeys($0) // 💡 OSS が管理しているキー(開閉状態の保持など)も除外
}
)
}
...
これは私の環境で確かめられたものを列挙しているだけですので、不足などがあれば Issue や PR を頂けるとありがたいです(あるいは他に良い方法があれば教えて頂けると助かります)。
この OSS では Property List でサポートされた形式に加え、JSONシリアライズされたData
や画像についてもサポートしています。
それらについては、その型で解釈できるかデコードを試み、
extension UserDefaults {
func lookup(forKey key: String) -> Any? {
...
//
// Data
//
if let data = value(forKey: key) as? Data {
...
//
// JSON encoded Data
//
if let decoded = try? JSONSerialization.jsonObject(with: data), let dict = decoded as? [String: Any] {
return JSONData(dictionary: dict)
}
}
//
// JSON encoded String
//
if let string = string(forKey: key),
string.hasPrefix("{"),
string.hasSuffix("}"),
let dict = string.jsonToDictionary()
{
return JSONString(dictionary: dict)
}
return value(forKey: key)
}
}
あとはデータ型に応じて整形して表示しています。
private var value: Value {
let value = defaults.lookup(forKey: key)
switch value {
//
// 💡 Note:
// `Array` and `Dictionary` are display as JSON string.
// Because editor of `[String: Any]` is input as JSON.
//
case let value as [Any]:
return .text(
value.isEmpty ? "[]" : value.prettyJSON
)
case let value as [String: Any]:
return .text(
value.isEmpty ? "{}" : value.prettyJSON
)
case let value as JSONData:
return .decodedJSON(value.dictionary.prettyJSON, "<Decoded JSON Data>")
case let value as JSONString:
return .decodedJSON(value.dictionary.prettyJSON, "<Decoded JSON String>")
case let value as UIImage:
return .image(value)
case let value as Date:
return .text(value.toString())
case let value as URL:
return .url(value)
case _ as Data:
return .data(prettyString(value))
default:
return .text(prettyString(value))
}
}
編集についても基本的には似たようなことをやっているだけですので、記事での解説は割愛させていただこうと思います(それほど難しいコードもありませんので、興味のある方はソースコードを読んでいただければと思います)。
UI についてはすべて SwiftUI で作られています(逆に言うと、SwiftUI の生産性があったからこそ作ろうと思ったとも言えます)。
そのため SwiftUI への API 提供は簡単だったのですが、UIKit の場合にどうやってランチャー用のボタンをセットアップするかに少し悩みました。
結論から言うと、常に最前面に常駐させるために UIWindow
を新しく作成することで実現しています。
private var overlayWindow: UIWindow?
public func setupUserDefaultsBrowserLauncher(...) {
if let rootWindow = UIApplication.rootWindow, let scene = UIApplication.rootScene {
let window = UIWindow(windowScene: scene)
window.backgroundColor = .clear
window.frame = CGRect(
origin: CGPoint(
x: 0,
y: UIScreen.main.bounds.height - (overlayWindowSize.height + rootWindow.safeAreaInsets.bottom)
),
size: overlayWindowSize
)
window.windowLevel = .alert + 1
let vc = UserDefaultsBrowserLauncherViewController(...)
window.rootViewController = vc
window.makeKeyAndVisible()
overlayWindow = window
}
}
基本的な実装の仕組みとしてはこんなところでしょうか。
感想
やはり SwiftUI の生産性は非常に高いと感じました。
とくにこのような見た目にそれほど拘る必要がないツール系については、かなりのスピードで開発できてしまうので、今後 macOS の SwiftUI サポートが充実すれば(現時点の 3.0 では単体では使い物にならないレベルだと感じます)、多くのエンジニアが様々な開発者向けツールを作るようになるのではないかと思います。
Contributions
この OSS の動作確認は自分の小さなアプリでしか試せていませんので、細かいエッジケースに対応できていないかと思います。
不具合については Issue や PR などを頂けると大変助かります。
また、改善提案や要望については GitHub Discussions も用意していますので、お気軽に書き込みいただければ幸いです🙏
蛇足
記事に Twitter の埋め込めるのは便利だと思うのですが、リツイートも含めて表示されてかなりの領域を取ってしまうので、なんだか宣伝行為っぽく見えてしまうのが難点ですね;