Skip to content

Commit 489a4cb

Browse files
committed
feat: Add AssociatedObject macro
feat(CI): Add `Test (macOS)` workflow feat: cleanup feat: Update dependencies - Update `swift-syntax` from `509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-08-15-a` to `509.0.0` - Update `swift-macro-toolkit` from `0.2.0` to `0.3.0 fix: Update formatting feat: AssociatedObject macro
1 parent ee410e8 commit 489a4cb

File tree

14 files changed

+1157
-11
lines changed

14 files changed

+1157
-11
lines changed

.github/workflows/test-macos.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
pull_request:
6+
branches: [ main, swift-macros ]
7+
8+
jobs:
9+
test-macos:
10+
runs-on: macos-13
11+
steps:
12+
- name: Select Xcode 15.0
13+
run: sudo xcode-select -switch /Applications/Xcode_15.0.app
14+
- name: Checkout
15+
uses: actions/checkout@v3
16+
- name: Test
17+
run: swift test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ xcuserdata/
66
DerivedData/
77
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
88
Package.resolved
9+
.swiftpm

Package.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
// swift-tools-version:5.8
1+
// swift-tools-version: 5.9
22

33
import PackageDescription
4+
import CompilerPluginSupport
45

56
let package = Package(
67
name: "swift-foundation-extensions",
78
platforms: [
89
.macOS(.v10_15),
10+
.macCatalyst(.v13),
911
.iOS(.v13),
1012
.tvOS(.v13),
1113
.watchOS(.v6)
@@ -14,7 +16,7 @@ let package = Package(
1416
.library(
1517
name: "FoundationExtensions",
1618
targets: ["FoundationExtensions"]
17-
),
19+
)
1820
],
1921
dependencies: [
2022
.package(
@@ -25,11 +27,24 @@ let package = Package(
2527
url: "https://github.com/pointfreeco/swift-custom-dump",
2628
.upToNextMajor(from: "1.0.0")
2729
),
30+
.package(
31+
url: "https://github.com/maximkrouk/swift-macro-toolkit.git",
32+
.upToNextMinor(from: "0.3.0")
33+
),
34+
.package(
35+
url: "https://github.com/pointfreeco/swift-macro-testing.git",
36+
.upToNextMinor(from: "0.1.0")
37+
),
38+
.package(
39+
url: "https://github.com/apple/swift-syntax.git",
40+
exact: "509.0.0"
41+
),
2842
],
2943
targets: [
3044
.target(
3145
name: "FoundationExtensions",
3246
dependencies: [
47+
.target(name: "FoundationExtensionsMacros"),
3348
.product(
3449
name: "FunctionalKeyPath",
3550
package: "swift-declarative-configuration"
@@ -40,11 +55,27 @@ let package = Package(
4055
),
4156
]
4257
),
58+
.macro(
59+
name: "FoundationExtensionsMacros",
60+
dependencies: [
61+
.product(
62+
name: "MacroToolkit",
63+
package: "swift-macro-toolkit"
64+
)
65+
]
66+
),
4367
.testTarget(
4468
name: "FoundationExtensionsTests",
4569
dependencies: [
4670
.target(name: "FoundationExtensions"),
4771
]
4872
),
73+
.testTarget(
74+
name: "FoundationExtensionsMacrosTests",
75+
dependencies: [
76+
.target(name: "FoundationExtensionsMacros"),
77+
.product(name: "MacroTesting", package: "swift-macro-testing"),
78+
]
79+
),
4980
]
5081
)

README.md

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# swift-foundation-extensions
22

3-
[![SwiftPM 5.6](https://img.shields.io/badge/swiftpm-5.8-ED523F.svg?style=flat)](https://swift.org/download/) ![Platforms](https://img.shields.io/badge/Platforms-iOS_13_|_macOS_10.15_|_tvOS_14_|_watchOS_7-ED523F.svg?style=flat) [![@maximkrouk](https://img.shields.io/badge/[email protected]?style=flat&logo=twitter)](https://twitter.com/capture_context)
3+
[![SwiftPM 5.9](https://img.shields.io/badge/swiftpm-5.9-ED523F.svg?style=flat)](https://swift.org/download/) ![Platforms](https://img.shields.io/badge/Platforms-iOS_13_|_macOS_10.15_|_tvOS_14_|_watchOS_7-ED523F.svg?style=flat) [![@capture_context](https://img.shields.io/badge/[email protected]?style=flat&logo=twitter)](https://twitter.com/capture_context)
44

55
Standard extensions for Foundation
66

@@ -50,6 +50,71 @@ func encode(to encoder: encoder) throws {
5050
- `assign(to:on:)` - assigns wrapped value to a specified target property by the keyPath
5151
- `ifLetAssign(to:on:)` - assigns wrapped value to a specified target property by the keyPath if an optional was not nil
5252

53+
### Undo/Redo management
54+
55+
```swift
56+
struct State {
57+
var value: Int = 0
58+
}
59+
60+
@Resettable
61+
let state = State()
62+
state.value = 1 // value == 1
63+
state.value *= 10 // value == 10
64+
state.undo() // value == 1
65+
state.value += 1 // value == 2
66+
state.undo() // value == 1
67+
state.redo() // value == 2
68+
```
69+
70+
### Indirect
71+
72+
CoW container, which allows you to recursively include single instances of value types
73+
74+
### PropertyProxy
75+
76+
```swift
77+
class MyView: UIView {
78+
private let label: UILabel
79+
80+
@PropertyProxy(\MyView.label.text)
81+
var text: String?
82+
}
83+
84+
let view: MyView = .init()
85+
view.label.text //
86+
view.text = "Hello, World!"
87+
```
88+
89+
### Object Association
90+
91+
> By default `@AssociatedObject` macro uses `.retain(.nonatomic)` for classes and `.copy(.nonatomic)` `objc_AssociationPolicy` for structs.
92+
93+
```swift
94+
extension SomeClass {
95+
@AssociatedObject
96+
var storedVariableInExtension: Int = 0
97+
98+
@AssociatedObject
99+
var optionalValue: Int?
100+
101+
@AssociatedObject
102+
var object: Int?
103+
104+
@AssociatedObject(.atomic)
105+
var threadSafeValue: Int?
106+
107+
@AssociatedObject(.atomic)
108+
var threadSafeObject: Object?
109+
110+
@AssociatedObject(.assign)
111+
var customPolicyValue: Int?
112+
113+
@AssociatedObject(.retain(.atomic))
114+
var customPolicyThreadSafeObject: Object?
115+
}
116+
```
117+
53118
## Installation
54119

55120
### Basic
@@ -68,7 +133,7 @@ If you use SwiftPM for your project, you can add StandardExtensions to your pack
68133
.package(
69134
name: "swift-foundation-extensions",
70135
url: "https://github.com/capturecontext/swift-foundation-extensions.git",
71-
.upToNextMinor(from: "0.2.0")
136+
branch: "swift-macros"
72137
)
73138
```
74139

