I know it's a weird title but there are lots of posts with similar titles and completely different problems. Mostly people writing other stuff than View
code inside their view, which I am not doing (as far as I can tell).
I'm trying to make Picker
compatible with other BinaryInteger
types since it doesn't work with anything but Int
, and I'm having some trouble getting the Previews to work. Here's the code :
import SwiftUI
struct CompatibilityPicker<Label, SelectionValue, Content> : View where Label : StringProtocol, SelectionValue : BinaryInteger, Content : View {
var content : () -> Content
var label : Label
@Binding private var _selection : SelectionValue
private var selection: Binding<Int> { Binding<Int>(
get: {
Int(_selection)
},
set: {
self._selection = SelectionValue($0)
})
}
init(_ label : Label, selection : SelectionValue, content : @escaping () -> Content) {
self.label = label
self._selection = selection
self.content = content
}
var body: some View {
Picker(label, selection: selection, content: content)
}
}
struct CompatibilityPicker_Previews: PreviewProvider {
@State static var selection : UInt8 = 4
static var previews: some View {
CompatibilityPicker("Difficulty", selection: selection) { //error : Type'()' cannot conform to 'View'
Text("Easy").tag(0)
Text("Normal").tag(1)
Text("Hard").tag(2)
}
}
}
What gives ? I have a normal Picker
that uses the exact same syntax and that works, I don't know what I'm doing wrong.
Thanks to @RobMayoff's solution, I am one step further ahead, however seemingly nonsensical errors have shown up that don't clear with cmd+shift+k :
init(_ label : Label, selection : SelectionValue, @ViewBuilder content : @escaping () -> Content) {
self.content = content
self.label = label
self._selection = selection //variable self._selection used before initialised
// This stays on this line if I change the order,
} // Return from initializer without initialising all stored properties
// That is not true, as far as I can tell
Shenaniganically, you are trying to use ViewBuilder
syntax in the trailing closure, but you didn't adorn content
with the @ViewBuilder
annotation. So Swift infers that the trailing closure returns ()
(also called Void
).
Change the init
declaration to mention @ViewBuilder
:
struct CompatibilityPicker<blah blah blah>: View where blah blah blah {
init(
_ label : Label,
selection : SelectionValue,
@ViewBuilder content : @escaping () -> Content
// ^^^^^^^^^^^^
) {
blah blah blah
@Binding private var _selection : SelectionValue
blah blah blah
init(_ label : Label, selection : SelectionValue, content : @escaping () -> Content) {
self.label = label
self._selection = selection
self.content = content
}
The _selection
variable is wrapped by the Binding
wrapper, which means that it is really a computed property. The stored property is named __selection
(note the two underscores) and has type Binding<SelectionValue>
. Because _selection
is a computed property, init
cannot mention it until all stored properties are initialized. Probably you should change init
to take a Binding<SelectionValue>
argument instead of a SelectionValue
argument:
init(
_ label : Label,
selection : Binding<SelectionValue>,
@ViewBuilder content : @escaping () -> Content
// ^^^^^^^^^^^^
) {
self.label = label
self.content = content
__selection = selection
}
I looked at your other question and your code here and I think I know what you mean by “it doesn't work with anything but Int
”.
The problem as that, when you say Text("Easy").tag(0)
, Swift treats 0
as an Int
. If your Picker
's SelectionValue
is, say, Int16
, then indeed the Picker
will not be able to use the 0
tag because the types don't match.
You can make your tag
work with Picker
by giving 0
the correct type. For example: Text("Easy").tag(0 as Int16)
.
However, my recommendation is that you stop mucking about with CompatibilityPicker
. It is a symptom of primitive obsession. The idiomatic solution is to use an enum
for your tags:
enum Difficulty: Hashable {
case easy
case medium
case hard
}
struct Demo1: View {
@State var difficulty: Difficulty = .easy
var body: some View {
Picker("Difficulty", selection: $difficulty) {
Text("Easy").tag(Difficulty.easy)
Text("Medium").tag(Difficulty.medium)
Text("Hard").tag(Difficulty.hard)
}
}
}
You could go even further and do this:
extension Difficulty: CaseIterable { }
extension Difficulty {
var stringKey: LocalizedStringKey {
switch self {
case .easy: return "Easy"
case .medium: return "Medium"
case .hard: return "Hard"
}
}
}
struct Demo2: View {
@State var difficulty: Difficulty = .easy
var body: some View {
Picker("Difficulty", selection: $difficulty) {
ForEach(Difficulty.allCases, id: \.self) {
Text($0.stringKey).tag($0)
}
}
}
}