Skip to content

Commit 53333d6

Browse files
authored
Add @Weak property wrapper (#341)
1 parent 07993a2 commit 53333d6

File tree

5 files changed

+137
-12
lines changed

5 files changed

+137
-12
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ Changelog
33

44
## master
55

6+
- Added: `@Weak` property wrapper (#341)
7+
68
## [0.11.1]
79

810
- Fixed: `@_spi` errors (#339)

README.md

+51-12
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,19 @@ TextField("Text Field", text: <#Binding<String>#>)
199199
}
200200
```
201201

202-
Implement your own selector
203-
---------------------------
202+
Advanced usage
203+
--------------
204+
205+
### Implement your own introspectable type
204206

205207
**Missing an element?** Please [create an issue](https://github.com./timbersoftware/SwiftUI-Introspect/issues).
206208

207-
In case SwiftUIIntrospect doesn't support the SwiftUI element that you're looking for, you can implement your own selector. For example, to introspect a `TextField`:
209+
In case SwiftUIIntrospect (unlikely) doesn't support the SwiftUI element that you're looking for, you can implement your own introspectable type.
210+
211+
For example, here's how the library implements the introspectable `TextField` type:
208212

209213
```swift
214+
import SwiftUI
210215
@_spi(Advanced) import SwiftUIIntrospect
211216

212217
public struct TextFieldType: IntrospectableViewType {}
@@ -246,14 +251,48 @@ extension macOSViewVersion<TextFieldType, NSTextField> {
246251
#endif
247252
```
248253

249-
Releasing
250-
---------
254+
### Introspect on future platform versions
255+
256+
By default, introspection applies per specific platform version. This is a sensible default for maximum predictability in regularly maintained codebases, but it's not always a good fit for e.g. library developers who may want to cover as many future platform versions as possible in order to provide the best chance for long-term future functionality of their library without regular maintenance.
257+
258+
For such cases, SwiftUI Introspect offers range-based platform version predicates behind the Advanced SPI:
259+
260+
```swift
261+
import SwiftUI
262+
@_spi(Advanced) import SwiftUIIntrospect
263+
264+
struct ContentView: View {
265+
var body: some View {
266+
ScrollView {
267+
// ...
268+
}
269+
.introspect(.scrollView, on: .iOS(.v13...)) { scrollView in
270+
// ...
271+
}
272+
}
273+
}
274+
```
275+
276+
Bear in mind this should be used cautiosly, and with full knowledge that any future OS version might break the expected introspection types unless explicitly available. For instance, if in the example above hypothetically iOS 18 stops using UIScrollView under the hood, the customization closure will never be called on said platform.
277+
278+
### Keep instances outside the customize closure
279+
280+
Sometimes, you might need to keep your introspected instance around for longer than the customization closure lifetime. In such cases, `@State` is not a good option because it produces retain cycles. Instead, SwiftUI Introspect offers a `@Weak` property wrapper behind the Advanced SPI:
281+
282+
```swift
283+
import SwiftUI
284+
@_spi(Advanced) import SwiftUIIntrospect
251285

252-
1. Update changelog with new version
253-
2. PR as 'Bump to X.Y.Z' and merge it
254-
3. Tag new version:
286+
struct ContentView: View {
287+
@Weak var scrollView: UIScrollView?
255288

256-
```sh
257-
$ git tag X.Y.Z
258-
$ git push origin --tags
259-
```
289+
var body: some View {
290+
ScrollView {
291+
// ...
292+
}
293+
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17)) { scrollView in
294+
self.scrollView = scrollView
295+
}
296+
}
297+
}
298+
```

Sources/Weak.swift

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@_spi(Advanced)
2+
@propertyWrapper
3+
public final class Weak<T: AnyObject> {
4+
private weak var _wrappedValue: T?
5+
6+
public var wrappedValue: T? {
7+
get { _wrappedValue }
8+
set { _wrappedValue = newValue }
9+
}
10+
11+
public init(wrappedValue: T? = nil) {
12+
self._wrappedValue = wrappedValue
13+
}
14+
}

Tests/Tests.xcodeproj/project.pbxproj

+14
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@
104104
D58D83462A66C5EF00A203BE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D58D83452A66C5EF00A203BE /* Assets.xcassets */; };
105105
D58D83492A66C5EF00A203BE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D58D83482A66C5EF00A203BE /* Preview Assets.xcassets */; };
106106
D58D83502A66C67A00A203BE /* TestCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58D834F2A66C67A00A203BE /* TestCases.swift */; };
107+
D591D1122A9CC2FF00AE05E8 /* WeakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */; };
108+
D591D1132A9CC2FF00AE05E8 /* WeakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */; };
109+
D591D1142A9CC30B00AE05E8 /* PageControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F26E012A561130001209E6 /* PageControlTests.swift */; };
110+
D591D1152A9CC30B00AE05E8 /* MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF56E2A502EF000CAFFB6 /* MapTests.swift */; };
111+
D591D1162A9CC30B00AE05E8 /* ViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F26E032A56E74B001209E6 /* ViewControllerTests.swift */; };
112+
D591D1172A9CC30B00AE05E8 /* SecureFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57E66F92A6956EB0092F43E /* SecureFieldTests.swift */; };
107113
D5983E7D2A66FD3F00C50953 /* TestCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58D834F2A66C67A00A203BE /* TestCases.swift */; };
108114
D5AAF56F2A502EF000CAFFB6 /* MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF56E2A502EF000CAFFB6 /* MapTests.swift */; };
109115
D5AD0D912A114B98003D8DEC /* TextFieldWithVerticalAxisTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AD0D902A114B98003D8DEC /* TextFieldWithVerticalAxisTests.swift */; };
@@ -209,6 +215,7 @@
209215
D58D83452A66C5EF00A203BE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
210216
D58D83482A66C5EF00A203BE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
211217
D58D834F2A66C67A00A203BE /* TestCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCases.swift; sourceTree = "<group>"; };
218+
D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakTests.swift; sourceTree = "<group>"; };
212219
D5AAF56E2A502EF000CAFFB6 /* MapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTests.swift; sourceTree = "<group>"; };
213220
D5AD0D902A114B98003D8DEC /* TextFieldWithVerticalAxisTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldWithVerticalAxisTests.swift; sourceTree = "<group>"; };
214221
D5ADFACB2A4A22AE009494FD /* SheetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetTests.swift; sourceTree = "<group>"; };
@@ -446,6 +453,7 @@
446453
children = (
447454
D5B67B852A0D3193007D5D9B /* ViewTypes */,
448455
D5F0BE6729C0DC4900AD95AB /* PlatformVersionTests.swift */,
456+
D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */,
449457
D58CE15729C621DD0081BFB0 /* TestUtils.swift */,
450458
);
451459
path = Tests;
@@ -697,10 +705,13 @@
697705
isa = PBXSourcesBuildPhase;
698706
buildActionMask = 2147483647;
699707
files = (
708+
D591D1172A9CC30B00AE05E8 /* SecureFieldTests.swift in Sources */,
700709
D5ADFAD42A4A4653009494FD /* FullScreenCoverTests.swift in Sources */,
701710
D50E2F5E2A2B9F6600BAFB03 /* ScrollViewTests.swift in Sources */,
702711
D50E2F5F2A2B9F6600BAFB03 /* NavigationStackTests.swift in Sources */,
703712
D50E2F602A2B9F6600BAFB03 /* DatePickerWithGraphicalStyleTests.swift in Sources */,
713+
D591D1152A9CC30B00AE05E8 /* MapTests.swift in Sources */,
714+
D591D1132A9CC2FF00AE05E8 /* WeakTests.swift in Sources */,
704715
D50E2F612A2B9F6600BAFB03 /* DatePickerWithCompactFieldStyleTests.swift in Sources */,
705716
D50E2F622A2B9F6600BAFB03 /* ToggleWithCheckboxStyleTests.swift in Sources */,
706717
D50E2F632A2B9F6600BAFB03 /* TabViewTests.swift in Sources */,
@@ -734,6 +745,7 @@
734745
D50E2F7B2A2B9F6600BAFB03 /* PlatformVersionTests.swift in Sources */,
735746
D50E2F7C2A2B9F6600BAFB03 /* TestUtils.swift in Sources */,
736747
D50E2F7D2A2B9F6600BAFB03 /* PickerWithSegmentedStyleTests.swift in Sources */,
748+
D591D1142A9CC30B00AE05E8 /* PageControlTests.swift in Sources */,
737749
D50E2F7E2A2B9F6600BAFB03 /* TabViewWithPageStyleTests.swift in Sources */,
738750
D50E2F7F2A2B9F6600BAFB03 /* DatePickerWithFieldStyleTests.swift in Sources */,
739751
D50E2F802A2B9F6600BAFB03 /* TableTests.swift in Sources */,
@@ -742,6 +754,7 @@
742754
D5ADFAD62A4A4653009494FD /* VideoPlayerTests.swift in Sources */,
743755
D50E2F832A2B9F6600BAFB03 /* ListCellTests.swift in Sources */,
744756
D50E2F842A2B9F6600BAFB03 /* SearchFieldTests.swift in Sources */,
757+
D591D1162A9CC30B00AE05E8 /* ViewControllerTests.swift in Sources */,
745758
D50E2F852A2B9F6600BAFB03 /* ViewTests.swift in Sources */,
746759
D50E2F862A2B9F6600BAFB03 /* ListWithGroupedStyleTests.swift in Sources */,
747760
D50E2F872A2B9F6600BAFB03 /* ProgressViewWithCircularStyleTests.swift in Sources */,
@@ -804,6 +817,7 @@
804817
D575069E2A27F80E00A628E4 /* ProgressViewWithLinearStyleTests.swift in Sources */,
805818
D57506862A27CA4100A628E4 /* ListWithBorderedStyleTests.swift in Sources */,
806819
D5F8D5ED2A1E7B490054E9AB /* NavigationViewWithStackStyleTests.swift in Sources */,
820+
D591D1122A9CC2FF00AE05E8 /* WeakTests.swift in Sources */,
807821
D57506942A27EED200A628E4 /* DatePickerWithStepperFieldStyleTests.swift in Sources */,
808822
D5AD0D912A114B98003D8DEC /* TextFieldWithVerticalAxisTests.swift in Sources */,
809823
D58119D02A23A62C0081F853 /* SliderTests.swift in Sources */,

Tests/Tests/WeakTests.swift

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
@_spi(Advanced) import SwiftUIIntrospect
2+
import XCTest
3+
4+
final class WeakTests: XCTestCase {
5+
final class Foo {}
6+
7+
var strongFoo: Foo? = Foo()
8+
9+
func testInit_nil() {
10+
@Weak var weakFoo: Foo?
11+
XCTAssertNil(weakFoo)
12+
}
13+
14+
func testInit_nonNil() {
15+
@Weak var weakFoo: Foo? = strongFoo
16+
XCTAssertIdentical(weakFoo, strongFoo)
17+
}
18+
19+
func testAssignment_nilToNil() {
20+
@Weak var weakFoo: Foo?
21+
weakFoo = nil
22+
XCTAssertNil(weakFoo)
23+
}
24+
25+
func testAssignment_nilToNonNil() {
26+
@Weak var weakFoo: Foo?
27+
let otherFoo = Foo()
28+
weakFoo = otherFoo
29+
XCTAssertIdentical(weakFoo, otherFoo)
30+
}
31+
32+
func testAssignment_nonNilToNil() {
33+
@Weak var weakFoo: Foo? = strongFoo
34+
weakFoo = nil
35+
XCTAssertNil(weakFoo)
36+
}
37+
38+
func testAssignment_nonNilToNonNil() {
39+
@Weak var weakFoo: Foo? = strongFoo
40+
let otherFoo = Foo()
41+
weakFoo = otherFoo
42+
XCTAssertIdentical(weakFoo, otherFoo)
43+
}
44+
45+
func testIndirectAssignment_nonNilToNil() {
46+
@Weak var weakFoo: Foo? = strongFoo
47+
strongFoo = nil
48+
XCTAssertNil(weakFoo)
49+
}
50+
51+
func testIndirectAssignment_nonNilToNonNil() {
52+
@Weak var weakFoo: Foo? = strongFoo
53+
strongFoo = Foo()
54+
XCTAssertNil(weakFoo)
55+
}
56+
}

0 commit comments

Comments
 (0)