Skip to content

Commit

Permalink
Add @Weak property wrapper (#341)
Browse files Browse the repository at this point in the history
  • Loading branch information
davdroman authored Aug 28, 2023
1 parent 07993a2 commit 53333d6
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 12 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Changelog

## master

- Added: `@Weak` property wrapper (#341)

## [0.11.1]

- Fixed: `@_spi` errors (#339)
Expand Down
63 changes: 51 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,19 @@ TextField("Text Field", text: <#Binding<String>#>)
}
```

Implement your own selector
---------------------------
Advanced usage
--------------

### Implement your own introspectable type

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

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`:
In case SwiftUIIntrospect (unlikely) doesn't support the SwiftUI element that you're looking for, you can implement your own introspectable type.

For example, here's how the library implements the introspectable `TextField` type:

```swift
import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect

public struct TextFieldType: IntrospectableViewType {}
Expand Down Expand Up @@ -246,14 +251,48 @@ extension macOSViewVersion<TextFieldType, NSTextField> {
#endif
```

Releasing
---------
### Introspect on future platform versions

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.

For such cases, SwiftUI Introspect offers range-based platform version predicates behind the Advanced SPI:

```swift
import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect

struct ContentView: View {
var body: some View {
ScrollView {
// ...
}
.introspect(.scrollView, on: .iOS(.v13...)) { scrollView in
// ...
}
}
}
```

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.

### Keep instances outside the customize closure

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:

```swift
import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect

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

```sh
$ git tag X.Y.Z
$ git push origin --tags
```
var body: some View {
ScrollView {
// ...
}
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17)) { scrollView in
self.scrollView = scrollView
}
}
}
```
14 changes: 14 additions & 0 deletions Sources/Weak.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@_spi(Advanced)
@propertyWrapper
public final class Weak<T: AnyObject> {
private weak var _wrappedValue: T?

public var wrappedValue: T? {
get { _wrappedValue }
set { _wrappedValue = newValue }
}

public init(wrappedValue: T? = nil) {
self._wrappedValue = wrappedValue
}
}
14 changes: 14 additions & 0 deletions Tests/Tests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@
D58D83462A66C5EF00A203BE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D58D83452A66C5EF00A203BE /* Assets.xcassets */; };
D58D83492A66C5EF00A203BE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D58D83482A66C5EF00A203BE /* Preview Assets.xcassets */; };
D58D83502A66C67A00A203BE /* TestCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58D834F2A66C67A00A203BE /* TestCases.swift */; };
D591D1122A9CC2FF00AE05E8 /* WeakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */; };
D591D1132A9CC2FF00AE05E8 /* WeakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */; };
D591D1142A9CC30B00AE05E8 /* PageControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F26E012A561130001209E6 /* PageControlTests.swift */; };
D591D1152A9CC30B00AE05E8 /* MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF56E2A502EF000CAFFB6 /* MapTests.swift */; };
D591D1162A9CC30B00AE05E8 /* ViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F26E032A56E74B001209E6 /* ViewControllerTests.swift */; };
D591D1172A9CC30B00AE05E8 /* SecureFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57E66F92A6956EB0092F43E /* SecureFieldTests.swift */; };
D5983E7D2A66FD3F00C50953 /* TestCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58D834F2A66C67A00A203BE /* TestCases.swift */; };
D5AAF56F2A502EF000CAFFB6 /* MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF56E2A502EF000CAFFB6 /* MapTests.swift */; };
D5AD0D912A114B98003D8DEC /* TextFieldWithVerticalAxisTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AD0D902A114B98003D8DEC /* TextFieldWithVerticalAxisTests.swift */; };
Expand Down Expand Up @@ -209,6 +215,7 @@
D58D83452A66C5EF00A203BE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D58D83482A66C5EF00A203BE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
D58D834F2A66C67A00A203BE /* TestCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCases.swift; sourceTree = "<group>"; };
D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakTests.swift; sourceTree = "<group>"; };
D5AAF56E2A502EF000CAFFB6 /* MapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTests.swift; sourceTree = "<group>"; };
D5AD0D902A114B98003D8DEC /* TextFieldWithVerticalAxisTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldWithVerticalAxisTests.swift; sourceTree = "<group>"; };
D5ADFACB2A4A22AE009494FD /* SheetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -446,6 +453,7 @@
children = (
D5B67B852A0D3193007D5D9B /* ViewTypes */,
D5F0BE6729C0DC4900AD95AB /* PlatformVersionTests.swift */,
D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */,
D58CE15729C621DD0081BFB0 /* TestUtils.swift */,
);
path = Tests;
Expand Down Expand Up @@ -697,10 +705,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D591D1172A9CC30B00AE05E8 /* SecureFieldTests.swift in Sources */,
D5ADFAD42A4A4653009494FD /* FullScreenCoverTests.swift in Sources */,
D50E2F5E2A2B9F6600BAFB03 /* ScrollViewTests.swift in Sources */,
D50E2F5F2A2B9F6600BAFB03 /* NavigationStackTests.swift in Sources */,
D50E2F602A2B9F6600BAFB03 /* DatePickerWithGraphicalStyleTests.swift in Sources */,
D591D1152A9CC30B00AE05E8 /* MapTests.swift in Sources */,
D591D1132A9CC2FF00AE05E8 /* WeakTests.swift in Sources */,
D50E2F612A2B9F6600BAFB03 /* DatePickerWithCompactFieldStyleTests.swift in Sources */,
D50E2F622A2B9F6600BAFB03 /* ToggleWithCheckboxStyleTests.swift in Sources */,
D50E2F632A2B9F6600BAFB03 /* TabViewTests.swift in Sources */,
Expand Down Expand Up @@ -734,6 +745,7 @@
D50E2F7B2A2B9F6600BAFB03 /* PlatformVersionTests.swift in Sources */,
D50E2F7C2A2B9F6600BAFB03 /* TestUtils.swift in Sources */,
D50E2F7D2A2B9F6600BAFB03 /* PickerWithSegmentedStyleTests.swift in Sources */,
D591D1142A9CC30B00AE05E8 /* PageControlTests.swift in Sources */,
D50E2F7E2A2B9F6600BAFB03 /* TabViewWithPageStyleTests.swift in Sources */,
D50E2F7F2A2B9F6600BAFB03 /* DatePickerWithFieldStyleTests.swift in Sources */,
D50E2F802A2B9F6600BAFB03 /* TableTests.swift in Sources */,
Expand All @@ -742,6 +754,7 @@
D5ADFAD62A4A4653009494FD /* VideoPlayerTests.swift in Sources */,
D50E2F832A2B9F6600BAFB03 /* ListCellTests.swift in Sources */,
D50E2F842A2B9F6600BAFB03 /* SearchFieldTests.swift in Sources */,
D591D1162A9CC30B00AE05E8 /* ViewControllerTests.swift in Sources */,
D50E2F852A2B9F6600BAFB03 /* ViewTests.swift in Sources */,
D50E2F862A2B9F6600BAFB03 /* ListWithGroupedStyleTests.swift in Sources */,
D50E2F872A2B9F6600BAFB03 /* ProgressViewWithCircularStyleTests.swift in Sources */,
Expand Down Expand Up @@ -804,6 +817,7 @@
D575069E2A27F80E00A628E4 /* ProgressViewWithLinearStyleTests.swift in Sources */,
D57506862A27CA4100A628E4 /* ListWithBorderedStyleTests.swift in Sources */,
D5F8D5ED2A1E7B490054E9AB /* NavigationViewWithStackStyleTests.swift in Sources */,
D591D1122A9CC2FF00AE05E8 /* WeakTests.swift in Sources */,
D57506942A27EED200A628E4 /* DatePickerWithStepperFieldStyleTests.swift in Sources */,
D5AD0D912A114B98003D8DEC /* TextFieldWithVerticalAxisTests.swift in Sources */,
D58119D02A23A62C0081F853 /* SliderTests.swift in Sources */,
Expand Down
56 changes: 56 additions & 0 deletions Tests/Tests/WeakTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@_spi(Advanced) import SwiftUIIntrospect
import XCTest

