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

Viewed 64

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
2 Answers

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

Here is a working variant based on my previous answer. Tested with Xcode 14 / iOS 16

There are several errors in code in question: selection value w/o binding, content w/o view builder, conflict of hidden binding autogenerated properties, etc., so I just updated a bit my previous solution.

demo

struct CompatibilityPicker<Label, SelectionValue, Content>: View where Label : StringProtocol, SelectionValue : BinaryInteger, Content : View {

    var label : Label
    @Binding var selection : SelectionValue
    var content : () -> Content

    init(_ label : Label, selection : Binding<SelectionValue>, @ViewBuilder content : @escaping () -> Content) {
        self.label = label
        self._selection = selection
        self.content = content
    }

    private var value: Binding<Int> { Binding<Int>(
        get: {
            Int(selection)
        },
        set: {
            self.selection = SelectionValue($0)
        })
    }

    var body: some View {
        Picker(label, selection: value, content: content)
    }
}

struct CompatibilityPicker_Previews: PreviewProvider {

    struct TestView: View {
        @State var selection : UInt8 = 1
        var body: some View {
            CompatibilityPicker("Difficulty", selection: $selection) {
                Text("Easy").tag(0)
                Text("Normal").tag(1)
                Text("Hard").tag(2)
            }
        }
    }
    static var previews: some View {
        TestView()
    }
}
Related