Skip to content

Commit 728e5ee

Browse files
committed
A bunch of updates, mostly window fixes
1 parent d6ea5c2 commit 728e5ee

File tree

11 files changed

+266
-58
lines changed

11 files changed

+266
-58
lines changed

Package.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,15 @@ 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)],
64+
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .macCatalyst(.v13)],
6565
products: [
6666
.library(name: "SwiftCrossUI", type: libraryType, targets: ["SwiftCrossUI"]),
6767
.library(name: "AppKitBackend", type: libraryType, targets: ["AppKitBackend"]),
6868
.library(name: "GtkBackend", type: libraryType, targets: ["GtkBackend"]),
6969
.library(name: "Gtk3Backend", type: libraryType, targets: ["Gtk3Backend"]),
7070
.library(name: "WinUIBackend", targets: ["WinUIBackend"]),
7171
.library(name: "DefaultBackend", type: libraryType, targets: ["DefaultBackend"]),
72+
.library(name: "UIKitBackend", type: libraryType, targets: ["UIKitBackend"]),
7273
.library(name: "Gtk", type: libraryType, targets: ["Gtk"]),
7374
.executable(name: "GtkExample", targets: ["GtkExample"]),
7475
// .library(name: "CursesBackend", type: libraryType, targets: ["CursesBackend"]),
@@ -149,7 +150,7 @@ let package = Package(
149150
// Non-desktop platforms need to be handled separately:
150151
// Only one backend is supported, and `#if` won't work because it's evaluated
151152
// on the compiling desktop, not the target.
152-
.target(name: "UIKitBackend", condition: .when(platforms: [.iOS, .tvOS])),
153+
.target(name: "UIKitBackend", condition: .when(platforms: [.iOS, .tvOS, .macCatalyst])),
153154
]
154155
),
155156
.target(name: "AppKitBackend", dependencies: ["SwiftCrossUI"]),

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ public final class AppKitBackend: AppBackend {
7171
)
7272
}
7373