final class WeakTests: XCTestCase {
final class Foo {}

var strongFoo: Foo? = Foo()

func testInit_nil() {
@Weak var weakFoo: Foo?
XCTAssertNil(weakFoo)
}

func testInit_nonNil() {
@Weak var weakFoo: Foo? = strongFoo
XCTAssertIdentical(weakFoo, strongFoo)
}

func testAssignment_nilToNil() {
@Weak var weakFoo: Foo?
weakFoo = nil
XCTAssertNil(weakFoo)
}

func testAssignment_nilToNonNil() {
@Weak var weakFoo: Foo?
let otherFoo = Foo()
weakFoo = otherFoo
XCTAssertIdentical(weakFoo, otherFoo)
}

func testAssignment_nonNilToNil() {
@Weak var weakFoo: Foo? = strongFoo
weakFoo = nil
XCTAssertNil(weakFoo)
}

func testAssignment_nonNilToNonNil() {
@Weak var weakFoo: Foo? = strongFoo
let otherFoo = Foo()
weakFoo = otherFoo
XCTAssertIdentical(weakFoo, otherFoo)
}

func testIndirectAssignment_nonNilToNil() {
@Weak var weakFoo: Foo? = strongFoo
strongFoo = nil
XCTAssertNil(weakFoo)
}

func testIndirectAssignment_nonNilToNonNil() {
@Weak var weakFoo: Foo? = strongFoo
strongFoo = Foo()
XCTAssertNil(weakFoo)
}
}

0 comments on commit 53333d6

Please sign in to comment.