6

I am trying to establish a SwiftUI connection between the child view and the parent view. By clicking on any child view, I want to redraw only the view that has been tapped, not the entire parent view.

The current implementation below does not allow you to redraw the view when you click on it, as it has a derived value.

I tried different scenarios by adding the BindableObject protocol to CustomColor, but without success.

class CustomColor: Identifiable {

    let id = UUID()
    var color: Color

    init(color: Color) {
        self.color = color
    }

    func change(to color: Color) {
        self.color = color
    }

}

class ColorStore: BindableObject {

    var colors: [CustomColor] = [] {
        didSet {
            didChange.send(self)
        }
    }

    var didChange = PassthroughSubject<ColorStore, Never>()

    init() {
        self.colors = Array.init(repeating: CustomColor(color: .red), count: 10)
    }

}


struct ContentView: View {

    @EnvironmentObject var colorStore: ColorStore

    var body: some View {
        NavigationView {
            List {
                ForEach(colorStore.colors) { color in
                    ColorShape(color: color)
                }
            }.navigationBarTitle(Text("Colors"))
        }
    }

}

struct ColorShape: View {

    var color: CustomColor

    var body: some View {
        Button(action:
            { self.color.change(to: .blue) }
            , label: {
            ShapeView(shape: Circle(), style: color.color)
        })
    }

}

Here is the UI I have

6
  • May I ask why you don't want to redraw everything? For performance? Commented Jun 8, 2019 at 13:01
  • 3
    Actually, I want to understand is it even possible? I know they said that you don't have to worry about such stuff, but still. Commented Jun 8, 2019 at 13:03
  • Attach a picture of your ui to have a clear understanding Commented Jun 8, 2019 at 18:34
  • @jsbeginnerNodeJS, I attached it to the question. Commented Jun 9, 2019 at 11:21
  • That's not possible. But You can make States for each row. Commented Jun 9, 2019 at 11:31

3 Answers 3

1

I think I've found a solution. The first problem was that I initialized array of colors by repeating the same element instead of adding independent ones.

