Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
JannThomas committed Jan 20, 2024
0 parents commit 6d97a02
Show file tree
Hide file tree
Showing 16 changed files with 650 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
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
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>
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
}
45 changes: 45 additions & 0 deletions Package.swift
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"),
]
),
]
)
54 changes: 54 additions & 0 deletions README.md
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!
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"
}
7 changes: 7 additions & 0 deletions Sources/PersistedStorage/PersistedStorage.swift
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")
4 changes: 4 additions & 0 deletions Sources/PersistedStorage/PersistedStorageIgnored.swift
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")
11 changes: 11 additions & 0 deletions Sources/PersistedStorage/PersistedStorageTracked.swift
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)
}
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 Sources/PersistedStorageMacros/Macros/PersistedStorageMacro.swift
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 {}")
]
}
}
Loading

0 comments on commit 6d97a02

Please sign in to comment.