Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to macro #2

Merged
merged 6 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ on:
jobs:
build:

runs-on: macos-latest
runs-on: macos-14

steps:
- uses: actions/checkout@v2
- name: Generate xcodeproj
run: swift package generate-xcodeproj
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 15.2
- name: Run tests
run: xcodebuild test -destination 'name=iPhone 11' -scheme 'UserDefaultsProperty-Package'
run: xcodebuild test -destination 'name=iPhone 11' -scheme 'UserDefaultsProperty'
14 changes: 14 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036",
"version" : "509.0.2"
}
}
],
"version" : 2
}
31 changes: 25 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
// swift-tools-version:5.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
// swift-tools-version:5.9

import CompilerPluginSupport
import PackageDescription

let package = Package(
name: "UserDefaultsProperty",
platforms: [
.iOS(.v14),
.macOS(.v10_15),
],
products: [
.library(
name: "UserDefaultsProperty",
targets: ["UserDefaultsProperty"]),
targets: ["UserDefaultsProperty"]
),
],
dependencies: [

.package(url: "https://github.com/apple/swift-syntax.git", exact: "509.0.2"),
],
targets: [
.target(
name: "UserDefaultsProperty",
dependencies: []),
dependencies: [
"UserDefaultsPropertyMacros"
]
),
.testTarget(
name: "UserDefaultsPropertyTests",
dependencies: ["UserDefaultsProperty"]),
dependencies: [
"UserDefaultsProperty",
"UserDefaultsPropertyMacros",
]
),
.macro(
name: "UserDefaultsPropertyMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]
),
]
)
110 changes: 95 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,112 @@
# UserDefaultsProperty

## Implementation

```swift
import Foundation
import UserDefaultsProperty

// To access this without importing UserDefaultsProperty on every file
typealias UserDefaultsStorage = UserDefaultsProperty.UserDefaultsStorage

class MyUserDefaults: UserDefaultsProvider {
let userDefaults = UserDefaults(suiteName: "custom")!
static let shared: MyUserDefaults = .init()

let userDefaults = UserDefaults.standard

@UserDefaultsProperty(key: "stringProperty")
var stringProperty: String?
// Use property name as key on UserDefaults
@UserDefaultsProperty
var note: String?

@UserDefaultsProperty(key: "dataProperty")
// Use custom key instead of property name
@UserDefaultsProperty(key: "data_property")
var dataProperty: Data?

// Non-optional property with default value.
// Returns the default value if the key is not present on UserDefaults.
// To remove custom value: MyUserDefaults.shared.userDefaults.removeObject(forKey: "userName")
@UserDefaultsProperty
var userName: String = "User"

// Optional property with default value.
// Because UserDefault cannot store nil information,
// it is imposible to different intentionaly assigned nil value and nonassigned value.
// For example:
// MyUserDefaults.shared.role = nil
// MyUserDefaults.shared.role == "Normal" //true
@UserDefaultsProperty
var role: String? = "Normal"
}
```

## Usage

```swift
import Foundation
import UserDefaultsProperty
func someFunction() {

class MyUserDefaults: UserDefaultsProvider, UserDefaultsCacheProvider {
let userDefaults = UserDefaults(suiteName: "custom")!
let cache = UserDefaultsPropertyCache()

@UserDefaultsProperty(key: "stringProperty")
var stringProperty: String?

@UserDefaultsProperty(key: "dataProperty")
var dataProperty: Data?
// Without fallback value

print(MyUserDefaults.shared.note) // nil
print(MyUserDefaults.shared.isSet(.\$note)) // false

MyUserDefaults.shared.note = "some note"

print(MyUserDefaults.shared.userName) // Optional("some note")
print(MyUserDefaults.shared.isSet(.\$note)) // true

MyUserDefaults.shared.note = nil

print(MyUserDefaults.shared.note) // nil
print(MyUserDefaults.shared.isSet(.\$note)) // false


// Non optional with fallback

print(MyUserDefaults.shared.userName) // "User"
print(MyUserDefaults.shared.isSet(.\$userName)) // false

MyUserDefaults.shared.userName = "User 2"

print(MyUserDefaults.shared.userName) // "User 2"
print(MyUserDefaults.shared.isSet(.\$userName)) // true

MyUserDefaults.shared.reset(.\$userName)// Cannot set to nil use helper funtion

print(MyUserDefaults.shared.userName) // "User"
print(MyUserDefaults.shared.isSet(.\$userName)) // false

// Optional with fallback (Not suggested, it does not feels OK)

print(MyUserDefaults.shared.role) // "Normal"
print(MyUserDefaults.shared.isSet(.\$role)) // false

MyUserDefaults.shared.role = "Admin"

print(MyUserDefaults.shared.role) // "Admin"
print(MyUserDefaults.shared.isSet(.\$role)) // true

MyUserDefaults.shared.role = nil // Setting nil removes the value, it will return default value

print(MyUserDefaults.shared.role) // "Normal" // It is not nil. Returns defaults value
print(MyUserDefaults.shared.isSet(.\$role)) // false

}

var observer: AnyCancellable? // Keep strong reference

func registerObserver() {
observer = MyUserDefaults.shared.observer(\.$userName) { newUserName in
print(newUserName)
}
}

struct SomeView: View {
@UserDefaultsStorage(AppDefaults.shared, \.$userName) private var userName

var body: some View {
Text(userName)
TextEditor(text: $userName)
}
}

```
4 changes: 2 additions & 2 deletions Sources/UserDefaultsProperty/Decoder/Decoder.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Foundation

