swiftuioverlayswiftui-zstack

Conditionally show text on top of an image


I want to display a list of images. Each image have an associated text, and I want to display that text like a "popover" in style (but how it is achieved is not important - it doesn't have to be as a popover "call"), i.e. on top of the image but only as large as the text actually is, the image is supposed to be seen still in the background around the text.

I have the images in a HStack or VStack, depending on how the user holds the device and I have a button which I want to use to trigger showing the text. I have tried various ways of adding .popover modifier in various places without managing to show anything, including the Button. But I haven't gotten it to work in any way.

I'm fairly inexperienced, so the answer might be obvious!

The GrannyStreamDataItem data structure uses named images at the moment, but that will of course change later on.

How do I trigger and show the text when the user taps the i-button?

//Data structure

struct GrannyStreamDataItem: Identifiable {
    var id = UUID()
    var image: String
    var comment: String = ""
    var commentPosition: Alignment = .bottomLeading
}

let grannyStreamData: [GrannyStreamDataItem] = [
    GrannyStreamDataItem(image: "Pisa1", comment: "Leaning tower of Pisa. Gosh, it is crowded!", commentPosition: .topLeading),
    GrannyStreamDataItem(image: "Pisa2", comment: "This is what the tower looks like from the inside :-)", commentPosition: .topTrailing),
]


// View showing images 

struct ContentView: View {
    
    var body: some View {
        GeometryReader { geometry in
            
            if geometry.size.width > geometry.size.height {
                ScrollView(.horizontal) {
                    VStack {
                        Spacer()
                        HStack {
                            ImageViews(dataItemArray: grannyStreamData)
                        }
                        Spacer()
                    }

                }
            } else {
                ScrollView(.vertical) {
                    HStack {
                        Spacer()
                        VStack {
                            ImageViews(dataItemArray: grannyStreamData)
                        }
                        Spacer()
                    }
                }
            }
        }
    }
}


struct ImageViews: View {
    let dataItemArray: [GrannyStreamDataItem]
    
    var body: some View { 
        
        ForEach(dataItemArray) { dataItem in
            ZStack(alignment:dataItem.commentPosition) {
                Image(dataItem.image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                Button {
                    showTextImage(description: dataItem.comment)
                } label: {
                    Image(systemName: "info.circle")
                        .padding(.all, 10)
                        .font(.largeTitle)
                        .foregroundColor(.black)
                }
                //.popover() here won't work... Why not?
                  
            }

        }
    }

    func showTextImage(description: String) {
        print("Image text: \(description)")
    }
    
}

Solution

  • This is how I would do it:
    I would favor a conditional view to a popover in this case. It has a more flexible appearance.

    struct ImageList: View {
        let dataItemArray: [GrannyStreamDataItem]
        
        var body: some View {
            ForEach(dataItemArray) { dataItem in
                ImageView(dataItem: dataItem)
            }
        }
    }
    
    struct ContentView: View {
        var body: some View {
            GeometryReader { geometry in
                if geometry.size.width > geometry.size.height {
                    ScrollView(.horizontal) {
                        HStack {
                            ImageList(dataItemArray: grannyStreamData)
                        }
                        .padding(10)
                    }
                } else {
                    ScrollView(.vertical) {
                        VStack {
                            ImageList(dataItemArray: grannyStreamData)
                        }
                        .padding(10)
                    }
                }
            }
        }
    }
    
    struct ImageView: View {
        let dataItem: GrannyStreamDataItem
        @State private var showComment: Bool = false
        
        var body: some View {
            Image(dataItem.image)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .overlay(alignment: dataItem.commentPosition) {
                    Group {
                        if showComment {
                            Text("Image text: \(dataItem.comment)")
                                .padding(10)
                                .background(.gray.opacity(0.6))
                                .cornerRadius(8)
                                .padding(10)
                        } else {
                            Image(systemName: "info.circle")
                                .padding(10)
                                .font(.largeTitle)
                                .foregroundColor(.black)
                        }
                    }
                    .onTapGesture {
                        withAnimation(.easeInOut(duration: 0.3)) {
                            showComment.toggle()
                        }
                    }
                }
        }
    }
    

    I renamed ImageViews to ImageList to avoid any name confusion later on.
    I removed excessive HStacks, VStacks and Spacers from contentView, they also are more trouble than use.

    I moved Image and Text to a new view ImageView. Like that, each of them can have its own @State variable showComment. This variable can be toggled via an onTapGesture and the info image or the comment text will conditionally show. Note the if-condition needs to be enclosed by a Group view to accept the onTapGesture modifier. This Group is enclosed in a overlay modifier to stack over the image with the specified alignment.

    I also added an animation on the state change to make it a little smoother.

    enter image description here