Search code examples
swiftui

Init with @Binding and init without @Binding - How to control the @State?


I try to create something similar to TabView. I want to have the possibility to pass a selection as a @Binding, so that you can control the selection like this:

@State var selection = 0
...
var body: some View {
  CustomTabView(selection: $selection) { .... }
}

But I also would like to leave it without a binding:

var body: some View {
  CustomTabView { ... }

I saw, that SwiftUI itself solves it with different initializers:

public init<C>(selection: Binding<SelectionValue>, @TabContentBuilder<SelectionValue> content: () -> C) where Content == TabContentBuilder<SelectionValue>.Content<C>, C : TabContent

....

public init<C>(@TabContentBuilder<Never> content: () -> C) where SelectionValue == Never, Content == TabContentBuilder<Never>.Content<C>, C : TabContent

I would like to do the same. But how can I control the selection?

struct CustomTabView: View {
    init(@CustomTabBuilder tabs: () -> [CustomTab]) {
        self._selectedTab = // ??????
        self.tabs = tabs()
    }
    
    init(selection: Binding<Int>, @CustomTabBuilder tabs: () -> [CustomTab]) {
        self._selectedTab = selection
        self.tabs = tabs()
    }

Solution

  • Just make the Binding optional.

    struct CustomSelectableView: View {
        let items: [String]
        private let _selection: Binding<String>?
        
        init(items: [String], selection: Binding<String>? = nil) {
            self.items = items
            self._selection = selection
        }
        ...
    }
    

    Check for nil in body:

    var body: some View {
        if let _selection {
            BindingSelection(items: items, selection: _selection)
        } else {
            StateSelection(items: items)
        }
    }
    

    BindingSelection is where you put the actual body of your view, and StateSelection will reuse BindingSelection and pass its own @State to it.

    private struct BindingSelection: View {
        let items: [String]
        @Binding var selection: String
        
        var body: some View {
            // Write your view here...
    
            /* as a macOS example:
            List(items, id: \.self, selection: $selection) {
                Text($0)
            }
            */
        }
    }
    
    private struct StateSelection: View {
        let items: [String]
        @State private var selection: String = ""
        
        var body: some View {
            // reuse BindingSelection here, so we avoid code duplication
            BindingSelection(items: items, selection: $selection)
        }
    }