You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI横竖屏适配:解决单元格与通知徽章布局异常问题

问题:横屏状态下SwiftUI单元格拉伸、通知徽章位置偏移

支持iOS 15,已实现包含图片、文字的垂直单元格,有通知时显示红色圆形徽章。但横屏状态下单元格过度拉伸,通知徽章位置异常:iPhone 11竖屏显示正常,横屏时徽章稍远;iPad 10代横屏时徽章距离极远。已找到临时解决方案,但不清楚原理。

原代码

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)
    }
}

临时解决方案(已验证有效)

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)
    }
}

解决方案原理解析

原代码问题根源

  • GeometryReader会占据父视图的全部可用宽度,而LazyVGrid的GridItem设为.flexible(),横屏时单元格宽度会大幅拉伸。徽章直接放在ZStack的.topTrailing位置,会对齐整个拉伸后单元格的右上角,导致和图片位置脱节。
  • 图片用min(geometry.size.width, 100)限制尺寸,在宽单元格中会居中显示,而徽章对齐单元格边缘,进一步加剧了位置偏差。

为什么新方案有效

  • 移除GeometryReader,直接给图片固定100x100的尺寸,VStack会根据内容自动适配宽度,避免单元格无意义拉伸。
  • 用.overlay将徽章绑定到图片上,对齐图片的.topTrailing,再通过.offset(x:10, y:-10)让徽章部分超出图片范围,确保无论单元格整体尺寸如何变化,徽章始终和图片保持相对固定的位置。
  • 保持VStack高度固定为150,保证不同设备上单元格高度统一,避免布局混乱。

内容的提问来源于stack exchange,提问作者biggreentree

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 13:33:12