Add contextMenu in the items inside of a List in SwiftUI

Viewed 810

I have created a photo library, there is list with many cells and in each cells. there are a few UIImages. I added contextMenu the images, but when I long press on each image, the entire cell will be called instead of each image. could anyone help me to how to add contextMenu to each the items inside of a List

struct PhotoList : View {

var photoLibrary = PhotoLibrary

var body : some View {
    GeometryReader { geometry in
        List(self.photoLibrary, id: \.self) { imageSet in
            HStack (alignment: .center) {
                ForEach(imageSet, id: \.self) { image in
                    Image(uiImage: image)
                        .scaledToFill()
                        .cornerRadius(7)
                        .padding(3)
                        .frame(width: 150, height: 150, alignment: .center)
                        .contextMenu {
                            VStack {
                                Button(action: {}) {
                                    HStack {
                                        Text("Add to Today")
                                        Image("plus.circle")
                                    }
                                }
                            }
                    }

                }
            }
        }
    }
}

}

4 Answers

The problem with this is that the list view will force touch events to be registered to the first child view. For example it is not possible to create a list with multiple buttons in each row as in the following code.

// Individual buttons can't be pressed
   List(someList, id: \.self) { _ in
        HStack {
             ForEach(1..<4) { k in
                Button("Button") {
                    //what ever
                }
            }
        }
    }

HStack will get all the touch events....

Though it is possible to solve your problem by using a combination of a scrollview and a vstack. If scrolling isn't necessary just remove that piece of code.

var body : some View {
    GeometryReader() { geometry in
        ScrollView {
            VStack {
                List(self.photoLibrary, id: \.self) { imageSet in
                    HStack (alignment: .center) {
                        ForEach(imageSet, id: \.self) { image in             
                            Image(uiImage: image)
                                .resizable()
                                .scaledToFill()
                                .cornerRadius(7)
                                .padding(3)
                                .frame(width: 150, height: 150, alignment: .center)
                                .contextMenu {
                                    VStack {
                                        Button(action: {}) {
                                            HStack {
                                                Text("Add to Today")
                                                Image("plus.circle")
                                            }
                                        }
                                    }
                                }
                        }
                    }
                }
            }
        }.position(x: geometry.size.width/2, y: geometry.size.height/2)
    }
}

Code needs to be corrected for your design though. Added .resizable to the image as well. Hope this can be a start for your code.

Replace your List with a ForEach and wrap it in a VStack or LazyVStack and you're good to go. If you need scrolling wrap it in a ScrollView. The List makes it behave like table rows.

So instead of

List(self.photoLibrary, id: \.self) { imageSet in
    // ...
}

do this:

ScrollView { // If scrolling is needed
    VStack {
        ForEach(self.photoLibrary, id: \.self) { imageSet in
            // ...
        }
    }
}

Pretty much as NoosaMaan explained in more detail:

"[…] the list view will force touch events to be registered to the first child view […]"

I'm not sure what your struct for photoLibrary is like, so I created my own. This code will need to be changed slightly depending on your struct.

This ContentView can be called passing a list of photoLibrary to display on the screen.

struct photoLibrary {
    var imageSet: [ImageItem]
    init(){
        self.imageSet = [ImageItem()]
    }
}
struct ImageItem{
    var image1: Image
    var image2: Image
    var image3: Image
    init(){
        self.image1 = Image(systemName: "xmark")
        self.image2 = Image(systemName: "xmark")
        self.image3 = Image(systemName: "xmark")
    }
}

struct ContentView: View {
    @State var photoLib: [photoLibrary]
    var body: some View {
        VStack {
            ScrollView{
                ForEach(self.photoLib.indices, id: \.self) { item in
                    VStack{
                        ForEach(photoLib[item].imageSet.indices, id: \.self) { image in
                            HStack{
                                ImageView(image: photoLib[item].imageSet[image].image1)
                                ImageView(image: photoLib[item].imageSet[image].image2)
                                ImageView(image: photoLib[item].imageSet[image].image3)
                            }
                        }
                    }
                }
            }
        }
    }
    
struct ImageView: View{
    @State var image: Image
    var body: some View{
        image
            .resizable()
            .scaledToFill()
            .cornerRadius(7)
            .padding(3)
            .frame(width: 150, height: 150, alignment: .center)
        Text("Add to Today").onTapGesture {
            //do something
        }
        Image(systemName: "plus.circle").onTapGesture {
            //do something
            }
        }
    }
}

Display example with xmarks as the image:

enter image description here

As I know a context menu has to have at least two buttons but the following way I think it would be enough:

Image(uiImage: image)
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
    Button(action: {}) {
        Text("Add to Today")
        Image("plus.circle")
    }
    Button(action: {}) {
        Text("Some Other Button")
        Image("globe")
    }
}
Related