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 11 portrait - all ok
  • on iPhone 11 landscape - notification set is a bit too distant
  • on iPad 10th landscape notification is very far away

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.