Skip to content

Commit 9c4facc

Browse files
committed
Merge branch 'main' into ci/2026-tests
2 parents a78cd37 + b65c4ee commit 9c4facc

File tree

7 files changed

+215
-59
lines changed

7 files changed

+215
-59
lines changed

.github/workflows/ci.yml

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ jobs:
2727
- name: Select Xcode version
2828
run: sudo xcodes select 16.4
2929

30+
- name: Install Runtimes
31+
uses: nick-fields/retry@v3
32+
with:
33+
timeout_minutes: 15
34+
max_attempts: 3
35+
command: xcodebuild -downloadAllPlatforms
36+
3037
- name: Lint Podspec
3138
run: |
3239
set -eo pipefail
@@ -43,82 +50,79 @@ jobs:
4350
- platform: [iOS, 15]
4451
runtime: iOS 15.5
4552
os: macos-15
46-
xcode: 16.4
53+
xcode: 26.0
4754
install: true
4855
- platform: [iOS, 16]
4956
runtime: iOS 16.4
5057
os: macos-15
51-
xcode: 16.4
58+
xcode: 26.0
5259
install: true
5360
- platform: [iOS, 17]
5461
runtime: iOS 17.5
5562
os: macos-15
56-
xcode: 16.4
63+
xcode: 26.0
5764
install: true
5865
- platform: [iOS, 18]
59-
runtime: iOS 18.5
66+
runtime: iOS 18.6
6067
os: macos-15
61-
xcode: 16.4
68+
xcode: 26.0
6269
- platform: [iOS, 26]
63-
runtime: iOS 26.0-beta1
70+
runtime: iOS 26.0
6471
os: macos-15
6572
xcode: 26.0
66-
install: true
6773

6874
- platform: [tvOS, 15]
6975
runtime: tvOS 15.4
7076
os: macos-15
71-
xcode: 16.4
77+
xcode: 26.0
7278
install: true
7379
- platform: [tvOS, 16]
7480
runtime: tvOS 16.4
7581
os: macos-15
76-
xcode: 16.4
82+
xcode: 26.0
7783
install: true
7884
- platform: [tvOS, 17]
7985
runtime: tvOS 17.5
8086
os: macos-15
81-
xcode: 16.4
87+
xcode: 26.0
8288
install: true
8389
- platform: [tvOS, 18]
8490
runtime: tvOS 18.5
8591
os: macos-15
86-
xcode: 16.4
92+
xcode: 26.0
8793
- platform: [tvOS, 26]
88-
runtime: tvOS 26.0-beta1
94+
runtime: tvOS 26.0
8995
os: macos-15
9096
xcode: 26.0
91-
install: true
9297

9398
- platform: [watchOS, 8]
9499
runtime: watchOS 8.5
95100
os: macos-15
96-
xcode: 16.4
101+
xcode: 26.0
97102
install: true
98103
- platform: [watchOS, 9]
99104
runtime: watchOS 9.4
100105
os: macos-15
101-
xcode: 16.4
106+
xcode: 26.0
102107
install: true
103108
- platform: [watchOS, 10]
104109
runtime: watchOS 10.5
105110
os: macos-15
106-
xcode: 16.4
111+
xcode: 26.0
107112
install: true
108113
- platform: [watchOS, 11]
109114
runtime: watchOS 11.5
110115
os: macos-15
111-
xcode: 16.4
116+
xcode: 26.0
112117
- platform: [watchOS, 26]
113-
runtime: watchOS 26.0-beta1
118+
runtime: watchOS 26.0
114119
os: macos-15
115120
xcode: 26.0
116-
install: true
117121

118122
- platform: [macOS, 15]
119123
runtime: macOS 15
120124
os: macos-15
121-
xcode: 16.4
125+
xcode: 26.0
122126
# - platform: [macOS, 26]
123127
# runtime: macOS 26.0
124128
# os: macos-26
@@ -127,24 +131,34 @@ jobs:
127131
- platform: [visionOS, 1]
128132
runtime: visionOS 1.2
129133
os: macos-15
130-
xcode: 16.4
134+
xcode: 26.0
131135
install: true
132136
- platform: [visionOS, 2]
133137
runtime: visionOS 2.5
134138
os: macos-15
135-
xcode: 16.4
139+
xcode: 26.0
136140
- platform: [visionOS, 26]
137-
runtime: visionOS 26.0-beta1
141+
runtime: visionOS 26.0
138142
os: macos-15
139143
xcode: 26.0
140-
install: true
141144
steps:
142145
- name: Git Checkout
143146
uses: actions/checkout@v4
144147

145148
- name: Set environment variables
146149
run: echo "SKIP_SLOW_FASTLANE_WARNING=1" >> $GITHUB_ENV
147150

