使用SwiftUI实现子视图等宽度

77

我正在尝试使用SwiftUI在watchOS上构建一个简单的用户界面,其中有两个信息并排放置在一个按钮上方。

我希望每一边(表示为VStackHStack内)占用可用宽度的一半(因此在黄色父视图中是均匀的50/50分割),以|字符在按钮中心为界。

我希望ShortLonger!!!文本分别在每个侧边的50%内居中显示。

我从以下代码开始,以便将元素放置在正确的位置并显示不同堆栈的边界:

var body: some View {
    VStack {
        HStack {
            
            VStack {
                Text("Short").font(.body)
            }
                .background(Color.green)
            
            VStack {
                Text("Longer!!!").font(.body)
            }
                .background(Color.blue)

        }
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(Color.yellow)
        Button (action: doSomething) {
            Text("|")
        }
    }
}

给我这个结果: 起点 然后,当涉及到使每个并排的 VStack 占用可用宽度的 50% 时,我卡住了。我以为在每个 VStack 上添加 .relativeWidth(0.5) 应该可以起作用,根据我的理解,这样可以使每个 VStack 的宽度是其父视图(HStack,带有黄色背景)的一半:
var body: some View {
    VStack {
        HStack {
            
            VStack {
                Text("Short").font(.body)
            }
                .relativeWidth(0.5)
                .background(Color.green)
            
            VStack {
                Text("Longer!!!").font(.body)
            }
                .relativeWidth(0.5)
                .background(Color.blue)

        }
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(Color.yellow)
        Button (action: doSomething) {
            Text("|")
        }
    }
}

但这是我得到的结果: {{link1:unexpected widths}}
我该如何使用SwiftUI获得我想要的行为?
更新:在更仔细地查看SwiftUI文档后,我发现这个示例设置了一个框架,然后相对于该框架定义了一个宽度,所以也许我不应该以这种方式使用relativeWidth
通过以下代码,我离我想要的目标更近了一步:
var body: some View {
    VStack {
        HStack {
            
            VStack {
                Text("Short").font(.body)
            }
                .frame(minWidth: 0, maxWidth: .infinity)
                .background(Color.green)
            
            VStack {
                Text("Longer!!!").font(.body)
            }
                .frame(minWidth: 0, maxWidth: .infinity)
                .background(Color.blue)

        }
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(Color.yellow)
        Button (action: doSomething) {
            Text("|")
        }
    }
}

生成以下结果:

closer to expected result

现在,我正在努力弄清楚是什么在两个之间创建了那个额外的空间。到目前为止,尝试去除填充和忽略安全区域似乎都没有影响它。

黄色区域是HStack的默认间距(您可以通过显式指定 HStack(spacing: 0) { .. } 来覆盖它)。 - Tomáš Kafka
黄色区域是HStack的默认间距(您可以通过明确指定HStack(spacing: 0) { .. }来覆盖它)。 - undefined
5个回答

113

我仍然困惑于何时以及如何使用relativeWidth,但我成功地实现了我的目标而没有使用它。(2019年7月18日编辑:根据iOS 13 Beta 4发布说明,relativeWidth现已弃用)

在我问题的最后一个更新中,我注意到两侧之间有一些额外的间距,意识到这是HStack默认的间距,我可以通过将其间距设置为0来去除它。以下是最终代码和结果:

var body: some View {
    VStack {
        HStack(spacing: 0) {

            VStack {
                Text("Short").font(.body)
            }
                .frame(minWidth: 0, maxWidth: .infinity)
                .background(Color.green)

            VStack {
                Text("Longer!!!").font(.body)
            }
                .frame(minWidth: 0, maxWidth: .infinity)
                .background(Color.blue)

        }
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(Color.yellow)
        Button (action: doSomething) {
            Text("|")
        }
    }
}

这是结果: 去除间隔的最终结果


通过这个设置,您不需要将每个“Text”包装在“VStack”中。框架会处理您想要的布局(只要文本适合视图宽度的一半)。 - Juul
1
你甚至不需要包装包装的 VStack。为子元素调用 .frame(maxWidth: .infinity) 就足够了。 - Mamouneyya
我认为这只适用于手表,因为它会将所有可用空间平均分配给按钮,但在iPad上看起来会很奇怪(但问题特别针对watchOS)。 - fruitcoder

