Home > OS >  SwiftUI: How to update another(not current) element in ForEach without necessity to update all eleme
SwiftUI: How to update another(not current) element in ForEach without necessity to update all eleme

Time:07-29

Imagine that you have some parent view that generate some number of child views:

struct CustomParent: View {
    var body: some View {
        HStack {
            ForEach(0..<10, id: \.self) { index in
                CustomChild(index: index)
            }
        }
    }
}
struct CustomChild: View {
    @State var index: Int
    @State private var text: String = ""

    var body: some View {
        Button(action: {
            // Here should be some update of background/text/opacity or whatever.
            // So how can I update background/text/opacity or whatever for button with index for example 3 from button with index for example 1?
        }) {
            Text(text)
        }
        .onAppear {
            text = String(index)
        }
    }
}

Question is included in the code as comment.

Thanks!

CodePudding user response:

Simpler Approach:

Although child views cannot access things that the host views have, it's possible to declare the child states in the host view and pass that state as a binding variable to the child view. In the code below, I have passed the childTexts variable to the child view, and (for your convenience) initialized the text so that it binds to the original element in the array (so that your onAppear works properly). Every change performed on the text and childTexts variable inside the child view reflects on the host view.

I strongly suggest not to do this though, as more elegant approaches exist.

struct CustomParent: View {
    @State var childTexts = [String](repeating: "", count: 10)
    
    var body: some View {
        HStack {
            ForEach(0..<10, id: \.self) { index in
                CustomChild(index: index, childTexts: $childTexts)
            }
        }
    }
}

struct CustomChild: View {
    let index: Int
    @Binding private var text: String
    @Binding private var childTexts: [String]
    
    init(index: Int, childTexts: Binding<[String]>) {
        self.index = index
        self._childTexts = childTexts
        self._text = childTexts[index]
    }
    
    var body: some View {
        Button(action: {
            //button behaviors goes here
            //for example
            childTexts[index   1] = "A"
        }) {
            Text(text)
        }
        .onAppear {
            text = String(index)
        }
    }
}

Advanced Approach:

By using the Combine framework, all your logics can be moved into an ObservableObject view model. This is much better as the button logic is no longer inside the view. In simplest terms, the @Published variable in the ObservableObject will publish a change when it senses its own mutation, while the @StateObjectand the @ObservedObject will listen and recalculate the view for you.

struct CustomParent: View {
    @StateObject var customViewModel = CustomViewModel()
    
    var body: some View {
        HStack {
            ForEach(0..<10, id: \.self) { index in
                CustomChild(index: index, customViewModel: customViewModel)
            }
        }
    }
}

struct CustomChild: View {
    let index: Int
    @ObservedObject var customViewModel: CustomViewModel
    
    var body: some View {
        Button(action: {
            customViewModel.buttonPushed(at: index)
        }) {
            Text(customViewModel.childTexts[index])
        }
    }
}

class CustomViewModel: ObservableObject {
    @Published var childTexts = [String](repeating: "", count: 10)
    
    init() {
        for i in 0..<childTexts.count {
            childTexts[i] = String(i)
        }
    }
    
    func buttonPushed(at index: Int) {
        //button behaviors goes here
        //for example:
        childTexts[index   1] = "A"
    }
}

CodePudding user response:

you could try this simple approach, works for me:

struct ContentView: View {
    var body: some View {
        CustomParent()
    }
}

struct CustomParent: View {
    @State var selectedIndex = -1 // <-- here
    
    var body: some View {
        HStack {
            ForEach(0..<10, id: \.self) { index in
                CustomChild(index: index, selectedIndex: $selectedIndex)
                    .background(selectedIndex == index ? .red : .blue)  // <-- here
            }
        }
    }
}

struct CustomChild: View {
    @State var index: Int  
    @Binding var selectedIndex: Int  // <-- here
    @State private var text: String = ""
    
    var body: some View {
        Button(action: {
            selectedIndex = index   2  // <-- your logic here
        }) {
            Text(text).foregroundColor(.yellow)
        }
        .onAppear {
            text = String(index)
        }
    }
}
  • Related