swiftswiftui

How to optionally pass in a Binding in SwiftUI?


I have a view that internally manages a @State variable that keeps track of the current index. So something like:

struct ReusableView: View {

    @State var index: Int = 0

    var body: some View {
        Text("The index is \(self.index)"
        // A button that changes the index
    }

}

This view is going to be reused throughout the app. Sometimes the parent view will need access to the index, so I refactored it like this:

struct ParentView: View {

    @State var index: Int = 0

    var body: some View {
      ReusableView($index)
    }

}

struct ReusableView: View {

    @Binding var index: Int

    var body: some View {
        Text("The index is \(self.index)"
        // A button that changes the index
    }

}

The problem

I don't want to enforce the parent view to always keep the state of the index. In other words, I want to optionally allow the parent view to be in charge of the state variable, but default to the Reusable View to maintain the state in case the parent view doesn't care about the index.

Attempt

I tried somehow to initialize the binding on the reusable view in case the parent view doesn't provide a binding:

struct ReusableView: View {

    @Binding var index: Int

    init(_ index: Binding<Int>? = nil) {
        if index != nil {
            self._index = index
        } else {
            // TODO: Parent didn't provide a binding, init myself.
            // ?
        }
    }

    var body: some View {
        Text("The index is \(self.index)"
        // A button that changes the index
    }

}

Thank you!


Solution

  • The main problem with what you want to achieve is that when the index is handled by the parent your View needs a @Binding to it, but when it handles the index itself it needs @State. There are two possible solutions.

    If the view can ignore the index property when it doesn't have one:

    struct ReusableView: View {
    
        @Binding var index: Int?
    
        init(_ index: Binding<Int?>) {
            self._index = index
        }
    
        init() {
           self._index = .constant(nil)
        }
    
        var body: some View {
            VStack {
                index.map { Text("The index is \($0)") }
            }
        }   
    }
    

    The advantage is that it is very straightforward - just two initializers, but you cannot change the value of the index when it is handled by ResusableView itself (its a constant).

    If the view cannot ignore the index property when it doesn't have one:

    struct ReusableView: View {
    
        private var content: AnyView
    
        init(_ index: Binding<Int>? = nil) {
            if let index = index {
                self.content = AnyView(DependentView(index: index))
            } else {
                self.content = AnyView(IndependentView())
            }
        }
    
        var body: some View {
            content
        }
    
        private struct DependentView: View {
    
            @Binding var index: Int
    
            var body: some View {
                Text("The index is \(index)")
            }
        }
    
        private struct IndependentView: View {
    
            @State private var index: Int = 0
    
            var body: some View {
                Text("The index is \(index)")
            }
        }
    
    }
    

    The clear advantage is that you have a view that can either be bound to a value or manage it as its own State. As you can see ReusableView is just a wrapper around two different views one managing its own @State and one being bound to @State of its parent view.