Search code examples
swiftswiftuiviewpicker

Type '()' cannot conform to View (except it definitely is a View, no shenanigans this time)


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

Solution

  • 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
    

    UPDATE

        @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
        }
    

    UPDATE 2

    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)
                }
            }
        }
    }