如何在SwiftUI自定义Layout中基于文本基线对齐子视图?
实现自定义EqualHStack的文本基线对齐
要解决自定义布局中的首行/末行文本基线对齐问题,核心是利用LayoutSubview提供的baselineOffset(from:)方法获取子视图的基线位置偏移,再通过计算统一调整每个子视图的Y轴位置。以下是修改后的完整实现:
import SwiftUI struct EqualHStack: Layout { static var layoutProperties: LayoutProperties { var properties = LayoutProperties() properties.stackOrientation = .horizontal return properties } var alignment = VerticalAlignment.center var spacing: CGFloat? func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let maxWidth = subviews.map { $0.sizeThatFits(.unspecified).width }.max() ?? 0 let spacingValues = spacing(subviews: subviews) let totalSpacing = spacingValues.reduce(0, +) let totalWidth = maxWidth * CGFloat(subviews.count) + totalSpacing // 根据对齐方式计算布局高度 let totalHeight: CGFloat switch alignment { case .firstTextBaseline: // 首行基线对齐时,高度 = 最大的首行基线顶部偏移 + 最大的首行基线到底部延伸距离 let firstBaselineOffsets = subviews.map { $0.baselineOffset(from: .top) } let maxTopOffset = firstBaselineOffsets.max() ?? 0 let maxBottomExtension = subviews.enumerated().map { $0.element.sizeThatFits(.unspecified).height - firstBaselineOffsets[$0.offset] }.max() ?? 0 totalHeight = maxTopOffset + maxBottomExtension case .lastTextBaseline: // 末行基线对齐时,高度 = 最大的末行基线顶部延伸距离 + 最大的末行基线底部偏移 let lastBaselineOffsets = subviews.map { $0.baselineOffset(from: .bottom) } let maxTopExtension = subviews.enumerated().map { $0.element.sizeThatFits(.unspecified).height - lastBaselineOffsets[$0.offset] }.max() ?? 0 let maxBottomOffset = lastBaselineOffsets.max() ?? 0 totalHeight = maxTopExtension + maxBottomOffset default: // 其他对齐方式沿用原逻辑 totalHeight = subviews.map { $0.sizeThatFits(.unspecified).height }.max() ?? 0 } return CGSize(width: totalWidth, height: totalHeight) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let maxWidth = subviews.map { $0.sizeThatFits(.unspecified).width }.max() ?? 0 let spacingValues = spacing(subviews: subviews) let subProposal = ProposedViewSize(width: maxWidth, height: nil) // 高度由子视图自身决定,避免强制拉伸 var currentX = bounds.minX switch alignment { case .top: for (index, subview) in subviews.enumerated() { let subviewSize = subview.sizeThatFits(subProposal) subview.place(at: CGPoint(x: currentX + maxWidth/2, y: bounds.minY + subviewSize.height/2), anchor: .center, proposal: subProposal) currentX += maxWidth + spacingValues[index] } case .center: for (index, subview) in subviews.enumerated() { subview.place(at: CGPoint(x: currentX + maxWidth/2, y: bounds.midY), anchor: .center, proposal: subProposal) currentX += maxWidth + spacingValues[index] } case .bottom: for (index, subview) in subviews.enumerated() { let subviewSize = subview.sizeThatFits(subProposal) subview.place(at: CGPoint(x: currentX + maxWidth/2, y: bounds.maxY - subviewSize.height/2), anchor: .center, proposal: subProposal) currentX += maxWidth + spacingValues[index] } case .firstTextBaseline: // 计算统一的首行基线Y坐标 let firstBaselineOffsets = subviews.map { $0.baselineOffset(from: .top) } let maxTopOffset = firstBaselineOffsets.max() ?? 0 let baselineY = bounds.minY + maxTopOffset for (index, subview) in subviews.enumerated() { let subviewSize = subview.sizeThatFits(subProposal) // 子视图顶部位置 = 基线Y - 自身首行基线到顶部的偏移 let subviewTopY = baselineY - firstBaselineOffsets[index] subview.place(at: CGPoint(x: currentX + maxWidth/2, y: subviewTopY + subviewSize.height/2), anchor: .center, proposal: subProposal) currentX += maxWidth + spacingValues[index] } case .lastTextBaseline: // 计算统一的末行基线Y坐标 let lastBaselineOffsets = subviews.map { $0.baselineOffset(from: .bottom) } let maxBottomOffset = lastBaselineOffsets.max() ?? 0 let baselineY = bounds.maxY - maxBottomOffset for (index, subview) in subviews.enumerated() { let subviewSize = subview.sizeThatFits(subProposal) // 子视图底部位置 = 基线Y + 自身末行基线到底部的偏移 let subviewBottomY = baselineY + lastBaselineOffsets[index] subview.place(at: CGPoint(x: currentX + maxWidth/2, y: subviewBottomY - subviewSize.height/2), anchor: .center, proposal: subProposal) currentX += maxWidth + spacingValues[index] } default: // fallback到居中对齐 for (index, subview) in subviews.enumerated() { subview.place(at: CGPoint(x: currentX + maxWidth/2, y: bounds.midY), anchor: .center, proposal: subProposal) currentX += maxWidth + spacingValues[index] } } } private func spacing(subviews: Subviews) -> [CGFloat] { subviews.indices.map { index in guard index < subviews.count - 1 else { return 0 } if let spacing { return spacing } return subviews[index].spacing.distance(to: subviews[index + 1].spacing, along: .horizontal) } } } #Preview { VStack(spacing: 20) { EqualHStack { Text("Item 1\nSecond Line") Text("Item twoooo") Text("I3") } .border(.red) EqualHStack(alignment: .top, spacing: 20) { Text("Item 1\nSecond Line") Text("Item twoooo") Text("I3") } .border(.red) EqualHStack(alignment: .bottom) { Text("Item 1\nSecond Line") Text("Item twoooo") Text("I3") } .border(.red) EqualHStack(alignment: .firstTextBaseline) { Text("Item 1\nSecond Line") Text("Item twoooo") Text("I3") } .border(.red) EqualHStack(alignment: .lastTextBaseline) { Text("Item 1\nSecond Line") Text("Item twoooo") Text("I3") } .border(.red) } .padding() }
关键修改说明:
基线偏移获取:
- 通过
subview.baselineOffset(from: .top)获取子视图首行基线到自身顶部的距离 - 通过
subview.baselineOffset(from: .bottom)获取子视图末行基线到自身底部的距离
- 通过
布局高度计算:
- 首行基线对齐时,取最大的首行基线顶部偏移量,加上最大的首行基线到底部的延伸距离,确保所有子视图完整显示
- 末行基线对齐时,取最大的末行基线顶部延伸距离,加上最大的末行基线底部偏移量,保证布局高度足够容纳所有子视图
子视图放置逻辑:
- 首行基线对齐:计算统一的基线Y坐标,每个子视图的顶部位置为基线Y减去自身首行基线偏移,确保所有首行基线在同一水平线
- 末行基线对齐:计算统一的基线Y坐标,每个子视图的底部位置为基线Y加上自身末行基线偏移,确保所有末行基线在同一水平线
- 宽度方向保持原逻辑,强制所有子视图使用最大宽度,保证等宽布局效果
内容的提问来源于stack exchange,提问作者jrturton
相关产品推荐
相关产品推荐

