Reputation: 500
Is there a way to access a view created in a ForEach
loop? I'm creating views (Rectangle
s) 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
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.
Upvotes: 1
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
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