Skip to content

Commit f55186c

Browse files
authored
Implement Picker for tvOS & Mac Catalyst (#99)
1 parent ad383d6 commit f55186c

File tree

3 files changed

+188
-84
lines changed

3 files changed

+188
-84
lines changed

Sources/SwiftCrossUI/Views/Picker.swift

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public struct Picker<Value: Equatable>: ElementaryView, View {
2929
backend: Backend,
3030
dryRun: Bool
3131
) -> ViewUpdateResult {
32-
// TODO: Implement picker sizing within SwiftCrossUI so that we can properly implement `dryRun`.
3332
backend.updatePicker(
3433
widget,
3534
options: options.map { "\($0)" },
@@ -44,8 +43,30 @@ public struct Picker<Value: Equatable>: ElementaryView, View {
4443
}
4544
backend.setSelectedOption(ofPicker: widget, to: selectedOptionIndex)
4645

47-
return ViewUpdateResult.leafView(
48-
size: ViewSize(fixedSize: backend.naturalSize(of: widget))
49-
)
46+
// Special handling for UIKitBackend:
47+
// When backed by a UITableView, its natural size is -1 x -1,
48+
// but it can and should be as large as reasonable
49+
let size = backend.naturalSize(of: widget)
50+
if size == SIMD2(-1, -1) {
51+
if !dryRun {
52+
backend.setSize(of: widget, to: proposedSize)
53+
}
54+
55+
return ViewUpdateResult.leafView(
56+
size: ViewSize(
57+
size: proposedSize,
58+
idealSize: SIMD2(10, 10),
59+
minimumWidth: 0,
60+
minimumHeight: 0,
61+
maximumWidth: nil,
62+
maximumHeight: nil
63+
)
64+
)
65+
} else {
66+
// TODO: Implement picker sizing within SwiftCrossUI so that we can properly implement `dryRun`.
67+
return ViewUpdateResult.leafView(
68+
size: ViewSize(fixedSize: size)
69+
)
70+
}
5071
}
5172
}

Sources/UIKitBackend/UIKitBackend+Control.swift

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -51,63 +51,6 @@ final class TextFieldWidget: WrapperWidget<UITextField>, UITextFieldDelegate {
5151
}
5252
}
5353

