WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: .onTapGesture not recognized in a ForEach

Forums > SwiftUI

I am trying to create my own picker from a SwiftData table of Images. I am using this to allow for selecting a subset of images (based on a type) and then allowing a user to pick one of the images. However the following code is not updating the selected Image...

struct GreetingCardsPicker: View {
    @Environment(\.modelContext) var modelContext
    @Environment(\.presentationMode) var presentationMode

    @Query(sort: \GreetingCard.cardName) private var greetingCards: [GreetingCard]

    @Binding var selectedGreetingCard: GreetingCard?
    @State private var isSelected = false

    private var gridLayout = [
        GridItem(.adaptive(minimum: 140), spacing: 10, alignment: .center)
    ]

    var eventType: EventType

    init(eventType: EventType, selectedGreetingCard: Binding<GreetingCard?>) {
        self.eventType = eventType
        _selectedGreetingCard = selectedGreetingCard
        let eventTypeID = eventType.persistentModelID // Note this is required to help in Macro Expansion
        _greetingCards = Query(
            filter: #Predicate {$0.eventType?.persistentModelID == eventTypeID },
            sort: [
                SortDescriptor(\GreetingCard.cardName, order: .reverse),
            ]
        )
    }

    var body: some View {
        VStack {
            LazyVGrid(columns: gridLayout, alignment: .center, spacing: 5) {
                ForEach(greetingCards, id: \.id) { greetingCard in
                    CardView(cardImage: UIImage(data: greetingCard.cardFront!)!)
                        .shadow(color: .green, radius: isSelected ? 2 : 0 )
                        .onTapGesture {
                            self.isSelected.toggle()
                            self.selectedGreetingCard = greetingCard
                            self.presentationMode.wrappedValue.dismiss()
                        }
                }
            }
        }
    }
}

I don't understand why I am getting a nil in the selectedGreetingCard in the calling view.

2      

Still no luck on figuring this one out.

2      

@ApApp is tap dancing around the .onTap() modifier with card objects.

Still no luck on figuring this one out.

Sometimes students just want an answer to a coding question. But I think your problem is application structure. Some find my answers a bit long, and tedious, preferring the short "here's how to fix" type answers.

For example, I think logic code does not belong in your view.

// The card object (in my opinion) should have one image to display
// based on its internal state. The view should not need to 
// know about the internal structure -- cardFront! -- of a card. 
// Hide this detail inside the Card object.
CardView(cardImage: UIImage(data: greetingCard.cardFront!)!)
        .shadow(color: .green, radius: isSelected ? 2 : 0 )
        .onTapGesture {
            // Why does a view care about modifying your card's state?
            // Tell the card what state is it, and allow the
            // view to redraw it if necessary.
             self.isSelected.toggle()
             self.selectedGreetingCard = greetingCard
             self.presentationMode.wrappedValue.dismiss()
         }

I think if you took another pass at restructuring your code, you'll find a simple solution. You tap a card which executes a method in the card struct (or class). This, in turn, causes any views containing the card to redraw itself, remove itself from the deck or tableau, or some other rule you may have.

Separate Logic and Views

To illustrate this, I might recommend you watch the other Paul's SwiftUI lectures. Paul Hagerty has been taping his excellent CS193p course at Stanford University for several years. In lectures 4 thru 9 he develops a nice card game. His game is complete with a stand alone deck that has all the game rules, but doesn't know how to draw itself. The game view is all about drawing the state of a card (face up, face down, matched, etc) but doesn't know any of the game's rules. It's a terrific example of separating logic from views.

You might also see how he displays cards in a view, captures the player's intentions with finger taps, and passes those intentions to the Card objects to change state and execute game logic.

Keep Coding!

Please return here and let us know how you solved this coding problem.

See -> cs193p.sites.stanford.edu/2023

3      

Thank you @Obelix this is the exact type of answer I am looking for. (It sticks a lot harder in the brain, and helps me with the concepts). I will give this a shot over the next few days.

2      

Well, still not there.. but the classes helped me clean up a bit of my code to improve computed properties and adding a few functions to my SwiftData classes. My challenge is that in my old version of this app, I could use a simple picker to pick images from the user's PhotoLibrary and save each image into the CoreData storage. If the same image was used twice, I'd have two copies of it in CoreData. As you can see that was a really bad design and took up a ton of space, when you send the same card to 10 people at Christmas or New Years, etc. So my goal was to create my own Card Gallery, which I have done with the GreetingCard object. The CardView is just a display View I use in multiple places to show an individual card. Here is what the picker looks like so far: PickerView

2      

