SwiftUI中的比例高度(或宽度)

82

我开始探索SwiftUI,但是我找不到一个简单的方法:我想让一个视图有比例高度(基本上是其父视图高度的百分比)。假设我有3个垂直堆叠的视图。我想要:

  • 第一个视图的高度为其父视图高度的43%
  • 第二个视图的高度为其父视图高度的37%
  • 最后一个视图的高度为其父视图高度的20%

我看了这个有趣的WWDC19自定义视图的视频(https://developer.apple.com/videos/play/wwdc2019/237/),并且我理解(如果我错了,请纠正我),基本上一个视图本身没有固定的大小,它的大小取决于其子视图。因此,父视图会询问其子视图的高度,他们回答类似于:“你的高度的一半!”然后...然后呢?这个布局系统(与我们所熟悉的布局系统不同)如何处理这种情况?

如果您编写以下代码:

struct ContentView : View {
    var body: some View {
        VStack(spacing: 0) {
            Rectangle()
                .fill(Color.red)
            Rectangle()
                .fill(Color.green)
            Rectangle()
                .fill(Color.yellow)
        }
    }
}

SwiftUI布局系统会将每个视图的高度设置为1/3,根据我在上面发布的视频来看是正确的。 您可以通过以下方式将矩形包裹在框架中:

struct ContentView : View {
    var body: some View {
        VStack(spacing: 0) {
            Rectangle()
                .fill(Color.red)
                .frame(height: 200)
            Rectangle()
                .fill(Color.green)
                .frame(height: 400)
            Rectangle()
                .fill(Color.yellow)
        }
    }
}

这种方式使布局系统将第一个矩形的高度设置为200,第二个矩形的高度设置为400,第三个矩形的高度自适应剩余空间。但是,以这种方式无法指定比例高度。


1
你可以使用GeometryReader获取父视图的大小,然后自己进行计算。 - Marc T.
根据链接上的幻灯片,父级可以为子级提出一个大小建议。然后子级自己管理并创建与该建议的大小关系。当您执行上述操作时,您得到了什么结果? - impression7vx
@impression7vx,请看一下编辑。 - superpuccio
2个回答

130

更新

如果您的部署目标至少为iOS 16、macOS 13、tvOS 16或watchOS 9,您可以编写自定义的Layout。例如:

import SwiftUI

struct MyLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        return proposal.replacingUnspecifiedDimensions()
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        precondition(subviews.count == 3)

        var p = bounds.origin
        let h0 = bounds.size.height * 0.43
        subviews[0].place(
            at: p,
            proposal: .init(width: bounds.size.width, height: h0)
        )
        p.y += h0

        let h1 = bounds.size.height * 0.37
        subviews[1].place(
            at: p,
            proposal: .init(width: bounds.size.width, height: h1)
        )
        p.y += h1

        subviews[2].place(
            at: p,
            proposal: .init(
                width: bounds.size.width,
                height: bounds.size.height - h0 - h1
            )
        )
    }
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(MyLayout {
    Color.pink
    Color.indigo
    Color.mint
}.frame(width: 50, height: 100).padding())

结果:

一个粉色方块,高43点,位于一个靛蓝色方块的顶部,后者高37点,而它本身则位于一个高20点的薄荷色方块之上

虽然这种方法需要的代码比下面介绍的GeometryReader多一些,但是它更容易调试,并且可以扩展到更复杂的布局。

原始文本

你可以使用GeometryReader。将其放置在其他所有视图的外部并使用其闭包值metrics来计算高度:

let propHeight = metrics.size.height * 0.43

按以下方式使用:

import SwiftUI

struct ContentView: View {
    var body: some View {
        GeometryReader { metrics in
            VStack(spacing: 0) {
                Color.red.frame(height: metrics.size.height * 0.43)
                Color.green.frame(height: metrics.size.height * 0.37)
                Color.yellow
            }
        }
    }
}

import PlaygroundSupport

PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())

