Skip to content

Commit 17195a7

Browse files
authored
Add visionOS support (#203)
1 parent ca8fced commit 17195a7

File tree

9 files changed

+99
-55
lines changed

9 files changed

+99
-55
lines changed

.github/workflows/build-test-and-docs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ jobs:
6565
- iPhone
6666
- iPad
6767
- TV
68+
- Vision
6869
steps:
6970
- name: Force Xcode 15.4
7071
run: sudo xcode-select -switch /Applications/Xcode_15.4.app

Examples/Sources/ControlsExample/ControlsApp.swift

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ struct ControlsApp: App {
3030
}
3131
.padding(.bottom, 20)
3232

33-
VStack {
34-
Text("Toggle button")
35-
Toggle("Toggle me!", active: $exampleButtonState)
36-
.toggleStyle(.button)
37-
Text("Currently enabled: \(exampleButtonState)")
38-
}
39-
.padding(.bottom, 20)
33+
#if !canImport(UIKitBackend)
34+
VStack {
35+
Text("Toggle button")
36+
Toggle("Toggle me!", active: $exampleButtonState)
37+
.toggleStyle(.button)
38+
Text("Currently enabled: \(exampleButtonState)")
39+
}
40+
.padding(.bottom, 20)
41+
#endif
4042

4143
VStack {
4244
Text("Toggle switch")
@@ -45,12 +47,14 @@ struct ControlsApp: App {
4547
Text("Currently enabled: \(exampleSwitchState)")
4648
}
4749

48-
VStack {
49-
Text("Checkbox")
50-
Toggle("Toggle me:", active: $exampleCheckboxState)
51-
.toggleStyle(.checkbox)
52-
Text("Currently enabled: \(exampleCheckboxState)")
53-
}
50+
#if !canImport(UIKitBackend)
51+
VStack {
52+
Text("Checkbox")
53+
Toggle("Toggle me:", active: $exampleCheckboxState)
54+
.toggleStyle(.checkbox)
55+
Text("Currently enabled: \(exampleCheckboxState)")
56+
}
57+
#endif
5458

5559
VStack {
5660
Text("Slider")

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] {
6161

6262
let package = Package(
6363
name: "swift-cross-ui",
64-
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .macCatalyst(.v13)],
64+
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .macCatalyst(.v13), .visionOS(.v1)],
6565
products: [
6666
.library(name: "SwiftCrossUI", type: libraryType, targets: ["SwiftCrossUI"]),
6767
.library(name: "AppKitBackend", type: libraryType, targets: ["AppKitBackend"]),
@@ -162,7 +162,7 @@ let package = Package(
162162
// on the compiling desktop, not the target.
163163
.target(
164164
name: "UIKitBackend",
165-
condition: .when(platforms: [.iOS, .tvOS, .macCatalyst])
165+
condition: .when(platforms: [.iOS, .tvOS, .macCatalyst, .visionOS])
166166
),
167167
]
168168
),

Sources/SwiftCrossUI/Values/TextStyle.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ extension Font.TextStyle {
7373
.desktop: desktopTextStyles,
7474
.phone: mobileTextStyles,
7575
.tablet: mobileTextStyles,
76-
.tv: tvTextStyles
76+
.tv: tvTextStyles,
7777
]
7878

7979
private static let desktopTextStyles: [Self: Resolved] = [

Sources/UIKitBackend/KeyboardToolbar.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import UIKit
99
/// containing the ``View/keyboardToolbar(animateChanges:body:)`` modifier is updated, so any
1010
/// state necessary for the toolbar should live in the view itself.
1111
@available(tvOS, unavailable)
12+
@available(visionOS, unavailable)
1213
public protocol ToolbarItem {
1314
/// The type of bar button item used to represent this item in UIKit.
1415
associatedtype ItemType: UIBarButtonItem
@@ -23,6 +24,7 @@ public protocol ToolbarItem {
2324
}
2425

2526
@available(tvOS, unavailable)
27+
@available(visionOS, unavailable)
2628
@resultBuilder
2729
public enum ToolbarBuilder {
2830
public enum Component {
@@ -61,6 +63,7 @@ public enum ToolbarBuilder {
6163
}
6264

6365
@available(tvOS, unavailable)
66+
@available(visionOS, unavailable)
6467
extension Button: ToolbarItem {
6568
public final class ItemType: UIBarButtonItem {
6669
var callback: @MainActor @Sendable () -> Void
@@ -100,6 +103,7 @@ extension Button: ToolbarItem {
100103
// See https://forums.swift.org/t/contradictory-available-s-are-required/78831
101104
@available(iOS 14, macCatalyst 14, *)
102105
@available(tvOS, unavailable, introduced: 14)
106+
@available(visionOS, unavailable)
103107
extension Spacer: ToolbarItem {
104108
public func createBarButtonItem() -> UIBarButtonItem {
105109
if let minLength, minLength > 0 {
@@ -120,6 +124,7 @@ extension Spacer: ToolbarItem {
120124
}
121125

122126
@available(tvOS, unavailable)
127+
@available(visionOS, unavailable)
123128
struct FixedWidthToolbarItem<Base: ToolbarItem>: ToolbarItem {
124129
var base: Base
125130
var width: Int?
@@ -143,6 +148,7 @@ struct FixedWidthToolbarItem<Base: ToolbarItem>: ToolbarItem {
143148
// Setting width on a flexible space is ignored, you must use a fixed space from the outset
144149
@available(iOS 14, macCatalyst 14, *)
145150
@available(tvOS, unavailable, introduced: 14)
151+
@available(visionOS, unavailable)
146152
struct FixedWidthSpacerItem: ToolbarItem {
147153
var width: Int?
148154

@@ -160,6 +166,7 @@ struct FixedWidthSpacerItem: ToolbarItem {
160166
}
161167

162168
@available(tvOS, unavailable)
169+
@available(visionOS, unavailable)
163170
struct ColoredToolbarItem<Base: ToolbarItem>: ToolbarItem {
164171
var base: Base
165172
var color: Color
@@ -177,6 +184,7 @@ struct ColoredToolbarItem<Base: ToolbarItem>: ToolbarItem {
177184
}
178185

179186
@available(tvOS, unavailable)
187+
@available(visionOS, unavailable)
180188
extension ToolbarItem {
181189
/// A toolbar item with the specified width.
182190
///
@@ -199,6 +207,7 @@ extension ToolbarItem {
199207
}
200208

201209
@available(tvOS, unavailable)
210+
@available(visionOS, unavailable)
202211
indirect enum ToolbarItemLocation: Hashable {
203212
case expression(inside: ToolbarItemLocation?)
204213
case block(index: Int, inside: ToolbarItemLocation?)
@@ -209,6 +218,7 @@ indirect enum ToolbarItemLocation: Hashable {
209218
}
210219

211220
@available(tvOS, unavailable)
221+
@available(visionOS, unavailable)
212222
final class KeyboardToolbar: UIToolbar {
213223
var locations: [ToolbarItemLocation: UIBarButtonItem] = [:]
214224

@@ -286,11 +296,13 @@ final class KeyboardToolbar: UIToolbar {
286296
}
287297

288298
@available(tvOS, unavailable)
299+
@available(visionOS, unavailable)
289300
enum ToolbarKey: EnvironmentKey {
290301
static let defaultValue: ((KeyboardToolbar) -> Void)? = nil
291302
}
292303

293304
@available(tvOS, unavailable)
305+
@available(visionOS, unavailable)
294306
extension EnvironmentValues {
295307
var updateToolbar: ((KeyboardToolbar) -> Void)? {
296308
get { self[ToolbarKey.self] }
@@ -305,6 +317,7 @@ extension View {
305317
/// updated
306318
/// - body: The toolbar's contents
307319
@available(tvOS, unavailable)
320+
@available(visionOS, unavailable)
308321
public func keyboardToolbar(
309322
animateChanges: Bool = true,
310323
@ToolbarBuilder body: @escaping () -> ToolbarBuilder.FinalResult

Sources/UIKitBackend/UIKitBackend+Container.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,17 @@ final class ScrollWidget: ContainerWidget {
5959

6060
public func updateScrollContainer(environment: EnvironmentValues) {
6161
#if os(iOS)
62-
scrollView.keyboardDismissMode = switch environment.scrollDismissesKeyboardMode {
63-
case .automatic:
64-
.interactive
65-
case .immediately:
66-
.onDrag
67-
case .interactively:
68-
.interactive
69-
case .never:
70-
.none
71-
}
62+
scrollView.keyboardDismissMode =
63+
switch environment.scrollDismissesKeyboardMode {
64+
case .automatic:
65+
.interactive
66+
case .immediately:
67+
.onDrag
68+
case .interactively:
69+
.interactive
70+
case .never:
71+
.none
72+
}
7273
#endif
7374
}
7475
}

Sources/UIKitBackend/UIKitBackend+Control.swift

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -318,17 +318,22 @@ extension UIKitBackend {
318318
textEditorWidget.child.inputAccessoryView = nil
319319
}
320320

321-
textEditorWidget.child.alwaysBounceVertical = environment.scrollDismissesKeyboardMode != .never
322-
textEditorWidget.child.keyboardDismissMode = switch environment.scrollDismissesKeyboardMode {
323-
case .automatic:
324-
textEditorWidget.child.inputAccessoryView == nil ? .interactive : .interactiveWithAccessory
325-
case .immediately:
326-
textEditorWidget.child.inputAccessoryView == nil ? .onDrag : .onDragWithAccessory
327-
case .interactively:
328-
textEditorWidget.child.inputAccessoryView == nil ? .interactive : .interactiveWithAccessory
329-
case .never:
330-
.none
331-
}
321+
textEditorWidget.child.alwaysBounceVertical =
322+
environment.scrollDismissesKeyboardMode != .never
323+
textEditorWidget.child.keyboardDismissMode =
324+
switch environment.scrollDismissesKeyboardMode {
325+
case .automatic:
326+
textEditorWidget.child.inputAccessoryView == nil
327+
? .interactive : .interactiveWithAccessory
328+
case .immediately:
329+
textEditorWidget.child.inputAccessoryView == nil
330+
? .onDrag : .onDragWithAccessory
331+
case .interactively:
332+
textEditorWidget.child.inputAccessoryView == nil
333+
? .interactive : .interactiveWithAccessory
334+
case .never:
335+
.none
336+
}
332337
#endif
333338
}
334339

@@ -409,7 +414,7 @@ extension UIKitBackend {
409414
}
410415
}
411416

412-
#if os(iOS)
417+
#if os(iOS) || os(visionOS)
413418
public func createSlider() -> Widget {
414419
SliderWidget()
415420
}

Sources/UIKitBackend/UIKitBackend+Window.swift

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,27 @@ final class RootViewController: UIViewController {
55
var resizeHandler: ((CGSize) -> Void)?
66
private var childWidget: (any WidgetProtocol)?
77

8-
init(backend: UIKitBackend) {
9-
self.backend = backend
10-
super.init(nibName: nil, bundle: nil)
11-
}
8+
#if os(visionOS)
9+
init(backend: UIKitBackend) {
10+
self.backend = backend
11+
super.init(nibName: nil, bundle: nil)
12+
13+
registerForTraitChanges([UITraitUserInterfaceStyle.self]) {
14+
(self: RootViewController, _: UITraitCollection) in
15+
self.backend.onTraitCollectionChange?()
16+
}
17+
}
18+
#else
19+
init(backend: UIKitBackend) {
20+
self.backend = backend
21+
super.init(nibName: nil, bundle: nil)
22+
}
23+
24+
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
25+
super.traitCollectionDidChange(previousTraitCollection)
26+
backend.onTraitCollectionChange?()
27+
}
28+
#endif
1229

1330
@available(*, unavailable)
1431
required init?(coder: NSCoder) {
@@ -29,11 +46,6 @@ final class RootViewController: UIViewController {
2946
resizeHandler?(size)
3047
}
3148

32-
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
33-
super.traitCollectionDidChange(previousTraitCollection)
34-
backend.onTraitCollectionChange?()
35-
}
36-
3749
func setChild(to child: some WidgetProtocol) {
3850
childWidget?.removeFromParentWidget()
3951
child.removeFromParentWidget()
@@ -110,19 +122,25 @@ extension UIKitBackend {
110122
}
111123

112124
public func isWindowProgrammaticallyResizable(_ window: Window) -> Bool {
113-
// On iPad, some windows are user-resizable, but UIKit windows are never
114-
// programmatically resizable.
115-
false
125+
#if os(visionOS)
126+
true
127+
#else
128+
false
129+
#endif
116130
}
117131

118132
public func setResizability(ofWindow window: Window, to resizable: Bool) {
119133
print("UIKitBackend: ignoring \(#function) call")
120134
}
121135

122136
public func setSize(ofWindow window: Window, to newSize: SIMD2<Int>) {
123-
print(
124-
"UIKitBackend: ignoring \(#function) call. Current window size: \(window.bounds.width) x \(window.bounds.height); proposed size: \(newSize.x) x \(newSize.y)"
125-
)
137+
#if os(visionOS)
138+
window.bounds.size = CGSize(width: CGFloat(newSize.x), height: CGFloat(newSize.y))
139+
#else
140+
print(
141+
"UIKitBackend: ignoring \(#function) call. Current window size: \(window.bounds.width) x \(window.bounds.height); proposed size: \(newSize.x) x \(newSize.y)"
142+
)
143+
#endif
126144
}
127145

128146
public func setMinimumSize(ofWindow window: Window, to minimumSize: SIMD2<Int>) {

Sources/UIKitBackend/UIKitBackend.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@ public final class UIKitBackend: AppBackend {
2727
switch UIDevice.current.userInterfaceIdiom {
2828
case .phone:
2929
.phone
30-
case .pad:
30+
case .pad, .vision:
3131
.tablet
3232
case .tv:
3333
.tv
3434
case .mac:
3535
.desktop
36-
case .unspecified, .carPlay, .vision:
36+
case .unspecified, .carPlay:
3737
// Seems like the safest fallback for now given that we don't
3838
// explicitly support these devices.
3939
.tablet
40+
@unknown default:
41+
.tablet
4042
}
4143
}
4244

0 commit comments

Comments
 (0)