SwiftUI自定义Layout实现子视图宽高相对父视图比例问题求助
SwiftUI自定义RelativeVStack布局:同时按比例设置子视图宽高异常问题
我基于Paul Hudson的《Pro SwiftUI》实现了自定义RelativeHStack和RelativeVStack布局,需求是子视图的宽高均按父视图尺寸的比例分配——比如高度占父视图50%时,宽度也占50%。但当我向ProposedViewSize同时指定固定宽高后,出现了无法解释的异常布局结果。
我搜索了其他自定义布局示例,发现大多只指定宽或高其中一个维度,另一个维度自适应内容。请问这是SwiftUI不支持的功能吗?如果支持,能否给出解释或示例?感谢!
更新:附上可直接在Playground运行的代码。RelativeVStack是RelativeHStack的垂直版本,垂直方向已正常工作,但尝试同时控制宽高时出现问题。
import SwiftUI struct ContentView: View { var body: some View { HStack(alignment: .center) { RelativeVStack() { Spacer() .layoutPriority(25) .border(.pink) LayoutStruct() .frame(maxWidth: .infinity, maxHeight: .infinity) .layoutPriority(50) Spacer() .layoutPriority(25) .border(.yellow) } .border(.red) } } } struct LayoutStruct: View { var body: some View { HStack(alignment: .center) { RelativeVStack() { Spacer() .layoutPriority(25) .border(.pink) ImageStruct() .frame(maxWidth: .infinity, maxHeight: .infinity) .layoutPriority(30) TextStruct() .frame(maxWidth: .infinity, maxHeight: .infinity) .layoutPriority(20) Spacer() .layoutPriority(25) .border(.yellow) } .frame(maxWidth: .infinity, maxHeight: .infinity) .border(.purple) } } } struct ImageStruct: View { var body: some View { Image(systemName: "globe") .frame(maxWidth: .infinity, maxHeight: .infinity) .imageScale(.large) .foregroundColor(.accentColor) .border(.green) } } struct TextStruct: View { var body: some View { Text("Hello, world!") .frame(maxWidth: .infinity, maxHeight: .infinity) .border(.blue) } } struct RelativeVStack: Layout { var spacing = 0.0 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize { let initialHeight = proposal.replacingUnspecifiedDimensions().height let initialWidth = proposal.replacingUnspecifiedDimensions().width let viewFrames = frames(for: subviews, in: CGRect(x: 0, y: 0, width: initialWidth, height: initialHeight)) let width = viewFrames.max { $0.maxX < $1.maxX } ?? .zero let height = viewFrames.max { $0.maxY < $1.maxY } ?? .zero return CGSize(width: width.maxX, height: height.maxY) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) { let viewFrames = frames(for: subviews, in: bounds) for index in subviews.indices { let frame = viewFrames[index] let position = CGPoint(x: bounds.midX - (frame.minX / 2), y: bounds.minY + frame.minY) let proposal = ProposedViewSize(frame.size) subviews[index].place(at: position, anchor: .top, proposal: proposal) } } func frames(for subviews: Subviews, in bounds: CGRect) -> [CGRect] { let totalSpacing = spacing * Double(subviews.count - 1) let availableHeight = bounds.height - totalSpacing let availableWidth = bounds.width let totalPriorities = subviews.reduce(0) { $0 + $1.priority } var viewFrames = [CGRect]() var y = 0.0 for subview in subviews { let subviewHeight = availableHeight * subview.priority / totalPriorities let subviewWidth = availableWidth * subview.priority / totalPriorities // passing nil for width gives correct result vertically, but content hugging horizontally // let proposal = ProposedViewSize(width: nil, height: subviewHeight) let proposal = ProposedViewSize(width: subviewWidth, height: subviewHeight) let size = subview.sizeThatFits(proposal) let frame = CGRect(x: 0, y: y, width: size.width, height: size.height) viewFrames.append(frame) y += size.height + spacing } return viewFrames } }
布局效果对比
仅设置高度时的布局效果:

同时设置宽高时的布局效果:

问题分析与修复方案
核心问题原因
SwiftUI完全支持同时指定宽高比例的自定义布局,你的异常是由两处逻辑错误导致:
- 尺寸计算依赖默认占位值:
sizeThatFits中使用replacingUnspecifiedDimensions()会在父视图未明确尺寸时使用SwiftUI默认占位尺寸(如1000pt宽度),导致比例计算完全偏离实际空间。 - 子视图定位与尺寸传递错误:
placeSubviews中错误的水平居中逻辑,以及依赖子视图sizeThatFits返回值而非强制使用分配尺寸,导致布局混乱。
修正后的RelativeVStack实现
struct RelativeVStack: Layout { var spacing = 0.0 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize { // 优先使用父视图明确指定的尺寸,未指定则基于子视图内容计算 let parentWidth = proposal.width ?? subviews.map { $0.sizeThatFits(.unspecified).width }.max() ?? 0 let parentHeight = proposal.height ?? subviews.map { $0.sizeThatFits(.unspecified).height }.reduce(0, +) + spacing * Double(subviews.count - 1) let viewFrames = frames(for: subviews, in: CGRect(x: 0, y: 0, width: parentWidth, height: parentHeight)) let finalWidth = parentWidth != 0 ? parentWidth : viewFrames.max { $0.maxX < $1.maxX }?.maxX ?? 0 let finalHeight = parentHeight != 0 ? parentHeight : viewFrames.max { $0.maxY < $1.maxY }?.maxY ?? 0 return CGSize(width: finalWidth, height: finalHeight) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) { let viewFrames = frames(for: subviews, in: bounds) for index in subviews.indices { let frame = viewFrames[index] // 子视图左上角对齐父视图对应位置 let position = CGPoint(x: bounds.minX, y: bounds.minY + frame.minY) // 传递精确分配的尺寸作为提案 let subProposal = ProposedViewSize(width: bounds.width, height: frame.height) subviews[index].place(at: position, anchor: .topLeading, proposal: subProposal) } } func frames(for subviews: Subviews, in bounds: CGRect) -> [CGRect] { let totalSpacing = spacing * Double(subviews.count - 1) let availableHeight = bounds.height - totalSpacing let totalPriorities = subviews.reduce(0) { $0 + $1.priority } var viewFrames = [CGRect]() var y = 0.0 for subview in subviews { let subviewHeight = availableHeight * subview.priority / totalPriorities // 宽度直接使用父视图完整宽度,确保水平填满 let subviewWidth = bounds.width let proposal = ProposedViewSize(width: subviewWidth, height: subviewHeight) // 强制使用分配的尺寸,避免子视图因内容自适应缩小 let frame = CGRect(x: 0, y: y, width: subviewWidth, height: subviewHeight) viewFrames.append(frame) y += subviewHeight + spacing } return viewFrames } }
关键修复点
- 尺寸计算逻辑优化:优先采用父视图明确尺寸,未指定时基于子视图内容动态计算,避免默认占位值干扰。
- 子视图定位修正:使用
.topLeading锚点对齐父视图左上角,确保水平方向填满父视图宽度。 - 强制比例尺寸:直接使用计算出的比例尺寸创建frame,不依赖子视图
sizeThatFits返回值,保证比例严格符合预期。 - 提案传递调整:传递父视图完整宽度作为提案,确保子视图的
.frame(maxWidth: .infinity)能正确填满分配空间。
修改后,子视图的宽高会严格按照优先级比例分配父视图尺寸,同时保持垂直布局逻辑正常运行。
内容的提问来源于stack exchange,提问作者janineanne
相关产品推荐
相关产品推荐

