17

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!

0

6 Answers 6

20
+100

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.

Sign up to request clarification or add additional context in comments.

3 Comments

I like it, from all the posts it seems that there is no canonical solution, but this is the most elegant I've seen.
You can make the .constant(nil) a default value to simplify things: init(_ index: Binding<Int?> = .constant(nil) ) {...} which removes the need for the second init() method
Note that the IndependentView body could be just an instance of DependentView as well, reducing duplicate code: var body: some View { DependentView(index: $index) }
2

If it is not the goal to use dependency injection for binding, there is only one thing to bound to internally - internal @State. So here is alternate approach to use internal binding to internal state, you can try.

struct ReusableView: View {

    @Binding private var externalIndex: Int
    @State private var internalIndex: Int = 0
    private var hasExternal = false

    init(_ index: Binding<Int>? = nil) {
        if index != nil {
            self._externalIndex = index!
            self.hasExternal = true
        } else {
            self._externalIndex = Binding<Int>(get: {0}, set: {_ in}) // initialization stub
        }
    }

    private var index: Binding<Int> {
        hasExternal ? $externalIndex : $internalIndex
    }

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

}

Comments

0

As you suggest to change in init, I think you are on the right direction.

In the UI world, when you try to init something, people prefer to add Views. So all you need one thing is to add a middle view between Parent and original reusable View. You don't need even change code of your views

           struct ParentView: View {
                @State var index: Int?
                var body: some View {
                    ReusableView(index: $index)
                }
            }

            struct ReusableView_Original: View {
                @Binding var index: Int
                var body: some View {
                    VStack{
                    Text("The index is \(self.index)")
                        Button.init("click", action:{
                            self.index += 1
                        })}
                }
            }

// Middle view is the following.

            struct ReusableView: View{
                @Binding var index: Int?
                var body: some View {
                    ReusableView_Original(index: Binding<Int>(get: {
                        return self.index == nil ? 0 : self.index!
                    }, set: { (int) in
                        self.index = int
                    }))
                }
            }

You can extend and test the result like this:

   struct ParentView: View {
                @State var index: Int? = 2
                var body: some View {
                    VStack{
                    ReusableView(index: $index)
                    ReusableView(index: $index, useDefault:  true)
                    }
                }
            }


            struct ReusableView: View{
                @Binding var index: Int?
                @State var useDefault = false
                @State var defaultValue = 0

                var body: some View {
                    ReusableView_Original(index: Binding<Int>(get: {
                        return self.useDefault ? self.defaultValue : self.index!
                    }, set: { (int) in
                        self.useDefault = false
                        self.index = int
                    }))
                }
            }

Comments

0

The best solution here (to my opinion) is custom Bindings with a a wrapper View.

That will give you two choices:

  1. Bind your (child) View with the parent View so that both parent and child can change the value
  2. Don't bind child View with parent so that ONLY the child will save the value internally.

It's actually pretty straitforward.

This is your example, changed to work. Keep in mind that the actual change is the wrapper View and that you have to call the wrapper View from parent (instead of child).

The parent View (same as yours except child view)

struct ContentView: View {
    @State var index: Int? = nil

    var body: some View {
        VStack {
            Text("Parent view state: \(index ?? 0)")
            WrapperOfReusableView(index: self.$index)
            // if you add  .constant(nil) it will use the internal storage
        }
    }
}

The wrapper View

struct WrapperOfReusableView: View {
    // The external index
    @Binding var index: Int?
    // The internal index
    @State private var localIndex = 0

    var body: some View {
        // Custom binding
        let localIndexBinding = Binding(
            get:{
                // if the parent sends nil, use the internal property
                self.index ?? self.localIndex
            },
            set: {
                // Set both internal and external
                self.localIndex = $0
                self.index = $0
            }
        )
        // return the actual View
        return ReusableView(index: localIndexBinding)
    }
}

The actual View (same as yours)

struct ReusableView: View {
    @Binding var index: Int

    var body: some View {
        VStack {
            Text("The Reusable index is \(index)")

            Button("Change Index") {
                self.index = self.index + 1
            }
        }
    }
}

Comments

0

Example for Bool:

import SwiftUI

struct SwitcherView: View {

    @State var intIsChecked: Bool = false
           var extIsChecked: Binding<Bool>? = nil

    init(isChecked: Binding<Bool>? = nil) {
        self.extIsChecked = isChecked
    }

    var body: some View {
        var isChecked: Bool {
            get { if (self.extIsChecked != nil) { self.extIsChecked!.wrappedValue            } else { self.intIsChecked            } }
            set { if (self.extIsChecked != nil) { self.extIsChecked!.wrappedValue = newValue } else { self.intIsChecked = newValue } }
        }
        Button {
            isChecked.toggle()
        } label: {
            Text("\(isChecked)")
        }
    }

}

Comments

-1

In my experience it would be cleaner and more convenient to wrap your index in an ObservableObject.

class IndexStore: ObservableObject {
    @Published index = 0
}

Then in your ReusableView

struct ReusableView: View {

    var indexStore = IndexStore()

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

}

Then you can instantiate it as ReusableView() for "internal" index, ReusableView(indexStore: parentIndexStore) for parent's index, and even share indexes among different ReusableView's.

2 Comments

Thanks, although this works I'm looking for a binding solution
I learned the hard way that you shouldn't force SwiftUI and Combine for things it wasn't designed for.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.