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

SwiftUI自定义Layout实现子视图宽高相对父视图比例问题求助

SwiftUI自定义RelativeVStack布局:同时按比例设置子视图宽高异常问题

我基于Paul Hudson的《Pro SwiftUI》实现了自定义RelativeHStack和RelativeVStack布局,需求是子视图的宽高均按父视图尺寸的比例分配——比如高度占父视图50%时,宽度也占50%。但当我向ProposedViewSize同时指定固定宽高后,出现了无法解释的异常布局结果。

我搜索了其他自定义布局示例,发现大多只指定宽或高其中一个维度,另一个维度自适应内容。请问这是SwiftUI不支持的功能吗?如果支持,能否给出解释或示例?感谢!

更新:附上可直接在Playground运行的代码。RelativeVStack是RelativeHStack的垂直版本,垂直方向已正常工作,但尝试同时控制宽高时出现问题。

import SwiftUI

struct ContentView: View {

  var body: some View {
    HStack(alignment: .center) {
      RelativeVStack() {
        Spacer()
          .layoutPriority(25)
          .border(.pink)
        LayoutStruct()
          .frame(maxWidth: .infinity, maxHeight: .infinity)
          .layoutPriority(50)
        Spacer()
          .layoutPriority(25)
          .border(.yellow)
      }
      .border(.red)
    }
  }
}

struct LayoutStruct: View {

  var body: some View {
    HStack(alignment: .center) {
      RelativeVStack() {
        Spacer()
          .layoutPriority(25)
          .border(.pink)
        ImageStruct()
          .frame(maxWidth: .infinity, maxHeight: .infinity)
          .layoutPriority(30)
        TextStruct()
          .frame(maxWidth: .infinity, maxHeight: .infinity)
          .layoutPriority(20)
        Spacer()
          .layoutPriority(25)
          .border(.yellow)
      }
      .frame(maxWidth: .infinity, maxHeight: .infinity)
      .border(.purple)
    }
  }
}

struct ImageStruct: View {

  var body: some View {
    Image(systemName: "globe")
      .frame(maxWidth: .infinity, maxHeight: .infinity)
      .imageScale(.large)
      .foregroundColor(.accentColor)
      .border(.green)
  }
}

struct TextStruct: View {

  var body: some View {
    Text("Hello, world!")
      .frame(maxWidth: .infinity, maxHeight: .infinity)
      .border(.blue)
  }
}

struct RelativeVStack: Layout {
  var spacing = 0.0

  func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
    let initialHeight = proposal.replacingUnspecifiedDimensions().height
    let initialWidth = proposal.replacingUnspecifiedDimensions().width
    let viewFrames = frames(for: subviews, in: CGRect(x: 0, y: 0, width: initialWidth, height: initialHeight))
    let width = viewFrames.max { $0.maxX < $1.maxX } ?? .zero
    let height = viewFrames.max { $0.maxY < $1.maxY } ?? .zero
    return CGSize(width: width.maxX, height: height.maxY)
  }

  func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
    let viewFrames = frames(for: subviews, in: bounds)

    for index in subviews.indices {
      let frame = viewFrames[index]
      let position = CGPoint(x: bounds.midX - (frame.minX / 2), y: bounds.minY + frame.minY)
      let proposal = ProposedViewSize(frame.size)
      subviews[index].place(at: position, anchor: .top, proposal: proposal)
    }
  }

  func frames(for subviews: Subviews, in bounds: CGRect) -> [CGRect] {
    let totalSpacing = spacing * Double(subviews.count - 1)
    let availableHeight = bounds.height - totalSpacing
    let availableWidth = bounds.width
    let totalPriorities = subviews.reduce(0) { $0 + $1.priority }

    var viewFrames = [CGRect]()
    var y = 0.0

    for subview in subviews {
      let subviewHeight = availableHeight * subview.priority / totalPriorities
      let subviewWidth = availableWidth * subview.priority / totalPriorities
// passing nil for width gives correct result vertically, but content hugging horizontally
//      let proposal = ProposedViewSize(width: nil, height: subviewHeight)
      let proposal = ProposedViewSize(width: subviewWidth, height: subviewHeight)
      let size = subview.sizeThatFits(proposal)
      let frame = CGRect(x: 0, y: y, width: size.width, height: size.height)
      viewFrames.append(frame)
      y += size.height + spacing
    }

    return viewFrames
  }
}

