问题描述
在适配《存钱猪猪》横屏界面时,发现当视图为横屏的情况下,其他的视图就会无法显示。
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,因此并不会影响到现有的视图尺寸,同时,图片的内容没有因为宽高比而影响显示。