9

以下是如何为 watchOS 9、iOS 16、tvOS 16 和 macOS 13 创建一个 EqualWidthHStack 的方法:

Equal width texts

这是用法:
struct ContentView: View {
    
    private let strings = ["Hello,", "very very very big", "world!"]

    var body: some View {

        EqualWidthHStack {

            ForEach(strings, id: \.self) { string in

                ZStack {

                    RoundedRectangle(cornerRadius: 10, style: .continuous)
                        .opacity(0.2)

                    Text(string)
                        .padding(10)
                }
            }
        }
    }
}

首先创建一个符合 Layoutstruct
struct EqualWidthHStack: Layout {
    ...
}

它将带有两种默认方法,以下是如何实现它们的方法。
适合尺寸:
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
    
    let maxSize = maxSize(subviews: subviews)
    let spacing = spacing(subviews: subviews)
    let totalSpacing = spacing.reduce(0.0, +)
    
    return CGSize(width: maxSize.width * CGFloat(subviews.count) + totalSpacing,
                  height: maxSize.height)
}

放置子视图:

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
    
    let maxSize = maxSize(subviews: subviews)
    let spacing = spacing(subviews: subviews)
    
    let sizeProposal = ProposedViewSize(width: maxSize.width,
                                        height: maxSize.height)
    
    var x = bounds.minX + maxSize.width / 2
    
    for index in subviews.indices {
        subviews[index].place(at: CGPoint(x: x, y: bounds.midY),
                              anchor: .center,
                              proposal: sizeProposal)
        x += maxSize.width + spacing[index]
    }
}

你需要以下两个辅助方法。
最大尺寸:
private func maxSize(subviews: Subviews) -> CGSize {

    let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }

    let maxSize: CGSize = subviewSizes.reduce(.zero, { result, size in
        CGSize(width: max(result.width, size.width),
               height: max(result.height, size.height))
    })

    return maxSize
}

间距:

private func spacing(subviews: Subviews) -> [CGFloat] {

    subviews.indices.map { index in

        guard index < subviews.count - 1 else { return 0.0 }

        return subviews[index].spacing.distance(to: subviews[index + 1].spacing, 
                                                along: .horizontal)
    }
}

这是苹果WWDC22关于如何制作它的视频:

使用SwiftUI组合自定义布局


@gohnjanotis 我知道这不完全是你想要的,因为它更紧凑而不是全宽,但我希望它能帮助其他人。 - Heestand XYZ

1
你已将HStackbackground设置为黄色,并且HStack具有一些默认的子视图间距。通过在HStack中添加spacing: 0 将解决此问题,请参见下面更新后的代码。
var body: some View {
    VStack {
        HStack(spacing: 0) { // Set spacing here
            VStack {
                Text("Short").font(.body)
            }
                .frame(minWidth: 0, maxWidth: .infinity)
                .background(Color.green)

            VStack {
                Text("Longer!!!").font(.body)
            }
                .frame(minWidth: 0, maxWidth: .infinity)
                .background(Color.blue)

        }
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(Color.yellow)
        Button (action: doSomething) {
            Text("|")
        }
    }
}

1
这就是我做事的方式,
struct CenteredItemHStack: View {
   var body: some View {
       ZStack {
           HStack {
               Text("Leading Action")
                   .background(Color.blue)
               Spacer()
           }
           Text("Center Action")
               .background(Color.red)
           HStack {
               Spacer()
               Text("Trailing Action")
                   .background(Color.blue)
           }
       }
       .background(Color.green)
   }
}

预览


0
有一个非常简单的解决方案:
HStack(alignment: .center, spacing: 0) {
    ForEach(0...6, id: \.self) { _ in
        Color(UIColor.white.withAlphaComponent(0.4))
            .cornerRadius(5)
            .padding(2)
            .frame(minWidth: 0, maxWidth: .infinity)
    }
}
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 70)

以下是结果:

enter image description here


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