├── .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 | |image|image|image|image| 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 | ![image](https://user-images.githubusercontent.com/2990285/167282686-e53f6621-d6d5-47bb-9f77-e62de33a41f3.png) 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 | --------------------------------------------------------------------------------