SwiftUI横屏下Spacer()顶出其他视图问题
SwiftUI横屏下Spacer()顶出其他视图问题

SwiftUI横屏下Spacer()顶出其他视图问题

问题描述

在适配《存钱猪猪》横屏界面时,发现当视图为横屏的情况下,其他的视图就会无法显示。

SwiftUI代码如下:

import SwiftUI

struct Home: View {
    @ViewBuilder
    private func backgroundImageView() -> some View {
        if !BackgroundImage.isEmpty {
            Image(BackgroundImage)
                .resizable()
                .scaledToFill()
                .ignoresSafeArea()
        } else {
            EmptyView()
        }
    }
    var body: some View {
        ZStack {
            // 背景图
            backgroundImageView()
            // 主视图
            VStack {
                Spacer()
                Button(action: {
                    // 跳转到创建视图
                    pageSteps = 3
                }, label: {
                    Text("Create")
                        .frame(width: 320,height: 60)
                        .foregroundColor(Color.white)
                        .background(Color(hex: "#FF4B00"))
                        .cornerRadius(10)
                })
            }
        }
        .frame(maxWidth: .infinity,maxHeight: .infinity)
    }
}

在竖屏的状态下,按钮正常显示,并被Spacer()顶到视图的底部。

但是,当将设备横屏显示时,按钮就不会显示。

问题排查

根据代码信息隐藏对比发现,当我隐藏Spacer()时,按钮是可以正常显示的,当使用Spacer()时,按钮就会被隐藏。

但是这个问题在竖屏下是正常的,因此我尝试给VStack添加背景颜色。

VStack {
    Spacer()
    Button()
}
.background(.red.opacity(0.3))

当设置VStack背景色为红色时,可以发现内容仍然是未显示的,Spacer()并没有将Button顶部视图的底部。

当我设置VStack的高度为固定值时,内容又一次显示出来。

VStack {
    Spacer()
    Button()
}
.frame(height: 300)
.background(.red.opacity(0.3))

但是,外层的视图又没有固定值,默认情况下,应该是占据整个视图。

在排查backgroundImageView方法时发现,如果将 scaledToFill 改为 scaledToFit 修饰符,内容就可以正常显示了。

@ViewBuilder
private func backgroundImageView() -> some View {
    if !BackgroundImage.isEmpty {
        Image(BackgroundImage)
            .resizable()
            .scaledToFit()
            .ignoresSafeArea()
    } else {
        EmptyView()
    }
}

通过查询,了解到当 Image 使用了 .resizable() 和 .scaledToFill() 时,图像会被缩放至填满父容器,甚至超出边界。如果没有显式限制父容器的尺寸,Image 会尝试占据整个 ZStack 的可用空间。

当我想要让背景图片填满整个屏幕时,发现如果使用 .scaledToFit() 和 frame 并不满足填满屏幕的需求:

Image(BackgroundImage)
    .resizable()
    .scaledToFit()
    .frame(width: .infinity, height: .infinity)
    .ignoresSafeArea()

但是,如果移除 scaledToFit() ,图片则可以正常的填满屏幕。

Image(BackgroundImage)
    .resizable()
    .frame(width: .infinity, height: .infinity)
    .ignoresSafeArea()

问题原因

看到图片显示的完整尺寸后,我最终意识到了问题的原因。

因为背景图片是一个矩形图片,当Image使用 .scaledToFill(),实际上这个矩形是按照原来的比例填满整个屏幕。

从这张演示图片中可以看到,当使用 .scaledToFill()时,图片会按照原尺寸铺满屏幕,因为是一个矩形,因此只能用宽度铺满屏幕,对等比例的高度则拉高了整个屏幕视图的高度。

也因此导致,当使用Spacer()填充空白区域时,实际上填充的是整个拉高后的屏幕,手机视图也就无法查看到显示的按钮。

当将按钮的区域设置一个固定的高度时,内容高度得到约束,从而可以看到按钮。

总结

总体来讲,这个问题的原因是因为图片设置了 .scaledToFill() 导致视图的实际区域拉高,当使用Spacer() 填充空白区域时,就会导致其他的视图内容被Spacer()推到顶部或底部,从而无法查看实际的视图内容。

最重要的原因,还是不了解 .scaledToFill() 的作用,因此,后续的Spacer() 顶出视图的内容,大概率是因为某些原因拉高或拉宽视图导致的。

最后是解决方案,如果想要在ZStack中使用一个Image作为背景图,那么 .scaledToFill() 注定会拉高或拉宽整个视图的尺寸,当时如果使用 .scaledToFit() ,可能会固定尺寸最短的一边铺满屏幕,就会存在空白区域。

因此,如果图片合适且不考虑切换横屏、竖屏的话,可以不使用 .scaledToFill() 或 .scaledToFit(),而是给Image添加frame,设置最大尺寸。

backgroundImageView()
    .frame(width: .infinity, height: .infinity)

还有一种结局方案,就是给图片设置background:

@ViewBuilder
    private func backgroundImageView() -> some View {
        if !BackgroundImage.isEmpty {
            Image(BackgroundImage)
                .resizable()
                .scaledToFill()
                .ignoresSafeArea()
        } else {
            EmptyView()
        }
    }
    var body: some View {
        
        NavigationStack {
            GeometryReader { geometry in
                // 通过 `geometry` 获取布局信息
                let width = geometry.size.width * 0.85
                let height = geometry.size.height
                ZStack {
                    ... // 内容
                }
                .frame(maxWidth: geometry.size.width ,maxHeight: geometry.size.height)
                .background(backgroundImageView())  // 背景图片
            }
        }
        
    }
}

在这段代码中,Image仍然使用的是 .scaledToFill(),但是因为应用在 background,因此并不会影响到现有的视图尺寸,同时,图片的内容没有因为宽高比而影响显示。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注