func cast<T>(_ value: Any) throws -> T {
func cast<T>(_ value: Any?) throws -> T {
if let value = value as? T {
return value
} else {
throw "Cannot decode \(value.self) to \(T.self)"
throw UserDefaultsPropertyError.cast(value: value, type: T.self)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ struct _KeyedDecodingContainerProtocol<Key: CodingKey>: KeyedDecodingContainerPr
}

func getData<T>(forKey key: String) throws -> T {
if let data = data.object(forKey: key) as? T {
return data
} else {
throw "Cannot decode \(data.self) to \(T.self)"
}
let value = data.object(forKey: key)
return try cast(value)
}

func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable {
Expand Down
4 changes: 2 additions & 2 deletions Sources/UserDefaultsProperty/Error.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation

extension String: LocalizedError {
public var errorDescription: String? { return self }
enum UserDefaultsPropertyError: Error {
case cast(value: Any?, type: Any)
}
96 changes: 4 additions & 92 deletions Sources/UserDefaultsProperty/UserDefaultsProperty.swift
Original file line number Diff line number Diff line change
@@ -1,92 +1,4 @@
import Foundation

public protocol UserDefaultsProvider {
var userDefaults: UserDefaults { get }
}

public protocol UserDefaultsCacheProvider {
var cache: UserDefaultsPropertyCache { get }
}

public class UserDefaultsPropertyCache: NSObject {
private let cache: NSCache<NSString, AnyObject>

public init(cache: NSCache<NSString, AnyObject> = .init()) {
self.cache = cache
}

class Wrapper<T> {
let value: T?

init(_ value: T?) {
self.value = value
}
}

fileprivate func set<T>(value: T?, forKey key: String) {
self.cache.setObject(Wrapper(value), forKey: key as NSString)
}

fileprivate func get<T>(forKey key: String) -> Wrapper<T>? {
return self.cache.object(forKey: key as NSString) as? Wrapper<T>
}
}

@propertyWrapper
public struct UserDefaultsProperty<T: Codable> {
let key: String

public init(key: String) {
self.key = key
}

public static subscript<E: UserDefaultsProvider>(
_enclosingInstance instance: E,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<E, T?>,
storage storageKeyPath: ReferenceWritableKeyPath<E, Self>
) -> T? {
get {
let wrapper = instance[keyPath: storageKeyPath]

if let wrapper: UserDefaultsPropertyCache.Wrapper<T> = (instance as? UserDefaultsCacheProvider)?.cache.get(forKey: wrapper.key) {
return wrapper.value
}

if let value = instance.userDefaults.value(forKey: wrapper.key) {
do {
let data = try UserDefaultsDecoder.decode(T.self, from: value)
(instance as? UserDefaultsCacheProvider)?.cache.set(value: data, forKey: wrapper.key)
return data
} catch {
print("error \(error)")
}
}
return nil
}
set {
let wrapper = instance[keyPath: storageKeyPath]
guard let newValue = newValue else {
instance.userDefaults.removeObject(forKey: wrapper.key)
(instance as? UserDefaultsCacheProvider)?.cache.set(value: nil as T?, forKey: wrapper.key)
return
}
do {
let value = try UserDefaultsEncoder.encode(newValue)
instance.userDefaults.set(value, forKey: wrapper.key)
(instance as? UserDefaultsCacheProvider)?.cache.set(value: value, forKey: wrapper.key)
} catch {
print("error \(error)")
}
}
}

@available(*, unavailable, message: "This property wrapper can only be applied to classes")
public var wrappedValue: T? {
get {
fatalError()
}
set {
fatalError()
}
}
}
@attached(accessor)
@attached(peer, names: prefixed(`$`))
public macro UserDefaultsProperty(key: String? = nil)
= #externalMacro(module: "UserDefaultsPropertyMacros", type: "UserDefaultsMacro")
11 changes: 11 additions & 0 deletions Sources/UserDefaultsProperty/UserDefaultsPropertyData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

public struct UserDefaultsPropertyData<T> {
public let key: String
public let fallback: T

public init(key: String, fallback: T) {
self.key = key
self.fallback = fallback
}
}
Loading
Loading