SwiftUI动态岛文本框尺寸自适应指南
2025-01-02 16:20:00
SwiftUI 动态岛文本框尺寸更新问题
动态岛上的实时活动常常需要在有限的空间内显示不断变化的数据,比如倒计时。当使用 Text(timerInterval:)
来显示时间时,由于文本内容的宽度会发生改变,例如从“12:34”变成“9:59”,这时可能会出现文本框大小与文本内容不匹配的问题,导致显示不美观。
问题分析
根本原因在于, SwiftUI 布局默认的自适应机制和 Text(timerInterval:)
生成文本动态变化之间的矛盾。具体来说,Text(timerInterval:)
会根据传入的时间间隔动态生成不同长度的文本。虽然 frame
提供了最大宽度、理想宽度等参数,但在实时活动这种场景下,它并不会根据 Text
内容的动态变化进行更新。因此,当 Text
内容变小时,父视图仍然会保持最初设置的尺寸,从而在右侧产生多余的空白。
解决方案一:使用GeometryReader
动态获取视图尺寸
使用 GeometryReader
可以读取到父视图提供的布局信息。我们可以在内部读取 Text
的实际宽度,并根据此宽度动态调整其周围的背景框大小。
操作步骤:
- 将需要自适应大小的
Text
包裹在GeometryReader
中。 - 利用
GeometryReader
返回的GeometryProxy
获取视图大小。 - 使用读取到的大小动态调整
.frame
和.background
。
代码示例:
import SwiftUI
import ActivityKit
struct MyActivityWidgetView: View {
@Environment( \.widgetRenderingContext ) var renderingContext
var timerInterval: ClosedRange<Date>
var body: some View {
HStack {
Label("JFK", systemImage: "airplane.departure")
.padding(4)
.background(ContainerRelativeShape().fill(.green.opacity(0.8)))
GeometryReader { geometry in
let timerText = Text("in ") + Text(timerInterval: timerInterval, showsHours: false) + Text(" min")
timerText
.fixedSize()
.padding(4)
.background(
ContainerRelativeShape().fill(.red).frame(width: max(geometry.size.width, timerText.getSize().width+8), height: geometry.size.height)
)
}
.frame(height:35, alignment: .trailing)
}
}
}
extension Text {
func getSize() -> CGSize {
let rect = self.boundingRect(
with: CGSize(width: .greatestFiniteMagnitude, height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
context: nil
)
return rect.size
}
}
#Preview{
MyActivityWidgetView(timerInterval:Date()...Date().addingTimeInterval(10 * 60 ))
.previewLayout(.sizeThatFits)
}
代码说明:
GeometryReader
: 接收外部环境传入的大小信息,用来进行计算。geometry.size
: 可以获得GeometryReader
的大小信息,利用其来撑开Text
的大小.getSize()
: 利用Text Extension方法计算 Text 的真实大小.fixedSize()
: 避免文字拉伸.background
: 根据 Text 的真实大小设置背景.frame(height:35)
:固定View高度
优点:
能够比较精准的适配Text
的真实宽度,让背景框根据文字内容的变化实时更新。
解决方案二:使用自定义文本视图进行精确布局控制
直接使用Text
以及其内置属性有时候在一些较为复杂的场景中不太灵活。此时,自定义一个View
进行绘制会得到更高的自由度。
操作步骤:
- 创建一个继承自
View
的新结构体。 - 使用
canvas
或者UILabel
在UIViewRepresentable
进行精确绘制文本,利用sizeThatFits
或相关的方法计算理想尺寸。 - 将文本渲染视图放置在所需的布局中。
代码示例:
import SwiftUI
import UIKit
import ActivityKit
struct DynamicTextView: UIViewRepresentable {
var timerInterval: ClosedRange<Date>
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 16) // 设置字体,或者其他字体样式。
label.textAlignment = .center;
label.text = formatTimeText(timerInterval: timerInterval)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = formatTimeText(timerInterval: timerInterval)
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UILabel, subviewSize: (Int) -> CGSize) -> CGSize {
uiView.sizeThatFits(CGSize(width: CGFloat(Int.max), height: proposal.height ?? .greatestFiniteMagnitude))
}
private func formatTimeText(timerInterval: ClosedRange<Date>)->String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute,.second]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
guard let formattedTime = formatter.string(from: timerInterval.lowerBound, to:timerInterval.upperBound) else{
return ""
}
return "in " + formattedTime + " min"
}
}
struct MyActivityWidgetView2: View {
@Environment( \.widgetRenderingContext ) var renderingContext
var timerInterval: ClosedRange<Date>
var body: some View {
HStack {
Label("JFK", systemImage: "airplane.departure")
.padding(4)
.background(ContainerRelativeShape().fill(.green.opacity(0.8)))
DynamicTextView(timerInterval: timerInterval)
.fixedSize()
.padding(4)
.background(ContainerRelativeShape().fill(.red))
}
}
}
#Preview {
MyActivityWidgetView2(timerInterval: Date()...Date().addingTimeInterval(50 * 60 ))
.previewLayout(.sizeThatFits)
}
代码说明:
UIViewRepresentable
:协议,可以创建使用 UIKit 的视图。UILabel
:可以更精确地控制文本属性以及尺寸。sizeThatFits
:用于获取理想的尺寸formatTimeText
:将TimeInterval
格式化成 "xx:xx" 的格式。.fixedSize()
: 防止View
发生拉伸
优点:
可以使用 UILabel
实现更细粒度的文本控制,包括自定义字体,以及布局,而且拥有更加精确的尺寸计算, 可以更完美的适配布局。
选择合适的解决方案
对于简单的时间显示,方案一利用 GeometryReader
可以快速解决问题,而方案二则使用更加精准的方式。 当对布局的灵活性有更高需求,例如自定义字体以及一些较为复杂的样式的时候可以选择方案二,开发者可以根据自己的具体场景进行取舍。