Chris
Chris

Reputation: 8091

Binding not working in view when view is generated in loop

Problem: i tried to make this example https://swiftui-lab.com/communicating-with-the-view-tree-part-1/ a bit swiftier by not using the same MonthView() 12 times but in a loop. Unfortunately when tapping the label/month the variable activeIdx won't be updated and I have no idea why... and my question is: how do i have to change the code that the binding works? Expected behaviour: When you tap on the month names the red border should mark the label you tapped.

import SwiftUI

let months : [String] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

struct ContentView : View {

    @State private var activeIdx: Int = 0

    var groupedGroupedViews : [[MonthView]] = []

    init() {

        var groupedViews : [MonthView] = []
        for idx in 0..<months.count {
            print(idx)
            groupedViews.append(MonthView(activeMonth: self.$activeIdx, label: months[idx], idx: idx))
            print(months[idx])
            if (idx + 1) % 4 == 0 {
                groupedGroupedViews.append(groupedViews)
                groupedViews = []
            }
        }
        groupedGroupedViews.append(groupedViews)
    }

    var body: some View {
        VStack {
            ForEach (groupedGroupedViews, id: \.self) { groupedViews in
                HStack {
                    ForEach (groupedViews, id: \.idx) { view in
                        view
                    }
                }
            }
        }
    }
}

struct MonthView: View, Hashable {

    func hash(into hasher: inout Hasher) {
        hasher.combine(idx)
    }

    static func == (lhs: MonthView, rhs: MonthView) -> Bool {
        return lhs.idx == rhs.idx
    }

    @Binding var activeMonth: Int
    let label: String
    var idx: Int

    var body: some View {
        Text(label)
            .padding(10)
            .onTapGesture {
                print(self.idx, " tapped")
                self.activeMonth = self.idx
                print("active = ", self.activeMonth)
        }

            .background(MonthBorder(show: activeMonth == idx))
    }
}

struct MonthBorder: View {
    let show: Bool

    var body: some View {
        RoundedRectangle(cornerRadius: 15)
            .stroke(lineWidth: 3.0).foregroundColor(show ? Color.red : Color.clear)
            .animation(.easeInOut(duration: 0.6))
    }
}

Upvotes: 0

Views: 55

Answers (2)

E.Coms
E.Coms

Reputation: 11531

The reason is you cannot init a binding View like that.

var myView : MonthView!
init() {
  myView =  MonthView(activeMonth: self.$activeIdx, label: months[0], idx: 1)
} 

var body: some View {
    myView}

If you run above code, you still can see activeMonth is not changing.

I think the init() mainly bridges some UIKit features and should be not used as a major way in SwiftUI. Cross platform is necessary but try to avoid if possible. Please appreciate the design merit of swiftUI not the overhead from traditional implementations.

One simple answer to this is moving the init to View. That reaches the same function you need.

 var body: some View {

            let groupedGroupedViews : [[MonthView]] = {
                var groupedGroupedViews : [[MonthView]]  = []
                    var groupedViews : [MonthView] = []
                    for idx in 0..<months.count {
                        print(idx)
                        groupedViews.append( MonthView(activeMonth: self.$activeIdx, label: months[idx], idx: idx))
                        print(months[idx])
                        if (idx + 1) % 4 == 0 {
                            groupedGroupedViews += [groupedViews]
                            groupedViews.removeAll()
                        }
                    }
                    groupedGroupedViews +=  [groupedViews]
                return groupedGroupedViews
            }()

         return   VStack {

                ForEach (groupedGroupedViews, id: \.self) { groupedView in
                    HStack {
                        ForEach (groupedView, id: \.idx) { view in
                        view
                        }
                    }
                }
            }
        }

Upvotes: 1

Chris
Chris

Reputation: 8091

I found it, thanks to E.Coms, i have changed my code and now it works. Here my solution (but i am pretty sure you could do better)

struct ViewValue : Hashable {

    var idx: Int
    var text: String
}


struct ContentView : View {

    @State private var activeIdx: Int = 0

    var groupedGroupedValues : [[ViewValue]] = []

    init() {

        var groupedValues : [ViewValue] = []
        for idx in 0..<months.count {
            print(idx)
            groupedValues.append(ViewValue(idx: idx, text: months[idx]))
            print(months[idx])
            if (idx + 1) % 4 == 0 {
                groupedGroupedValues.append(groupedValues)
                groupedValues = []
            }
        }
        groupedGroupedValues.append(groupedValues)
    }

    var body: some View {
        VStack {
            ForEach (groupedGroupedValues, id: \.self) { groupedValue in
                HStack {
                    ForEach (groupedValue, id: \.idx) { viewValue in
                        MonthView(activeMonth: self.$activeIdx, label: viewValue.text, idx: viewValue.idx)
                    }
                }
            }
        }
    }
}

Upvotes: 0

Related Questions