├── .swift-version
├── .swiftformat
├── Makefile
├── Mintfile
├── Example
├── Shared
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── picture.imageset
│ │ │ ├── picture.jpeg
│ │ │ └── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── ContentView.swift
│ └── ExampleApp.swift
├── UIKitApp
│ ├── Resources
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── picture.imageset
│ │ │ │ ├── picture.jpeg
│ │ │ │ └── Contents.json
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── UIKitApp.entitlements
│ │ ├── Info.plist
│ │ └── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ ├── BrowserViewController.swift
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift
│ └── ViewController.swift
├── Example.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── project.pbxproj
├── iOS
│ └── iOS.entitlements
└── macOS
│ └── macOS.entitlements
├── Sources
└── UserDefaultsBrowser
│ ├── DisplayStyle.swift
│ ├── Core
│ ├── UserDefaultsType.swift
│ ├── JSONData.swift
│ ├── JSONString.swift
│ └── UserDefaultsContainer.swift
│ ├── Extension
│ ├── UUID+.swift
│ ├── SwiftUI
│ │ ├── Font+.swift
│ │ ├── TextField+.swift
│ │ └── TextEditor+.swift
│ ├── Sequence+.swift
│ ├── EnvironmentValues+.swift
│ ├── String+.swift
│ ├── Array+.swift
│ ├── Dictionary+.swift
│ └── UserDefaults+.swift
│ ├── UIKit
│ ├── DisplayStyle+UIKit.swift
│ ├── UIApplication+.swift
│ ├── EntryPoint.swift
│ ├── UserDefaultsBrowserViewController.swift
│ └── UserDefaultsBrowserLauncherViewController.swift
│ ├── View
│ ├── Editor
│ │ ├── BoolEditor.swift
│ │ ├── DateEditor.swift
│ │ └── StringRepresentableEditor.swift
│ ├── SearchTextField.swift
│ ├── SearchContainerView.swift
│ ├── _
│ │ └── _UserDefaultsStringArrayEditor.swift
│ ├── SectionView.swift
│ ├── RowView.swift
│ └── ValueEditView.swift
│ ├── UserDefaultsBrowserContainer.swift
│ └── UserDefaultsBrowserView.swift
├── Tests
└── UserDefaultsBrowserTests
│ └── UserDefaultsBrowserTests.swift
├── .github
└── workflows
│ └── build-examples.yml
├── Package.swift
├── LICENSE
├── .gitignore
└── README.md
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.6
2 |
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --ifdef no-indent
2 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | format:
2 | mint run swiftformat .
3 |
--------------------------------------------------------------------------------
/Mintfile:
--------------------------------------------------------------------------------
1 | nicklockwood/SwiftFormat@0.49.7
2 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/UIKitApp/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/picture.imageset/picture.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YusukeHosonuma/UserDefaultsBrowser/HEAD/Example/Shared/Assets.xcassets/picture.imageset/picture.jpeg
--------------------------------------------------------------------------------
/Example/UIKitApp/Resources/Assets.xcassets/picture.imageset/picture.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YusukeHosonuma/UserDefaultsBrowser/HEAD/Example/UIKitApp/Resources/Assets.xcassets/picture.imageset/picture.jpeg
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/iOS/iOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/UIKitApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/DisplayStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/07.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum DisplayStyle {
11 | case sheet
12 | case fullScreen
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Core/UserDefaultsType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import Foundation
9 |
10 | enum UserDefaultsType: String {
11 | case user
12 | case system
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/UUID+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/05.
6 | //
7 |
8 | import Foundation
9 |
10 | extension UUID {
11 | mutating func refresh() {
12 | self = UUID()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Core/JSONData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import Foundation
9 |
10 | //
11 | // Stored as JSON data.
12 | //
13 | struct JSONData {
14 | let dictionary: [String: Any]
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/SwiftUI/Font+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/04/13.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension Font {
11 | static let codeStyle: Self = .system(size: 14, weight: .regular, design: .monospaced)
12 | }
13 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/Sequence+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Sequence {
11 | func exclude(_ isExcluded: (Element) throws -> Bool) rethrows -> [Element] {
12 | try filter { try isExcluded($0) == false }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Core/JSONString.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import Foundation
9 |
10 | //
11 | // Stored as JSON string.
12 | //
13 | // e.g.
14 | // `{"rawValue":{"red":0,"alpha":1,"blue":0,"green":0}}`)
15 | //
16 | struct JSONString {
17 | let dictionary: [String: Any]
18 | }
19 |
--------------------------------------------------------------------------------
/Example/UIKitApp/Resources/UIKitApp.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.UserDefaultsBrowser
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/macOS/macOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/UIKit/DisplayStyle+UIKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/07.
6 | //
7 |
8 | import UIKit
9 |
10 | extension DisplayStyle {
11 | var modalPresentationStyle: UIModalPresentationStyle {
12 | switch self {
13 | case .sheet: return .pageSheet
14 | case .fullScreen: return .fullScreen
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/UserDefaultsBrowserTests/UserDefaultsBrowserTests.swift:
--------------------------------------------------------------------------------
1 | @testable import UserDefaultsBrowser
2 | import XCTest
3 |
4 | final class UserDefaultsBrowserTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | XCTFail()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/UIKit/UIApplication+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/07.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIApplication {
11 | static var rootWindow: UIWindow? {
12 | Self.shared.windows.first
13 | }
14 |
15 | static var rootScene: UIWindowScene? {
16 | Self.shared.connectedScenes.first as? UIWindowScene
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/picture.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "picture.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Example/UIKitApp/Resources/Assets.xcassets/picture.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "picture.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/EnvironmentValues+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct CustomAccentColor: EnvironmentKey {
11 | public static var defaultValue: Color = .accentColor
12 | }
13 |
14 | public extension EnvironmentValues {
15 | var customAccentColor: Color {
16 | get { self[CustomAccentColor.self] }
17 | set { self[CustomAccentColor.self] = newValue }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/View/Editor/BoolEditor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/04.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BoolEditor: View {
11 | @Binding var value: Bool
12 |
13 | var body: some View {
14 | Picker(selection: $value) {
15 | Text("true").tag(true)
16 | Text("false").tag(false)
17 | } label: {
18 | EmptyView()
19 | }
20 | .pickerStyle(.segmented)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/SwiftUI/TextField+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/04.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension TextField {
11 | enum Style {
12 | case valueEditor
13 | }
14 |
15 | @ViewBuilder
16 | func style(_ style: Style) -> some View {
17 | switch style {
18 | case .valueEditor:
19 | padding()
20 | .border(.gray.opacity(0.5))
21 | .font(.codeStyle)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/String+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | // 🌱 Special Thanks.
12 | // https://stackoverflow.com/questions/30480672/how-to-convert-a-json-string-to-a-dictionary
13 | func jsonToDictionary() -> [String: Any]? {
14 | if let data = data(using: .utf8) {
15 | return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
16 | }
17 | return nil
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/SwiftUI/TextEditor+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/04.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension TextEditor {
11 | enum Style {
12 | case valueEditor
13 | }
14 |
15 | @ViewBuilder
16 | func style(_ style: Style) -> some View {
17 | switch style {
18 | case .valueEditor:
19 | border(.gray.opacity(0.5))
20 | .autocapitalization(.none)
21 | .disableAutocorrection(true)
22 | .font(.codeStyle)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Example/UIKitApp/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 | UISceneStoryboardFile
19 | Main
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/Array+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Array where Element == Any {
11 | static func from(jsonString: String) -> [Any]? {
12 | guard
13 | let data = jsonString.data(using: .utf8),
14 | let decoded = try? JSONSerialization.jsonObject(with: data),
15 | let array = decoded as? [Any] else { return nil }
16 |
17 | return array
18 | }
19 |
20 | var prettyJSON: String {
21 | let jsonData = try? JSONSerialization.data(
22 | withJSONObject: self,
23 | options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
24 | )
25 | return String(data: jsonData!, encoding: .utf8)!
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/build-examples.yml:
--------------------------------------------------------------------------------
1 | name: Build Examples
2 |
3 | on:
4 | pull_request
5 |
6 | jobs:
7 | build-examples:
8 | runs-on: macos-12
9 | timeout-minutes: 30
10 | concurrency:
11 | group: ${{ github.workflow }}-${{ github.ref }}
12 | cancel-in-progress: true
13 | strategy:
14 | matrix:
15 | scheme: ['Example (iOS)', 'UIKitApp']
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: SwiftPM cache
20 | uses: actions/cache@v3
21 | with:
22 | path: SourcePackages
23 | key: ${{ runner.os }}-swiftpm-${{ hashFiles('**/Package.resolved') }}
24 | - name: Build
25 | run: xcodebuild -project Example/Example.xcodeproj -scheme "${{ matrix.scheme }}" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 13,OS=latest' -clonedSourcePackagesDirPath SourcePackages
26 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "UserDefaultsBrowser",
8 | platforms: [
9 | .iOS(.v14),
10 | ],
11 | products: [
12 | .library(name: "UserDefaultsBrowser", targets: ["UserDefaultsBrowser"]),
13 | ],
14 | dependencies: [
15 | .package(url: "https://github.com/YusukeHosonuma/SwiftPrettyPrint.git", from: "1.3.0"),
16 | .package(url: "https://github.com/pointfreeco/swift-case-paths.git", from: "0.8.0"),
17 | .package(url: "https://github.com/YusukeHosonuma/SwiftUI-Common.git", from: "1.0.0"),
18 | ],
19 | targets: [
20 | .target(name: "UserDefaultsBrowser", dependencies: [
21 | "SwiftPrettyPrint",
22 | .product(name: "CasePaths", package: "swift-case-paths"),
23 | .product(name: "SwiftUICommon", package: "SwiftUI-Common"),
24 | ]),
25 | .testTarget(name: "UserDefaultsBrowserTests", dependencies: ["UserDefaultsBrowser"]),
26 | ]
27 | )
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Yusuke Hosonuma
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/View/Editor/DateEditor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/05.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DateEditor: View {
11 | @Binding var date: Date
12 | @Binding var isValid: Bool
13 |
14 | //
15 | // 💡 Note:
16 | // Just updating via binding is not enough to update text value. (why?)
17 | // Therefore update via `id`.
18 | //
19 | @State private var dateEditorID = UUID()
20 |
21 | var body: some View {
22 | //
23 | // ⚠️ FIXME: The display is corrupted when the keyboard is shown.
24 | //
25 | VStack {
26 | StringRepresentableEditor($date, isValid: $isValid)
27 | .id(dateEditorID)
28 |
29 | DatePicker(selection: .init(
30 | get: { date },
31 | set: {
32 | date = $0
33 | dateEditorID.refresh()
34 | }
35 | )) {
36 | EmptyView()
37 | }
38 | .datePickerStyle(.graphical)
39 | .padding()
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Example/UIKitApp/BrowserViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BrowserViewController.swift
3 | // UIKitApp
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/07.
6 | //
7 |
8 | import UIKit
9 | import UserDefaultsBrowser
10 |
11 | class BrowserViewController: UIViewController {
12 | override func viewDidLoad() {
13 | let vc = UserDefaultsBrowserViewController(
14 | suiteNames: [groupID],
15 | excludeKeys: { $0.hasPrefix("not-display-key") },
16 | accentColor: .systemPurple
17 | )
18 |
19 | addChild(vc)
20 | view.addSubview(vc.view)
21 | vc.didMove(toParent: self)
22 |
23 | //
24 | // Same size of self.
25 | //
26 | vc.view.translatesAutoresizingMaskIntoConstraints = false
27 | vc.view.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
28 | vc.view.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
29 | vc.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
30 | vc.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/View/SearchTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/04/30.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SearchTextField: View {
11 | @Binding private var text: String
12 | @State private var showCancelButton = false
13 |
14 | private let title: String
15 |
16 | init(_ title: String, text: Binding) {
17 | self.title = title
18 | _text = text
19 | }
20 |
21 | var body: some View {
22 | HStack {
23 | Image(systemName: "magnifyingglass")
24 |
25 | TextField(title, text: $text, onEditingChanged: { _ in
26 | self.showCancelButton = true
27 | })
28 | .autocapitalization(.none)
29 | .disableAutocorrection(true)
30 |
31 | Button {
32 | text = ""
33 | } label: {
34 | Image(systemName: "xmark.circle.fill")
35 | .opacity(text == "" ? 0 : 1)
36 | }
37 | }
38 | .padding(8)
39 | .foregroundColor(.secondary)
40 | .background(Color(.secondarySystemBackground))
41 | .cornerRadius(10.0)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/View/SearchContainerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/01.
6 | //
7 |
8 | import SwiftPrettyPrint
9 | import SwiftUI
10 |
11 | struct SearchContainerView: View {
12 | let type: UserDefaultsType
13 | let defaultsContainers: [UserDefaultsContainer]
14 |
15 | init(type: UserDefaultsType, defaults: [UserDefaultsContainer]) {
16 | self.type = type
17 | defaultsContainers = defaults
18 | }
19 |
20 | @State private var searchText = ""
21 | @State private var isPresentedEditSheet = false
22 | @State private var isPresentedDeleteConfirmAlert = false
23 | @State private var editDefaults: UserDefaults? = nil
24 | @State private var editKey: String? = nil
25 |
26 | var body: some View {
27 | VStack {
28 | SearchTextField("Search by key...", text: $searchText)
29 | .padding()
30 |
31 | Form {
32 | ForEach(defaultsContainers) { defaults in
33 | SectionView(
34 | defaults: defaults,
35 | type: type,
36 | searchText: $searchText
37 | )
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/Dictionary+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/04.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Dictionary where Key == String, Value == Any {
11 | static func from(jsonString: String) -> [String: Any]? {
12 | guard
13 | let data = jsonString.data(using: .utf8),
14 | let decoded = try? JSONSerialization.jsonObject(with: data),
15 | let dictionary = decoded as? [String: Any] else { return nil }
16 |
17 | return dictionary
18 | }
19 |
20 | var prettyJSON: String {
21 | do {
22 | let data = try JSONSerialization.data(
23 | withJSONObject: self,
24 | options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
25 | )
26 | return String(data: data, encoding: .utf8)!
27 | } catch {
28 | preconditionFailure(error.localizedDescription)
29 | }
30 | }
31 |
32 | var serializedJSON: String? {
33 | do {
34 | let data = try JSONSerialization.data(withJSONObject: self)
35 | return String(data: data, encoding: .utf8)
36 | } catch {
37 | preconditionFailure(error.localizedDescription)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Example/UIKitApp/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // UIKitApp
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
13 | true
14 | }
15 |
16 | // MARK: UISceneSession Lifecycle
17 |
18 | func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
19 | // Called when a new scene session is being created.
20 | // Use this method to select a configuration to create the new scene with.
21 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
22 | }
23 |
24 | func application(_: UIApplication, didDiscardSceneSessions _: Set) {
25 | // Called when the user discards a scene session.
26 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
27 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Example/Shared/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Shared
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import SwiftUI
9 | import UserDefaultsBrowser
10 |
11 | struct ContentView: View {
12 | @State private var isPresented = false
13 |
14 | var body: some View {
15 | TabView {
16 | NavigationView {
17 | Button("Open Browser") {
18 | isPresented.toggle()
19 | }
20 | .navigationTitle("Example")
21 | .sheet(isPresented: $isPresented) {
22 | UserDefaultsBrowserView(
23 | suiteNames: [groupID],
24 | excludeKeys: { $0.hasPrefix("not-display-key") },
25 | accentColor: .orange
26 | )
27 | }
28 | }
29 | .navigationViewStyle(.stack) // ⚠️ This is not work. (SwiftUI bug?)
30 | .tabItem {
31 | Label("Example", systemImage: "swift")
32 | }
33 |
34 | UserDefaultsBrowserView(
35 | suiteNames: [groupID],
36 | excludeKeys: { $0.hasPrefix("not-display-key") },
37 | accentColor: .orange
38 | )
39 | .tabItem {
40 | Label("Browser", systemImage: "externaldrive")
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/UIKit/EntryPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/07.
6 | //
7 |
8 | import SwiftUI
9 |
10 | private var overlayWindow: UIWindow?
11 | private let overlayWindowSize = CGSize(width: 44, height: 44)
12 |
13 | //
14 | // Display launcher icon on leading-bottom.
15 | //
16 | public func setupUserDefaultsBrowserLauncher(
17 | suiteNames: [String] = [],
18 | excludeKeys: @escaping (String) -> Bool = { _ in false },
19 | accentColor: UIColor = .systemBlue,
20 | imageName: String = "externaldrive",
21 | displayStyle: DisplayStyle = .sheet
22 | ) {
23 | if let rootWindow = UIApplication.rootWindow, let scene = UIApplication.rootScene {
24 | let window = UIWindow(windowScene: scene)
25 | window.backgroundColor = .clear
26 | window.frame = CGRect(
27 | origin: CGPoint(
28 | x: 0,
29 | y: UIScreen.main.bounds.height - (overlayWindowSize.height + rootWindow.safeAreaInsets.bottom)
30 | ),
31 | size: overlayWindowSize
32 | )
33 | window.windowLevel = .alert + 1
34 |
35 | let vc = UserDefaultsBrowserLauncherViewController(
36 | rootWindow: rootWindow,
37 | suiteNames: suiteNames,
38 | excludeKeys: excludeKeys,
39 | accentColor: accentColor,
40 | imageName: imageName,
41 | displayStyle: displayStyle
42 | )
43 | window.rootViewController = vc
44 |
45 | window.makeKeyAndVisible()
46 | overlayWindow = window
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Example/UIKitApp/Resources/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Extension/UserDefaults+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | extension UserDefaults {
12 | static let keyRepository = "YusukeHosonuma/UserDefaultsBrowser"
13 | static let keyVersion = "1.0.0" // 💡 Please update version number when data incompatibility occur.
14 |
15 | static let keyPrefix: String = "\(keyRepository)/\(keyVersion)/"
16 |
17 | func lookup(forKey key: String) -> Any? {
18 | //
19 | // Data
20 | //
21 | if let data = value(forKey: key) as? Data {
22 | //
23 | // URL
24 | //
25 | if let url = url(forKey: key) {
26 | return url
27 | }
28 |
29 | //
30 | // UIImage
31 | //
32 | if let image = UIImage(data: data) {
33 | return image
34 | }
35 |
36 | //
37 | // JSON encoded Data
38 | //
39 | if let decoded = try? JSONSerialization.jsonObject(with: data), let dict = decoded as? [String: Any] {
40 | return JSONData(dictionary: dict)
41 | }
42 | }
43 |
44 | //
45 | // Dictionary
46 | //
47 | if let dict = dictionary(forKey: key) {
48 | return dict
49 | }
50 |
51 | //
52 | // JSON encoded String
53 | //
54 | if let string = string(forKey: key),
55 | string.hasPrefix("{"),
56 | string.hasSuffix("}"),
57 | let dict = string.jsonToDictionary()
58 | {
59 | return JSONString(dictionary: dict)
60 | }
61 |
62 | return value(forKey: key)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/UIKit/UserDefaultsBrowserViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/07.
6 | //
7 |
8 | import SwiftUI
9 |
10 | //
11 | // Wrapper of `UserDefaultsBrowserView`.
12 | //
13 | public class UserDefaultsBrowserViewController: UIViewController {
14 | private let suiteNames: [String]
15 | private let excludeKeys: (String) -> Bool
16 | private let accentColor: UIColor
17 |
18 | public init(
19 | suiteNames: [String],
20 | excludeKeys: @escaping (String) -> Bool,
21 | accentColor: UIColor
22 | ) {
23 | self.suiteNames = suiteNames
24 | self.excludeKeys = excludeKeys
25 | self.accentColor = accentColor
26 | super.init(nibName: nil, bundle: nil)
27 | }
28 |
29 | override public func viewDidLoad() {
30 | let browserView = UserDefaultsBrowserView(
31 | suiteNames: suiteNames,
32 | excludeKeys: excludeKeys,
33 | accentColor: Color(accentColor)
34 | )
35 |
36 | let vc = UIHostingController(rootView: browserView)
37 | addChild(vc)
38 | view.addSubview(vc.view)
39 | vc.didMove(toParent: self)
40 |
41 | //
42 | // Same size of self.
43 | //
44 | vc.view.translatesAutoresizingMaskIntoConstraints = false
45 | vc.view.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
46 | vc.view.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
47 | vc.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
48 | vc.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
49 | }
50 |
51 | @available(*, unavailable)
52 | required init?(coder _: NSCoder) {
53 | fatalError("init(coder:) has not been implemented")
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Example/UIKitApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/Core/UserDefaultsContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import Foundation
9 |
10 | private let userDefaultsSystemKeys: [String] = [
11 | "AddingEmojiKeybordHandled",
12 | "CarCapabilities",
13 | "MSVLoggingMasterSwitchEnabledKey",
14 | "PreferredLanguages",
15 | ]
16 |
17 | private let userDefaultsSystemKeyPrefixes: [String] = [
18 | "Apple",
19 | "com.apple.",
20 | "internalSettings.",
21 | "METAL_",
22 | "INNext",
23 | "AK",
24 | "NS",
25 | "PK",
26 | "WebKit",
27 | ]
28 |
29 | struct UserDefaultsContainer: Identifiable {
30 | var id: String { name }
31 |
32 | let name: String
33 | let defaults: UserDefaults
34 | let excludeKeys: (String) -> Bool
35 |
36 | var allKeys: [String] {
37 | Array(
38 | defaults.dictionaryRepresentation().keys.exclude {
39 | isOSSKey($0) || excludeKeys($0)
40 | }
41 | )
42 | }
43 |
44 | var systemKeys: [String] {
45 | allKeys.filter { isSystemKey($0) }
46 | }
47 |
48 | var userKeys: [String] {
49 | allKeys.filter { isSystemKey($0) == false }
50 | }
51 |
52 | func extractKeys(of type: UserDefaultsType) -> [String] {
53 | switch type {
54 | case .user: return userKeys
55 | case .system: return systemKeys
56 | }
57 | }
58 |
59 | func removeAll(of type: UserDefaultsType) {
60 | for key in extractKeys(of: type) {
61 | defaults.removeObject(forKey: key)
62 | }
63 | }
64 |
65 | func lookup(forKey key: String) -> Any? {
66 | defaults.lookup(forKey: key)
67 | }
68 |
69 | // MARK: Private
70 |
71 | private func isOSSKey(_ key: String) -> Bool {
72 | key.hasPrefix(UserDefaults.keyRepository)
73 | }
74 |
75 | private func isSystemKey(_ key: String) -> Bool {
76 | userDefaultsSystemKeys.contains(key) ||
77 | userDefaultsSystemKeyPrefixes.contains { key.hasPrefix($0) }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/UserDefaultsBrowserContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftUICommon
10 |
11 | public struct UserDefaultsBrowserContainer: View {
12 | private let suiteNames: [String]
13 | private let excludeKeys: (String) -> Bool
14 | private let accentColor: Color
15 | private let imageName: String
16 | private let displayStyle: DisplayStyle
17 | private var content: () -> Content
18 |
19 | public init(
20 | suiteNames: [String] = [],
21 | excludeKeys: @escaping (String) -> Bool = { _ in false },
22 | accentColor: Color = .blue,
23 | imageName: String = "externaldrive",
24 | displayStyle: DisplayStyle = .sheet,
25 | @ViewBuilder content: @escaping () -> Content
26 | ) {
27 | self.suiteNames = suiteNames
28 | self.excludeKeys = excludeKeys
29 | self.accentColor = accentColor
30 | self.imageName = imageName
31 | self.displayStyle = displayStyle
32 | self.content = content
33 | }
34 |
35 | @State private var isPresentedSheet = false
36 | @State private var isPresentedFullScreenCover = false
37 |
38 | public var body: some View {
39 | ZStack(alignment: .bottomLeading) {
40 | content()
41 |
42 | Button {
43 | isPresentedSheet.toggle()
44 | } label: {
45 | Image(systemName: imageName)
46 | .padding()
47 | .contentShape(Rectangle())
48 | }
49 | .accentColor(accentColor)
50 | }
51 | .extend { parent in
52 | switch displayStyle {
53 | case .sheet:
54 | parent
55 | .sheet(isPresented: $isPresentedSheet) {
56 | browser()
57 | }
58 | case .fullScreen:
59 | parent
60 | .fullScreenCover(isPresented: $isPresentedSheet) {
61 | browser()
62 | }
63 | }
64 | }
65 | }
66 |
67 | private func browser() -> some View {
68 | UserDefaultsBrowserView(
69 | suiteNames: suiteNames,
70 | excludeKeys: excludeKeys,
71 | accentColor: accentColor
72 | )
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Example/UIKitApp/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // UIKitApp
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 | var window: UIWindow?
12 |
13 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
14 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
15 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
16 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
17 | guard let _ = (scene as? UIWindowScene) else { return }
18 | }
19 |
20 | func sceneDidDisconnect(_: UIScene) {
21 | // Called as the scene is being released by the system.
22 | // This occurs shortly after the scene enters the background, or when its session is discarded.
23 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
24 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
25 | }
26 |
27 | func sceneDidBecomeActive(_: UIScene) {
28 | // Called when the scene has moved from an inactive state to an active state.
29 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
30 | }
31 |
32 | func sceneWillResignActive(_: UIScene) {
33 | // Called when the scene will move from an active state to an inactive state.
34 | // This may occur due to temporary interruptions (ex. an incoming phone call).
35 | }
36 |
37 | func sceneWillEnterForeground(_: UIScene) {
38 | // Called as the scene transitions from the background to the foreground.
39 | // Use this method to undo the changes made on entering the background.
40 | }
41 |
42 | func sceneDidEnterBackground(_: UIScene) {
43 | // Called as the scene transitions from the foreground to the background.
44 | // Use this method to save data, release shared resources, and store enough scene-specific state information
45 | // to restore the scene back to its current state.
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## MacOS
6 | .DS_Store
7 |
8 | ## User settings
9 | xcuserdata/
10 |
11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
12 | *.xcscmblueprint
13 | *.xccheckout
14 |
15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
16 | build/
17 | DerivedData/
18 | *.moved-aside
19 | *.pbxuser
20 | !default.pbxuser
21 | *.mode1v3
22 | !default.mode1v3
23 | *.mode2v3
24 | !default.mode2v3
25 | *.perspectivev3
26 | !default.perspectivev3
27 |
28 | ## Obj-C/Swift specific
29 | *.hmap
30 |
31 | ## App packaging
32 | *.ipa
33 | *.dSYM.zip
34 | *.dSYM
35 |
36 | ## Playgrounds
37 | timeline.xctimeline
38 | playground.xcworkspace
39 |
40 | # Swift Package Manager
41 | #
42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
43 | Packages/
44 | Package.pins
45 | Package.resolved
46 | # *.xcodeproj
47 |
48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
49 | # hence it is not needed unless you have added a package configuration file to your project
50 | # .swiftpm
51 |
52 | .build/
53 |
54 | # CocoaPods
55 | #
56 | # We recommend against adding the Pods directory to your .gitignore. However
57 | # you should judge for yourself, the pros and cons are mentioned at:
58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
59 | #
60 | # Pods/
61 | #
62 | # Add this line if you want to avoid checking in source code from the Xcode workspace
63 | # *.xcworkspace
64 |
65 | # Carthage
66 | #
67 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
68 | # Carthage/Checkouts
69 |
70 | Carthage/Build/
71 |
72 | # Accio dependency management
73 | Dependencies/
74 | .accio/
75 |
76 | # fastlane
77 | #
78 | # It is recommended to not store the screenshots in the git repo.
79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
80 | # For more information about the recommended setup visit:
81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
82 |
83 | fastlane/report.xml
84 | fastlane/Preview.html
85 | fastlane/screenshots/**/*.png
86 | fastlane/test_output
87 |
88 | # Code Injection
89 | #
90 | # After new code Injection tools there's a generated folder /iOSInjectionProject
91 | # https://github.com/johnno1962/injectionforxcode
92 |
93 | iOSInjectionProject/
94 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "colorizeswift",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/mtynior/ColorizeSwift.git",
7 | "state" : {
8 | "revision" : "2a354639173d021f4648cf1912b2b00a3a7cd83c",
9 | "version" : "1.6.0"
10 | }
11 | },
12 | {
13 | "identity" : "curry",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/thoughtbot/Curry.git",
16 | "state" : {
17 | "revision" : "4331dd50bc1db007db664a23f32e6f3df93d4e1a",
18 | "version" : "4.0.2"
19 | }
20 | },
21 | {
22 | "identity" : "flatten",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/YusukeHosonuma/Flatten.git",
25 | "state" : {
26 | "revision" : "5286148aa255f57863e0d7e2b827ca6b91677051",
27 | "version" : "0.1.0"
28 | }
29 | },
30 | {
31 | "identity" : "shlist",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/YusukeHosonuma/SHList.git",
34 | "state" : {
35 | "revision" : "6c61f5382dd07a64d76bc8b7fad8cec0d8a4ff7a",
36 | "version" : "0.1.0"
37 | }
38 | },
39 | {
40 | "identity" : "swift-case-paths",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/pointfreeco/swift-case-paths.git",
43 | "state" : {
44 | "revision" : "ce9c0d897db8a840c39de64caaa9b60119cf4be8",
45 | "version" : "0.8.1"
46 | }
47 | },
48 | {
49 | "identity" : "swiftparamtest",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/YusukeHosonuma/SwiftParamTest.git",
52 | "state" : {
53 | "revision" : "f513e1dbbdd86e2ca2b672537f4bcb4417f94c27",
54 | "version" : "2.2.1"
55 | }
56 | },
57 | {
58 | "identity" : "swiftprettyprint",
59 | "kind" : "remoteSourceControl",
60 | "location" : "https://github.com/YusukeHosonuma/SwiftPrettyPrint.git",
61 | "state" : {
62 | "revision" : "65b8a439c9e499aebb838c0418c72f7d0e66c6ae",
63 | "version" : "1.3.0"
64 | }
65 | },
66 | {
67 | "identity" : "swiftui-common",
68 | "kind" : "remoteSourceControl",
69 | "location" : "https://github.com/YusukeHosonuma/SwiftUI-Common.git",
70 | "state" : {
71 | "revision" : "8e2672dbb3fea32d78236fc6048874718119307b",
72 | "version" : "1.0.0"
73 | }
74 | }
75 | ],
76 | "version" : 2
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/View/_/_UserDefaultsStringArrayEditor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/03.
6 | //
7 | //
8 | // import SwiftUI
9 | //
10 | // struct UserDefaultsStringArrayEditor: View {
11 | // @Binding var strings: [String]
12 | // @State var editMode: EditMode = .inactive
13 | //
14 | // var body: some View {
15 | // ScrollViewReader { scrollProxy in
16 | // List {
17 | // ForEach(Array(strings.indices), id: \.self) { index in
18 | // if editMode.isEditing {
19 | // Text(strings[index])
20 | // } else {
21 | // //
22 | // // ⚠️ Do not use `Binding` directly. (it cause crash when `onDelete`)
23 | // // https://zenn.dev/usk2000/articles/563a79015bf59a
24 | // //
25 | // TextEditor(text: .init(
26 | // get: { strings[index] },
27 | // set: { strings[index] = $0 }
28 | // ))
29 | // .style(.valueEditor)
30 | // }
31 | // }
32 | // .onMove { source, destination in
33 | // strings.move(fromOffsets: source, toOffset: destination)
34 | // }
35 | // .when(editMode.isEditing) {
36 | // $0.onDelete { indexSet in
37 | // strings.remove(atOffsets: indexSet)
38 | // }
39 | // }
40 | // }
41 | // .toolbar {
42 | // ToolbarItemGroup(placement: .bottomBar) {
43 | // Spacer()
44 | // //
45 | // // Edit / Done
46 | // //
47 | // if editMode.isEditing {
48 | // Button("Done") {
49 | // editMode = .inactive
50 | // }
51 | // } else {
52 | // Button("Edit") {
53 | // editMode = .active
54 | // }
55 | // }
56 | // //
57 | // //
58 | // //
59 | // Button {
60 | // strings.append("")
61 | // withAnimation {
62 | // scrollProxy.scrollTo(strings.count - 2, anchor: .top)
63 | // }
64 | // } label: {
65 | // Image(systemName: "plus")
66 | // }
67 | // }
68 | // }
69 | // .environment(\.editMode, $editMode)
70 | // }
71 | // }
72 | // }
73 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/UIKit/UserDefaultsBrowserLauncherViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/07.
6 | //
7 |
8 | import SwiftUI
9 | import UIKit
10 |
11 | //
12 | // Launcher icon on leading-bottom.
13 | //
14 | final class UserDefaultsBrowserLauncherViewController: UIViewController {
15 | private let rootWindow: UIWindow
16 | private let suiteNames: [String]
17 | private let excludeKeys: (String) -> Bool
18 | private let accentColor: UIColor
19 | private let imageName: String
20 | private let displayStyle: DisplayStyle
21 |
22 | init(
23 | rootWindow: UIWindow,
24 | suiteNames: [String],
25 | excludeKeys: @escaping (String) -> Bool,
26 | accentColor: UIColor,
27 | imageName: String,
28 | displayStyle: DisplayStyle
29 | ) {
30 | self.rootWindow = rootWindow
31 | self.suiteNames = suiteNames
32 | self.excludeKeys = excludeKeys
33 | self.accentColor = accentColor
34 | self.imageName = imageName
35 | self.displayStyle = displayStyle
36 | super.init(nibName: nil, bundle: nil)
37 | }
38 |
39 | override func viewDidLoad() {
40 | let button = ExpansionButton()
41 | button.setImage(UIImage(systemName: imageName), for: .normal)
42 | button.tintColor = accentColor
43 | button.insets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
44 | view.addSubview(button)
45 |
46 | // Both-center
47 | button.translatesAutoresizingMaskIntoConstraints = false
48 | button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
49 | button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
50 |
51 | button.addTarget(self, action: #selector(tapButton), for: .touchUpInside)
52 | }
53 |
54 | @objc private func tapButton() {
55 | let vc = UserDefaultsBrowserViewController(
56 | suiteNames: suiteNames,
57 | excludeKeys: excludeKeys,
58 | accentColor: accentColor
59 | )
60 | vc.modalPresentationStyle = displayStyle.modalPresentationStyle
61 | rootWindow.rootViewController?.present(vc, animated: true)
62 | }
63 |
64 | @available(*, unavailable)
65 | required init?(coder _: NSCoder) {
66 | fatalError("init(coder:) has not been implemented")
67 | }
68 | }
69 |
70 | // 🌱 Special Thanks.
71 | // https://qiita.com/KokiEnomoto/items/264f26bfa92d06b1996e
72 | private class ExpansionButton: UIButton {
73 | var insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
74 |
75 | override func point(inside point: CGPoint, with _: UIEvent?) -> Bool {
76 | var rect = bounds
77 | rect.origin.x -= insets.left
78 | rect.origin.y -= insets.top
79 | rect.size.width += insets.left + insets.right
80 | rect.size.height += insets.top + insets.bottom
81 | return rect.contains(point)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | },
93 | {
94 | "idiom" : "mac",
95 | "scale" : "1x",
96 | "size" : "16x16"
97 | },
98 | {
99 | "idiom" : "mac",
100 | "scale" : "2x",
101 | "size" : "16x16"
102 | },
103 | {
104 | "idiom" : "mac",
105 | "scale" : "1x",
106 | "size" : "32x32"
107 | },
108 | {
109 | "idiom" : "mac",
110 | "scale" : "2x",
111 | "size" : "32x32"
112 | },
113 | {
114 | "idiom" : "mac",
115 | "scale" : "1x",
116 | "size" : "128x128"
117 | },
118 | {
119 | "idiom" : "mac",
120 | "scale" : "2x",
121 | "size" : "128x128"
122 | },
123 | {
124 | "idiom" : "mac",
125 | "scale" : "1x",
126 | "size" : "256x256"
127 | },
128 | {
129 | "idiom" : "mac",
130 | "scale" : "2x",
131 | "size" : "256x256"
132 | },
133 | {
134 | "idiom" : "mac",
135 | "scale" : "1x",
136 | "size" : "512x512"
137 | },
138 | {
139 | "idiom" : "mac",
140 | "scale" : "2x",
141 | "size" : "512x512"
142 | }
143 | ],
144 | "info" : {
145 | "author" : "xcode",
146 | "version" : 1
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Example/Shared/ExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleApp.swift
3 | // Shared
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import SwiftUI
9 | import UserDefaultsBrowser
10 |
11 | let groupID = "group.UserDefaultsBrowser"
12 |
13 | @main
14 | struct ExampleApp: App {
15 | @AppStorage("isFirstLaunched") private var isFirstLaunched = false
16 |
17 | init() {
18 | setupExampleData()
19 | }
20 |
21 | var body: some Scene {
22 | WindowGroup {
23 | UserDefaultsBrowserContainer(
24 | suiteNames: [groupID],
25 | excludeKeys: { $0.hasPrefix("not-display-key") },
26 | accentColor: .orange,
27 | imageName: "wrench.and.screwdriver",
28 | displayStyle: .fullScreen
29 | ) {
30 | ContentView()
31 | }
32 | }
33 | }
34 |
35 | // MARK: Private
36 |
37 | private func setupExampleData() {
38 | if isFirstLaunched == false {
39 | defer { isFirstLaunched = true }
40 |
41 | struct User: Codable {
42 | let name: String
43 | let age: Int
44 | let date: Date
45 | let url: URL
46 | }
47 |
48 | //
49 | // UserDefaults.standard
50 | //
51 | let standard = UserDefaults.standard
52 | standard.set("Hello!", forKey: "message")
53 | standard.set("Not display in browser", forKey: "not-display-key")
54 | standard.set(7.5, forKey: "number")
55 | standard.set(URL(string: "https://github.com/YusukeHosonuma/UserDefaultsBrowser")!, forKey: "url")
56 | standard.set(Date(), forKey: "date")
57 | standard.set([String](), forKey: "emptyArray")
58 | standard.set(["Apple", "Orange"], forKey: "array")
59 | standard.set([String: Any](), forKey: "emptyDictionary")
60 | standard.set([
61 | "int": 42,
62 | "float": Float(3.14),
63 | "bool": true,
64 | "string": "String",
65 | "array": ["one", "two"],
66 | ], forKey: "dictionary")
67 |
68 | let user = User(
69 | name: "tobi462",
70 | age: 17,
71 | date: Date(),
72 | url: URL(string: "https://github.com/YusukeHosonuma/UserDefaultsBrowser")!
73 | )
74 | let data = try! JSONEncoder().encode(user)
75 | standard.set(data, forKey: "user")
76 |
77 | let pngData = UIImage(systemName: "swift")!.pngData()!
78 | standard.set(pngData, forKey: "imagePngData")
79 |
80 | let jpegData = UIImage(named: "picture")!.jpegData(compressionQuality: 0.7)!
81 | standard.set(jpegData, forKey: "imageJpegData")
82 |
83 | let stringData = "https://github.com/YusukeHosonuma/UserDefaultsBrowser".data(using: .utf8)!
84 | standard.set(stringData, forKey: "stringData")
85 |
86 | //
87 | // AppGroup
88 | //
89 | guard let group = UserDefaults(suiteName: groupID) else { preconditionFailure() }
90 | group.set("Goodbye", forKey: "message")
91 | group.set(42.195, forKey: "number")
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/View/Editor/StringRepresentableEditor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/03.
6 | //
7 |
8 | import SwiftUI
9 |
10 | protocol StringEditable {
11 | init?(_ string: String)
12 | func toString() -> String
13 | }
14 |
15 | extension Int: StringEditable {
16 | func toString() -> String {
17 | "\(self)"
18 | }
19 | }
20 |
21 | extension Float: StringEditable {
22 | func toString() -> String {
23 | "\(self)"
24 | }
25 | }
26 |
27 | extension Double: StringEditable {
28 | func toString() -> String {
29 | "\(self)"
30 | }
31 | }
32 |
33 | extension URL: StringEditable {
34 | init?(_ string: String) {
35 | self.init(string: string)
36 | }
37 |
38 | func toString() -> String {
39 | absoluteString
40 | }
41 | }
42 |
43 | extension Date: StringEditable {
44 | init?(_ string: String) {
45 | if let date = Self.formatter.date(from: string) {
46 | self = date
47 | } else {
48 | return nil
49 | }
50 | }
51 |
52 | func toString() -> String {
53 | Self.formatter.string(from: self)
54 | }
55 |
56 | static var formatter: ISO8601DateFormatter {
57 | let f = ISO8601DateFormatter()
58 | f.timeZone = .current
59 | return f
60 | }
61 | }
62 |
63 | struct ArrayWrapper: StringEditable {
64 | let array: [Any]
65 |
66 | init(_ array: [Any]) {
67 | self.array = array
68 | }
69 |
70 | init?(_ string: String) {
71 | if let array = [Any].from(jsonString: string) {
72 | self.init(array)
73 | } else {
74 | return nil
75 | }
76 | }
77 |
78 | func toString() -> String {
79 | array.prettyJSON
80 | }
81 | }
82 |
83 | struct DictionaryWrapper: StringEditable {
84 | let dictionary: [String: Any]
85 |
86 | init(_ dictionary: [String: Any]) {
87 | self.dictionary = dictionary
88 | }
89 |
90 | init?(_ string: String) {
91 | if let dict = [String: Any].from(jsonString: string) {
92 | self.init(dict)
93 | } else {
94 | return nil
95 | }
96 | }
97 |
98 | func toString() -> String {
99 | dictionary.prettyJSON
100 | }
101 | }
102 |
103 | struct StringRepresentableEditor: View {
104 | enum Style {
105 | case single
106 | case multiline
107 | }
108 |
109 | @Binding private var value: Value
110 | @Binding private var isValid: Bool
111 | @State private var text: String = ""
112 |
113 | private let style: Style
114 |
115 | init(_ value: Binding, isValid: Binding, style: Style = .single) {
116 | _value = value
117 | _isValid = isValid
118 | self.style = style
119 | }
120 |
121 | var body: some View {
122 | Group {
123 | switch style {
124 | case .single:
125 | TextField("", text: $text)
126 | .style(.valueEditor)
127 |
128 | case .multiline:
129 | TextEditor(text: $text)
130 | .style(.valueEditor)
131 | }
132 | }
133 | .padding([.horizontal])
134 | .onChange(of: text) {
135 | if let newValue = Value($0) {
136 | value = newValue
137 | isValid = true
138 | } else {
139 | isValid = false
140 | }
141 | }
142 | .onAppear {
143 | self.text = value.toString()
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/Example/UIKitApp/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // UIKitApp
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/06.
6 | //
7 |
8 | import SwiftUI
9 | import UIKit
10 | import UserDefaultsBrowser
11 |
12 | let groupID = "group.UserDefaultsBrowser"
13 |
14 | class ViewController: UIViewController {
15 | var window: UIWindow!
16 | var coveringWindow: UIWindow?
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | setupExampleData()
21 | UserDefaultsBrowser.setupUserDefaultsBrowserLauncher(
22 | suiteNames: [groupID],
23 | excludeKeys: { $0.hasPrefix("not-display-key") },
24 | accentColor: .systemOrange,
25 | imageName: "wrench.and.screwdriver",
26 | displayStyle: .fullScreen
27 | )
28 | }
29 |
30 | @IBAction func tapOpenBrowserButton(_: Any) {
31 | let vc = UserDefaultsBrowserViewController(
32 | suiteNames: [groupID],
33 | excludeKeys: { $0.hasPrefix("not-display-key") },
34 | accentColor: .systemOrange
35 | )
36 | vc.modalPresentationStyle = .pageSheet
37 | present(vc, animated: true)
38 | }
39 |
40 | // MARK: Private
41 |
42 | private func setupExampleData() {
43 | let standard = UserDefaults.standard
44 |
45 | if standard.bool(forKey: "isFirstLaunched") == false {
46 | defer {
47 | standard.set(true, forKey: "isFirstLaunched")
48 | }
49 |
50 | struct User: Codable {
51 | let name: String
52 | let age: Int
53 | let date: Date
54 | let url: URL
55 | }
56 |
57 | //
58 | // UserDefaults.standard
59 | //
60 | standard.set("Hello!", forKey: "message")
61 | standard.set("Not display in browser", forKey: "not-display-key")
62 | standard.set(7.5, forKey: "number")
63 | standard.set(URL(string: "https://github.com/YusukeHosonuma/UserDefaultsBrowser")!, forKey: "url")
64 | standard.set(Date(), forKey: "date")
65 | standard.set([String](), forKey: "emptyArray")
66 | standard.set(["Apple", "Orange"], forKey: "array")
67 | standard.set([String: Any](), forKey: "emptyDictionary")
68 | standard.set([
69 | "int": 42,
70 | "float": Float(3.14),
71 | "bool": true,
72 | "string": "String",
73 | "array": ["one", "two"],
74 | ], forKey: "dictionary")
75 |
76 | let user = User(
77 | name: "tobi462",
78 | age: 17,
79 | date: Date(),
80 | url: URL(string: "https://github.com/YusukeHosonuma/UserDefaultsBrowser")!
81 | )
82 | let data = try! JSONEncoder().encode(user)
83 | standard.set(data, forKey: "user")
84 |
85 | let pngData = UIImage(systemName: "swift")!.pngData()!
86 | standard.set(pngData, forKey: "imagePngData")
87 |
88 | let jpegData = UIImage(named: "picture")!.jpegData(compressionQuality: 0.7)!
89 | standard.set(jpegData, forKey: "imageJpegData")
90 |
91 | let stringData = "https://github.com/YusukeHosonuma/UserDefaultsBrowser".data(using: .utf8)!
92 | standard.set(stringData, forKey: "stringData")
93 |
94 | //
95 | // AppGroup
96 | //
97 | guard let group = UserDefaults(suiteName: groupID) else { preconditionFailure() }
98 | group.set("Goodbye", forKey: "message")
99 | group.set(42.195, forKey: "number")
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/UserDefaultsBrowserView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/04/30.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct UserDefaultsBrowserView: View {
11 | // 💡 iOS 15+: `\.dismiss`
12 | @Environment(\.presentationMode) private var presentationMode
13 |
14 | private let suiteNames: [String]
15 | private let accentColor: Color
16 | private let excludeKeys: (String) -> Bool
17 |
18 | public init(
19 | suiteNames: [String] = [],
20 | excludeKeys: @escaping (String) -> Bool = { _ in false },
21 | accentColor: Color = .accentColor
22 | ) {
23 | self.suiteNames = suiteNames
24 | self.excludeKeys = excludeKeys
25 | self.accentColor = accentColor
26 | }
27 |
28 | private var defaults: [UserDefaultsContainer] {
29 | let standard = UserDefaultsContainer(
30 | name: "standard",
31 | defaults: .standard,
32 | excludeKeys: excludeKeys
33 | )
34 |
35 | return [standard] + suiteNames.compactMap { name in
36 | UserDefaults(suiteName: name).map {
37 | UserDefaultsContainer(
38 | name: name,
39 | defaults: $0,
40 | excludeKeys: excludeKeys
41 | )
42 | }
43 | }
44 | }
45 |
46 | public var body: some View {
47 | Group {
48 | if presentationMode.wrappedValue.isPresented {
49 | TabView {
50 | //
51 | // User
52 | //
53 | tabContent(title: "User") {
54 | SearchContainerView(type: .user, defaults: defaults)
55 | }
56 | .tabItem {
57 | Label("User", systemImage: "person")
58 | }
59 |
60 | //
61 | // System
62 | //
63 | tabContent(title: "System") {
64 | SearchContainerView(type: .system, defaults: defaults)
65 | }
66 | .tabItem {
67 | Label("System", systemImage: "iphone")
68 | }
69 | }
70 | } else {
71 | NavigationView {
72 | List {
73 | //
74 | // User
75 | //
76 | NavigationLink {
77 | SearchContainerView(type: .user, defaults: defaults)
78 | .navigationTitle("User")
79 | } label: {
80 | Label("User", systemImage: "person")
81 | }
82 |
83 | //
84 | // System
85 | //
86 | NavigationLink {
87 | SearchContainerView(type: .system, defaults: defaults)
88 | .navigationTitle("System")
89 | } label: {
90 | Label("System", systemImage: "iphone")
91 | }
92 | }
93 | .navigationTitle("UserDefaults Browser")
94 | .navigationBarTitleDisplayMode(.inline)
95 | }
96 | }
97 | }
98 | .accentColor(accentColor)
99 | .environment(\.customAccentColor, accentColor)
100 | }
101 |
102 | private func tabContent(title: String, content: () -> SearchContainerView) -> some View {
103 | NavigationView {
104 | content()
105 | .navigationTitle(title)
106 | .navigationBarTitleDisplayMode(.inline)
107 | .toolbar {
108 | ToolbarItem(placement: .primaryAction) {
109 | Button("Done") {
110 | presentationMode.wrappedValue.dismiss()
111 | }
112 | }
113 | }
114 | }
115 | .navigationViewStyle(.stack)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UserDefaults-Browser
2 |
3 | Browse and edit [UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults) on your app. (SwiftUI or UIKit)
4 |
5 | | Browse | Edit (as JSON) | Edit (Date) | Export |
6 | | -- | -- | -- | -- |
7 | |
|
|
|
|
8 |
9 |
10 | **Note:**
11 |
12 | We recommend to use [SwiftUI-Simulator](https://github.com/YusukeHosonuma/SwiftUI-Simulator), if you use it in an app built with SwiftUI.
13 | (This feature is also included)
14 |
15 | ## Supported Types
16 |
17 | - [Property List Types](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/PropertyList.html)
18 | - [x] `Array`
19 | - [x] `Dictionary`
20 | - [x] `String`
21 | - [x] `Date`
22 | - [x] `Int`
23 | - [x] `Float`
24 | - [x] `Double`
25 | - [x] `Bool`
26 | - Other
27 | - [x] `URL`
28 | - [x] `UIImage` (Read-only)
29 | - JSON encoded
30 | - [x] `Data`
31 | - [x] `String`
32 |
33 | [AppGroups](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups) (`UserDefaults(suiteName: "group.xxx")`) is also supported, please see [Configurations](#Configurations).
34 |
35 | ## Quick Start
36 |
37 | 1. Add `https://github.com/YusukeHosonuma/UserDefaultsBrowser` in the Xcode or `Package.swift`:
38 |
39 | ```swift
40 | let package = Package(
41 | dependencies: [
42 | .package(url: "https://github.com/YusukeHosonuma/UserDefaultsBrowser", from: "1.0.0"),
43 | ],
44 | targets: [
45 | .target(name: "", dependencies: [
46 | "UserDefaultsBrowser",
47 | ]),
48 | ]
49 | )
50 | ```
51 |
52 | 2. Setup launcher button.
53 |
54 | **SwiftUI:** Surround the root view with `UserDefaultsBrowserContainer`.
55 |
56 | ```swift
57 | import UserDefaultsBrowser
58 |
59 | @main
60 | struct ExampleApp: App {
61 | var body: some Scene {
62 | WindowGroup {
63 | UserDefaultsBrowserContainer {
64 | ContentView() // 💡 Your root view.
65 | }
66 | }
67 | }
68 | }
69 | ```
70 |
71 | **UIKit:** Call `setupUserDefaultsBrowserLauncher` in `viewDidLoad` of your root ViewController.
72 |
73 | ```swift
74 | import UserDefaultsBrowser
75 |
76 | class ViewController: UIViewController { // 💡 Your root ViewController.
77 |
78 | override func viewDidLoad() {
79 | super.viewDidLoad()
80 |
81 | UserDefaultsBrowser.setupUserDefaultsBrowserLauncher()
82 | }
83 | }
84 | ```
85 |
86 | 3. Tap launcher button at leading bottom.
87 |
88 | 
89 |
90 | ## Configuration
91 |
92 | Both SwiftUI and UIKit have the same options like follows.
93 |
94 | ```swift
95 | UserDefaultsBrowserContainer(
96 | suiteNames: ["group.xxx"], // AppGroups IDs
97 | excludeKeys: { $0.hasPrefix("not-display-key") }, // Exclude keys
98 | accentColor: .orange, // Your favorite color (`UIColor` type in UIKit-based API)
99 | imageName: "wrench.and.screwdriver", // SFSymbols name
100 | displayStyle: .fullScreen // `.sheet` or `.fullScreen`
101 | )
102 | ```
103 |
104 | ## Add to some View or ViewController
105 |
106 | For example, for tab-based applications, it is useful to have a tab for browsing.
107 |
108 | ### SwiftUI
109 |
110 | ```swift
111 | var body: some View {
112 | TabView {
113 | ...
114 | UserDefaultsBrowserView()
115 | .tabItem {
116 | Label("Browser", systemImage: "externaldrive")
117 | }
118 | }
119 | }
120 | ```
121 |
122 | ### UIKit
123 |
124 | ```swift
125 | class TabItemViewController: UIViewController {
126 | override func viewDidLoad() {
127 | let vc = UserDefaultsBrowserViewController()
128 | addChild(vc)
129 | view.addSubview(vc.view)
130 | vc.didMove(toParent: self)
131 |
132 | vc.view.translatesAutoresizingMaskIntoConstraints = false
133 | vc.view.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
134 | vc.view.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
135 | vc.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
136 | vc.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
137 | }
138 | }
139 | ```
140 |
141 | ## Requirements
142 |
143 | - iOS 14+
144 |
145 | ## Contributions
146 |
147 | Issues and PRs are welcome, even for minor improvements and corrections.
148 |
149 | ## Author
150 |
151 | Yusuke Hosonuma / [@tobi462](https://twitter.com/tobi462)
152 |
--------------------------------------------------------------------------------
/Example/UIKitApp/Resources/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/View/SectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/04.
6 | //
7 |
8 | import SwiftPrettyPrint
9 | import SwiftUI
10 |
11 | struct SectionView: View {
12 | let defaults: UserDefaultsContainer
13 | let type: UserDefaultsType
14 | @Binding var searchText: String
15 |
16 | @State private var toDeleteDefaults: UserDefaultsContainer?
17 | @State private var contentID = UUID() // for force-update to view.
18 | @State private var isExpanded: Bool = false
19 |
20 | private var allKeys: [String] {
21 | defaults.extractKeys(of: type)
22 | }
23 |
24 | private var filteredKeys: [String] {
25 | if searchText.isEmpty {
26 | return allKeys
27 | } else {
28 | return allKeys.filter { $0.localizedStandardContains(searchText) }
29 | }
30 | }
31 |
32 | var body: some View {
33 | DisclosureGroup(isExpanded: $isExpanded) {
34 | if filteredKeys.isEmpty {
35 | Text("No results.")
36 | .foregroundColor(.gray)
37 | } else {
38 | VStack(alignment: .leading) {
39 | //
40 | // Value
41 | //
42 | ForEach(filteredKeys.sorted(), id: \.self) { key in
43 | RowView(
44 | defaults: defaults,
45 | key: key,
46 | onUpdate: { contentID = UUID() }
47 | )
48 | }
49 | }
50 | .id(contentID)
51 | }
52 | } label: {
53 | HStack {
54 | //
55 | // standard / group.xxx
56 | //
57 | Label(
58 | defaults.name,
59 | systemImage: defaults.name == "standard" ? "person" : "externaldrive.connected.to.line.below"
60 | )
61 |
62 | Spacer()
63 |
64 | //
65 | //
66 | //
67 | if type == .user {
68 | Menu {
69 | Group {
70 | //
71 | // Dump as Swift code
72 | //
73 | Button {
74 | print(exportSwiftCode)
75 | } label: {
76 | Label("Dump as Swift code", systemImage: "terminal")
77 | }
78 |
79 | //
80 | // Dump as JSON
81 | //
82 | Button {
83 | print(exportJSON)
84 | } label: {
85 | Label("Dump as JSON", systemImage: "terminal")
86 | }
87 |
88 | Divider() // ----
89 | }
90 |
91 | Group {
92 | //
93 | // Copy as Swift code
94 | //
95 | Button {
96 | UIPasteboard.general.string = exportSwiftCode
97 | } label: {
98 | Label("Copy as Swift code", systemImage: "doc.on.doc")
99 | }
100 |
101 | //
102 | // Copy as JSON
103 | //
104 | Button {
105 | UIPasteboard.general.string = exportJSON
106 | } label: {
107 | Label("Copy as JSON", systemImage: "doc.on.doc")
108 | }
109 |
110 | Divider() // ----
111 | }
112 |
113 | //
114 | // Delete All Keys
115 | //
116 | Group {
117 | if #available(iOS 15.0, *) {
118 | Button(role: .destructive) {
119 | toDeleteDefaults = defaults
120 | } label: {
121 | Label("Delete All Keys", systemImage: "trash")
122 | }
123 | } else {
124 | Button {
125 | toDeleteDefaults = defaults
126 | } label: {
127 | Label("Delete All Keys", systemImage: "trash")
128 | }
129 | }
130 | }
131 | .disabled(filteredKeys.isEmpty)
132 | } label: {
133 | Image(systemName: "ellipsis.circle")
134 | .padding(.trailing, 8)
135 | }
136 | }
137 | }
138 | }
139 | .textCase(nil)
140 | .onAppear {
141 | isExpanded = UserDefaults.standard.bool(forKey: isExpandedKey)
142 | }
143 | .onChange(of: isExpanded) {
144 | UserDefaults.standard.set($0, forKey: isExpandedKey)
145 | }
146 | .alert(item: $toDeleteDefaults) { defaultsWrapper in
147 | //
148 | // ⚠️ Delete all keys
149 | //
150 | Alert(
151 | title: Text("Delete All Keys?"),
152 | message: Text("Are you delete all keys from '\(defaultsWrapper.name)'?"),
153 | primaryButton: .cancel(),
154 | secondaryButton: .destructive(Text("Delete"), action: {
155 | defaults.removeAll(of: type)
156 | })
157 | )
158 | }
159 | }
160 |
161 | private var isExpandedKey: String {
162 | //
163 | // e.g. "xxx/xxx/isExpanded/user.standard"
164 | //
165 | UserDefaults.keyPrefix + "isExpanded/\(type.rawValue).\(defaults.name)"
166 | }
167 |
168 | private var exportSwiftCode: String {
169 | let dict = allKeys.reduce(into: [String: Any]()) { dict, key in
170 | dict[key] = defaults.lookup(forKey: key)
171 | }
172 |
173 | var string = ""
174 | Pretty.prettyPrintDebug(dict, to: &string)
175 | return string
176 | }
177 |
178 | private var exportJSON: String {
179 | let dict = allKeys
180 | .reduce(into: [String: Any]()) { dict, key in
181 | let value = defaults.lookup(forKey: key)
182 |
183 | switch value {
184 | case let url as URL:
185 | dict[key] = url.absoluteString
186 | case let date as Date:
187 | dict[key] = date.toString()
188 | default:
189 | dict[key] = value
190 | }
191 | }
192 |
193 | return dict.prettyJSON
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/View/RowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/03.
6 | //
7 |
8 | import SwiftPrettyPrint
9 | import SwiftUI
10 | import SwiftUICommon
11 |
12 | private enum Value {
13 | case text(String)
14 | case url(URL)
15 | case decodedJSON(String, String)
16 | case image(UIImage)
17 | case data(String)
18 |
19 | var isEditable: Bool {
20 | switch self {
21 | case .image, .data:
22 | return false
23 | case .text, .url, .decodedJSON:
24 | return true
25 | }
26 | }
27 |
28 | var exportString: String {
29 | switch self {
30 | case let .text(text):
31 | return text
32 |
33 | case let .url(url):
34 | return url.absoluteString
35 |
36 | case let .decodedJSON(text, message):
37 | return
38 | """
39 | \(text)
40 | \(message)
41 | """
42 |
43 | case .image:
44 | return ""
45 |
46 | case let .data(text):
47 | return text
48 | }
49 | }
50 | }
51 |
52 | struct RowView: View {
53 | @Environment(\.customAccentColor) private var customAccentColor
54 |
55 | private var defaults: UserDefaultsContainer
56 | private var key: String
57 | private var onUpdate: () -> Void
58 |
59 | init(defaults: UserDefaultsContainer, key: String, onUpdate: @escaping () -> Void) {
60 | self.defaults = defaults
61 | self.key = key
62 | self.onUpdate = onUpdate
63 | }
64 |
65 | @State private var isPresentedEditSheet = false
66 |
67 | private var value: Value {
68 | let value = defaults.lookup(forKey: key)
69 |
70 | switch value {
71 | //
72 | // 💡 Note:
73 | // `Array` and `Dictionary` are display as JSON string.
74 | // Because editor of `[String: Any]` is input as JSON.
75 | //
76 | case let value as [Any]:
77 | return .text(
78 | value.isEmpty ? "[]" : value.prettyJSON
79 | )
80 | case let value as [String: Any]:
81 | return .text(
82 | value.isEmpty ? "{}" : value.prettyJSON
83 | )
84 | case let value as JSONData:
85 | return .decodedJSON(value.dictionary.prettyJSON, "")
86 | case let value as JSONString:
87 | return .decodedJSON(value.dictionary.prettyJSON, "")
88 | case let value as UIImage:
89 | return .image(value)
90 | case let value as Date:
91 | return .text(value.toString())
92 | case let value as URL:
93 | return .url(value)
94 | case _ as Data:
95 | return .data(prettyString(value))
96 | default:
97 | return .text(prettyString(value))
98 | }
99 | }
100 |
101 | private var exportString: String {
102 | """
103 |
104 | \(key):
105 | \(value.exportString)
106 | """
107 | }
108 |
109 | var body: some View {
110 | GroupBox {
111 | content()
112 | .padding(.top, 2)
113 | } label: {
114 | HStack {
115 | Text(key)
116 | .font(.system(size: 14, weight: .bold, design: .monospaced))
117 | .lineLimit(1)
118 | .truncationMode(.middle)
119 | .foregroundColor(.gray)
120 | Spacer()
121 |
122 | Group {
123 | //
124 | // Console
125 | //
126 | Button {
127 | print(exportString)
128 | } label: {
129 | Image(systemName: "terminal")
130 | }
131 | .padding(.trailing, 2)
132 |
133 | //
134 | // Copy
135 | //
136 | Button {
137 | UIPasteboard.general.string = exportString
138 | } label: {
139 | Image(systemName: "doc.on.doc")
140 | }
141 | .padding(.trailing, 2)
142 |
143 | //
144 | // Edit
145 | //
146 | Button {
147 | isPresentedEditSheet.toggle()
148 | } label: {
149 | Image(systemName: "pencil")
150 | }
151 | .enabled(value.isEditable)
152 | }
153 | .font(.system(size: 16, weight: .regular))
154 | }
155 | }
156 | .sheet(isPresented: $isPresentedEditSheet, onDismiss: { onUpdate() }) {
157 | ValueEditView(defaults: defaults, key: key)
158 | //
159 | // ⚠️ SwiftUI Bug: AccentColor is not inherited to sheet.
160 | //
161 | .accentColor(customAccentColor)
162 | }
163 | }
164 |
165 | @ViewBuilder
166 | func content() -> some View {
167 | HStack {
168 | VStack(alignment: .leading) {
169 | switch value {
170 | case let .text(text):
171 | Text(text)
172 |
173 | case let .decodedJSON(text, message):
174 | Text(text)
175 | Text(message)
176 | .foregroundColor(.gray)
177 | .padding(.top, 2)
178 |
179 | case let .url(url):
180 | Link(url.absoluteString, destination: url)
181 |
182 | case let .image(uiImage):
183 | //
184 | // ⚠️ Display is corrupted with iOS 14. (SwiftUI bug, maybe)
185 | //
186 | if #available(iOS 15, *) {
187 | ResizableImage(uiImage: uiImage, contentMode: .fit)
188 | } else {
189 | if uiImage.size.width < 200 {
190 | Image(uiImage: uiImage)
191 | } else {
192 | Image(uiImage: uiImage)
193 | .resizable()
194 | .scaledToFit()
195 | .frame(width: 200)
196 | }
197 | }
198 |
199 | case let .data(text):
200 | Text(text)
201 | .lineLimit(1)
202 | Text("")
203 | .foregroundColor(.gray)
204 | .padding(.top, 2)
205 | }
206 | }
207 | Spacer()
208 | }
209 | .lineLimit(nil)
210 | .fixedSize(horizontal: false, vertical: true)
211 | .font(.codeStyle)
212 | }
213 | }
214 |
215 | private func prettyString(_ value: Any?) -> String {
216 | guard let value = value else { return "nil" }
217 |
218 | var option = Pretty.sharedOption
219 | option.indentSize = 2
220 |
221 | var output = ""
222 | Pretty.prettyPrintDebug(value, option: option, to: &output)
223 | return output
224 | }
225 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsBrowser/View/ValueEditView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yusuke Hosonuma on 2022/05/03.
6 | //
7 |
8 | import CasePaths
9 | import SwiftUI
10 |
11 | private enum ValueType: Identifiable {
12 | case string(String)
13 | case bool(Bool)
14 | case int(Int)
15 | case float(Float)
16 | case double(Double)
17 | case url(URL)
18 | case date(Date)
19 | case array([Any])
20 | case dictionary([String: Any])
21 | case jsonData(JSONData)
22 | case jsonString(JSONString)
23 | case unknown
24 |
25 | var typeName: String {
26 | switch self {
27 | case .string: return "String"
28 | case .bool: return "Bool"
29 | case .int: return "Int"
30 | case .float: return "Float"
31 | case .double: return "Double"
32 | case .url: return "URL"
33 | case .date: return "Date"
34 | case .array: return "[Any]"
35 | case .dictionary: return "[String: Any]"
36 | case .jsonData: return "Data"
37 | case .jsonString: return "String"
38 | case .unknown: return "(Unkonwn)"
39 | }
40 | }
41 |
42 | var id: String { typeName }
43 |
44 | var isUnknown: Bool {
45 | if case .unknown = self {
46 | return true
47 | } else {
48 | return false
49 | }
50 | }
51 | }
52 |
53 | struct ValueEditView: View {
54 | // 💡 iOS 15+: `\.dismiss`
55 | @Environment(\.presentationMode) private var presentationMode
56 |
57 | private let name: String
58 | private let defaults: UserDefaults
59 | private let key: String
60 |
61 | init(defaults: UserDefaultsContainer, key: String) {
62 | name = defaults.name
63 | self.defaults = defaults.defaults
64 | self.key = key
65 | }
66 |
67 | @State private var value: ValueType = .unknown
68 | @State private var isValid = true
69 | @State private var isPresentedConfirmDelete = false
70 |
71 | var body: some View {
72 | NavigationView {
73 | VStack(alignment: .leading) {
74 | Text("\(key): \(value.typeName)")
75 | .font(.system(size: 14, weight: .bold, design: .monospaced))
76 | .foregroundColor(.gray)
77 | .padding(.horizontal)
78 |
79 | valueEditor()
80 | }
81 | .navigationTitle(name)
82 | .navigationBarTitleDisplayMode(.inline)
83 | .toolbar {
84 | ToolbarItemGroup(placement: .destructiveAction) {
85 | Button("Save") {
86 | save()
87 | presentationMode.wrappedValue.dismiss()
88 | }
89 | .disabled(value.isUnknown || isValid == false)
90 | }
91 | ToolbarItem(placement: .cancellationAction) {
92 | Button("Cancel") {
93 | presentationMode.wrappedValue.dismiss()
94 | }
95 | }
96 | ToolbarItem(placement: .bottomBar) {
97 | //
98 | // ⚠️ SwiftUI Bug: Not align to trailing on iPhone XS (15.4.1) of real-device.
99 | // (No problem in the simulator)
100 | //
101 | HStack {
102 | Spacer()
103 | Button {
104 | isPresentedConfirmDelete.toggle()
105 | } label: {
106 | Image(systemName: "trash")
107 | }
108 | }
109 | }
110 | }
111 | }
112 | .onAppear {
113 | load()
114 | }
115 | //
116 | // ⚠️ Delete Key?
117 | //
118 | .alert(isPresented: $isPresentedConfirmDelete) {
119 | Alert(
120 | title: Text("Delete Key?"),
121 | message: Text("Are you delete '\(key)'?"),
122 | primaryButton: .cancel(),
123 | secondaryButton: .destructive(Text("Delete"), action: {
124 | delete()
125 | presentationMode.wrappedValue.dismiss()
126 | })
127 | )
128 | }
129 | }
130 |
131 | // MARK: Editor
132 |
133 | @ViewBuilder
134 | private func valueEditor() -> some View {
135 | //
136 | // 💡 Note: `switch` statement is only for completeness check by compiler.
137 | //
138 | switch value {
139 | case .string:
140 | if let binding = $value.case(/ValueType.string) {
141 | TextEditor(text: binding)
142 | .style(.valueEditor)
143 | .padding([.horizontal, .bottom])
144 | }
145 |
146 | case .bool:
147 | if let binding = $value.case(/ValueType.bool) {
148 | BoolEditor(value: binding)
149 | .padding([.horizontal, .bottom])
150 | }
151 |
152 | case .int:
153 | if let binding = $value.case(/ValueType.int) {
154 | StringRepresentableEditor(binding, isValid: $isValid)
155 | }
156 |
157 | case .float:
158 | if let binding = $value.case(/ValueType.float) {
159 | StringRepresentableEditor(binding, isValid: $isValid)
160 | }
161 |
162 | case .double:
163 | if let binding = $value.case(/ValueType.double) {
164 | StringRepresentableEditor(binding, isValid: $isValid)
165 | }
166 |
167 | case .url:
168 | if let binding = $value.case(/ValueType.url) {
169 | StringRepresentableEditor(binding, isValid: $isValid, style: .multiline)
170 | }
171 |
172 | case .date:
173 | if let binding = $value.case(/ValueType.date) {
174 | DateEditor(date: binding, isValid: $isValid)
175 | }
176 |
177 | case .array:
178 | if let binding = $value.case(/ValueType.array) {
179 | jsonEditor(
180 | binding.map(get: ArrayWrapper.init, set: { $0.array })
181 | )
182 | }
183 |
184 | case .dictionary:
185 | if let binding = $value.case(/ValueType.dictionary) {
186 | jsonEditor(
187 | binding.map(get: DictionaryWrapper.init, set: { $0.dictionary })
188 | )
189 | }
190 |
191 | case .jsonData:
192 | if let binding = $value.case(/ValueType.jsonData) {
193 | jsonEditor(
194 | binding.map(
195 | get: { DictionaryWrapper($0.dictionary) },
196 | set: { JSONData(dictionary: $0.dictionary) }
197 | )
198 | )
199 | }
200 |
201 | case .jsonString:
202 | if let binding = $value.case(/ValueType.jsonString) {
203 | jsonEditor(
204 | binding.map(
205 | get: { DictionaryWrapper($0.dictionary) },
206 | set: { JSONString(dictionary: $0.dictionary) }
207 | )
208 | )
209 | }
210 |
211 | case .unknown:
212 | VStack(spacing: 16) {
213 | Text("This type was not supported yet.")
214 | Text("Contributions are welcome!")
215 | Link("YusukeHosonuma/SwiftUI-Simulator",
216 | destination: URL(string: "https://github.com/YusukeHosonuma/SwiftUI-Simulator")!)
217 | }
218 | .font(.system(size: 14, weight: .regular, design: .monospaced))
219 | .padding()
220 | }
221 | }
222 |
223 | private func jsonEditor(_ binding: Binding) -> some View {
224 | VStack {
225 | StringRepresentableEditor(binding, isValid: $isValid, style: .multiline)
226 | Text("Please input as JSON.")
227 | .font(.footnote)
228 | .foregroundColor(.gray)
229 | .padding(.bottom)
230 | }
231 | }
232 |
233 | // MARK: Load / Save
234 |
235 | private func load() {
236 | switch defaults.lookup(forKey: key) {
237 | case let v as String:
238 | value = .string(v)
239 |
240 | case let v as Bool:
241 | value = .bool(v)
242 |
243 | case let v as Int:
244 | value = .int(v)
245 |
246 | case let v as Float:
247 | value = .float(v)
248 |
249 | case let v as Double:
250 | value = .double(v)
251 |
252 | case let v as URL:
253 | value = .url(v)
254 |
255 | case let v as Date:
256 | value = .date(v)
257 |
258 | case let v as [Any]:
259 | value = .array(v)
260 |
261 | case let v as [String: Any]:
262 | value = .dictionary(v)
263 |
264 | case let v as JSONData:
265 | value = .jsonData(v)
266 |
267 | case let v as JSONString:
268 | value = .jsonString(v)
269 |
270 | default:
271 | let object = defaults.object(forKey: key)
272 | value = .unknown
273 | print("type: \(String(describing: object.self))")
274 | }
275 | }
276 |
277 | private func save() {
278 | func write(_ value: T?) {
279 | defaults.set(value, forKey: key)
280 | }
281 |
282 | switch value {
283 | case let .string(value):
284 | write(value)
285 |
286 | case let .bool(value):
287 | write(value)
288 |
289 | case let .int(value):
290 | write(value)
291 |
292 | case let .float(value):
293 | write(value)
294 |
295 | case let .double(value):
296 | write(value)
297 |
298 | case let .url(value):
299 | //
300 | // ⚠️ This code cause crash, why?
301 | //
302 | // write(valueURL)
303 | //
304 | defaults.set(value, forKey: key)
305 |
306 | case let .date(value):
307 | write(value)
308 |
309 | case let .array(value):
310 | write(value)
311 |
312 | case let .dictionary(value):
313 | write(value)
314 |
315 | case let .jsonData(value):
316 | if let data = value.dictionary.prettyJSON.data(using: .utf8) {
317 | write(data)
318 | } else {
319 | preconditionFailure("Can't save JSON as `Data` type.")
320 | }
321 |
322 | case let .jsonString(value):
323 | if let json = value.dictionary.serializedJSON {
324 | write(json)
325 | } else {
326 | preconditionFailure("Can't save JSON as `String` type.")
327 | }
328 |
329 | case .unknown:
330 | return
331 | }
332 | }
333 |
334 | private func delete() {
335 | defaults.removeObject(forKey: key)
336 | }
337 | }
338 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 707790AF2825628500A851C7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707790AE2825628500A851C7 /* AppDelegate.swift */; };
11 | 707790B12825628500A851C7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707790B02825628500A851C7 /* SceneDelegate.swift */; };
12 | 707790B32825628500A851C7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707790B22825628500A851C7 /* ViewController.swift */; };
13 | 707790B62825628500A851C7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 707790B42825628500A851C7 /* Main.storyboard */; };
14 | 707790B82825628600A851C7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 707790B72825628600A851C7 /* Assets.xcassets */; };
15 | 707790BB2825628600A851C7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 707790B92825628600A851C7 /* LaunchScreen.storyboard */; };
16 | 707790C128256C8C00A851C7 /* UserDefaultsBrowser in Frameworks */ = {isa = PBXBuildFile; productRef = 707790C028256C8C00A851C7 /* UserDefaultsBrowser */; };
17 | 70A4A51F282519B7007F2033 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A4A50F282519B5007F2033 /* ExampleApp.swift */; };
18 | 70A4A520282519B7007F2033 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A4A50F282519B5007F2033 /* ExampleApp.swift */; };
19 | 70A4A521282519B7007F2033 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A4A510282519B6007F2033 /* ContentView.swift */; };
20 | 70A4A522282519B7007F2033 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A4A510282519B6007F2033 /* ContentView.swift */; };
21 | 70A4A523282519B7007F2033 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 70A4A511282519B7007F2033 /* Assets.xcassets */; };
22 | 70A4A524282519B7007F2033 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 70A4A511282519B7007F2033 /* Assets.xcassets */; };
23 | 70A4A53228251B1D007F2033 /* UserDefaultsBrowser in Frameworks */ = {isa = PBXBuildFile; productRef = 70A4A53128251B1D007F2033 /* UserDefaultsBrowser */; };
24 | 70F03A912826266600D86CAB /* BrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F03A902826266600D86CAB /* BrowserViewController.swift */; };
25 | /* End PBXBuildFile section */
26 |
27 | /* Begin PBXFileReference section */
28 | 707790AC2825628500A851C7 /* UIKitApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIKitApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
29 | 707790AE2825628500A851C7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
30 | 707790B02825628500A851C7 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
31 | 707790B22825628500A851C7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
32 | 707790B52825628500A851C7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
33 | 707790B72825628600A851C7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
34 | 707790BA2825628600A851C7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
35 | 707790BC2825628600A851C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
36 | 707790C228256C9F00A851C7 /* UIKitApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = UIKitApp.entitlements; sourceTree = ""; };
37 | 70A4A50F282519B5007F2033 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; };
38 | 70A4A510282519B6007F2033 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
39 | 70A4A511282519B7007F2033 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
40 | 70A4A516282519B7007F2033 /* SwiftUIExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUIExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
41 | 70A4A51C282519B7007F2033 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
42 | 70A4A51E282519B7007F2033 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; };
43 | 70A4A52E28251A19007F2033 /* UserDefaults-Browser */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "UserDefaults-Browser"; path = ..; sourceTree = ""; };
44 | 70A4A52F28251AB3007F2033 /* iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iOS.entitlements; sourceTree = ""; };
45 | 70B1D331282C870A000D0386 /* SwiftUI-Common */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "SwiftUI-Common"; path = "../../SwiftUI-Common"; sourceTree = ""; };
46 | 70F03A902826266600D86CAB /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = ""; };
47 | /* End PBXFileReference section */
48 |
49 | /* Begin PBXFrameworksBuildPhase section */
50 | 707790A92825628500A851C7 /* Frameworks */ = {
51 | isa = PBXFrameworksBuildPhase;
52 | buildActionMask = 2147483647;
53 | files = (
54 | 707790C128256C8C00A851C7 /* UserDefaultsBrowser in Frameworks */,
55 | );
56 | runOnlyForDeploymentPostprocessing = 0;
57 | };
58 | 70A4A513282519B7007F2033 /* Frameworks */ = {
59 | isa = PBXFrameworksBuildPhase;
60 | buildActionMask = 2147483647;
61 | files = (
62 | 70A4A53228251B1D007F2033 /* UserDefaultsBrowser in Frameworks */,
63 | );
64 | runOnlyForDeploymentPostprocessing = 0;
65 | };
66 | 70A4A519282519B7007F2033 /* Frameworks */ = {
67 | isa = PBXFrameworksBuildPhase;
68 | buildActionMask = 2147483647;
69 | files = (
70 | );
71 | runOnlyForDeploymentPostprocessing = 0;
72 | };
73 | /* End PBXFrameworksBuildPhase section */
74 |
75 | /* Begin PBXGroup section */
76 | 707790AD2825628500A851C7 /* UIKitApp */ = {
77 | isa = PBXGroup;
78 | children = (
79 | 70A5B35328278206004E31B7 /* Resources */,
80 | 707790AE2825628500A851C7 /* AppDelegate.swift */,
81 | 707790B02825628500A851C7 /* SceneDelegate.swift */,
82 | 707790B22825628500A851C7 /* ViewController.swift */,
83 | 70F03A902826266600D86CAB /* BrowserViewController.swift */,
84 | );
85 | path = UIKitApp;
86 | sourceTree = "";
87 | };
88 | 70A4A509282519B5007F2033 = {
89 | isa = PBXGroup;
90 | children = (
91 | 70B1D331282C870A000D0386 /* SwiftUI-Common */,
92 | 70A4A52D28251A19007F2033 /* Packages */,
93 | 70A4A50E282519B5007F2033 /* Shared */,
94 | 70A4A53328251C48007F2033 /* iOS */,
95 | 70A4A51D282519B7007F2033 /* macOS */,
96 | 707790AD2825628500A851C7 /* UIKitApp */,
97 | 70A4A517282519B7007F2033 /* Products */,
98 | 70A4A53028251B1D007F2033 /* Frameworks */,
99 | );
100 | sourceTree = "";
101 | };
102 | 70A4A50E282519B5007F2033 /* Shared */ = {
103 | isa = PBXGroup;
104 | children = (
105 | 70A4A50F282519B5007F2033 /* ExampleApp.swift */,
106 | 70A4A510282519B6007F2033 /* ContentView.swift */,
107 | 70A4A511282519B7007F2033 /* Assets.xcassets */,
108 | );
109 | path = Shared;
110 | sourceTree = "";
111 | };
112 | 70A4A517282519B7007F2033 /* Products */ = {
113 | isa = PBXGroup;
114 | children = (
115 | 70A4A516282519B7007F2033 /* SwiftUIExample.app */,
116 | 70A4A51C282519B7007F2033 /* Example.app */,
117 | 707790AC2825628500A851C7 /* UIKitApp.app */,
118 | );
119 | name = Products;
120 | sourceTree = "";
121 | };
122 | 70A4A51D282519B7007F2033 /* macOS */ = {
123 | isa = PBXGroup;
124 | children = (
125 | 70A4A51E282519B7007F2033 /* macOS.entitlements */,
126 | );
127 | path = macOS;
128 | sourceTree = "";
129 | };
130 | 70A4A52D28251A19007F2033 /* Packages */ = {
131 | isa = PBXGroup;
132 | children = (
133 | 70A4A52E28251A19007F2033 /* UserDefaults-Browser */,
134 | );
135 | name = Packages;
136 | sourceTree = "";
137 | };
138 | 70A4A53028251B1D007F2033 /* Frameworks */ = {
139 | isa = PBXGroup;
140 | children = (
141 | );
142 | name = Frameworks;
143 | sourceTree = "";
144 | };
145 | 70A4A53328251C48007F2033 /* iOS */ = {
146 | isa = PBXGroup;
147 | children = (
148 | 70A4A52F28251AB3007F2033 /* iOS.entitlements */,
149 | );
150 | path = iOS;
151 | sourceTree = "";
152 | };
153 | 70A5B35328278206004E31B7 /* Resources */ = {
154 | isa = PBXGroup;
155 | children = (
156 | 707790B42825628500A851C7 /* Main.storyboard */,
157 | 707790B92825628600A851C7 /* LaunchScreen.storyboard */,
158 | 707790B72825628600A851C7 /* Assets.xcassets */,
159 | 707790BC2825628600A851C7 /* Info.plist */,
160 | 707790C228256C9F00A851C7 /* UIKitApp.entitlements */,
161 | );
162 | path = Resources;
163 | sourceTree = "";
164 | };
165 | /* End PBXGroup section */
166 |
167 | /* Begin PBXNativeTarget section */
168 | 707790AB2825628500A851C7 /* UIKitApp */ = {
169 | isa = PBXNativeTarget;
170 | buildConfigurationList = 707790BF2825628600A851C7 /* Build configuration list for PBXNativeTarget "UIKitApp" */;
171 | buildPhases = (
172 | 707790A82825628500A851C7 /* Sources */,
173 | 707790A92825628500A851C7 /* Frameworks */,
174 | 707790AA2825628500A851C7 /* Resources */,
175 | );
176 | buildRules = (
177 | );
178 | dependencies = (
179 | );
180 | name = UIKitApp;
181 | packageProductDependencies = (
182 | 707790C028256C8C00A851C7 /* UserDefaultsBrowser */,
183 | );
184 | productName = UIKitApp;
185 | productReference = 707790AC2825628500A851C7 /* UIKitApp.app */;
186 | productType = "com.apple.product-type.application";
187 | };
188 | 70A4A515282519B7007F2033 /* Example (iOS) */ = {
189 | isa = PBXNativeTarget;
190 | buildConfigurationList = 70A4A527282519B7007F2033 /* Build configuration list for PBXNativeTarget "Example (iOS)" */;
191 | buildPhases = (
192 | 70A4A512282519B7007F2033 /* Sources */,
193 | 70A4A513282519B7007F2033 /* Frameworks */,
194 | 70A4A514282519B7007F2033 /* Resources */,
195 | );
196 | buildRules = (
197 | );
198 | dependencies = (
199 | );
200 | name = "Example (iOS)";
201 | packageProductDependencies = (
202 | 70A4A53128251B1D007F2033 /* UserDefaultsBrowser */,
203 | );
204 | productName = "Example (iOS)";
205 | productReference = 70A4A516282519B7007F2033 /* SwiftUIExample.app */;
206 | productType = "com.apple.product-type.application";
207 | };
208 | 70A4A51B282519B7007F2033 /* Example (macOS) */ = {
209 | isa = PBXNativeTarget;
210 | buildConfigurationList = 70A4A52A282519B7007F2033 /* Build configuration list for PBXNativeTarget "Example (macOS)" */;
211 | buildPhases = (
212 | 70A4A518282519B7007F2033 /* Sources */,
213 | 70A4A519282519B7007F2033 /* Frameworks */,
214 | 70A4A51A282519B7007F2033 /* Resources */,
215 | );
216 | buildRules = (
217 | );
218 | dependencies = (
219 | );
220 | name = "Example (macOS)";
221 | productName = "Example (macOS)";
222 | productReference = 70A4A51C282519B7007F2033 /* Example.app */;
223 | productType = "com.apple.product-type.application";
224 | };
225 | /* End PBXNativeTarget section */
226 |
227 | /* Begin PBXProject section */
228 | 70A4A50A282519B5007F2033 /* Project object */ = {
229 | isa = PBXProject;
230 | attributes = {
231 | BuildIndependentTargetsInParallel = 1;
232 | LastSwiftUpdateCheck = 1330;
233 | LastUpgradeCheck = 1330;
234 | TargetAttributes = {
235 | 707790AB2825628500A851C7 = {
236 | CreatedOnToolsVersion = 13.3.1;
237 | };
238 | 70A4A515282519B7007F2033 = {
239 | CreatedOnToolsVersion = 13.3.1;
240 | };
241 | 70A4A51B282519B7007F2033 = {
242 | CreatedOnToolsVersion = 13.3.1;
243 | };
244 | };
245 | };
246 | buildConfigurationList = 70A4A50D282519B5007F2033 /* Build configuration list for PBXProject "Example" */;
247 | compatibilityVersion = "Xcode 13.0";
248 | developmentRegion = en;
249 | hasScannedForEncodings = 0;
250 | knownRegions = (
251 | en,
252 | Base,
253 | );
254 | mainGroup = 70A4A509282519B5007F2033;
255 | productRefGroup = 70A4A517282519B7007F2033 /* Products */;
256 | projectDirPath = "";
257 | projectRoot = "";
258 | targets = (
259 | 70A4A515282519B7007F2033 /* Example (iOS) */,
260 | 70A4A51B282519B7007F2033 /* Example (macOS) */,
261 | 707790AB2825628500A851C7 /* UIKitApp */,
262 | );
263 | };
264 | /* End PBXProject section */
265 |
266 | /* Begin PBXResourcesBuildPhase section */
267 | 707790AA2825628500A851C7 /* Resources */ = {
268 | isa = PBXResourcesBuildPhase;
269 | buildActionMask = 2147483647;
270 | files = (
271 | 707790BB2825628600A851C7 /* LaunchScreen.storyboard in Resources */,
272 | 707790B82825628600A851C7 /* Assets.xcassets in Resources */,
273 | 707790B62825628500A851C7 /* Main.storyboard in Resources */,
274 | );
275 | runOnlyForDeploymentPostprocessing = 0;
276 | };
277 | 70A4A514282519B7007F2033 /* Resources */ = {
278 | isa = PBXResourcesBuildPhase;
279 | buildActionMask = 2147483647;
280 | files = (
281 | 70A4A523282519B7007F2033 /* Assets.xcassets in Resources */,
282 | );
283 | runOnlyForDeploymentPostprocessing = 0;
284 | };
285 | 70A4A51A282519B7007F2033 /* Resources */ = {
286 | isa = PBXResourcesBuildPhase;
287 | buildActionMask = 2147483647;
288 | files = (
289 | 70A4A524282519B7007F2033 /* Assets.xcassets in Resources */,
290 | );
291 | runOnlyForDeploymentPostprocessing = 0;
292 | };
293 | /* End PBXResourcesBuildPhase section */
294 |
295 | /* Begin PBXSourcesBuildPhase section */
296 | 707790A82825628500A851C7 /* Sources */ = {
297 | isa = PBXSourcesBuildPhase;
298 | buildActionMask = 2147483647;
299 | files = (
300 | 707790B32825628500A851C7 /* ViewController.swift in Sources */,
301 | 70F03A912826266600D86CAB /* BrowserViewController.swift in Sources */,
302 | 707790AF2825628500A851C7 /* AppDelegate.swift in Sources */,
303 | 707790B12825628500A851C7 /* SceneDelegate.swift in Sources */,
304 | );
305 | runOnlyForDeploymentPostprocessing = 0;
306 | };
307 | 70A4A512282519B7007F2033 /* Sources */ = {
308 | isa = PBXSourcesBuildPhase;
309 | buildActionMask = 2147483647;
310 | files = (
311 | 70A4A521282519B7007F2033 /* ContentView.swift in Sources */,
312 | 70A4A51F282519B7007F2033 /* ExampleApp.swift in Sources */,
313 | );
314 | runOnlyForDeploymentPostprocessing = 0;
315 | };
316 | 70A4A518282519B7007F2033 /* Sources */ = {
317 | isa = PBXSourcesBuildPhase;
318 | buildActionMask = 2147483647;
319 | files = (
320 | 70A4A522282519B7007F2033 /* ContentView.swift in Sources */,
321 | 70A4A520282519B7007F2033 /* ExampleApp.swift in Sources */,
322 | );
323 | runOnlyForDeploymentPostprocessing = 0;
324 | };
325 | /* End PBXSourcesBuildPhase section */
326 |
327 | /* Begin PBXVariantGroup section */
328 | 707790B42825628500A851C7 /* Main.storyboard */ = {
329 | isa = PBXVariantGroup;
330 | children = (
331 | 707790B52825628500A851C7 /* Base */,
332 | );
333 | name = Main.storyboard;
334 | sourceTree = "";
335 | };
336 | 707790B92825628600A851C7 /* LaunchScreen.storyboard */ = {
337 | isa = PBXVariantGroup;
338 | children = (
339 | 707790BA2825628600A851C7 /* Base */,
340 | );
341 | name = LaunchScreen.storyboard;
342 | sourceTree = "";
343 | };
344 | /* End PBXVariantGroup section */
345 |
346 | /* Begin XCBuildConfiguration section */
347 | 707790BD2825628600A851C7 /* Debug */ = {
348 | isa = XCBuildConfiguration;
349 | buildSettings = {
350 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
351 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
352 | CODE_SIGN_ENTITLEMENTS = UIKitApp/Resources/UIKitApp.entitlements;
353 | CODE_SIGN_STYLE = Automatic;
354 | CURRENT_PROJECT_VERSION = 1;
355 | DEVELOPMENT_TEAM = P437HSA6PY;
356 | GENERATE_INFOPLIST_FILE = YES;
357 | INFOPLIST_FILE = UIKitApp/Resources/Info.plist;
358 | INFOPLIST_KEY_CFBundleDisplayName = UIKitExample;
359 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
360 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
361 | INFOPLIST_KEY_UIMainStoryboardFile = Main;
362 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
363 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
364 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
365 | LD_RUNPATH_SEARCH_PATHS = (
366 | "$(inherited)",
367 | "@executable_path/Frameworks",
368 | );
369 | MARKETING_VERSION = 1.0;
370 | PRODUCT_BUNDLE_IDENTIFIER = tech.penginmura.UIKitApp;
371 | PRODUCT_NAME = "$(TARGET_NAME)";
372 | SDKROOT = iphoneos;
373 | SWIFT_EMIT_LOC_STRINGS = YES;
374 | SWIFT_VERSION = 5.0;
375 | TARGETED_DEVICE_FAMILY = "1,2";
376 | };
377 | name = Debug;
378 | };
379 | 707790BE2825628600A851C7 /* Release */ = {
380 | isa = XCBuildConfiguration;
381 | buildSettings = {
382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
383 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
384 | CODE_SIGN_ENTITLEMENTS = UIKitApp/Resources/UIKitApp.entitlements;
385 | CODE_SIGN_STYLE = Automatic;
386 | CURRENT_PROJECT_VERSION = 1;
387 | DEVELOPMENT_TEAM = P437HSA6PY;
388 | GENERATE_INFOPLIST_FILE = YES;
389 | INFOPLIST_FILE = UIKitApp/Resources/Info.plist;
390 | INFOPLIST_KEY_CFBundleDisplayName = UIKitExample;
391 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
392 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
393 | INFOPLIST_KEY_UIMainStoryboardFile = Main;
394 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
395 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
396 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
397 | LD_RUNPATH_SEARCH_PATHS = (
398 | "$(inherited)",
399 | "@executable_path/Frameworks",
400 | );
401 | MARKETING_VERSION = 1.0;
402 | PRODUCT_BUNDLE_IDENTIFIER = tech.penginmura.UIKitApp;
403 | PRODUCT_NAME = "$(TARGET_NAME)";
404 | SDKROOT = iphoneos;
405 | SWIFT_EMIT_LOC_STRINGS = YES;
406 | SWIFT_VERSION = 5.0;
407 | TARGETED_DEVICE_FAMILY = "1,2";
408 | VALIDATE_PRODUCT = YES;
409 | };
410 | name = Release;
411 | };
412 | 70A4A525282519B7007F2033 /* Debug */ = {
413 | isa = XCBuildConfiguration;
414 | buildSettings = {
415 | ALWAYS_SEARCH_USER_PATHS = NO;
416 | CLANG_ANALYZER_NONNULL = YES;
417 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
418 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
419 | CLANG_ENABLE_MODULES = YES;
420 | CLANG_ENABLE_OBJC_ARC = YES;
421 | CLANG_ENABLE_OBJC_WEAK = YES;
422 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
423 | CLANG_WARN_BOOL_CONVERSION = YES;
424 | CLANG_WARN_COMMA = YES;
425 | CLANG_WARN_CONSTANT_CONVERSION = YES;
426 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
427 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
428 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
429 | CLANG_WARN_EMPTY_BODY = YES;
430 | CLANG_WARN_ENUM_CONVERSION = YES;
431 | CLANG_WARN_INFINITE_RECURSION = YES;
432 | CLANG_WARN_INT_CONVERSION = YES;
433 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
434 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
435 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
436 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
437 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
438 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
439 | CLANG_WARN_STRICT_PROTOTYPES = YES;
440 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
441 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
442 | CLANG_WARN_UNREACHABLE_CODE = YES;
443 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
444 | COPY_PHASE_STRIP = NO;
445 | DEBUG_INFORMATION_FORMAT = dwarf;
446 | ENABLE_STRICT_OBJC_MSGSEND = YES;
447 | ENABLE_TESTABILITY = YES;
448 | GCC_C_LANGUAGE_STANDARD = gnu11;
449 | GCC_DYNAMIC_NO_PIC = NO;
450 | GCC_NO_COMMON_BLOCKS = YES;
451 | GCC_OPTIMIZATION_LEVEL = 0;
452 | GCC_PREPROCESSOR_DEFINITIONS = (
453 | "DEBUG=1",
454 | "$(inherited)",
455 | );
456 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
457 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
458 | GCC_WARN_UNDECLARED_SELECTOR = YES;
459 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
460 | GCC_WARN_UNUSED_FUNCTION = YES;
461 | GCC_WARN_UNUSED_VARIABLE = YES;
462 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
463 | MTL_FAST_MATH = YES;
464 | ONLY_ACTIVE_ARCH = YES;
465 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
466 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
467 | };
468 | name = Debug;
469 | };
470 | 70A4A526282519B7007F2033 /* Release */ = {
471 | isa = XCBuildConfiguration;
472 | buildSettings = {
473 | ALWAYS_SEARCH_USER_PATHS = NO;
474 | CLANG_ANALYZER_NONNULL = YES;
475 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
476 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
477 | CLANG_ENABLE_MODULES = YES;
478 | CLANG_ENABLE_OBJC_ARC = YES;
479 | CLANG_ENABLE_OBJC_WEAK = YES;
480 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
481 | CLANG_WARN_BOOL_CONVERSION = YES;
482 | CLANG_WARN_COMMA = YES;
483 | CLANG_WARN_CONSTANT_CONVERSION = YES;
484 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
485 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
486 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
487 | CLANG_WARN_EMPTY_BODY = YES;
488 | CLANG_WARN_ENUM_CONVERSION = YES;
489 | CLANG_WARN_INFINITE_RECURSION = YES;
490 | CLANG_WARN_INT_CONVERSION = YES;
491 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
492 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
493 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
494 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
495 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
496 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
497 | CLANG_WARN_STRICT_PROTOTYPES = YES;
498 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
499 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
500 | CLANG_WARN_UNREACHABLE_CODE = YES;
501 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
502 | COPY_PHASE_STRIP = NO;
503 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
504 | ENABLE_NS_ASSERTIONS = NO;
505 | ENABLE_STRICT_OBJC_MSGSEND = YES;
506 | GCC_C_LANGUAGE_STANDARD = gnu11;
507 | GCC_NO_COMMON_BLOCKS = YES;
508 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
509 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
510 | GCC_WARN_UNDECLARED_SELECTOR = YES;
511 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
512 | GCC_WARN_UNUSED_FUNCTION = YES;
513 | GCC_WARN_UNUSED_VARIABLE = YES;
514 | MTL_ENABLE_DEBUG_INFO = NO;
515 | MTL_FAST_MATH = YES;
516 | SWIFT_COMPILATION_MODE = wholemodule;
517 | SWIFT_OPTIMIZATION_LEVEL = "-O";
518 | };
519 | name = Release;
520 | };
521 | 70A4A528282519B7007F2033 /* Debug */ = {
522 | isa = XCBuildConfiguration;
523 | buildSettings = {
524 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
525 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
526 | CODE_SIGN_ENTITLEMENTS = iOS/iOS.entitlements;
527 | CODE_SIGN_STYLE = Automatic;
528 | CURRENT_PROJECT_VERSION = 1;
529 | DEVELOPMENT_TEAM = P437HSA6PY;
530 | ENABLE_PREVIEWS = YES;
531 | GENERATE_INFOPLIST_FILE = YES;
532 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
533 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
534 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
535 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
536 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
537 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
538 | LD_RUNPATH_SEARCH_PATHS = (
539 | "$(inherited)",
540 | "@executable_path/Frameworks",
541 | );
542 | MARKETING_VERSION = 1.0;
543 | PRODUCT_BUNDLE_IDENTIFIER = tech.penginmura.Example;
544 | PRODUCT_NAME = SwiftUIExample;
545 | SDKROOT = iphoneos;
546 | SWIFT_EMIT_LOC_STRINGS = YES;
547 | SWIFT_VERSION = 5.0;
548 | TARGETED_DEVICE_FAMILY = "1,2";
549 | };
550 | name = Debug;
551 | };
552 | 70A4A529282519B7007F2033 /* Release */ = {
553 | isa = XCBuildConfiguration;
554 | buildSettings = {
555 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
556 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
557 | CODE_SIGN_ENTITLEMENTS = iOS/iOS.entitlements;
558 | CODE_SIGN_STYLE = Automatic;
559 | CURRENT_PROJECT_VERSION = 1;
560 | DEVELOPMENT_TEAM = P437HSA6PY;
561 | ENABLE_PREVIEWS = YES;
562 | GENERATE_INFOPLIST_FILE = YES;
563 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
564 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
565 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
566 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
567 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
568 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
569 | LD_RUNPATH_SEARCH_PATHS = (
570 | "$(inherited)",
571 | "@executable_path/Frameworks",
572 | );
573 | MARKETING_VERSION = 1.0;
574 | PRODUCT_BUNDLE_IDENTIFIER = tech.penginmura.Example;
575 | PRODUCT_NAME = SwiftUIExample;
576 | SDKROOT = iphoneos;
577 | SWIFT_EMIT_LOC_STRINGS = YES;
578 | SWIFT_VERSION = 5.0;
579 | TARGETED_DEVICE_FAMILY = "1,2";
580 | VALIDATE_PRODUCT = YES;
581 | };
582 | name = Release;
583 | };
584 | 70A4A52B282519B7007F2033 /* Debug */ = {
585 | isa = XCBuildConfiguration;
586 | buildSettings = {
587 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
588 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
589 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
590 | CODE_SIGN_STYLE = Automatic;
591 | COMBINE_HIDPI_IMAGES = YES;
592 | CURRENT_PROJECT_VERSION = 1;
593 | DEVELOPMENT_TEAM = P437HSA6PY;
594 | ENABLE_HARDENED_RUNTIME = YES;
595 | ENABLE_PREVIEWS = YES;
596 | GENERATE_INFOPLIST_FILE = YES;
597 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
598 | LD_RUNPATH_SEARCH_PATHS = (
599 | "$(inherited)",
600 | "@executable_path/../Frameworks",
601 | );
602 | MACOSX_DEPLOYMENT_TARGET = 12.3;
603 | MARKETING_VERSION = 1.0;
604 | PRODUCT_BUNDLE_IDENTIFIER = tech.penginmura.Example;
605 | PRODUCT_NAME = Example;
606 | SDKROOT = macosx;
607 | SWIFT_EMIT_LOC_STRINGS = YES;
608 | SWIFT_VERSION = 5.0;
609 | };
610 | name = Debug;
611 | };
612 | 70A4A52C282519B7007F2033 /* Release */ = {
613 | isa = XCBuildConfiguration;
614 | buildSettings = {
615 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
616 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
617 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
618 | CODE_SIGN_STYLE = Automatic;
619 | COMBINE_HIDPI_IMAGES = YES;
620 | CURRENT_PROJECT_VERSION = 1;
621 | DEVELOPMENT_TEAM = P437HSA6PY;
622 | ENABLE_HARDENED_RUNTIME = YES;
623 | ENABLE_PREVIEWS = YES;
624 | GENERATE_INFOPLIST_FILE = YES;
625 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
626 | LD_RUNPATH_SEARCH_PATHS = (
627 | "$(inherited)",
628 | "@executable_path/../Frameworks",
629 | );
630 | MACOSX_DEPLOYMENT_TARGET = 12.3;
631 | MARKETING_VERSION = 1.0;
632 | PRODUCT_BUNDLE_IDENTIFIER = tech.penginmura.Example;
633 | PRODUCT_NAME = Example;
634 | SDKROOT = macosx;
635 | SWIFT_EMIT_LOC_STRINGS = YES;
636 | SWIFT_VERSION = 5.0;
637 | };
638 | name = Release;
639 | };
640 | /* End XCBuildConfiguration section */
641 |
642 | /* Begin XCConfigurationList section */
643 | 707790BF2825628600A851C7 /* Build configuration list for PBXNativeTarget "UIKitApp" */ = {
644 | isa = XCConfigurationList;
645 | buildConfigurations = (
646 | 707790BD2825628600A851C7 /* Debug */,
647 | 707790BE2825628600A851C7 /* Release */,
648 | );
649 | defaultConfigurationIsVisible = 0;
650 | defaultConfigurationName = Release;
651 | };
652 | 70A4A50D282519B5007F2033 /* Build configuration list for PBXProject "Example" */ = {
653 | isa = XCConfigurationList;
654 | buildConfigurations = (
655 | 70A4A525282519B7007F2033 /* Debug */,
656 | 70A4A526282519B7007F2033 /* Release */,
657 | );
658 | defaultConfigurationIsVisible = 0;
659 | defaultConfigurationName = Release;
660 | };
661 | 70A4A527282519B7007F2033 /* Build configuration list for PBXNativeTarget "Example (iOS)" */ = {
662 | isa = XCConfigurationList;
663 | buildConfigurations = (
664 | 70A4A528282519B7007F2033 /* Debug */,
665 | 70A4A529282519B7007F2033 /* Release */,
666 | );
667 | defaultConfigurationIsVisible = 0;
668 | defaultConfigurationName = Release;
669 | };
670 | 70A4A52A282519B7007F2033 /* Build configuration list for PBXNativeTarget "Example (macOS)" */ = {
671 | isa = XCConfigurationList;
672 | buildConfigurations = (
673 | 70A4A52B282519B7007F2033 /* Debug */,
674 | 70A4A52C282519B7007F2033 /* Release */,
675 | );
676 | defaultConfigurationIsVisible = 0;
677 | defaultConfigurationName = Release;
678 | };
679 | /* End XCConfigurationList section */
680 |
681 | /* Begin XCSwiftPackageProductDependency section */
682 | 707790C028256C8C00A851C7 /* UserDefaultsBrowser */ = {
683 | isa = XCSwiftPackageProductDependency;
684 | productName = UserDefaultsBrowser;
685 | };
686 | 70A4A53128251B1D007F2033 /* UserDefaultsBrowser */ = {
687 | isa = XCSwiftPackageProductDependency;
688 | productName = UserDefaultsBrowser;
689 | };
690 | /* End XCSwiftPackageProductDependency section */
691 | };
692 | rootObject = 70A4A50A282519B5007F2033 /* Project object */;
693 | }
694 |
--------------------------------------------------------------------------------