Skip to content

Commit

Permalink
Migrate to macro
Browse files Browse the repository at this point in the history
  • Loading branch information
fthdgn committed Mar 7, 2024
1 parent ed149a0 commit e2542d6
Show file tree
Hide file tree
Showing 21 changed files with 871 additions and 246 deletions.
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

0 comments on commit e2542d6

Please sign in to comment.