-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 6d97a02
Showing
16 changed files
with
650 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/configuration/registries.json | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata | ||
.netrc |
8 changes: 8 additions & 0 deletions
8
.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>IDEDidComputeMac32BitWarning</key> | ||
<true/> | ||
</dict> | ||
</plist> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// swift-tools-version: 5.9 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
import CompilerPluginSupport | ||
|
||
let package = Package( | ||
name: "PersistedStorage", | ||
platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17), .watchOS(.v10), .macCatalyst(.v17)], | ||
products: [ | ||
// Products define the executables and libraries a package produces, making them visible to other packages. | ||
.library( | ||
name: "PersistedStorage", | ||
targets: ["PersistedStorage"] | ||
) | ||
], | ||
dependencies: [ | ||
// Depend on the Swift 5.9 release of SwiftSyntax | ||
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), | ||
], | ||
targets: [ | ||
// Targets are the basic building blocks of a package, defining a module or a test suite. | ||
// Targets can depend on other targets in this package and products from dependencies. | ||
// Macro implementation that performs the source transformation of a macro. | ||
.macro( | ||
name: "PersistedStorageMacros", | ||
dependencies: [ | ||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"), | ||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax") | ||
] | ||
), | ||
|
||
// Library that exposes a macro as part of its API, which is used in client programs. | ||
.target(name: "PersistedStorage", dependencies: ["PersistedStorageMacros"]), | ||
|
||
// A test target used to develop the macro implementation. | ||
.testTarget( | ||
name: "PersistedStorageTests", | ||
dependencies: [ | ||
"PersistedStorageMacros", | ||
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), | ||
] | ||
), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# PersistedStorage | ||
|
||
PersistedStorage is a Swift Package providing persisted iCloud-synced storage for Swift projects using `NSUbiquitousKeyValueStore` internally. | ||
|
||
## Description | ||
|
||
Apply the `@PersistedStorage` macro to a class containing stored properties, and the class (e.g., `Settings`) will become a singleton accessible by `Settings.shared`. All properties are stored and synced via iCloud Key-Value Storage automatically. The default value is the initializer value you provide. | ||
|
||
## Features | ||
|
||
- Automatic iCloud synchronization using `NSUbiquitousKeyValueStore`. | ||
- Singleton pattern for easy access (`Settings.shared`). | ||
- Support for various property types, including `String`, `Int`, `Double`, `Bool`, `Data`, optional versions of these and enums with raw values. | ||
- Conforms to the Observation Framework's `Observable` protocol for SwiftUI integration. | ||
|
||
## Usage Example | ||
|
||
```Swift | ||
enum Test: String { | ||
case eel, oil | ||
} | ||
|
||
@PersistedStorage | ||
class Settings { | ||
var test: String = "eel" | ||
var testee: Int = 0 | ||
var douTest: Double = 0 | ||
var datTest: Data? = .init() | ||
|
||
@PersistedStorageTracked(customTypeKind: .enumWithRawValue(type: String.self)) | ||
var enumedValue: Test = .eel | ||
|
||
@PersistedStorageIgnored | ||
var ignoredProperty: Bool = false | ||
} | ||
``` | ||
|
||
How to use the object somewhere in code: | ||
```Swift | ||
Settings.shared.testee = 5 | ||
``` | ||
|
||
## Configuration | ||
|
||
The package supports enums with raw values. Use the @PersistedStorageTracked macro with the customTypeKind argument to specify the custom type. For example: | ||
|
||
```Swift | ||
@PersistedStorageTracked(customTypeKind: .enumWithRawValue(type: Int.self)) | ||
var myEnum: MyEnum = .defaultCase | ||
``` | ||
|
||
## Contributing | ||
|
||
Feel free to contribute to the project through pull requests. We welcome your contributions! |
5 changes: 5 additions & 0 deletions
5
Sources/PersistedStorage/Other/PersistedStorageConstants.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Foundation | ||
|
||
public enum PersistedStorageConstants { | ||
public static let optionalString = "3c9d506f-f32b-4e6f-bf78-d6cc97e0c6e6" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import Foundation | ||
|
||
/// A macro that | ||
@attached(extension, conformances: Observable) | ||
@attached(memberAttribute) | ||
@attached(member, names: named(shared), named(Keys), named(init), named(storage), named(_$observationRegistrar), named(access), named(withMutation), named(reloadValue), named(didChangeExternally)) | ||
public macro PersistedStorage() = #externalMacro(module: "PersistedStorageMacros", type: "PersistedStorageMacro") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import Foundation | ||
|
||
@attached(peer) | ||
public macro PersistedStorageIgnored() = #externalMacro(module: "PersistedStorageMacros", type: "PersistedStorageIgnoredMacro") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import Foundation | ||
|
||
@attached(accessor, names: named(init), named(get), named(set)) | ||
@attached(peer, names: prefixed(`_`)) | ||
public macro PersistedStorageTracked( | ||
customTypeKind: CustomTypeKind? = nil | ||
) = #externalMacro(module: "PersistedStorageMacros", type: "PersistedStorageTrackedMacro") | ||
|
||
public enum CustomTypeKind { | ||
case enumWithRawValue(type: Any.Type) | ||
} |
13 changes: 13 additions & 0 deletions
13
Sources/PersistedStorageMacros/Macros/PersistedStorageIgnoredMacro.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import Foundation | ||
import SwiftCompilerPlugin | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
public struct PersistedStorageIgnoredMacro {} | ||
|
||
extension PersistedStorageIgnoredMacro: PeerMacro { | ||
public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { | ||
[] | ||
} | ||
} |
156 changes: 156 additions & 0 deletions
156
Sources/PersistedStorageMacros/Macros/PersistedStorageMacro.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import Foundation | ||
import SwiftCompilerPlugin | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
public struct PersistedStorageMacro {} | ||
|
||
extension PersistedStorageMacro: MemberAttributeMacro { | ||
public static func expansion(of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, providingAttributesFor member: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.AttributeSyntax] { | ||
guard | ||
try VariableInformation(decl: member as! DeclSyntax)?.isAlreadyTracked == false | ||
else { return [] } | ||
|
||
return ["@PersistedStorageTracked"] | ||
} | ||
} | ||
|
||
extension PersistedStorageMacro: MemberMacro { | ||
public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { | ||
guard let classDecl = declaration as? ClassDeclSyntax else { | ||
throw PersistedStorageError.message("@PersistedStorage can only be applied to class declarations.") | ||
} | ||
|
||
let className = classDecl.name | ||
|
||
var keys: [String] = [] | ||
var reloadCases: [String] = [] | ||
|
||
for member in classDecl.memberBlock.members { | ||
guard let info = try VariableInformation(decl: member.decl) else { continue } | ||
|
||
let variableName = info.variableName | ||
let initializer = info.initialValue | ||
|
||
keys.append(variableName) | ||
|
||
let initialValueSubstitute: String | ||
switch info.typeKind { | ||
case .String: | ||
initialValueSubstitute = "storage.string(forKey: keyValue)" | ||
case .Data: | ||
initialValueSubstitute = "storage.data(forKey: keyValue)" | ||
case .Int: | ||
initialValueSubstitute = "(storage.object(forKey: keyValue) as? NSNumber)?.intValue" | ||
case .Double: | ||
initialValueSubstitute = "(storage.object(forKey: keyValue) as? NSNumber)?.doubleValue" | ||
case .Bool: | ||
initialValueSubstitute = "(storage.object(forKey: keyValue) as? NSNumber)?.boolValue" | ||
} | ||
|
||
let updateBody = if info.isOptionalWithoutNilInitialValue { | ||
""" | ||
if isNil() { | ||
_\(variableName) = nil | ||
} else { | ||
_\(variableName) = \(initialValueSubstitute) ?? \(initializer) | ||
} | ||
""" | ||
} else { | ||
"_\(variableName) = \(initialValueSubstitute) ?? \(initializer)" | ||
} | ||
let caseBody = | ||
""" | ||
withMutation(keyPath: \\.\(variableName)) { | ||
\(updateBody) | ||
} | ||
""" | ||
|
||
reloadCases.append("case .\(variableName): \(caseBody)") | ||
} | ||
|
||
return [ | ||
""" | ||
static let shared = \(className)() | ||
""", | ||
""" | ||
private enum Keys: String, CaseIterable { | ||
case \(raw: keys.joined(separator: ", ")) | ||
} | ||
""", | ||
""" | ||
private init() { | ||
for key in Keys.allCases { | ||
reloadValue(for: key) | ||
} | ||
NotificationCenter.default.addObserver( | ||
forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification, | ||
object: storage, | ||
queue: .main, | ||
using: didChangeExternally | ||
) | ||
} | ||
""", | ||
""" | ||
private let storage = NSUbiquitousKeyValueStore.default | ||
""", | ||
""" | ||
@ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar() | ||
""", | ||
""" | ||
internal nonisolated func access<Member>( | ||
keyPath: KeyPath<\(className), Member> | ||
) { | ||
_$observationRegistrar.access(self, keyPath: keyPath) | ||
} | ||
""", | ||
""" | ||
internal nonisolated func withMutation<Member, MutationResult>( | ||
keyPath: KeyPath<\(className), Member>, | ||
_ mutation: () throws -> MutationResult | ||
) rethrows -> MutationResult { | ||
try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) | ||
} | ||
""", | ||
""" | ||
private func reloadValue(for key: Keys) { | ||
let keyValue = key.rawValue | ||
func isNil() -> Bool { | ||
storage.string(forKey: keyValue) == PersistedStorageConstants.optionalString | ||
} | ||
switch key { | ||
\(raw: reloadCases.joined(separator: "\n")) | ||
} | ||
} | ||
""", | ||
""" | ||
private func didChangeExternally(notification: Notification) { | ||
guard let changedKeys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] else { return } | ||
for rawKey in changedKeys { | ||
guard let key = Keys(rawValue: rawKey) else { continue } | ||
self.reloadValue(for: key) | ||
} | ||
} | ||
""" | ||
] | ||
} | ||
} | ||
|
||
extension PersistedStorageMacro: ExtensionMacro { | ||
public static func expansion(of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax] { | ||
guard let classDecl = declaration as? ClassDeclSyntax else { | ||
throw PersistedStorageError.message("@PersistedStorage can only be applied to class declarations.") | ||
} | ||
|
||
let className = classDecl.name | ||
|
||
return [ | ||
try .init("extension \(className): Observation.Observable {}") | ||
] | ||
} | ||
} |
Oops, something went wrong.