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

问题描述 投票:0回答:2

我开始探索 SwiftUI,但找不到一种方法来获得简单的东西:我希望 View 具有成比例的高度(基本上是其父级高度的百分比)。假设我有 3 个垂直堆叠的视图。我想要:

  • 第一个高度为(其父级高度)43%
  • 第二个高度为(其父级高度的)37%
  • 最后一个高度为(其父级高度的)20%

我在 WWDC19 上观看了这段关于 SwiftUI 中自定义视图的有趣视频 (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,第三个矩形的调整大小以适合所有剩余空间。再说一遍,这很好。 你不能(用这种方式)指定一个比例高度。

ios swift autolayout swiftui
2个回答
129
投票

更新

如果您的部署目标至少是 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())

结果:

虽然这比

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())

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. 您需要添加
    .frame(height: .zero)
    来强制
    GeometryReader
    尊重其内容大小。如果你不添加这个, GeometryReader 将在设备上全屏显示。 U 可以在 GeometryReader 范围上添加
    .background(.gray)
    来查看 副作用!
  2. 如果您需要为
    HStack
    添加间距,则它不会按预期工作。它不会扣除/排除间距然后布局子视图,而是将其他子视图推出屏幕!你可能想对每个子视图使用
    .padding

iOS 16+ 之后:

我可以看到其他人已经提到过

Layout
,但下面是我的尖峰。 CustomHStack 与
spacing
配合良好,但可能需要一些额外的工作,例如(保护 widthWeights 的数量必须与子视图计数相同,并且可能实现
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)
    }
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.