What is more CustomColor itself should have BindableObject conformance, not the model (we don't change the array of colors, we change each color). Lastly, we don't need to wrap objects in ForEach element (we loose reusability that way), and instead we put them in List element.

With this implementation, only the view that has been changed will be redrawn, not the entire collection.

Here is the code:

class CustomColor: BindableObject, Identifiable {

    var didChange = PassthroughSubject<CustomColor, Never>()

    let id = UUID()
    var color: Color {
        didSet {
            self.didChange.send(self)
        }
    }

    init(color: Color) {
        self.color = color
    }

    func change(toColor color: Color) {
        self.color = color
    }

}

class ColorStore {

    var colors: [CustomColor] = []

    init() {
        (0...10).forEach { _ in colors.append(CustomColor(color: .red)) }
    }

}


struct ContentView: View {

    let colorStore: ColorStore

    var body: some View {
        NavigationView {
            List(colorStore.colors) { color in
                ColorShape(color: color)
            }.navigationBarTitle(Text("Colors"))
        }
    }

}

struct ColorShape: View {

    @ObjectBinding var color: CustomColor

    var body: some View {
        Button(action: { self.color.change(toColor: .blue) }, label: {
            ShapeView(shape: Circle(), style: color.color)
        })
    }

}

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

Comments

0

At now there is no possibility to update specific child view and it cannot be expected I think. As was told on Data flow Through Swift UI session once you are changing @State property or Bindable object - all the changes flow down through view hierarchy and SwiftUI framework is comparing all the views and rendering again only what has changed.

3 Comments

I added the answer. Seems it works as I wanted to. Please, have a look.
But in this case of you will add new item to the colors array the ContentView will not be updated
Yes, it won't be updated. You have to return the BindableObject conformance back to the ColorStore and put a button in NavigationBar with an action. Here is the link to my gist.
0

I can offer three versions with subtle differences. All Of them toggle individual buttons and keep the whole model - ColorStore var in sync. Allows adding and removing of elements in the array of colours. Also note, we can go without Identifiable conformance for array elements to list them.

Version 1. The closest to the question: all models are classes.

class CustomColor: ObservableObject, Identifiable {

    var didChange = PassthroughSubject<CustomColor, Never>()

    let id = UUID()
    var color: Color {
        didSet {
            objectWillChange.send()
        }
    }

    init(color: Color) {
        self.color = color
    }

    func change(to color: Color) {
        self.color = color
    }

}

class ColorStore: ObservableObject {

    var didChange = PassthroughSubject<ColorStore, Never>()

    var colors: [CustomColor] = [] {
        didSet {
            objectWillChange.send()
        }
    }

    init() {
        (0...10).forEach { _ in colors.append(CustomColor(color: .red)) }
    }

}


struct ContentView: View {

    @ObservedObject var colorStore: ColorStore = ColorStore()

    var body: some View {
        NavigationView {
            List(colorStore.colors) { c in
                    ColorShape(color: c)
            }
// will work without `Identifiable`
//            List(colorStore.colors.indices, id: \.self) { c in
//                    ColorShape(color: self.colorStore.colors[c])
//            }
            .navigationBarTitle(Text("Colors"))
            .navigationBarItems(leading:
                Button(action: { self.colorStore.colors.append(CustomColor(color: .green)) }) {
                    Text("Add")
                }, trailing:
                Button(action: {
                    self.colorStore.colors.removeLast()
                    print(self.colorStore.colors)
                }, label: { Text("Remove") }))
        }
    }

}

struct ColorShape: View {

    @ObservedObject var color: CustomColor

    var body: some View {
        Button(action:
            { self.color.change(to: .blue)
                print(self.color)
        }
            , label: {
                Circle().fill(color.color)
        })
    }

}

Version 2. The CustomColor is rewritten as struct.

// No need for manual `ObservableObject, Identifiable` conformance
struct CustomColor /*: Identifiable */ {

    // let id = UUID()
    var color: Color

    init(color: Color) {
        self.color = color
    }

    mutating func change(to color: Color) {
        self.color = color
    }

}

class ColorStore: ObservableObject {

    var didChange = PassthroughSubject<ColorStore, Never>()
    // If `CustomColor` is a `struct` i.e. value type, we can populate array with independent values, not with the same reference by using `repeating:` init.
    var colors: [CustomColor] = Array(repeating: CustomColor(color: .red), count: 10) {
        didSet {
            objectWillChange.send()
        }
    }

    /* init() {
        (0...10).forEach { _ in colors.append(CustomColor(color: .red)) }
    } */

}


struct ContentView: View {

    @ObservedObject var colorStore: ColorStore = ColorStore()

    var body: some View {
        NavigationView {
            List {
                // Strange, bu if we omit ForEach, we will get an error on element removal from array.
                ForEach(colorStore.colors.indices, id: \.self)
                { c in
                    ColorShape(color: self.$colorStore.colors[c])
                }

            }
            .navigationBarTitle(Text("Colors"))
            .navigationBarItems(leading:
                Button(action: { self.colorStore.colors.append(CustomColor(color: .green)) }) {
                    Text("Add")
                }, trailing:
                Button(action: {
                    self.colorStore.colors.removeLast()
                    print(self.colorStore.colors)
                }, label: { Text("Remove") }))
        }
    }

}

struct ColorShape: View {

    @Binding var color: CustomColor

    var body: some View {
        Button(action:
            { self.color.change(to: .blue)
                print(self.color)
        }
            , label: {
                Circle().fill(color.color)
        })
    }

}

Version 3. The main model ColorStore and it's subtype CustomColor are rewritten as structs. No need to manually conform to ObservableObject.

struct CustomColor /* : Identifiable */ {

    // let id = UUID()
    var color: Color

    init(color: Color) {
        self.color = color
    }

    mutating func change(to color: Color) {
        self.color = color
    }

}

struct ColorStore {
    // If `CustomColor` is a `struct` i.e. value type, we can populate array with independent values, not with the same reference by using `repeating:` init.
    var colors: [CustomColor] = Array(repeating: CustomColor(color: .red), count: 10)

}


struct ContentView: View {

    @State var colorStore: ColorStore = ColorStore()

    var body: some View {
        NavigationView {
            List{
                ForEach(colorStore.colors.indices, id: \.self) { i in
                    return ColorShape(color: self.$colorStore.colors[i])
                }
            }
            .navigationBarTitle(Text("Colors"))
            .navigationBarItems(leading:
                Button(action: { self.colorStore.colors.append(CustomColor(color: .green)) }) {
                    Text("Add")
            }, trailing:
                // Removing causes index out of bound error (bug?)
                Button(action: {
                    self.colorStore.colors.removeLast()
                    print(self.colorStore.colors)}) {
                    Text("Remove") })
        }
    }
}

struct ColorShape: View {

    @Binding var color: CustomColor

    var body: some View {
        Button(action: {
            self.color.change(to: .blue)
            print(self.color)
        }) {
            Circle().fill(color.color)
        }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.