布局效果对比

  • 仅设置高度时的布局效果:
    仅设置高度的布局

  • 同时设置宽高时的布局效果:
    同时设置宽高的布局


问题分析与修复方案

核心问题原因

SwiftUI完全支持同时指定宽高比例的自定义布局,你的异常是由两处逻辑错误导致:

  1. 尺寸计算依赖默认占位值:sizeThatFits中使用replacingUnspecifiedDimensions()会在父视图未明确尺寸时使用SwiftUI默认占位尺寸(如1000pt宽度),导致比例计算完全偏离实际空间。
  2. 子视图定位与尺寸传递错误:placeSubviews中错误的水平居中逻辑,以及依赖子视图sizeThatFits返回值而非强制使用分配尺寸,导致布局混乱。

修正后的RelativeVStack实现

struct RelativeVStack: Layout {
  var spacing = 0.0

  func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
    // 优先使用父视图明确指定的尺寸,未指定则基于子视图内容计算
    let parentWidth = proposal.width ?? subviews.map { $0.sizeThatFits(.unspecified).width }.max() ?? 0
    let parentHeight = proposal.height ?? subviews.map { $0.sizeThatFits(.unspecified).height }.reduce(0, +) + spacing * Double(subviews.count - 1)
    
    let viewFrames = frames(for: subviews, in: CGRect(x: 0, y: 0, width: parentWidth, height: parentHeight))
    let finalWidth = parentWidth != 0 ? parentWidth : viewFrames.max { $0.maxX < $1.maxX }?.maxX ?? 0
    let finalHeight = parentHeight != 0 ? parentHeight : viewFrames.max { $0.maxY < $1.maxY }?.maxY ?? 0
    
    return CGSize(width: finalWidth, height: finalHeight)
  }

  func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
    let viewFrames = frames(for: subviews, in: bounds)

    for index in subviews.indices {
      let frame = viewFrames[index]
      // 子视图左上角对齐父视图对应位置
      let position = CGPoint(x: bounds.minX, y: bounds.minY + frame.minY)
      // 传递精确分配的尺寸作为提案
      let subProposal = ProposedViewSize(width: bounds.width, height: frame.height)
      subviews[index].place(at: position, anchor: .topLeading, proposal: subProposal)
    }
  }

  func frames(for subviews: Subviews, in bounds: CGRect) -> [CGRect] {
    let totalSpacing = spacing * Double(subviews.count - 1)
    let availableHeight = bounds.height - totalSpacing
    let totalPriorities = subviews.reduce(0) { $0 + $1.priority }

    var viewFrames = [CGRect]()
    var y = 0.0

    for subview in subviews {
      let subviewHeight = availableHeight * subview.priority / totalPriorities
      // 宽度直接使用父视图完整宽度,确保水平填满
      let subviewWidth = bounds.width
      
      let proposal = ProposedViewSize(width: subviewWidth, height: subviewHeight)
      // 强制使用分配的尺寸,避免子视图因内容自适应缩小
      let frame = CGRect(x: 0, y: y, width: subviewWidth, height: subviewHeight)
      viewFrames.append(frame)
      y += subviewHeight + spacing
    }

    return viewFrames
  }
}

关键修复点

  • 尺寸计算逻辑优化:优先采用父视图明确尺寸,未指定时基于子视图内容动态计算,避免默认占位值干扰。
  • 子视图定位修正:使用.topLeading锚点对齐父视图左上角,确保水平方向填满父视图宽度。
  • 强制比例尺寸:直接使用计算出的比例尺寸创建frame,不依赖子视图sizeThatFits返回值,保证比例严格符合预期。
  • 提案传递调整:传递父视图完整宽度作为提案,确保子视图的.frame(maxWidth: .infinity)能正确填满分配空间。

修改后,子视图的宽高会严格按照优先级比例分配父视图尺寸,同时保持垂直布局逻辑正常运行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 02:35:03