@@ -81,7 +146,6 @@ Do not forget about target dependencies:
81146
)
82147
```
83148

84-
85149
## License
86150

87151
This library is released under the MIT license. See [LICENSE](LICENSE) for details.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import FoundationExtensionsMacros
2+
3+
@attached(accessor)
4+
public macro AssociatedObject(
5+
_ policy: objc_AssociationPolicy
6+
) = #externalMacro(
7+
module: "FoundationExtensionsMacros",
8+
type: "AssociatedObjectMacro"
9+
)
10+
11+
@attached(accessor)
12+
public macro AssociatedObject(
13+
_ threadSafety: _AssociationPolicyThreadSafety = .nonatomic
14+
) = #externalMacro(
15+
module: "FoundationExtensionsMacros",
16+
type: "AssociatedObjectMacro"
17+
)

Sources/FoundationExtensions/General/AssociatingObject.swift renamed to Sources/FoundationExtensions/General/AssociatingObject/AssociatingObject.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ extension AssociatingObject {
2222
public func setAssociatedObject<Object>(
2323
_ object: Object,
2424
forKey key: StaticString,
25-
policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC
25+
policy: objc_AssociationPolicy = .retain(.nonatomic)
2626
) -> Bool {
2727
return _setAssociatedObject(
2828
object,
@@ -50,9 +50,28 @@ public func _setAssociatedObject<Object>(
5050
_ object: Object,
5151
to associatingObject: AnyObject,
5252
forKey key: StaticString,
53-
policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC
53+
threadSafety: _AssociationPolicyThreadSafety = .nonatomic
5454
) -> Bool {
55-
key.withUTF8Buffer { pointer in
55+
_setAssociatedObject(
56+
object,
57+
to: associatingObject,
58+
forKey: key,
59+
policy: .init(
60+
object is AnyClass ? .retain : .copy,
61+
threadSafety
62+
)
63+
)
64+
}
65+
66+
@inlinable
67+
@discardableResult
68+
public func _setAssociatedObject<Object>(
69+
_ object: Object,
70+
to associatingObject: AnyObject,
71+
forKey key: StaticString,
72+
policy: objc_AssociationPolicy
73+
) -> Bool {
74+
return key.withUTF8Buffer { pointer in
5675
if let p = pointer.baseAddress.map(UnsafeRawPointer.init) {
5776
objc_setAssociatedObject(associatingObject, p, object, policy)
5877
return true
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/// Helper for creating `objc_AssociationPolicy`
2+
public enum _AssociationPolicyKind: String {
3+
/// `OBJC_ASSOCIATION_COPY` / `OBJC_ASSOCIATION_COPY_NONATOMIC`
4+
///
5+
/// Use this policy when you need the value of the object as it was at the moment the property was set,
6+
/// and the object is possibly mutable.
7+
case copy
8+
9+
/// `OBJC_ASSOCIATION_ASSIGN`
10+
///
11+
/// Use this when you're associating a raw pointer, or for a weak reference to an object.
12+
/// It does not extend the lifetime of the associated object.
13+
case assign
14+
15+
16+
/// `OBJC_ASSOCIATION_RETAIN` / `OBJC_ASSOCIATION_RETAIN_NONATOMIC`
17+
///
18+
/// Use this to specify a strong reference to the associated object
19+
case retain
20+
}
21+
22+
/// Helper for creating `objc_AssociationPolicy`
23+
///
24+
/// Remember, `.atomic` properties ensure that an entire value is set/get
25+
/// before another operation can take place on it - these are thread-safe but slower.
26+
/// On the other hand, `.nonatomic` properties don't have that restriction - they're faster but not thread-safe.
27+
public enum _AssociationPolicyThreadSafety: String {
28+
/// It is the default behaviour. If an object is declared as atomic then it becomes thread-safe.
29+
///
30+
/// Thread-safe means, at a time only one thread of a particular instance of that class can have the control over that object.
31+
case atomic
32+
33+
/// Disable thread-safety. it’s faster to access a nonatomic property than an atomic one.
34+
///
35+
/// You can use the nonatomic property attribute to specify that synthesized accessors simply set or return a value directly,
36+
/// with no guarantees about what happens if that same value is accessed simultaneously from different threads.
37+
/// For this reason,
38+
case nonatomic
39+
}
40+
41+
extension objc_AssociationPolicy {
42+
@inlinable
43+
public init(
44+
_ kind: _AssociationPolicyKind,
45+
_ threadSafety: _AssociationPolicyThreadSafety
46+
) {
47+
switch kind {
48+
case .copy:
49+
self = .copy(threadSafety)
50+
case .assign:
51+
self = .assign
52+
case .retain:
53+
self = .retain(threadSafety)
54+
}
55+
}
56+
57+
/// `OBJC_ASSOCIATION_ASSIGN`
58+
///
59+
/// Use this when you're associating a raw pointer, or for a weak reference to an object.
60+
/// It does not extend the lifetime of the associated object.
61+
@inlinable
62+
public static var assign: Self { .OBJC_ASSOCIATION_ASSIGN }
63+
64+
/// `OBJC_ASSOCIATION_RETAIN` / `OBJC_ASSOCIATION_RETAIN_NONATOMIC`
65+
///
66+
/// Use `.retain(.atomic)` to specify a strong reference to the associated object and it's thread-safe.
67+
/// Use this when you're working in a multithreaded environment and you need to ensure that
68+
/// the associated object isn't deallocated before you're done with it.
69+
///
70+
/// `.retain(.nonatomic)` specifies a strong reference to the associated object and is not thread-safe.
71+
/// This is appropriate when you're not concerned with performance in multithreaded scenarios.
72+
@inlinable
73+
public static func retain(_ threadSafety: _AssociationPolicyThreadSafety) -> Self {
74+
switch threadSafety {
75+
case .atomic:
76+
.OBJC_ASSOCIATION_RETAIN
77+
case .nonatomic:
78+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
79+
}
80+
}
81+
82+
/// `OBJC_ASSOCIATION_COPY` / `OBJC_ASSOCIATION_COPY_NONATOMIC`
83+
///
84+
/// `.copy(.atomic)` as well as `.retain` also creates a copy of the associated object,
85+
/// and the copy is made in an atomic way (thread-safe). Use this policy when
86+
/// you're working in a multithreaded environment and you need the value of the object
87+
/// as it was at the moment the property was set, and the object is possibly mutable.
88+
///
89+
/// `.copy(.nonatomic)` creates a copy of the associated object, and the copy is made in a non-atomic way.
90+
/// Use this policy when you need the value of the object as it was at the moment the property was set,
91+
/// and the object is possibly mutable.
92+
@inlinable
93+
public static func copy(_ threadSafety: _AssociationPolicyThreadSafety) -> Self {
94+
switch threadSafety {
95+
case .atomic:
96+
.OBJC_ASSOCIATION_COPY
97+
case .nonatomic:
98+
.OBJC_ASSOCIATION_COPY_NONATOMIC
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)