151+
- name: Select Xcode ${{ matrix.xcode }}
152+
run: sudo xcodes select ${{ matrix.xcode }}
153+
154+
- if: ${{ matrix.xcode == '26.0' && matrix.platform[0] != 'macOS' }}
155+
name: Install 2026 Runtime
156+
uses: nick-fields/retry@v3
157+
with:
158+
timeout_minutes: 15
159+
max_attempts: 3
160+
command: xcodebuild -downloadPlatform ${{ matrix.platform[0] }}
161+
148162
- if: ${{ matrix.install }}
149163
name: "[Debug] List Available Installable Runtimes"
150164
run: xcodes runtimes --include-betas

Sources/Introspect.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ struct IntrospectModifier<SwiftUIViewType: IntrospectableViewType, PlatformSpeci
107107
}
108108

109109
@MainActor
110-
public protocol PlatformEntity: AnyObject, Sendable {
110+
public protocol PlatformEntity: AnyObject {
111111
associatedtype Base: PlatformEntity
112112

113113
@_spi(Internals)

Sources/ViewTypes/SearchField.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#if !os(watchOS)
22
/// An abstract representation of the search field displayed via the `.searchable` modifier in SwiftUI.
33
///
4-
/// ### iOS
4+
/// ### iOS 15 - 18
55
///
66
/// ```swift
77
/// struct ContentView: View {
@@ -13,7 +13,40 @@
1313
/// .searchable(text: $searchTerm)
1414
/// }
1515
/// .navigationViewStyle(.stack)
16-
/// .introspect(.searchField, on: .iOS(.v15, .v16, .v17, .v18, .v26)) {
16+
/// .introspect(.searchField, on: .iOS(.v15, .v16, .v17, .v18)) {
17+
/// print(type(of: $0)) // UISearchBar
18+
/// }
19+
/// }
20+
/// }
21+
/// ```
22+
///
23+
/// ### iOS 26+
24+
///
25+
/// From iOS 26 onward, search bar is only backed by UIKit when `.searchable` is used within a
26+
/// `NavigationView` or `NavigationStack` contained inside a `TabView`.
27+
///
28+
/// If `.searchable` is used outside of these containers, it is backed by SwiftUI's own implementation,
29+
/// and there is no UIKit view to introspect.
30+
///
31+
/// The only exception to this is on iPad, where double column `NavigationView` and `NavigationSplitView`
32+
/// still use `UISearchBar` even outside of a `TabView` (for now...).
33+
///
34+
/// ```swift
35+
/// struct ContentView: View {
36+
/// @State var searchTerm = ""
37+
///
38+
/// var body: some View {
39+
/// TabView {
40+
/// NavigationView {
41+
/// Text("Root")
42+
/// .searchable(text: $searchTerm)
43+
/// }
44+
/// .navigationViewStyle(.stack)
45+
/// .tabItem {
46+
/// Label("Home", systemImage: "house")
47+
/// }
48+
/// }
49+
/// .introspect(.searchField, on: .iOS(.v26)) {
1750
/// print(type(of: $0)) // UISearchBar
1851
/// }
1952
/// }

Sources/ViewTypes/TabView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
/// }
3434
/// ```
3535
///
36-
/// ### macOS
36+
/// ### macOS 10.15 - 14
3737
///
3838
/// ```swift
3939
/// struct ContentView: View {

Tests/Tests/TestUtils.swift

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ enum TestUtils {
4646
@discardableResult
4747
func introspection<Entity: AnyObject & Sendable>(
4848
of type: Entity.Type,
49+
timeout: TimeInterval = 3,
50+
sourceLocation: SourceLocation = #_sourceLocation,
4951
@ViewBuilder view: (
5052
_ spy1: @escaping (Entity) -> Void
5153
) -> some View
5254
) async throws -> Entity {
5355
var entity1: Entity?
54-
return try await confirmation(expectedCount: 1...) { confirmation1 in
56+
return try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation1 in
5557
let view = view(
5658
{
5759
confirmation1()
@@ -61,27 +63,33 @@ func introspection<Entity: AnyObject & Sendable>(
6163

6264
TestUtils.present(view: view)
6365

64-
while entity1 == nil {
66+
let startInstant = Date()
67+
while
68+
Date().timeIntervalSince(startInstant) < timeout,
69+
entity1 == nil
70+
{
6571
await Task.yield()
6672
}
6773

68-
return try #require(entity1)
74+
return try #require(entity1, sourceLocation: sourceLocation)
6975
}
7076
}
7177

7278
@MainActor
7379
@discardableResult
7480
func introspection<Entity: AnyObject & Sendable>(
7581
of type: Entity.Type,
82+
timeout: TimeInterval = 3,
83+
sourceLocation: SourceLocation = #_sourceLocation,
7684
@ViewBuilder view: (
7785
_ spy1: @escaping (Entity) -> Void,
7886
_ spy2: @escaping (Entity) -> Void
7987
) -> some View
8088
) async throws -> (Entity, Entity) {
8189
var entity1: Entity?
8290
var entity2: Entity?
83-
return try await confirmation(expectedCount: 1...) { confirmation1 in
84-
try await confirmation(expectedCount: 1...) { confirmation2 in
91+
return try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation1 in
92+
try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation2 in
8593
let view = view(
8694
{
8795
confirmation1()
@@ -95,16 +103,18 @@ func introspection<Entity: AnyObject & Sendable>(
95103

96104
TestUtils.present(view: view)
97105

106+
let startInstant = Date()
98107
while
108+
Date().timeIntervalSince(startInstant) < timeout,
99109
entity1 == nil ||
100110
entity2 == nil
101111
{
102112
await Task.yield()
103113
}
104114

105115
return try (
106-
#require(entity1),
107-
#require(entity2),
116+
#require(entity1, sourceLocation: sourceLocation),
117+
#require(entity2, sourceLocation: sourceLocation),
108118
)
109119
}
110120
}
@@ -114,6 +124,8 @@ func introspection<Entity: AnyObject & Sendable>(
114124
@discardableResult
115125
func introspection<Entity: AnyObject & Sendable>(
116126
of type: Entity.Type,
127+
timeout: TimeInterval = 3,
128+
sourceLocation: SourceLocation = #_sourceLocation,
117129
@ViewBuilder view: (
118130
_ spy1: @escaping (Entity) -> Void,
119131
_ spy2: @escaping (Entity) -> Void,
@@ -123,9 +135,9 @@ func introspection<Entity: AnyObject & Sendable>(
123135
var entity1: Entity?
124136
var entity2: Entity?
125137
var entity3: Entity?
126-
return try await confirmation(expectedCount: 1...) { confirmation1 in
127-
try await confirmation(expectedCount: 1...) { confirmation2 in
128-
try await confirmation(expectedCount: 1...) { confirmation3 in
138+
return try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation1 in
139+
try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation2 in
140+
try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation3 in
129141
let view = view(
130142
{
131143
confirmation1()
@@ -143,7 +155,9 @@ func introspection<Entity: AnyObject & Sendable>(
143155

144156
TestUtils.present(view: view)
145157

158+
let startInstant = Date()
146159
while
160+
Date().timeIntervalSince(startInstant) < timeout,
147161
entity1 == nil ||
148162
entity2 == nil ||
149163
entity3 == nil
@@ -152,9 +166,9 @@ func introspection<Entity: AnyObject & Sendable>(
152166
}
153167

154168
return try (
155-
#require(entity1),
156-
#require(entity2),
157-
#require(entity3),
169+
#require(entity1, sourceLocation: sourceLocation),
170+
#require(entity2, sourceLocation: sourceLocation),
171+
#require(entity3, sourceLocation: sourceLocation),
158172
)
159173
}
160174
}
@@ -165,6 +179,8 @@ func introspection<Entity: AnyObject & Sendable>(
165179
@discardableResult
166180
func introspection<Entity: AnyObject & Sendable>(
167181
of type: Entity.Type,
182+
timeout: TimeInterval = 3,
183+
sourceLocation: SourceLocation = #_sourceLocation,
168184
@ViewBuilder view: (
169185
_ spy1: @escaping (Entity) -> Void,
170186
_ spy2: @escaping (Entity) -> Void,
@@ -176,10 +192,10 @@ func introspection<Entity: AnyObject & Sendable>(
176192
var entity2: Entity?
177193
var entity3: Entity?
178194
var entity4: Entity?
179-
return try await confirmation(expectedCount: 1...) { confirmation1 in
180-
try await confirmation(expectedCount: 1...) { confirmation2 in
181-
try await confirmation(expectedCount: 1...) { confirmation3 in
182-
try await confirmation(expectedCount: 1...) { confirmation4 in
195+
return try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation1 in
196+
try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation2 in
197+
try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation3 in
198+
try await confirmation(expectedCount: 1..., sourceLocation: sourceLocation) { confirmation4 in
183199
let view = view(
184200
{
185201
confirmation1()
@@ -201,7 +217,9 @@ func introspection<Entity: AnyObject & Sendable>(
201217

202218
TestUtils.present(view: view)
203219

220+
let startInstant = Date()
204221
while
222+
Date().timeIntervalSince(startInstant) < timeout,
205223
entity1 == nil ||
206224
entity2 == nil ||
207225
entity3 == nil ||
@@ -211,10 +229,10 @@ func introspection<Entity: AnyObject & Sendable>(
211229
}
212230

213231
return try (
214-
#require(entity1),
215-
#require(entity2),
216-
#require(entity3),
217-
#require(entity4),
232+
#require(entity1, sourceLocation: sourceLocation),
233+
#require(entity2, sourceLocation: sourceLocation),
234+
#require(entity3, sourceLocation: sourceLocation),
235+
#require(entity4, sourceLocation: sourceLocation),
218236
)
219237
}
220238
}

0 commit comments

Comments
 (0)