Search code examples
listviewgridviewswiftuiios13xcode13

SwiftUI: Create a list of mx2 column grid of images on iOS 13 with single selection


I'm new to SwiftUI and I'm trying to display a list of images from API as mx2 grids on iOS 13. I was able to create the grid and also able to download the images. Now I have 2 issues, all cells are showing the last downloaded image, and I need to figure out a way for highlighting the background of the selected cell which should work as a single selection, ie. if I select any other cell only that should be selected while the rest becomes deselected. Any help is appreciated. Below is my code so far.

struct MyListView: View {
    @State var cardImage: Image = Image ("placeholder")
    var myCard: [ImageDataModel] = []
    let column = 2
    var body: some View {
        let itemsCount = myCard.count
        let rowCount = itemsCount % 2 == 0 ? itemsCount/2 : itemsCount/2 + 1
        ScrollView {
            VStack(spacing: 20) {
                ForEach(0..<rowCount, id: \.self) { row in
                    HStack(spacing: 20) {
                        ForEach(0..<column, id: \.self) { col in
                            if row == rowCount-1 && itemsCount%2 != 0 && col != 0 {
                                Rectangle()
                                    .fill(Color.white)
                                        .frame(width: UIScreen.main.bounds.width/2-20)
                                        .aspectRatio(CGSize(width:148, height: 84), contentMode: .fit)
                            } else {
                                cardImage
                                    .resizable()
                                    .frame(width: UIScreen.main.bounds.width/2-20)
                                
                                .cornerRadius(20)
                                .aspectRatio(CGSize(width:148, height: 84), contentMode: .fit)
                                .onTapGesture {
                                    print("Tapped on image")
                                }
                                .onAppear {
                                    getImage(cardModel: self.myCard[(row*2)+col])
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    fileprivate func getImage(cardModel: ImageDataModel) {
        let imageData = ImageData(withUrl: cardModel.imageUrl.medium,
                                      completion: { (response) in
            DispatchQueue.main.async {
                switch response {
                case .success(let downloadedImage, _):
                    //This image download works but fills all cell image with last downloaded data
                    cardImage = Image(uiImage: downloadedImage)
                case .failure(let error):
                    print("Image Failure \(error)")
                }
            }
        })
        ImageDownloader.shared.downloadImage(withData: imageData)
    }
}

struct ImageDataModel {
    var imageId: Int
    var imageUrl: String
}

Solution

  • I was able to solve this using this library WaterFallGrid

    struct MyListView: View {
        var myCard: [ImageDataModel] = []
        var cardImage: Image = Image("placeholder")
        @State var selection = ""
        var body: some View {
            ScrollView {
              WaterfallGrid(myCard) { card in
                  CardImageView(myCard: card, cardImage: cardImage)
                      .border(selection == "\(card.id)" ? Color.blue : Color.white, width: 2.0)
                      .cornerRadius(5)
                      .onTapGesture {
                          print("Tapped on updated image = \(card.card)")
                          selection = "\(card.id)"
                      }
              }
              .gridStyle(
                columns: 2
              )
              .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
            }
        }
    }
    
    struct CardImageView: View {
        @State var myCard: ImageDataModel
        @State var cardImage: Image
    
        var body: some View {
            VStack {
                cardImage
                    .resizable()
                    .frame(width: (UIScreen.main.bounds.width)/2.5)
                    .aspectRatio(5/3, contentMode: .fit)
                    .cornerRadius(5)
                    .onAppear {
                        getImage(cardModel: myCard)
                    }
            }.padding(5)
        }
    
    struct ImageDataModel: Identifiable {
        let id = UUID()
        let imageUrl: String
    }