如何让SwiftUI按钮在标签内容变化时保持固定尺寸?
问题
我有一个SwiftUI Button,点击时标签内容会切换,但切换时按钮尺寸会跟着变化。代码如下:
struct MyButton: View { var label: String @State var loading = false var body: some View { Button(action: { loading.toggle() }) { if (loading) { Text("...") } else { Text(label) } } .buttonStyle(.borderedProminent) } } struct MyButton_Previews: PreviewProvider { static var previews: some View { MyButton(label: "This is a button") } }
点击按钮切换标签时,按钮尺寸会随文本长度变化。我需要一个无需硬编码frame尺寸的通用解决方案,让按钮能适配任意标签,同时在状态切换时保持固定尺寸——就像Jetpack Compose里可以通过自定义布局同时测量两种状态的文本,取最大尺寸作为按钮尺寸,只显示当前状态的文本那样,SwiftUI里有类似实现吗?
解决方案
SwiftUI里可以通过两种方式实现这个需求,都不需要硬编码尺寸,完美适配任意标签:
方式一:叠加透明文本(简单高效)
核心思路是同时布局两种状态的文本,将非当前状态的文本设为透明且不响应交互,让按钮自动以尺寸较大的文本为基准确定自身大小,切换时仅显示对应状态的文本。
struct MyButton: View { var label: String @State var loading = false var body: some View { Button(action: { loading.toggle() }) { // 显示当前状态的文本 Text(loading ? "..." : label) // 叠加另一种状态的文本,透明化用来撑住按钮尺寸 .overlay( Text(loading ? label : "...") .opacity(0) .allowsHitTesting(false) ) } .buttonStyle(.borderedProminent) } }
方式二:自定义Layout(灵活扩展)
如果需要更精细的布局控制,可以自定义Layout组件,手动测量两种状态文本的尺寸,取宽高最大值作为容器尺寸,再居中布局当前状态的文本,完全对应Compose的自定义布局思路。
// 自定义布局:测量两个子视图的最大尺寸,仅布局当前需要显示的视图 struct MaxSizeLayout: Layout { let showFirst: Bool func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { guard subviews.count == 2 else { return .zero } // 分别测量两个文本的尺寸 let normalSize = subviews[0].sizeThatFits(proposal) let loadingSize = subviews[1].sizeThatFits(proposal) // 取宽高的最大值作为布局尺寸 return CGSize( width: max(normalSize.width, loadingSize.width), height: max(normalSize.height, loadingSize.height) ) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let targetView = showFirst ? subviews[0] : subviews[1] // 将目标文本居中布局 targetView.place( at: CGPoint( x: bounds.midX - targetView.sizeThatFits(proposal).width / 2, y: bounds.midY - targetView.sizeThatFits(proposal).height / 2 ), proposal: proposal ) // 将另一个文本移出可视区域隐藏 let hiddenView = showFirst ? subviews[1] : subviews[0] hiddenView.place(at: CGPoint(x: -1000, y: -1000), proposal: .zero) } } // 使用自定义布局的按钮 struct MyButton: View { var label: String @State var loading = false var body: some View { Button(action: { loading.toggle() }) { MaxSizeLayout(showFirst: !loading) { Text(label) Text("...") } } .buttonStyle(.borderedProminent) } }
两种方案都能让按钮在状态切换时保持固定尺寸,且支持任意长度的标签文本,无需硬编码frame参数。
内容的提问来源于stack exchange,提问作者Bradleycorn
相关产品推荐
相关产品推荐

