├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Package.swift ├── README.md ├── Sources └── UserDefault │ └── UserDefault.swift └── Tests └── UserDefaultTests ├── UserDefaultTests.swift └── UserDefaults+Extension.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | # macOS temporary items 71 | .DS_Store 72 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 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: "UserDefault", 8 | products: [ 9 | .library( 10 | name: "UserDefault", 11 | targets: ["UserDefault"] 12 | ), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "UserDefault", 17 | dependencies: [] 18 | ), 19 | .testTarget( 20 | name: "UserDefaultTests", 21 | dependencies: ["UserDefault"] 22 | ), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UserDefault 2 | 3 | ### UserDefault wrapper enables you to use `UserDefaults` in the simplest way with the power of `@propertyWrapper`. 4 | 5 | Primitive types of Swift which conforms `Codable` protocol internally are supported. *(String, Int, Float, Double, Data, Bool, Date, URL)* 6 | 7 | ## Installation 8 | 9 | ### Swift Package Manager: 10 | 11 | To integrate using Apple's Swift package manager, add the following as a dependency to your Package.swift: 12 | 13 | ```swift 14 | dependencies: [ 15 | .package(url: "https://github.com/strawb3rryx7/UserDefault.git", from: "master") 16 | ] 17 | ``` 18 | 19 | ### Manually 20 | 21 | Just drag the `UserDefault.swift` file into your project directory. It's all done. 22 | 23 | ## Usage 24 | 25 | ```swift 26 | import UserDefault 27 | 28 | struct Defaults { 29 | @UserDefault("is_discount_provided", defaultValue: false) 30 | static var isDiscountProvided: Bool 31 | } 32 | ``` 33 | 34 | ## How to Modify? 35 | 36 | Well, that's pretty simple. You only have to access the variable through the `Defaults` struct, and set it the value. 37 | 38 | ```swift 39 | Defaults.isDiscountProvided = true 40 | ``` 41 | 42 | In addition, Custom models which conforms `Codable` protocol can be stored too. 43 | 44 | ### Custom models 45 | 46 | ```swift 47 | struct User: Codable { 48 | let firstName: String 49 | let lastName: String 50 | } 51 | 52 | struct Defaults { 53 | @UserDefault("default_user") 54 | static var defaultUser: User? 55 | } 56 | ``` 57 | 58 | You can specify any `UserDefaults` suite for each one. If you want to store on standard suite, no needed to specify it. Default is `UserDefaults.standard`. 59 | 60 | ### Usage of custom UserDefaults suite 61 | 62 | ```swift 63 | struct Contact: Codable { 64 | let firstName: String 65 | let lastName: String 66 | let phoneNumber: String 67 | } 68 | 69 | private let appSuite = UserDefaults(suiteName: "com.strawb3rryx7.userdefault.appsuite")! 70 | private let customSuite = UserDefaults(suiteName: "com.strawb3rryx7.userdefault.customsuite")! 71 | 72 | struct Defaults { 73 | @UserDefault("primary_contact", suite: appSuite) 74 | static var primaryContact: Contact? 75 | 76 | @UserDefault("has_seen_purchase_screen", defaultValue: false, suite: customSuite) 77 | static var hasSeenPurchaseScreen: Bool 78 | } 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /Sources/UserDefault/UserDefault.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | public struct UserDefault { 5 | // MARK: - Properties 6 | private let key: String 7 | private let defaultValue: T 8 | private let defaults: UserDefaults 9 | private let encoder: JSONEncoder 10 | private let decoder: JSONDecoder 11 | 12 | public var wrappedValue: T { 13 | get { 14 | getValue(for: key) ?? defaultValue 15 | } set { 16 | setValue(for: key, data: newValue) 17 | } 18 | } 19 | 20 | /// Returns the object from the relevant suite with the given key. 21 | /// 22 | /// - Parameter key: Key of the data. 23 | /// - Returns: An object which gathered from the relevant suite. 24 | private func getValue(for key: String) -> T? { 25 | guard let value = defaults.string(forKey: key) else { return nil } 26 | return defaults.decode(value, decoder: decoder) 27 | } 28 | 29 | /// Sets data to the relevant suite with the given key. 30 | /// 31 | /// - Parameter key: Key of the data. 32 | /// - Parameter data: A data that should be set to `UserDefaults`. 33 | private func setValue(for key: String, data: T) { 34 | guard let value = defaults.encode(data, encoder: encoder) else { return } 35 | defaults.set(value, forKey: key) 36 | } 37 | } 38 | 39 | public extension UserDefault where T: ExpressibleByNilLiteral { 40 | /// Creates `UserDefault` with given key, defaultValue and userDefaults. 41 | /// 42 | /// - Parameter key: Key of the data. 43 | /// - Parameter defaultValue: Default value. 44 | /// - Parameter defaults: UserDefaults instance. 45 | /// - Parameter encoder: JSONEncoder instance. 46 | /// - Parameter decoder: JSONDecoder instance. 47 | init(_ key: String, 48 | defaultValue: T = nil, 49 | defaults: UserDefaults = .standard, 50 | encoder: JSONEncoder = JSONEncoder(), 51 | decoder: JSONDecoder = JSONDecoder()) { 52 | self.key = key 53 | self.defaultValue = defaultValue 54 | self.defaults = defaults 55 | self.encoder = encoder 56 | self.decoder = decoder 57 | } 58 | } 59 | 60 | public extension UserDefault { 61 | /// Creates `UserDefault` with given key, defaultValue and userDefaults. 62 | /// 63 | /// - Parameter key: Key of the data. 64 | /// - Parameter defaultValue: Default value. 65 | /// - Parameter defaults: UserDefaults instance. 66 | /// - Parameter encoder: JSONEncoder instance. 67 | /// - Parameter decoder: JSONDecoder instance. 68 | init(_ key: String, 69 | defaultValue: T, 70 | defaults: UserDefaults = .standard, 71 | encoder: JSONEncoder = JSONEncoder(), 72 | decoder: JSONDecoder = JSONDecoder()) { 73 | self.key = key 74 | self.defaultValue = defaultValue 75 | self.defaults = defaults 76 | self.encoder = encoder 77 | self.decoder = decoder 78 | } 79 | } 80 | 81 | // MARK: - UserDefaults helpers 82 | private extension UserDefaults { 83 | func encode(_ value: T, encoder: JSONEncoder) -> String? { 84 | do { 85 | let data = try encoder.encode(value) 86 | return String(data: data, encoding: .utf8) 87 | } catch { 88 | return nil 89 | } 90 | } 91 | 92 | func decode(_ value: String, decoder: JSONDecoder) -> T? { 93 | guard let data = value.data(using: .utf8) else { 94 | return nil 95 | } 96 | return try? decoder.decode(T.self, from: data) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/UserDefaultTests/UserDefaultTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import UserDefault 3 | 4 | final class UserDefaultTests: XCTestCase { 5 | private struct Employee: Codable { 6 | let fullName: String 7 | let age: Int 8 | } 9 | 10 | private enum EmployeeType: String, Codable { 11 | case fullTime 12 | case partTime 13 | case contractor 14 | } 15 | 16 | private struct StandardValueContainer { 17 | @UserDefault("appLaunchCount", defaultValue: 3) 18 | static var appLaunchCount: Int 19 | 20 | @UserDefault("onboardingDesc") 21 | static var onboardingDesc: String? 22 | 23 | @UserDefault("onboardingSeen", defaultValue: false) 24 | static var onboardingSeen: Bool 25 | 26 | @UserDefault("lastOrderAmount", defaultValue: 0.0) 27 | static var lastOrderAmount: Double 28 | 29 | @UserDefault("supportURL", defaultValue: URL(string: "https://developer.apple.com")!) 30 | static var supportURL: URL 31 | } 32 | 33 | private struct SpecializedValueContainer { 34 | @UserDefault("employee", defaults: .testSuite) 35 | static var employee: Employee? 36 | 37 | @UserDefault("employeeType", defaultValue: .partTime, defaults: .testSuite) 38 | static var employeeType: EmployeeType 39 | } 40 | 41 | override func setUp() { 42 | super.setUp() 43 | UserDefaults.cleanAllSuites() 44 | } 45 | 46 | func test_GetAndSet_OptStringValueWithStandardSuite() { 47 | XCTAssertNil(StandardValueContainer.onboardingDesc) 48 | 49 | StandardValueContainer.onboardingDesc = "Greetings, John" 50 | 51 | XCTAssertEqual(StandardValueContainer.onboardingDesc, "Greetings, John") 52 | } 53 | 54 | func test_GetAndSet_BoolValueWithStandardSuite() { 55 | XCTAssertFalse(StandardValueContainer.onboardingSeen) 56 | 57 | StandardValueContainer.onboardingSeen = true 58 | 59 | XCTAssertTrue(StandardValueContainer.onboardingSeen) 60 | } 61 | 62 | func test_GetAndSet_IntegerValueWithStandardSuite() { 63 | XCTAssertEqual(StandardValueContainer.appLaunchCount, 3) 64 | 65 | StandardValueContainer.appLaunchCount = 5 66 | 67 | XCTAssertEqual(StandardValueContainer.appLaunchCount, 5) 68 | } 69 | 70 | func test_GetAndSet_DoubleValueWithStandardSuite() { 71 | XCTAssertEqual(StandardValueContainer.lastOrderAmount, 0.0) 72 | 73 | StandardValueContainer.lastOrderAmount = 149.99 74 | 75 | XCTAssertEqual(StandardValueContainer.lastOrderAmount, 149.99) 76 | } 77 | 78 | func test_GetAndSet_URLValueWithStandardSuite() { 79 | XCTAssertEqual(StandardValueContainer.supportURL.absoluteString, "https://developer.apple.com") 80 | 81 | StandardValueContainer.supportURL = URL(string: "https://apple.com/support")! 82 | 83 | XCTAssertEqual(StandardValueContainer.supportURL.absoluteString, "https://apple.com/support") 84 | } 85 | 86 | func test_GetAndSet_CustomTypeWithSpecializedSuite() { 87 | XCTAssertNil(SpecializedValueContainer.employee) 88 | 89 | SpecializedValueContainer.employee = Employee(fullName: "John Doe", age: 30) 90 | 91 | XCTAssertEqual(SpecializedValueContainer.employee?.fullName, "John Doe") 92 | XCTAssertEqual(SpecializedValueContainer.employee?.age, 30) 93 | } 94 | 95 | func test_GetAndSet_EnumTypeWithSpecializedSuite() { 96 | XCTAssertEqual(SpecializedValueContainer.employeeType, .partTime) 97 | 98 | SpecializedValueContainer.employeeType = .fullTime 99 | 100 | XCTAssertEqual(SpecializedValueContainer.employeeType, .fullTime) 101 | 102 | SpecializedValueContainer.employeeType = .contractor 103 | 104 | XCTAssertEqual(SpecializedValueContainer.employeeType, .contractor) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/UserDefaultTests/UserDefaults+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UserDefaults { 4 | private static let testSuiteName = "com.batuhan.userdefaulttests" 5 | static let testSuite = UserDefaults(suiteName: testSuiteName)! 6 | 7 | static func cleanAllSuites() { 8 | for suite in [Self.standard, testSuite] { 9 | for (key, _) in suite.dictionaryRepresentation() { 10 | suite.removeObject(forKey: key) 11 | } 12 | } 13 | } 14 | } 15 | --------------------------------------------------------------------------------