谢谢你的帮助。有机会的话,你能稍微改进一下你的答案吗?我不明白的是为什么你必须指定“layoutPriority(1)”修饰符(我知道layoutPriority的作用,但我不明白为什么它在这里是必需的)。另外,你能看一下我的编辑并解释一下为什么“relativeHeight”修饰符在这里行不通吗?再次感谢你。 - superpuccio
1
你必须指定layoutPriority,否则它不起作用。我不明白为什么没有指定也不能正常工作。这可能是SwiftUI的一个bug。我所知道的是,通过实验,我发现使用layoutPriority可以使其正常工作。至于relativeHeight,它已经被弃用了,所以我不感兴趣研究如何让它工作。 - rob mayoff
谢谢。我不知道relativeHeight已经被弃用了。对于layoutPriority问题,我会继续努力找出发生了什么。 - superpuccio
2
看起来beta 5修复了.layoutPriority()的问题,这个方法可能不再需要使用才能使其正常工作。 - kontiki
刚刚更新了MacOS和xCode到Beta 5,正如@kontiki建议的那样,不再需要使用.layoutPriority()。Rob,你能否更新一下你的答案?谢谢大家。 - superpuccio
相对高度和相对宽度完全消失了 :) - superpuccio

0
这是SwiftUI中比例宽度的一个示例。
在iOS 16之前:
struct GeometryReaderHStack: View {
  var body: some View {
    GeometryReader { geometry in
      HStack(spacing: .zero) {
        let component1Width = geometry.size.width * 0.75
        let component2Width = geometry.size.width * 0.25
        Text("3/4")
          .frame(width: component1Width)
          .background(.red)

        Text("1/4")
          .frame(width: component2Width)
          .background(.blue)
      }
    }
    .frame(height: .zero)
  }
}

注意事项:
1. 你需要在GeometryReader中添加.frame(height: .zero)来强制GeometryReader尊重其内容大小。如果不添加这个,GeometryReader将会占满整个设备屏幕。你可以在GeometryReader的范围内添加.background(.gray)来查看副作用!
2. 如果你需要为HStack添加间距,它不会按预期工作。它不会减去/排除间距然后布局子视图,而是会将其他子视图推出屏幕外!你可能想为每个子视图使用.padding
在iOS 16+之后:
我看到其他人已经提到了Layout,但下面是我的尝试。这是一个名为CustomHStack的自定义堆栈,它可以很好地使用spacing,但可能需要一些额外的工作,比如(保护关于宽度权重的数量必须与子视图数量相同,以及可能的VerticalAlignment实现)。
struct CustomHStack: Layout {
  let widthWeights: [CGFloat]
  let spacing: CGFloat

  init(widthWeights: [CGFloat], spacing: CGFloat? = nil) {
    self.widthWeights = widthWeights
    self.spacing = spacing ?? .zero
  }

  func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
    let maxHeight = subviews
      .map { proxy in
        return proxy.sizeThatFits(.unspecified).height
      }
      .max() ?? .zero

    return CGSize(width: proposal.width ?? .zero, height: maxHeight)
  }

  func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
    let subviewSizes = subviews.map { proxy in
      return proxy.sizeThatFits(.infinity)
    }

    var x = bounds.minX
    let y = bounds.minY
    let parentTotalWith = (proposal.width ?? .zero) - (CGFloat((widthWeights.count - 1)) * spacing)

    for index in subviews.indices {
      let widthWeight = widthWeights[index]
      let proposedWidth = widthWeight * parentTotalWith

      let sizeProposal = ProposedViewSize(width: proposedWidth, height: subviewSizes[index].height)

      subviews[index]
        .place(
          at: CGPoint(x: x, y: y),
          anchor: .topLeading,
          proposal: sizeProposal
        )

      x += (proposedWidth + spacing)
    }
  }
}

使用方法:

struct ProportionalWidthHStack: View {
  var body: some View {
    CustomHStack(widthWeights: [0.75, 0.25]) {
      Text("3/4")
        .frame(maxWidth: .infinity)
        .background(.red)
      Text("1/4")
        .frame(maxWidth: .infinity)
        .background(.blue)
    }
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接