You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌入WaveFormView后VStack高度波动,如何稳定其高度?

解决VStack随WaveFormView相位动画波动高度的问题

问题描述

制作吉他调音器的WaveFormView时遇到以下问题:

  • 将WaveFormView嵌入VStack后,VStack高度会随WaveFormView的相位动画周期波动;
  • 设备旋转为横屏再切回竖屏后,VStack高度稳定为全屏状态,波动问题消失;
  • 仅在嵌入栈视图后出现该行为,单独使用WaveFormView无异常。

相关代码

WaveFormView实现

struct WaveFormView: View {
  @State private var phase = 0.0
  let amplitude: Double
  let frequency: Double
  let waveColor: Color
  let echoes: Double
  
  var body: some View {
    ZStack {
      ForEach(0..<5) { i in
        Wave(amplitude: amplitude, frequency: frequency, phase: self.phase)
          .stroke(waveColor.opacity((Double(i)/abs(echoes))), lineWidth: 2)
          .offset(y: -CGFloat(i) * CGFloat(echoes))
      }
      .mask {
        LinearGradient(gradient: Gradient(colors: [.clear, .white, . clear]), startPoint: .leading, endPoint: .trailing)
      }
      .onAppear {
        withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
          self.phase = .pi * 2
        }
      }
    }
  }
}

TunerView实现

struct TunerView: View {
  @StateObject var controller = PitchData()
  var pitchEngine = PitchEngine()
  
  var body: some View {
    VStack {
      Spacer()
      Text(controller.pitchString)     // Note letter
        .font(.system(size: 100))
      Spacer()

      ZStack{                                // Here are the waves 
        WaveFormView(amplitude: 30,
                     frequency: controller.pitchFrequency,
                     waveColor: controller.isTuned ? .blue : .red,
                     echoes: controller.offsetCents)
        WaveFormView(amplitude: 30,
                     frequency: controller.closestOffsetFrequency,
                     waveColor: controller.isTuned ? .blue : .red,
                     echoes: 1)                  
        .frame(height: 400)
        .border(.blue)
        .onAppear { startPitchEngine() }
      } 
    }
    .border(.red)
  }
}

解决方案

问题根源是WaveFormView没有明确的尺寸约束,SwiftUI在相位动画过程中会不断重新计算视图边界,进而带动父VStack高度波动。旋转设备后系统重新布局时会确定稳定尺寸,因此波动消失。可通过以下方式解决:

1. 给所有WaveFormView添加固定尺寸约束

确保两个WaveFormView都有明确高度限制,避免布局计算随动画变化:

ZStack{                                // Here are the waves 
  WaveFormView(amplitude: 30,
               frequency: controller.pitchFrequency,
               waveColor: controller.isTuned ? .blue : .red,
               echoes: controller.offsetCents)
  .frame(height: 400) // 与第二个WaveFormView保持一致高度
  WaveFormView(amplitude: 30,
               frequency: controller.closestOffsetFrequency,
               waveColor: controller.isTuned ? .blue : .red,
               echoes: 1)                  
  .frame(height: 400)
}
.frame(height: 400) // 给父ZStack固定高度,进一步稳定布局
.border(.blue)

2. 在WaveFormView内部固定尺寸

直接在WaveFormView的body中添加frame约束,确保所有实例默认有稳定尺寸:

struct WaveFormView: View {
  // 原有属性不变
  
  var body: some View {
    ZStack {
      // 原有内容不变
    }
    .frame(height: 400) // 固定高度
  }
}

3. 分离动画与布局计算

如果希望Wave绘制区域自适应,但避免动画影响布局,可使用GeometryReader固定绘制范围,让相位动画仅影响路径绘制:

struct Wave: Shape {
  var amplitude: Double
  var frequency: Double
  var phase: Double
  
  // 自定义animatableData,仅让phase参与动画
  var animatableData: Double {
    get { phase }
    set { phase = newValue }
  }
  
  func path(in rect: CGRect) -> Path {
    var path = Path()
    let width = rect.width
    let height = rect.height
    let midY = height / 2
    
    path.move(to: CGPoint(x: 0, y: midY + sin(phase) * amplitude))
    
    for x in stride(from: 0, to: width, by: 1) {
      let progress = Double(x) / width
      let angle = progress * frequency * .pi * 2 + phase
      let y = midY + sin(angle) * amplitude
      path.addLine(to: CGPoint(x: x, y: y))
    }
    
    return path
  }
}

修改后Wave的绘制始终限制在给定rect范围内,不会因动画导致视图尺寸变化,从根源解决VStack高度波动问题。

内容的提问来源于stack exchange,提问作者Licurgen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 01:03:10