0

I am doing a ForEach and creating views and adding an on tap gesture

// objects = [MyObject]
                    VStack {
                        ForEach(objects, id: \.id) { obj in
                            MyView(imageURL: obj.imageURL)// <=== This is the RIGHT url for each object
                            .onTapGesture {
                                didTap(obj.objectId) // <== This is the WRONG id for each object
                            }
                        }
                    }

The id is a string based on some properties that make it unique

struct MyObject: Codable, Identifiable {
    var id: String {
        get { return uniquePropertyString }
    }

    var imageURL: String
    var objectId: String

The on tap gesture is returning the ID of an object 2 indexes deeper in the array. I thought by having a unique unchanging id property this wouldn't happen but it still is.

Can you suggest why the onTap gesture is wrong in this ForEach?

UPDATE In my case, it was eventually found that there was an invisible hitbox extending outside of the views. So tapping on a view was actually tapping another view (overlapping hitboxes).

UPDATE There was an Async image in the view which was large and resized smaller. Even though it was clipped and fit inside it, the fullsize of the image (hidden) was acting as a hitbox outside the view

Solution Adding .contentShape(Rectangle()) to the AsyncImage fixed it (answer from: Make SwiftUI tap not extend beyond bounds when clipped)

Aggressor
  • 13,323
  • 24
  • 103
  • 182
  • to debug, try using `let id = UUID().uuidString` and see if the problem is still there. Also, show a minimal reproducible code that produces your issue, see: [minimal code](https://stackoverflow.com/help/minimal-reproducible-example) – workingdog support Ukraine Apr 13 '23 at 01:36
  • Note, your computed property `var id: String {...}`, gives you a new value, every time the view is refreshed. In the `ForEach` loop, the `id` should be unique (and it is), **and** stable (it is not). – workingdog support Ukraine Apr 13 '23 at 02:03
  • I can confirm using the let UUID.string made no difference. I thought having a stable value means its the same every time it its computed? The unique property value is fetched from the server and never changes, is that not stable enough or am I missing something? – Aggressor Apr 13 '23 at 02:06
  • you mean `uniquePropertyString` is an asynchronous function? Show a minimal reproducible code that produces your issue, see: [minimal code](https://stackoverflow.com/help/minimal-reproducible-example) All works very well in my simple tests. Put `let _ = print("-----> id: \(obj.id)")` inside the `ForEach` loop, and see if you get different id, as it refreshes. – workingdog support Ukraine Apr 13 '23 at 02:21
  • Not an async function just a unique value. As for minimal code I feel I distilled it down to the core issue I am not sure what else you wanted see? – Aggressor Apr 13 '23 at 02:39
  • That being said I will investigate this stability issue you raise – Aggressor Apr 13 '23 at 02:40
  • Note, since your `MyObject` are `Identifiable`, there is no need for the `ForEach(objects, id: \.id)`, just `ForEach(objects)` should do it. As I said, all works very well in my simple tests. So I conclude that there must be some other things going on, that your code does not show. If you are interested I can post my test code. – workingdog support Ukraine Apr 13 '23 at 02:45

1 Answers1

1

Here is the code I use to test your issue. Let us know if this does not work for you.

See also: Identifiable

struct MyObject: Codable, Identifiable {
//    var id: String {
//        get { return uniquePropertyString }
//    }
    
     let id = UUID().uuidString  // <--- this works
    
//    var uniquePropertyString: String {
//        UUID().uuidString  // <-- for testing
//    }
    
    var imageURL: String
    var objectId: String
}

struct MyView: View {
    @State var imageURL: String
    
    var body: some View {
        Text(imageURL).border(.red)
    }
}

struct ContentView: View {
    let objects = [MyObject(imageURL: "url1", objectId: "obj-1"),
                   MyObject(imageURL: "url2", objectId: "obj-2"),
                   MyObject(imageURL: "url3", objectId: "obj-3")]
    
    var body: some View {
        VStack(spacing: 33) {
            ForEach(objects) { obj in
                //let _ = print("-----> id: \(obj.id)")
                MyView(imageURL: obj.imageURL)
                    .onTapGesture {
                        didTap(obj.objectId)
                    }
            }
        }
    }
    
    func didTap(_ objid: String) {
        print("-----> objid: \(objid)")
    }
}
  • Thanks for the effort. Ill be digging into this and report back when I can resolve – Aggressor Apr 13 '23 at 21:53
  • I verified the order of the array (i.e. ids in order of the array being foreached) before setting on the view state. Inside the foreach I verified the id for each view created matches. It seems that the onTapGesture, which uses the object reference just seems to lose the right reference somehow. Its very very odd. As a workaround I am going to use array indexes and see if that helps – Aggressor Apr 14 '23 at 00:52
  • Even using the indexes failed! I'd tap on the 2nd item and it would say its index 3. So weird... – Aggressor Apr 14 '23 at 01:05
  • I have mine exactly as yours (this was my implementation as well) and yea it seems to lose the obj reference. In your code, where you create the view, the imageURL would be correct, but then the didTap(obj.objectId) is wrong (in my scenario). – Aggressor Apr 14 '23 at 01:18
  • Some more interesting data, I put the tab gesture inside the `MyView` object, and while initially giving me 1 wrong value, it changed to another wrong value after continually tapping. There seems to be some instability in the ForEach theres nothing happening so I don't understanding why its redrawing and changing the references when it has stable ids... – Aggressor Apr 14 '23 at 01:23
  • 1
    There is a tap issue. The view is too large some how, even though the view hierarchy is not showing it, tapping outside a view is counting for it. So the issue is the bounds of the tap gesture extend outside the view – Aggressor Apr 14 '23 at 01:33
  • Going to upvote and select your answer as it is 'Correct' but also add a note about mine – Aggressor Apr 14 '23 at 01:34