返回

SwiftUI动态岛文本框尺寸自适应指南

IOS

SwiftUI 动态岛文本框尺寸更新问题

动态岛上的实时活动常常需要在有限的空间内显示不断变化的数据,比如倒计时。当使用 Text(timerInterval:) 来显示时间时,由于文本内容的宽度会发生改变,例如从“12:34”变成“9:59”,这时可能会出现文本框大小与文本内容不匹配的问题,导致显示不美观。

问题分析

根本原因在于, SwiftUI 布局默认的自适应机制和 Text(timerInterval:) 生成文本动态变化之间的矛盾。具体来说,Text(timerInterval:)会根据传入的时间间隔动态生成不同长度的文本。虽然 frame 提供了最大宽度、理想宽度等参数,但在实时活动这种场景下,它并不会根据 Text 内容的动态变化进行更新。因此,当 Text 内容变小时,父视图仍然会保持最初设置的尺寸,从而在右侧产生多余的空白。

解决方案一:使用GeometryReader动态获取视图尺寸

使用 GeometryReader 可以读取到父视图提供的布局信息。我们可以在内部读取 Text 的实际宽度,并根据此宽度动态调整其周围的背景框大小。

操作步骤:

  1. 将需要自适应大小的 Text 包裹在 GeometryReader 中。
  2. 利用 GeometryReader 返回的 GeometryProxy 获取视图大小。
  3. 使用读取到的大小动态调整 .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进行绘制会得到更高的自由度。

操作步骤:

  1. 创建一个继承自 View 的新结构体。
  2. 使用 canvas 或者 UILabelUIViewRepresentable 进行精确绘制文本,利用sizeThatFits 或相关的方法计算理想尺寸。
  3. 将文本渲染视图放置在所需的布局中。

代码示例:

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 可以快速解决问题,而方案二则使用更加精准的方式。 当对布局的灵活性有更高需求,例如自定义字体以及一些较为复杂的样式的时候可以选择方案二,开发者可以根据自己的具体场景进行取舍。