So far it correctly changes the list of images at the bottom from the gallery, based on the eventType (I still can't get it to be flush with the Form up top). But if I select the image it doesn't do anything. I have tried changing the image to a Button in the picker, but I think the @Binding is not correct.

import os
import SwiftData
import SwiftUI

struct GreetingCardsPicker: View {
    @Environment(\.modelContext) var modelContext
    @Environment(\.presentationMode) var presentationMode

    @Query(sort: \GreetingCard.cardName) private var greetingCards: [GreetingCard]

    @Binding var selectedGreetingCard: GreetingCard?
    @State private var isSelected = false

    private var gridLayout = [
        GridItem(.adaptive(minimum: 140), spacing: 10, alignment: .center)
    ]

    var eventType: EventType

    init(eventType: EventType, selectedGreetingCard: Binding<GreetingCard?>) {
        self.eventType = eventType
        _selectedGreetingCard = selectedGreetingCard
        let eventTypeID = eventType.persistentModelID // Note this is required to help in Macro Expansion
        _greetingCards = Query(
            filter: #Predicate {$0.eventType?.persistentModelID == eventTypeID },
            sort: [
                SortDescriptor(\GreetingCard.cardName, order: .reverse),
            ]
        )
    }

    var body: some View {
        VStack {
            LazyVGrid(columns: gridLayout, alignment: .center, spacing: 5) {
                ForEach(greetingCards, id: \.id) { greetingCard in
                    Button(action:  {
                            self.selectedGreetingCard = greetingCard
                            self.presentationMode.wrappedValue.dismiss()
                        print("Selected is \(String(describing: selectedGreetingCard))")
                    }, label: {
                        CardView(cardImage: greetingCard.cardUIImage())
                            .aspectRatio(1, contentMode: .fit)
                            .shadow(color: .green, radius: isSelected ? 2 : 0 )
                    })

                }
            }
        }
    }
}

I am still not grocking the correct approach here...

2      

After reading Paul's post here - How to use MVVM to separate SwiftData from your views, I am still trying to understand how seperating to MVVM would improve my situtation. I know fundementally I should do this, but I would still have the issue that the value in the final view (in this case the Picker) is not allowed to bubble up to the NewCardView. And it I try to switch to having NewCardView use @State for the selectedGreetingCard, I am having issues with GreetingCardPickerView, which for some reason do not happen for the usage of @State for eventType. eventType is only being read by GreettingCardPickerView in order to filter the greetingCards Query. I want to know which greetingCard was selected, so it can be used by NewCardView to create the new Card instance, however, any change to selectedGreetingCard seems to be disallowed. Something is still not lining up.

2      

test

2      

OK, not an ellegant solution, but I just figured it out (will redesign my view from a form back to the older style I had). I have changed the code in the "NewCardView" to use a navigationLink:

var body: some View {
        NavigationView {
            VStack {
                Form {
                    Section("Card Information") {
                        Picker("Select type", selection: $selectedEvent) {
                            Text("Unknown Event")
                                .tag(Optional<EventType>.none) //basically added empty tag and it solve the case

                            if events.isEmpty == false {
                                Divider()

                                ForEach(events) { eventType in
                                    Text(eventType.eventName)
                                        .tag(Optional(eventType))
                                }
                            }
                        }

                        DatePicker(
                            "Event Date",
                            selection: $cardDate,
                            displayedComponents: [.date])
                    }
                }
                .padding(.bottom, 5)
                if selectedEvent != nil {
                    NavigationLink("Select card:", destination: GreetingCardsPickerView(eventType: selectedEvent!, selectedGreetingCard: $selectedGreetingCard))
                }
                Spacer()
            }
            .onChange(of: selectedGreetingCard) {
                print("Selected a Greeting Card - \(String(describing: selectedGreetingCard)))")
            }
            .padding([.leading, .trailing], 10)
            .navigationBarTitle("\(recipient.fullName)")
            .navigationBarItems(trailing:
                                    HStack {
                Button(action: {
                    saveCard()
                    self.presentationMode.wrappedValue.dismiss()
                }, label: {
                    Image(systemName: "square.and.arrow.down")
                        .font(.largeTitle)
                        .foregroundColor(.accentColor)
                })
                Button(action: {
                    self.presentationMode.wrappedValue.dismiss()
                }, label: {
                    Image(systemName: "chevron.down.circle.fill")
                        .font(.largeTitle)
                        .foregroundColor(.accentColor)
                })
            }
            )
        }
        .foregroundColor(.accentColor)
    }
And then change my GreetingCardPickerView to much simplier

```
    var body: some View {
    VStack {
        LazyVGrid(columns: gridLayout, alignment: .center, spacing: 5) {
            ForEach(greetingCards, id: \.id) { greetingCard in
                Image(uiImage: greetingCard.cardUIImage())
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 130, height: 130)
                    .clipShape(RoundedRectangle(cornerRadius: 5))
                    .onTapGesture {
                        selectedGreetingCard = greetingCard
                        self.presentationMode.wrappedValue.dismiss()
                    }
            }
        }
        .padding()
        .navigationTitle("Select \(eventType.eventName) Card")
    }
}
```

2      

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.