Carlos Maria Caraccia
Carlos Maria Caraccia

Reputation: 500

SwiftUI access a view created in a ForEach loop

Is there a way to access a view created in a ForEach loop? I'm creating views (Rectangles) with this struct on this loop. I want to change the fill color of the rects upon the tapped gestures.

struct DisplayingRect:Identifiable {

    var id = UUID()
    var width:CGFloat = 0
    var height:CGFloat = 0
    var xAxis:CGFloat = 0
    var yAxis:CGFloat = 0

    init(width:CGFloat, height:CGFloat, xAxis:CGFloat, yAxis:CGFloat) {
        self.width = width
        self.height = height
        self.xAxis = xAxis
        self.yAxis = yAxis
    }
}

ForEach(self.rects) { rect in
    Rectangle()
        .fill(Color.init(.sRGB, red: 1, green: 0, blue: 0, opacity: 0.2))
        .frame(width: rect.width, height: rect.height)
        .offset(x: rect.xAxis, y: rect.yAxis)
        .id(rect.id)
        .onTapGesture {
            print("Clicked")
            self.rectTapped = rect.width
            print(rect.width)
            print(rect.id)
            if !self.didTap {
                self.didTap = true
            } else {
                self.didTap = false
            }
         }

I can assign each view with an id setting its id property, but I don't know where they are stored or how to modify them upon the click. I can create function that returns a view (Rectangle) and store them in an array, and display them in the screen, but again I don't know how to access them and modify the one I want.

Upvotes: 1

Views: 2470

Answers (3)

Seb Jachec
Seb Jachec

Reputation: 3061

SwiftUI encourages a declarative approach – you shouldn't need to (and in fact can't) access any view directly to store a reference to it. Your views can be given data, and whenever that data changes, they'll update.

In this case, you could have your DisplayingRect store a color property, then have the tap gesture on each Rectangle look up the right struct in your rects array by ID, and modify the color property.

To separate out the logic from your view and make more of this unit testable, you might want to create some kind of view model class that encompasses this, but putting it all inside your view would work without these benefits.

This approach could look something like this (test locally & working):

struct DisplayingRect: Identifiable {
    let id = UUID()
    var color = Color.red
    var width: CGFloat
    var height: CGFloat
    var xAxis: CGFloat
    var yAxis: CGFloat

    init(
        width: CGFloat,
        height: CGFloat,
        xAxis: CGFloat = 0,
        yAxis: CGFloat = 0)
    {
        self.width = width
        self.height = height
        self.xAxis = xAxis
        self.yAxis = yAxis
    }
}

final class ContentViewModel: ObservableObject {
    @Published
    private(set) var rects: [DisplayingRect] = [
        .init(width: 100, height: 100),
        .init(width: 100, height: 100),
        .init(width: 100, height: 100)
    ]

    func didTapRectangle(id: UUID) {
        guard let rectangleIndex = rects.firstIndex(where: { $0.id == id }) else {
            return
        }

        rects[rectangleIndex].color = .blue
    }
}

struct ContentView: View {
    @ObservedObject
    var viewModel = ContentViewModel()

    var body: some View {
        VStack {
            ForEach(viewModel.rects) { rect in
                Rectangle()
                    .fill(rect.color)
                    .frame(width: rect.width, height: rect.height)
                    .offset(x: rect.xAxis, y: rect.yAxis)
                    .onTapGesture {
                        self.viewModel.didTapRectangle(id: rect.id)
                    }
            }
        }
    }
}

In this case, the @ObservedObject property wrapper along with ObservableObject protocol allow the view to update itself whenever data it uses from viewModel is changed. To automatically signal properties that should cause the view to refresh, the @Published property wrapper is used.

https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects

Upvotes: 1

Chris
Chris

Reputation: 8091

you can do it like this:

struct DisplayingRect:Identifiable, Hashable {

    static var counter = 0

    var id : Int = DisplayingRect.counter

    var width:CGFloat = 0
    var height:CGFloat = 0
    var xAxis:CGFloat = 0
    var yAxis:CGFloat = 0
    var color: Color = Color.red

    init(width:CGFloat, height:CGFloat, xAxis:CGFloat, yAxis:CGFloat) {
        self.width = width
        self.height = height
        self.xAxis = xAxis
        self.yAxis = yAxis
        DisplayingRect.counter = DisplayingRect.counter + 1
    }
}

struct ContentView : View {

    @State var rects  : [DisplayingRect] = [
    DisplayingRect(width: 30, height: 30, xAxis: 0, yAxis: 0),
    DisplayingRect(width: 50, height: 50, xAxis: 50, yAxis: 50)
    ]

    func setColorToID(_ id: Int) {
        rects[id].color = Color.blue
    }

    var body: some View {

        ForEach(self.rects, id: \.self) { rect in
            Rectangle()
                .fill(rect.color)
                .frame(width: rect.width, height: rect.height)
                .offset(x: rect.xAxis, y: rect.yAxis)
                .id(rect.id)
                .onTapGesture {
                    print(rect.id)
                    self.setColorToID(rect.id)
            }
        }
    }
}

Upvotes: 1

Josh Homann
Josh Homann

Reputation: 16327

Keep a @State to track which indices are highlighted then make your color a function of that state. Here is an example with animation:

struct ContentView: View {
  @State private var selectedIndices = Set<Int>()

  var body: some View {
    ForEach (0..<3) { index in
      Color(self.selectedIndices.contains(index) ? .yellow : .blue)
        .frame(width: 200, height: 200)
        .animation(.easeInOut(duration: 0.25))
        .onTapGesture {
          if self.selectedIndices.contains(index) {
            self.selectedIndices.remove(index)
          } else {
            self.selectedIndices.insert(index)
          }
      }
    }
  }
}

Upvotes: 2

Related Questions