54-
@available(tvOS, unavailable)
55-
@available(macCatalyst, unavailable)
56-
final class PickerWidget: WrapperWidget<UIPickerView>, UIPickerViewDataSource,
57-
UIPickerViewDelegate
58-
{
59-
var options: [String] = [] {
60-
didSet {
61-
child.reloadComponent(0)
62-
}
63-
}
64-
var onSelect: ((Int?) -> Void)?
65-
66-
init() {
67-
super.init(child: UIPickerView())
68-
69-
child.dataSource = self
70-
child.delegate = self
71-
72-
child.selectRow(0, inComponent: 0, animated: false)
73-
}
74-
75-
func numberOfComponents(in _: UIPickerView) -> Int {
76-
1
77-
}
78-
79-
func pickerView(_: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
80-
options.count + 1
81-
}
82-
83-
// For some reason, if compiling for tvOS, the compiler complains if I even attempt
84-
// to define these methods.
85-
#if os(iOS)
86-
func pickerView(
87-
_: UIPickerView,
88-
titleForRow row: Int,
89-
forComponent _: Int
90-
) -> String? {
91-
switch row {
92-
case 0:
93-
""
94-
case 1...options.count:
95-
options[row - 1]
96-
default:
97-
nil
98-
}
99-
}
100-
101-
func pickerView(
102-
_: UIPickerView,
103-
didSelectRow row: Int,
104-
inComponent _: Int
105-
) {
106-
onSelect?(row > 0 ? row - 1 : nil)
107-
}
108-
#endif
109-
}
110-
11154
#if os(tvOS)
11255
final class SwitchWidget: WrapperWidget<UISegmentedControl> {
11356
var onChange: ((Bool) -> Void)?
@@ -273,29 +216,6 @@ extension UIKitBackend {
273216
return textFieldWidget.child.text ?? ""
274217
}
275218

276-
#if os(iOS) && !targetEnvironment(macCatalyst)
277-
public func createPicker() -> Widget {
278-
PickerWidget()
279-
}
280-
281-
public func updatePicker(
282-
_ picker: Widget,
283-
options: [String],
284-
environment: EnvironmentValues,
285-
onChange: @escaping (Int?) -> Void
286-
) {
287-
let pickerWidget = picker as! PickerWidget
288-
pickerWidget.onSelect = onChange
289-
pickerWidget.options = options
290-
}
291-
292-
public func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) {
293-
let pickerWidget = picker as! PickerWidget
294-
pickerWidget.child.selectRow(
295-
(selectedOption ?? -1) + 1, inComponent: 0, animated: false)
296-
}
297-
#endif
298-
299219
public func createSwitch() -> Widget {
300220
SwitchWidget()
301221
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import SwiftCrossUI
2+
import UIKit
3+
4+
protocol Picker: BaseWidget {
5+
func setOptions(to options: [String])
6+
func setChangeHandler(to onChange: @escaping (Int?) -> Void)
7+
func setSelectedOption(to index: Int?)
8+
}
9+
10+
@available(tvOS, unavailable)
11+
final class UIPickerViewPicker: WrapperWidget<UIPickerView>, Picker, UIPickerViewDataSource,
12+
UIPickerViewDelegate
13+
{
14+
private var options: [String] = []
15+
private var onSelect: ((Int?) -> Void)?
16+
17+
init() {
18+
super.init(child: UIPickerView())
19+
20+
child.dataSource = self
21+
child.delegate = self
22+
23+
child.selectRow(0, inComponent: 0, animated: false)
24+
}
25+
26+
func setOptions(to options: [String]) {
27+
self.options = options
28+
child.reloadComponent(0)
29+
}
30+
31+
func setChangeHandler(to onChange: @escaping (Int?) -> Void) {
32+
onSelect = onChange
33+
}
34+
35+
func setSelectedOption(to index: Int?) {
36+
child.selectRow(
37+
(index ?? -1) + 1, inComponent: 0, animated: false)
38+
}
39+
40+
func numberOfComponents(in _: UIPickerView) -> Int {
41+
1
42+
}
43+
44+
func pickerView(_: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
45+
options.count + 1
46+
}
47+
48+
// For some reason, if compiling for tvOS, the compiler complains if I even attempt
49+
// to define these methods.
50+
#if !os(tvOS)
51+
func pickerView(
52+
_: UIPickerView,
53+
titleForRow row: Int,
54+
forComponent _: Int
55+
) -> String? {
56+
switch row {
57+
case 0:
58+
""
59+
case 1...options.count:
60+
options[row - 1]
61+
default:
62+
nil
63+
}
64+
}
65+
66+
func pickerView(
67+
_: UIPickerView,
68+
didSelectRow row: Int,
69+
inComponent _: Int
70+
) {
71+
onSelect?(row > 0 ? row - 1 : nil)
72+
}
73+
#endif
74+
}
75+
76+
final class UITableViewPicker: WrapperWidget<UITableView>, Picker, UITableViewDelegate,
77+
UITableViewDataSource
78+
{
79+
private static let reuseIdentifier =
80+
"__SwiftCrossUI_UIKitBackend_UITableViewPicker.reuseIdentifier"
81+
82+
private var options: [String] = []
83+
private var onSelect: ((Int?) -> Void)?
84+
85+
init() {
86+
super.init(child: UITableView(frame: .zero, style: .plain))
87+
88+
child.delegate = self
89+
child.dataSource = self
90+
91+
child.register(UITableViewCell.self, forCellReuseIdentifier: Self.reuseIdentifier)
92+
}
93+
94+
func setOptions(to options: [String]) {
95+
self.options = options
96+
child.reloadData()
97+
}
98+
99+
func setChangeHandler(to onChange: @escaping (Int?) -> Void) {
100+
onSelect = onChange
101+
}
102+
103+
func setSelectedOption(to index: Int?) {
104+
if let index {
105+
child.selectRow(
106+
at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .middle)
107+
} else {
108+
child.selectRow(at: nil, animated: false, scrollPosition: .none)
109+
}
110+
}
111+
112+
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
113+
options.count
114+
}
115+
116+
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
117+
let cell = tableView.dequeueReusableCell(
118+
withIdentifier: Self.reuseIdentifier, for: indexPath)
119+
120+
cell.textLabel!.text = options[indexPath.row]
121+
122+
return cell
123+
}
124+
125+
func tableView(
126+
_: UITableView,
127+
didSelectRowAt indexPath: IndexPath
128+
) {
129+
onSelect?(indexPath.row)
130+
}
131+
}
132+
133+
extension UIKitBackend {
134+
public func createPicker() -> Widget {
135+
#if targetEnvironment(macCatalyst)
136+
if UIDevice.current.userInterfaceIdiom == .mac {
137+
return UITableViewPicker()
138+
} else {
139+
return UIPickerViewPicker()
140+
}
141+
#elseif os(tvOS)
142+
return UITableViewPicker()
143+
#else
144+
return UIPickerViewPicker()
145+
#endif
146+
}
147+
148+
public func updatePicker(
149+
_ picker: Widget,
150+
options: [String],
151+
environment _: EnvironmentValues,
152+
onChange: @escaping (Int?) -> Void
153+
) {
154+
let pickerWidget = picker as! any Picker
155+
pickerWidget.setChangeHandler(to: onChange)
156+
pickerWidget.setOptions(to: options)
157+
}
158+
159+
public func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) {
160+
let pickerWidget = picker as! any Picker
161+
pickerWidget.setSelectedOption(to: selectedOption)
162+
}
163+
}

0 commit comments

Comments
 (0)