listswiftuicellgeometryreader

swiftui cannot use geometry reader to make look good a cell bot in portrait and landscape on Avery device


Supporting iOS 15. With tis code I get a vertical cell with an image and a text, if there are notifications, I show a red round text. matter is on landscape my cell stretches too much, and notification is too far away. any solution I try if solves the iPad issue, maybe centers the red label instead putting it on right high corner.

on iPhone portrait

on iPhone portrait

on iPad landscape

on iPad landscape

import SwiftUI

struct FavouritesWebAppsMainView: View {
    
    @EnvironmentObject var classFromEntryPoint: ClassFromEntryPoint

    //code for presenting sheet.
    @State private var selectedItem: WebAppModel?
    //related to customized wkwebViews
    @State private var errorMessage: String = "none"
    @State private var showAlert = false
    
    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
    ]
    
    var body: some View {
        VStack {
            headerView
            mainBodyWithGrid
            .sheet(item: $selectedItem) { webApp in
                let test = webApp.appUrl
                if let url = URL(string: test) {
                    CustomWebView(url: url, messageErrorFromWebView: $errorMessage)
                    HStack {
                        Spacer()
                        Button(action: {
                            selectedItem = nil //Used to dismiss the sheet
                        }) {
                            Label("", systemImage: "xmark")
                        }
                    }
                    .padding()
                    .background(Color.gray.opacity(0.2))
                } else {
                    //never called
                    let _ = self.errorMessage = "Not valid URL"
                    let _ = self.showAlert = true
                    let _ = Logger.error("failed url: \(webApp.appUrl)")
                }
            }
        } //most external view
        .backgroundImage()

        .onChange(of: errorMessage, perform: { newValue in
            self.showAlert = true
        })
        .alert("Errore", isPresented: $showAlert) {
            Button("ok", role: .cancel) {}
        } message: {
            Text(self.errorMessage)
        }
    }
    
    //MARK: single views
    private var headerView: some View {
        VStack {
            HStack {
                Text("Welvome")
                    .font(.largeTitle)
                    .foregroundColor(.blue)
                    .lineLimit(1)
                    .minimumScaleFactor(0.5)
                    .padding()
                Spacer()
            }
            Text("Le tue app")
                .padding(.bottom)
        }
    }
    
    private var mainBodyWithGrid: some View {
        ZStack(alignment: .bottom) {
            ScrollView {
                if classFromEntryPoint.preferredAppList.isEmpty {
                    Text("Add App to the store")
                } else {
                    LazyVGrid(columns: columns, spacing: 30) {
                        ForEach(classFromEntryPoint.preferredAppList) { item in
                            VerticalCellView(item: item)
                                .onTapGesture {
                                    self.selectedItem = item
                                }
                        }
                    }
                    .padding(.horizontal)
                }
            }
            
            logoutButton
        }
    }
    
    private var logoutButton: some View {
        HStack {
            Button("Logout") {
                Logger.debug("logout")
                classFromEntryPoint.performLogout()
            }
            .padding()
            .background(.red)
            .foregroundStyle(.white)
            .clipShape(Capsule())
            .padding()
            Spacer()
        }
    }

    
}


//MARK: - cell

struct VerticalCellView: View {

    var item: WebAppModel

    var body: some View {
        ZStack(alignment: .topTrailing) {
            GeometryReader { geometry in
                VStack {
                    ImageDownloadedFromWeb(url: item.logoUrl ?? "N/D")
                        .frame(width: min(geometry.size.width, 100), height: min(geometry.size.width, 100))
                    Text(item.name)
                        .font(.headline)
                        .frame(maxWidth: .infinity)
                    //                            Text(item.description)
                    //                                .font(.subheadline)
                    //                                .lineLimit(2)
                    //                                .truncationMode(.tail)
                }
                .frame(width: geometry.size.width)
            }
            let notificationCount = (Int.random(in: 0...20))
            Text(notificationCount > 10 ? "99+" : "\(notificationCount)")
                            .font(.system(size: 12))
                            .frame(width: 25, height: 25)
                            .background(.red)
                            .foregroundColor(.white)
                            .clipShape(Circle())
        }
        .frame(height: 150)
    }
}

POSSIBLE SOLUTION but not clear why it is working

struct VerticalCellView: View {

    var item: WebAppModel

    var body: some View {
        VStack {
            ImageDownloadedFromWeb(url: item.logoUrl ?? "N/D")
                .frame(width: 100, height: 100)
                .overlay(
                    notificationBadge,
                    alignment: .topTrailing
                )
            Text(item.name)
                .font(.headline)
            //                            Text(item.description)
            //                                .font(.subheadline)
            //                                .lineLimit(2)
            //                                .truncationMode(.tail)
        }
        .frame(height: 150)
    }

    private var notificationBadge: some View {
        let notificationCount = (Int.random(in: 0...20))
        return Text(notificationCount > 10 ? "99+" : "\(notificationCount)")
            .font(.system(size: 12))
            .frame(width: 25, height: 25)
            .background(.red)
            .foregroundColor(.white)
            .clipShape(Circle())
            .offset(x: 10, y: -10)
    }
}

Solution

  • To follow up on the comments, I would suggest showing the badges as an overlay over the images. This way, the badges will never be detached from the images.

    Here is a stripped-down version of your example to illustrate how you can do it with overlays. In the spirit of a minimal reproducible example I left out everything that wasn't needed or relevant. In particular, I found myself wondering, why you were using a GeometryReader at all? The size it was giving you was the size of the grid cell, but this is not particularly useful. So I think it can be solved without using a GeometryReader.

    struct WebAppModel: Identifiable {
        let id = UUID()
        let logo: String
        let name: String
    }
    
    struct FavouritesWebAppsMainView: View {
    
        let items: [WebAppModel] = [
            WebAppModel(logo: "lizard", name: "test1"),
            WebAppModel(logo: "ant", name: "test2"),
            WebAppModel(logo: "tortoise", name: "test3"),
            WebAppModel(logo: "fossil.shell", name: "test4"),
        ]
    
        let columns = [
            GridItem(.flexible()),
            GridItem(.flexible()),
            GridItem(.flexible()),
        ]
    
        var body: some View {
            mainBodyWithGrid
        }
    
        private var mainBodyWithGrid: some View {
            ScrollView {
                LazyVGrid(columns: columns, spacing: 30) {
                    ForEach(items) { item in
                        VerticalCellView(item: item)
                    }
                }
                .padding(.horizontal)
            }
        }
    }
    
    struct VerticalCellView: View {
    
        var item: WebAppModel
    
        private var randomNotificationBadge: some View {
            let notificationCount = (Int.random(in: 0...20))
            return Text(notificationCount > 10 ? "99+" : "\(notificationCount)")
                .font(.system(size: 12))
                .frame(width: 25, height: 25)
                .background(.red)
                .foregroundColor(.white)
                .clipShape(Circle())
        }
    
        var body: some View {
            VStack {
                Image(systemName: item.logo)
                    .resizable()
                    .scaledToFit()
                    .frame(maxHeight: .infinity)
                    .background(.yellow.opacity(0.2))
                    .overlay(alignment: .topTrailing) {
                        randomNotificationBadge
                    }
                Text(item.name)
                    .font(.headline)
            }
            .frame(height: 150)
        }
    }
    

    Portrait

    Landscape

    The screenshots are from an iPad 10th gen.