├── .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 |
--------------------------------------------------------------------------------