Skip to content

Commit cc33af4

Browse files
committed
Add alerts and compile on tvOS
1 parent 728e5ee commit cc33af4

File tree

6 files changed

+249
-66
lines changed

6 files changed

+249
-66
lines changed

Sources/UIKitBackend/BaseWidget.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ public class BaseWidget: UIView {
4848
internal init() {
4949
super.init(frame: .zero)
5050

51-
self.clipsToBounds = true
5251
self.translatesAutoresizingMaskIntoConstraints = false
5352
}
5453

@@ -93,6 +92,10 @@ public class BaseWidget: UIView {
9392
}
9493
}
9594

95+
extension UIKitBackend {
96+
public typealias Widget = BaseWidget
97+
}
98+
9699
internal class WrapperWidget<View: UIView>: BaseWidget {
97100
init(child: View) {
98101
super.init()

Sources/UIKitBackend/UIColor+Color.swift

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,19 @@ extension UIColor {
1717
alpha: CGFloat(color.alpha)
1818
)
1919
}
20+
}
2021

21-
internal var swiftCrossUIColor: Color {
22-
let ciColor = CIColor(color: self)
22+
extension Color {
23+
internal init(_ uiColor: UIColor) {
24+
let ciColor = CIColor(color: uiColor)
2325

24-
return Color(
26+
self.init(
2527
Float(ciColor.red),
2628
Float(ciColor.green),
2729
Float(ciColor.blue),
2830
Float(ciColor.alpha)
2931
)
3032
}
31-
}
32-
33-
extension Color {
34-
internal init(_ uiColor: UIColor) {
35-
self = uiColor.swiftCrossUIColor
36-
}
3733

3834
internal var uiColor: UIColor {
3935
UIColor(color: self)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// UIKitBackend+Alert.swift
3+
// swift-cross-ui
4+
//
5+
// Created by William Baker on 1/10/25.
6+
//
7+
8+
import SwiftCrossUI
9+
import UIKit
10+
11+
extension UIKitBackend {
12+
public final class Alert {
13+
internal let controller: UIAlertController
14+
internal var handler: ((Int) -> Void)?
15+
16+
internal init() {
17+
self.controller = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
18+
}
19+
}
20+
21+
public func createAlert() -> Alert {
22+
Alert()
23+
}
24+
25+
public func updateAlert(
26+
_ alert: Alert,
27+
title: String,
28+
actionLabels: [String],
29+
environment _: EnvironmentValues
30+
) {
31+
alert.controller.title = title
32+
33+
for (i, actionLabel) in actionLabels.enumerated() {
34+
let action = UIAlertAction(title: actionLabel, style: .default) {
35+
[unowned self, weak alert] _ in
36+
guard let alert else { return }
37+
alert.handler!(i)
38+
}
39+
alert.controller.addAction(action)
40+
}
41+
}
42+
43+
public func showAlert(
44+
_ alert: Alert,
45+
window: Window?,
46+
responseHandler handleResponse: @escaping (Int) -> Void
47+
) {
48+
guard let window = window ?? mainWindow else {
49+
assertionFailure("Could not find window in which to display alert")
50+
return
51+
}
52+
53+
alert.handler = handleResponse
54+
window.rootViewController!.present(alert.controller, animated: false)
55+
}
56+
57+
public func dismissAlert(_ alert: Alert, window: Window?) {
58+
alert.controller.dismiss(animated: false)
59+
60+
}
61+
}

Sources/UIKitBackend/UIKitBackend+Control.swift

Lines changed: 168 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ internal final class TextFieldWidget: WrapperWidget<UITextField>, UITextFieldDel
4949
}
5050
}
5151

52+
@available(tvOS, unavailable)
53+
@available(macCatalyst, unavailable)
5254
internal final class PickerWidget: WrapperWidget<UIPickerView>, UIPickerViewDataSource,
5355
UIPickerViewDelegate
5456
{
@@ -76,50 +78,126 @@ internal final class PickerWidget: WrapperWidget<UIPickerView>, UIPickerViewData
7678
options.count + 1
7779
}
7880

79-
func pickerView(
80-
_: UIPickerView,
81-
titleForRow row: Int,
82-
forComponent _: Int
83-
) -> String? {
84-
switch row {
85-
case 0:
86-
""
87-
case 1...options.count:
88-
options[row - 1]
89-
default:
90-
nil
81+
#if os(iOS)
82+
func pickerView(
83+
_: UIPickerView,
84+
titleForRow row: Int,
85+
forComponent _: Int
86+
) -> String? {
87+
switch row {
88+
case 0:
89+
""
90+
case 1...options.count:
91+
options[row - 1]
92+
default:
93+
nil
94+
}
9195
}
92-
}
9396

94-
func pickerView(
95-
_: UIPickerView,
96-
didSelectRow row: Int,
97-
inComponent _: Int
98-
) {
99-
onSelect?(row > 0 ? row - 1 : nil)
100-
}
97+
func pickerView(
98+
_: UIPickerView,
99+
didSelectRow row: Int,
100+
inComponent _: Int
101+
) {
102+
onSelect?(row > 0 ? row - 1 : nil)
103+
}
104+
#endif
101105
}
102106

103-
internal final class SwitchWidget: WrapperWidget<UISwitch> {
104-
var onChange: ((Bool) -> Void)?
107+
#if os(tvOS)
108+
internal final class SwitchWidget: WrapperWidget<UISegmentedControl> {
109+
var onChange: ((Bool) -> Void)?
105110

106-
@objc
107-
func switchFlipped() {
108-
onChange?(child.isOn)
111+
@objc
112+
func switchFlipped() {
113+
onChange?(child.selectedSegmentIndex == 1)
114+
}
115+
116+
init() {
117+
// TODO: localization?
118+
super.init(
119+
child: UISegmentedControl(items: [
120+
"OFF" as NSString,
121+
"ON" as NSString,
122+
]))
123+
124+
child.addTarget(self, action: #selector(switchFlipped), for: .valueChanged)
125+
}
126+
127+
func setOn(_ on: Bool) {
128+
child.selectedSegmentIndex = on ? 1 : 0
129+
}
109130
}
131+
#else
132+
internal final class SwitchWidget: WrapperWidget<UISwitch> {
133+
var onChange: ((Bool) -> Void)?
110134

111-
init() {
112-
super.init(child: UISwitch())
135+
@objc
136+
func switchFlipped() {
137+
onChange?(child.isOn)
138+
}
113139

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)
140+
init() {
141+
super.init(child: UISwitch())
142+
143+
// On iOS 14 and later, UISwitch can be either a switch or a checkbox (and I believe
144+
// it's a checkbox by default on Mac Catalyst). We have no control over this on
145+
// iOS 13, but when possible, prefer a switch.
117146
if #available(iOS 14, macCatalyst 14, *) {
118147
child.preferredStyle = .sliding
119148
}
120-
#endif
121149

122-
child.addTarget(self, action: #selector(switchFlipped), for: .valueChanged)
150+
child.addTarget(self, action: #selector(switchFlipped), for: .valueChanged)
151+
}
152+
153+
func setOn(_ on: Bool) {
154+
child.setOn(on, animated: true)
155+
}
156+
}
157+
#endif
158+
159+
internal final class ClickableWidget: WrapperWidget<BaseWidget> {
160+
private var gestureRecognizer: UITapGestureRecognizer!
161+
var onClick: (() -> Void)?
162+
163+
override init(child: BaseWidget) {
164+
super.init(child: child)
165+
166+
gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(viewTouched))
167+
gestureRecognizer.cancelsTouchesInView = true
168+
child.addGestureRecognizer(gestureRecognizer)
169+
}
170+
171+
@objc
172+
func viewTouched() {
173+
onClick?()
174+
}
175+
}
176+
177+
@available(tvOS, unavailable)
178+
internal final class SliderWidget: WrapperWidget<UISlider> {
179+
var onChange: ((Double) -> Void)?
180+
181+
private var _decimalPlaces = 17
182+
var decimalPlaces: Int {
183+
get { _decimalPlaces }
184+
set {
185+
_decimalPlaces = max(0, min(newValue, 17))
186+
}
187+
}
188+
189+
@objc
190+
func sliderMoved() {
191+
onChange?(
192+
(Double(child.value) * pow(10.0, Double(decimalPlaces)))
193+
.rounded(.toNearestOrEven)
194+
/ pow(10.0, Double(decimalPlaces))
195+
)
196+
}
197+
198+
init() {
199+
super.init(child: UISlider())
200+
child.addTarget(self, action: #selector(sliderMoved), for: .valueChanged)
123201
}
124202
}
125203

@@ -174,25 +252,28 @@ extension UIKitBackend {
174252
return textFieldWidget.child.text ?? ""
175253
}
176254

177-
public func createPicker() -> Widget {
178-
PickerWidget()
179-
}
255+
#if os(iOS)
256+
public func createPicker() -> Widget {
257+
PickerWidget()
258+
}
180259

181-
public func updatePicker(
182-
_ picker: Widget,
183-
options: [String],
184-
environment: EnvironmentValues,
185-
onChange: @escaping (Int?) -> Void
186-
) {
187-
let pickerWidget = picker as! PickerWidget
188-
pickerWidget.onSelect = onChange
189-
pickerWidget.options = options
190-
}
260+
public func updatePicker(
261+
_ picker: Widget,
262+
options: [String],
263+
environment: EnvironmentValues,
264+
onChange: @escaping (Int?) -> Void
265+
) {
266+
let pickerWidget = picker as! PickerWidget
267+
pickerWidget.onSelect = onChange
268+
pickerWidget.options = options
269+
}
191270

192-
public func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) {
193-
let pickerWidget = picker as! PickerWidget
194-
pickerWidget.child.selectRow((selectedOption ?? -1) + 1, inComponent: 0, animated: false)
195-
}
271+
public func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) {
272+
let pickerWidget = picker as! PickerWidget
273+
pickerWidget.child.selectRow(
274+
(selectedOption ?? -1) + 1, inComponent: 0, animated: false)
275+
}
276+
#endif
196277

197278
public func createSwitch() -> Widget {
198279
SwitchWidget()
@@ -205,6 +286,43 @@ extension UIKitBackend {
205286

206287
public func setState(ofSwitch switchWidget: Widget, to state: Bool) {
207288
let wrapper = switchWidget as! SwitchWidget
208-
wrapper.child.setOn(state, animated: true)
289+
wrapper.setOn(state)
290+
}
291+
292+
public func createClickTarget(wrapping child: Widget) -> Widget {
293+
ClickableWidget(child: child)
294+
}
295+
296+
public func updateClickTarget(
297+
_ clickTarget: Widget,
298+
clickHandler handleClick: @escaping () -> Void
299+
) {
300+
let wrapper = clickTarget as! ClickableWidget
301+
wrapper.onClick = handleClick
209302
}
303+
304+
#if os(iOS) || targetEnvironment(macCatalyst)
305+
public func createSlider() -> Widget {
306+
SliderWidget()
307+
}
308+
309+
public func updateSlider(
310+
_ slider: Widget,
311+
minimum: Double,
312+
maximum: Double,
313+
decimalPlaces: Int,
314+
onChange: @escaping (Double) -> Void
315+
) {
316+
let sliderWidget = slider as! SliderWidget
317+
sliderWidget.child.minimumValue = Float(minimum)
318+
sliderWidget.child.maximumValue = Float(maximum)
319+
sliderWidget.onChange = onChange
320+
sliderWidget.decimalPlaces = decimalPlaces
321+
}
322+
323+
public func setValue(ofSlider slider: Widget, to value: Double) {
324+
let sliderWidget = slider as! SliderWidget
325+
sliderWidget.child.setValue(Float(value), animated: true)
326+
}
327+
#endif
210328
}

Sources/UIKitBackend/UIKitBackend+Progress.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ extension UIKitBackend {
2121
}
2222

2323
public func createProgressBar() -> Widget {
24-
WrapperWidget(child: UIProgressView(progressViewStyle: .bar))
24+
let style: UIProgressView.Style
25+
#if os(tvOS)
26+
style = .default
27+
#else
28+
style = .bar
29+
#endif
30+
return WrapperWidget(child: UIProgressView(progressViewStyle: style))
2531
}
2632

2733
public func updateProgressBar(

0 commit comments

Comments
 (0)