Skip to content

Add @Weak property wrapper #341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}