我开始探索 SwiftUI,但找不到一种方法来获得简单的东西:我希望 View 具有成比例的高度(基本上是其父级高度的百分比)。假设我有 3 个垂直堆叠的视图。我想要:
我在 WWDC19 上观看了这段关于 SwiftUI 中自定义视图的有趣视频 (https://developer.apple.com/videos/play/wwdc2019/237/),我明白了(如果我错了,请纠正我)视图本身没有大小,大小是其子视图的大小。因此,父视图询问其子视图他们有多高。他们的回答是这样的:“你身高的一半!”然后什么?布局系统(与我们习惯的布局系统不同)如何处理这种情况?
如果您编写以下代码:
struct ContentView : View {
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.red)
Rectangle()
.fill(Color.green)
Rectangle()
.fill(Color.yellow)
}
}
}
SwiftUI 布局系统将每个视图的大小设置为 1/3 高,根据我上面发布的视频,这是正确的。 您可以这样将矩形包裹在框架中:
struct ContentView : View {
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.red)
.frame(height: 200)
Rectangle()
.fill(Color.green)
.frame(height: 400)
Rectangle()
.fill(Color.yellow)
}
}
}
这样,布局系统将第一个矩形的大小调整为 200 高,第二个矩形的高度调整为 400,第三个矩形的调整大小以适合所有剩余空间。再说一遍,这很好。 你不能(用这种方式)指定一个比例高度。
如果您的部署目标至少是 iOS 16、macOS 13、tvOS 16 或 watchOS 9,您可以编写自定义
Layout
。例如:
import SwiftUI
struct MyLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
return proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
precondition(subviews.count == 3)
var p = bounds.origin
let h0 = bounds.size.height * 0.43
subviews[0].place(
at: p,
proposal: .init(width: bounds.size.width, height: h0)
)
p.y += h0
let h1 = bounds.size.height * 0.37
subviews[1].place(
at: p,
proposal: .init(width: bounds.size.width, height: h1)
)
p.y += h1
subviews[2].place(
at: p,
proposal: .init(
width: bounds.size.width,
height: bounds.size.height - h0 - h1
)
)
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(MyLayout {
Color.pink
Color.indigo
Color.mint
}.frame(width: 50, height: 100).padding())
结果:
虽然这比
GeometryReader
解决方案(如下)的代码更多,但它更容易调试并扩展到更复杂的布局。
您可以利用
GeometryReader
。将阅读器包裹在所有其他视图周围,并使用其闭合值 metrics
来计算高度:
let propHeight = metrics.size.height * 0.43
使用方法如下:
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { metrics in
VStack(spacing: 0) {
Color.red.frame(height: metrics.size.height * 0.43)
Color.green.frame(height: metrics.size.height * 0.37)
Color.yellow
}
}
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
这是 SwiftUI 中比例宽度的示例
iOS 16 之前:
struct GeometryReaderHStack: View {
var body: some View {
GeometryReader { geometry in
HStack(spacing: .zero) {
let component1Width = geometry.size.width * 0.75
let component2Width = geometry.size.width * 0.25
Text("3/4")
.frame(width: component1Width)
.background(.red)
Text("1/4")
.frame(width: component2Width)
.background(.blue)
}
}
.frame(height: .zero)
}
}
备注:
.frame(height: .zero)
来强制
GeometryReader
尊重其内容大小。如果你不添加这个,
GeometryReader 将在设备上全屏显示。 U
可以在 GeometryReader 范围上添加 .background(.gray)
来查看
副作用!HStack
添加间距,则它不会按预期工作。它不会扣除/排除间距然后布局子视图,而是将其他子视图推出屏幕!你可能想对每个子视图使用.padding
iOS 16+ 之后:
我可以看到其他人已经提到过
Layout
,但下面是我的尖峰。 CustomHStack 与 spacing
配合良好,但可能需要一些额外的工作,例如(保护 widthWeights 的数量必须与子视图计数相同,并且可能实现 VerticalAlignment
)
struct CustomHStack: Layout {
let widthWeights: [CGFloat]
let spacing: CGFloat
init(widthWeights: [CGFloat], spacing: CGFloat? = nil) {
self.widthWeights = widthWeights
self.spacing = spacing ?? .zero
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxHeight = subviews
.map { proxy in
return proxy.sizeThatFits(.unspecified).height
}
.max() ?? .zero
return CGSize(width: proposal.width ?? .zero, height: maxHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let subviewSizes = subviews.map { proxy in
return proxy.sizeThatFits(.infinity)
}
var x = bounds.minX
let y = bounds.minY
let parentTotalWith = (proposal.width ?? .zero) - (CGFloat((widthWeights.count - 1)) * spacing)
for index in subviews.indices {
let widthWeight = widthWeights[index]
let proposedWidth = widthWeight * parentTotalWith
let sizeProposal = ProposedViewSize(width: proposedWidth, height: subviewSizes[index].height)
subviews[index]
.place(
at: CGPoint(x: x, y: y),
anchor: .topLeading,
proposal: sizeProposal
)
x += (proposedWidth + spacing)
}
}
}
用途:
struct ProportionalWidthHStack: View {
var body: some View {
CustomHStack(widthWeights: [0.75, 0.25]) {
Text("3/4")
.frame(maxWidth: .infinity)
.background(.red)
Text("1/4")
.frame(maxWidth: .infinity)
.background(.blue)
}
}
}