74+
public func isFixedSizeWindow(_ window: Window) -> Bool {
75+
!window.styleMask.contains(.fullScreen)
76+
}
77+
7478
public func setSize(ofWindow window: Window, to newSize: SIMD2<Int>) {
7579
window.setContentSize(NSSize(width: newSize.x, height: newSize.y))
7680
}
@@ -175,18 +179,27 @@ public final class AppKitBackend: AppBackend {
175179
public static func createDefaultAboutMenu() -> NSMenu {
176180
let appName = ProcessInfo.processInfo.processName
177181
let appMenu = NSMenu(title: appName)
178-
appMenu.addItem(withTitle: "About \(appName)", action: #selector(NSApp.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
182+
appMenu.addItem(
183+
withTitle: "About \(appName)",
184+
action: #selector(NSApp.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
179185
appMenu.addItem(NSMenuItem.separator())
180186

181-
let hideMenu = appMenu.addItem(withTitle: "Hide \(appName)", action: #selector(NSApp.hide(_:)), keyEquivalent: "h")
187+
let hideMenu = appMenu.addItem(
188+
withTitle: "Hide \(appName)", action: #selector(NSApp.hide(_:)), keyEquivalent: "h")
182189
hideMenu.keyEquivalentModifierMask = .command
183190

184-
let hideOthers = appMenu.addItem(withTitle: "Hide Others", action: #selector(NSApp.hideOtherApplications(_:)), keyEquivalent: "h")
191+
let hideOthers = appMenu.addItem(
192+
withTitle: "Hide Others", action: #selector(NSApp.hideOtherApplications(_:)),
193+
keyEquivalent: "h")
185194
hideOthers.keyEquivalentModifierMask = [.option, .command]
186195

187-
appMenu.addItem(withTitle: "Show All", action: #selector(NSApp.unhideAllApplications(_:)), keyEquivalent: "")
196+
appMenu.addItem(
197+
withTitle: "Show All", action: #selector(NSApp.unhideAllApplications(_:)),
198+
keyEquivalent: "")
188199

189-
let quitMenu = appMenu.addItem(withTitle: "Quit \(appName)", action: #selector(NSApp.terminate(_:)), keyEquivalent: "q")
200+
let quitMenu = appMenu.addItem(
201+
withTitle: "Quit \(appName)", action: #selector(NSApp.terminate(_:)), keyEquivalent: "q"
202+
)
190203
quitMenu.keyEquivalentModifierMask = .command
191204

192205
return appMenu

Sources/GtkBackend/GtkBackend.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ public final class GtkBackend: AppBackend {
117117
return SIMD2(size.width, size.height)
118118
}
119119

120+
public func isFixedSizeWindow(_ window: Window) -> Bool {
121+
// TODO: Detect whether window is fullscreen
122+
return false
123+
}
124+
120125
public func setSize(ofWindow window: Window, to newSize: SIMD2<Int>) {
121126
let child = window.getChild() as! CustomRootWidget
122127
window.size = Size(

Sources/SwiftCrossUI/Backend/AppBackend.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import Foundation
1111
/// ``AppBackend/setTitle(ofWindow:to:)``, ``AppBackend/setResizability(ofWindow:to:)``,
1212
/// ``AppBackend/setChild(ofWindow:to:)``, ``AppBackend/show(window:)``,
1313
/// ``AppBackend/runMainLoop()``, ``AppBackend/runInMainThread(action:)``,
14-
/// ``AppBackend/show(widget:)``. Many of these can simply be given dummy
15-
/// implementations until you're ready to implement them properly.
14+
/// ``AppBackend/isFixedSizeWindow(_:)``, ``AppBackend/show(widget:)``.
15+
/// Many of these can simply be given dummy implementations until you're ready
16+
/// to implement them properly.
1617
///
1718
/// If you need to modify the children of a widget after creation but there
1819
/// aren't update methods available, this is an intentional limitation to
@@ -112,6 +113,9 @@ public protocol AppBackend {
112113
func setChild(ofWindow window: Window, to child: Widget)
113114
/// Gets the size of the given window in pixels.
114115
func size(ofWindow window: Window) -> SIMD2<Int>
116+
/// Check whether a window is programmatically resizable. This value does not necessarily
117+
/// reflect whether the window is resizable by the user.
118+
func isFixedSizeWindow(_ window: Window) -> Bool
115119
/// Sets the size of the given window in pixels.
116120
func setSize(ofWindow window: Window, to newSize: SIMD2<Int>)
117121
/// Sets the minimum width and height of the window. Prevents the user from making the

Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,16 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
6666
fatalError("Scene updated with a backend incompatible with the window it was given")
6767
}
6868

69+
let isFixedSize = backend.isFixedSizeWindow(window)
70+
6971
_ = update(
7072
newScene,
71-
proposedWindowSize: isFirstUpdate
73+
proposedWindowSize: isFirstUpdate && !isFixedSize
7274
? (newScene ?? scene).defaultSize
7375
: backend.size(ofWindow: window),
7476
backend: backend,
75-
environment: environment
77+
environment: environment,
78+
windowSizeIsFinal: isFixedSize
7679
)
7780
}
7881

@@ -147,17 +150,12 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
147150
}
148151
}
149152

150-
let finalContentResult: ViewUpdateResult
151-
if windowSizeIsFinal {
152-
finalContentResult = contentResult
153-
} else {
154-
finalContentResult = viewGraph.update(
155-
with: newScene?.body,
156-
proposedSize: proposedWindowSize,
157-
environment: environment,
158-
dryRun: false
159-
)
160-
}
153+
let finalContentResult = viewGraph.update(
154+
with: newScene?.body,
155+
proposedSize: proposedWindowSize,
156+
environment: environment,
157+
dryRun: false
158+
)
161159

162160
// The Gtk 3 backend has some broken sizing code that can't really be
163161
// fixed due to the design of Gtk 3. Our layout system underestimates

Sources/UIKitBackend/BaseWidget.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,15 @@ public class BaseWidget: UIView {
6161
leftConstraint?.isActive = false
6262
guard let superview else { return }
6363
leftConstraint = self.leftAnchor.constraint(
64-
equalTo: superview.leftAnchor, constant: CGFloat(x))
64+
equalTo: superview.safeAreaLayoutGuide.leftAnchor, constant: CGFloat(x))
6565
leftConstraint!.isActive = true
6666
}
6767

6868
private func updateTopConstraint() {
6969
topConstraint?.isActive = false
7070
guard let superview else { return }
7171
topConstraint = self.topAnchor.constraint(
72-
equalTo: superview.topAnchor, constant: CGFloat(y))
72+
equalTo: superview.safeAreaLayoutGuide.topAnchor, constant: CGFloat(y))
7373
topConstraint!.isActive = true
7474
}
7575

@@ -114,4 +114,8 @@ internal class WrapperWidget<View: UIView>: BaseWidget {
114114
var child: View {
115115
subviews[0] as! View
116116
}
117+
118+
override var intrinsicContentSize: CGSize {
119+
child.intrinsicContentSize
120+
}
117121
}

Sources/UIKitBackend/UIKitBackend+Container.swift

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,60 @@
88
import SwiftCrossUI
99
import UIKit
1010

11+
final class ScrollWidget: WrapperWidget<UIScrollView> {
12+
private var childWidthConstraint: NSLayoutConstraint?
13+
private var childHeightConstraint: NSLayoutConstraint?
14+
15+
private let innerChild: BaseWidget
16+
17+
init(child innerChild: BaseWidget) {
18+
self.innerChild = innerChild
19+
super.init(child: UIScrollView())
20+
21+
child.addSubview(innerChild)
22+
23+
NSLayoutConstraint.activate([
24+
innerChild.topAnchor.constraint(equalTo: child.contentLayoutGuide.topAnchor),
25+
innerChild.bottomAnchor.constraint(equalTo: child.contentLayoutGuide.bottomAnchor),
26+
innerChild.leftAnchor.constraint(equalTo: child.contentLayoutGuide.leftAnchor),
27+
innerChild.rightAnchor.constraint(equalTo: child.contentLayoutGuide.rightAnchor),
28+
])
29+
}
30+
31+
func setScrollBars(
32+
hasVerticalScrollBar: Bool,
33+
hasHorizontalScrollBar: Bool
34+
) {
35+
switch (hasVerticalScrollBar, childHeightConstraint?.isActive) {
36+
case (true, true):
37+
childHeightConstraint!.isActive = false
38+
case (false, nil):
39+
childHeightConstraint = innerChild.heightAnchor.constraint(
40+
equalTo: child.heightAnchor)
41+
fallthrough
42+
case (false, false):
43+
childHeightConstraint!.isActive = true
44+
default:
45+
break
46+
}
47+
48+
switch (hasHorizontalScrollBar, childWidthConstraint?.isActive) {
49+
case (true, true):
50+
childWidthConstraint!.isActive = false
51+
case (false, nil):
52+
childWidthConstraint = innerChild.widthAnchor.constraint(equalTo: child.widthAnchor)
53+
fallthrough
54+
case (false, false):
55+
childWidthConstraint!.isActive = true
56+
default:
57+
break
58+
}
59+
60+
child.showsVerticalScrollIndicator = hasVerticalScrollBar
61+
child.showsHorizontalScrollIndicator = hasHorizontalScrollBar
62+
}
63+
}
64+
1165
extension UIKitBackend {
1266
public func createContainer() -> Widget {
1367
BaseWidget()
@@ -68,5 +122,18 @@ extension UIKitBackend {
68122
widget.height = size.y
69123
}
70124

71-
// TODO: Scroll containers
125+
public func createScrollContainer(for child: Widget) -> Widget {
126+
ScrollWidget(child: child)
127+
}
128+
129+
public func setScrollBarPresence(
130+
ofScrollContainer scrollView: Widget,
131+
hasVerticalScrollBar: Bool,
132+
hasHorizontalScrollBar: Bool
133+
) {
134+
let scrollWidget = scrollView as! ScrollWidget
135+
scrollWidget.setScrollBars(
136+
hasVerticalScrollBar: hasVerticalScrollBar,
137+
hasHorizontalScrollBar: hasHorizontalScrollBar)
138+
}
72139
}

Sources/UIKitBackend/UIKitBackend+Control.swift

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,37 +57,69 @@ internal final class PickerWidget: WrapperWidget<UIPickerView>, UIPickerViewData
5757
child.reloadComponent(0)
5858
}
5959
}
60-
var onSelect: ((Int) -> Void)?
60+
var onSelect: ((Int?) -> Void)?
6161

6262
init() {
6363
super.init(child: UIPickerView())
6464

6565
child.dataSource = self
6666
child.delegate = self
67+
68+
child.selectRow(0, inComponent: 0, animated: false)
6769
}
6870

6971
func numberOfComponents(in _: UIPickerView) -> Int {
7072
1
7173
}
7274

7375
func pickerView(_: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
74-
options.count
76+
options.count + 1
7577
}
7678

7779
func pickerView(
7880
_: UIPickerView,
7981
titleForRow row: Int,
8082
forComponent _: Int
8183
) -> String? {
82-
options.indices ~= row ? options[row] : nil
84+
switch row {
85+
case 0:
86+
""
87+
case 1...options.count:
88+
options[row - 1]
89+
default:
90+
nil
91+
}
8392
}
8493

8594
func pickerView(
8695
_: UIPickerView,
8796
didSelectRow row: Int,
8897
inComponent _: Int
8998
) {
90-
onSelect?(row)
99+
onSelect?(row > 0 ? row - 1 : nil)
100+
}
101+
}
102+
103+
internal final class SwitchWidget: WrapperWidget<UISwitch> {
104+
var onChange: ((Bool) -> Void)?
105+
106+
@objc
107+
func switchFlipped() {
108+
onChange?(child.isOn)
109+
}
110+
111+
init() {
112+
super.init(child: UISwitch())
113+
114+
// On iOS 14 and later, UISwitch can be either a switch or a checkbox. We have no
115+
// control over this on iOS 13 or any tvOS, but when possible, prefer a switch.
116+
#if os(iOS) || targetEnvironment(macCatalyst)
117+
if #available(iOS 14, macCatalyst 14, *) {
118+
child.preferredStyle = .sliding
119+
}
120+
#endif
121+
122+
child.addTarget(self, action: #selector(switchFlipped), for: .valueChanged)
91123
}
92124
}
93125

@@ -158,8 +190,21 @@ extension UIKitBackend {
158190
}
159191

160192
public func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) {
161-
guard let selectedOption else { return }
162193
let pickerWidget = picker as! PickerWidget
163-
pickerWidget.child.selectRow(selectedOption, inComponent: 0, animated: false)
194+
pickerWidget.child.selectRow((selectedOption ?? -1) + 1, inComponent: 0, animated: false)
195+
}
196+
197+
public func createSwitch() -> Widget {
198+
SwitchWidget()
199+
}
200+
201+
public func updateSwitch(_ switchWidget: Widget, onChange: @escaping (Bool) -> Void) {
202+
let wrapper = switchWidget as! SwitchWidget
203+
wrapper.onChange = onChange
204+
}
205+
206+
public func setState(ofSwitch switchWidget: Widget, to state: Bool) {
207+
let wrapper = switchWidget as! SwitchWidget
208+
wrapper.child.setOn(state, animated: true)
164209
}
165210
}

0 commit comments

Comments
 (0)