From 1f3521dfcf6c3de4e4d94d0a822c006e0552a956 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Mon, 28 Aug 2023 02:11:40 +0100 Subject: [PATCH 1/7] wip --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f3a280c..12df5da8 100644 --- a/README.md +++ b/README.md @@ -199,12 +199,16 @@ TextField("Text Field", text: <#Binding#>) } ``` -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 @_spi(Advanced) import SwiftUIIntrospect @@ -246,6 +250,10 @@ extension macOSViewVersion { #endif ``` +### Introspect on future platform versions + +### Keep instances outside the customize closure + Releasing --------- From 2fda50020d6715b05d42362aebd8f757260e0314 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Mon, 28 Aug 2023 12:56:07 +0100 Subject: [PATCH 2/7] wip --- Sources/Weak.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Sources/Weak.swift diff --git a/Sources/Weak.swift b/Sources/Weak.swift new file mode 100644 index 00000000..f8ec5773 --- /dev/null +++ b/Sources/Weak.swift @@ -0,0 +1,14 @@ +@_spi(Advanced) +@propertyWrapper +public final class Weak { + private weak var _wrappedValue: T? + + public var wrappedValue: T? { + get { _wrappedValue } + set { _wrappedValue = newValue } + } + + public init(wrappedValue: T? = nil) { + self._wrappedValue = wrappedValue + } +} From d2c9f64393d96e7c89368a180ed8b57e78d15578 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Mon, 28 Aug 2023 12:57:42 +0100 Subject: [PATCH 3/7] wip --- Tests/Tests.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/Tests.xcodeproj/project.pbxproj b/Tests/Tests.xcodeproj/project.pbxproj index 03bd41c5..e0250e99 100644 --- a/Tests/Tests.xcodeproj/project.pbxproj +++ b/Tests/Tests.xcodeproj/project.pbxproj @@ -104,6 +104,10 @@ 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 */; }; + 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 */; }; @@ -697,10 +701,12 @@ 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 */, D50E2F612A2B9F6600BAFB03 /* DatePickerWithCompactFieldStyleTests.swift in Sources */, D50E2F622A2B9F6600BAFB03 /* ToggleWithCheckboxStyleTests.swift in Sources */, D50E2F632A2B9F6600BAFB03 /* TabViewTests.swift in Sources */, @@ -734,6 +740,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 */, @@ -742,6 +749,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 */, From 7cb92d3f8225ed41814c4d8be811a24aa10d18c6 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:35:29 +0100 Subject: [PATCH 4/7] wip --- Tests/Tests.xcodeproj/project.pbxproj | 6 +++ Tests/Tests/WeakTests.swift | 56 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 Tests/Tests/WeakTests.swift diff --git a/Tests/Tests.xcodeproj/project.pbxproj b/Tests/Tests.xcodeproj/project.pbxproj index e0250e99..1924c353 100644 --- a/Tests/Tests.xcodeproj/project.pbxproj +++ b/Tests/Tests.xcodeproj/project.pbxproj @@ -104,6 +104,8 @@ 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 */; }; @@ -213,6 +215,7 @@ D58D83452A66C5EF00A203BE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D58D83482A66C5EF00A203BE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; D58D834F2A66C67A00A203BE /* TestCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCases.swift; sourceTree = ""; }; + D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakTests.swift; sourceTree = ""; }; D5AAF56E2A502EF000CAFFB6 /* MapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTests.swift; sourceTree = ""; }; D5AD0D902A114B98003D8DEC /* TextFieldWithVerticalAxisTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldWithVerticalAxisTests.swift; sourceTree = ""; }; D5ADFACB2A4A22AE009494FD /* SheetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetTests.swift; sourceTree = ""; }; @@ -450,6 +453,7 @@ children = ( D5B67B852A0D3193007D5D9B /* ViewTypes */, D5F0BE6729C0DC4900AD95AB /* PlatformVersionTests.swift */, + D591D1112A9CC2FF00AE05E8 /* WeakTests.swift */, D58CE15729C621DD0081BFB0 /* TestUtils.swift */, ); path = Tests; @@ -707,6 +711,7 @@ 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 */, @@ -812,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 */, diff --git a/Tests/Tests/WeakTests.swift b/Tests/Tests/WeakTests.swift new file mode 100644 index 00000000..a626b29d --- /dev/null +++ b/Tests/Tests/WeakTests.swift @@ -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) + } +} From 9a518a748e0afc0d016b830da43ef599b1680000 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:55:28 +0100 Subject: [PATCH 5/7] wip --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 12df5da8..13d91757 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,7 @@ In case SwiftUIIntrospect (unlikely) doesn't support the SwiftUI element that yo For example, here's how the library implements the introspectable `TextField` type: ```swift +import SwiftUI @_spi(Advanced) import SwiftUIIntrospect public struct TextFieldType: IntrospectableViewType {} @@ -252,16 +253,46 @@ extension macOSViewVersion { ### 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 -Releasing ---------- +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 will result in large memory consumption due to memory leaks. 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 + } + } +} +``` From e5164b5fe95655c085be1b582edf5b26c8b28e98 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:59:20 +0100 Subject: [PATCH 6/7] wip [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13d91757..f11c0115 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ Bear in mind this should be used cautiosly, and with full knowledge that any fut ### 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 will result in large memory consumption due to memory leaks. Instead, SwiftUI Introspect offers a `@Weak` property wrapper behind the Advanced SPI: +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 From 52fb3b53a0512448c7323b162b5a433b8e7a4e50 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Mon, 28 Aug 2023 18:49:03 +0100 Subject: [PATCH 7/7] wip [skip ci] --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b478488f..f9c414dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ Changelog ## master +- Added: `@Weak` property wrapper (#341) + ## [0.11.1] - Fixed: `@_spi` errors (#339)