SwiftUI 中的布局与水平和垂直对齐

Posted

技术标签:

【中文标题】SwiftUI 中的布局与水平和垂直对齐【英文标题】:Layout in SwiftUI with horizontal and vertical alignment 【发布时间】:2019-10-30 02:08:41 【问题描述】:

我正在尝试完成这个布局

如果我尝试用 VStack 包装 HStack,我会得到:

如果我尝试用 HStack 包装 VStack,我会得到:

有没有办法将文本与文本字段进行基线对齐,并获得从最长标签到对齐文本字段开头的标准间距?

【问题讨论】:

前几天我试图用 SwiftUI 做一个非常相似的事情。 WWDC 演讲 Building Custom Views With SwiftUI 涵盖了一些关于自定义对齐行为的内容,您可能可以利用这些内容(查看 21 分钟标记)。我可以想象制作自定义对齐指南以及包裹在 VStack 中的 HStack 以获得您想要的对齐方式。 是的,我看过视频的那部分,但我认为它不能满足我的需要。谢谢! 疯了,这并不容易,因为它在 UIKit 中是多么容易 【参考方案1】:

看起来这会起作用:

extension HorizontalAlignment 
    private enum MyAlignment: AlignmentID 
        static func defaultValue(in context: ViewDimensions) -> Length 
            context[.trailing]
        
    
    static let myAlignmentGuide = HorizontalAlignment(MyAlignment.self)


struct ContentView : View 
    @State var username: String = ""
    @State var email: String = ""
    @State var password: String = ""

    var body: some View 
        VStack(alignment: .myAlignmentGuide) 
            HStack 
                Text("Username").alignmentGuide(.myAlignmentGuide, computeValue:  d in d[.trailing] )
                TextField($username)
                    .textFieldStyle(.roundedBorder)
                    .frame(maxWidth: 200)
            
            HStack 
                Text("Email")
                    .alignmentGuide(.myAlignmentGuide, computeValue:  d in d[.trailing] )
                TextField($email)
                    .textFieldStyle(.roundedBorder)
                    .frame(maxWidth: 200)
            
            HStack 
                Text("Password")
                    .alignmentGuide(.myAlignmentGuide, computeValue:  d in d[.trailing] )
                TextField($password)
                    .textFieldStyle(.roundedBorder)
                    .frame(maxWidth: 200)
            
        
    

使用该代码,我可以实现这种布局:

这里需要注意的是,我必须为TextFields 指定最大宽度。在不受约束的情况下,我在 cmets 中链接的 WWDC 演讲中描述的布局系统检索了 TextField 的大小在发生对齐之前,导致电子邮件的 TextField 超出了末尾另外两个。我不知道如何解决这个问题,让TextFields 扩展到包含视图的大小而不会超过...

【讨论】:

使用自定义对齐指南会移动整个 HStack 的位置,这就是为什么您必须右对齐标签以使其看起来正常的原因。但这不是我需要的布局。谢谢! 这显然不是所要求的布局。【参考方案2】:
    var body: some View 

    VStack 
        HStack 
            Text("Username")
            Spacer()
            TextField($username)
                .textFieldStyle(.roundedBorder)
                .frame(maxWidth: 200)
                .foregroundColor(.gray)
                .accentColor(.red)
        
        .padding(.horizontal, 20)
        HStack 
            Text("Email")
            Spacer()
            TextField($email)
                .textFieldStyle(.roundedBorder)
                .frame(maxWidth: 200)
                .foregroundColor(.gray)
            
            .padding(.horizontal, 20)
        HStack 
            Text("Password")
            Spacer()
            TextField($password)
                .textFieldStyle(.roundedBorder)
                .frame(maxWidth: 200)
                .foregroundColor(.gray)
            
            .padding(.horizontal, 20)
    

【讨论】:

调整框架宽度以允许它们填充更多空间。不过,我不确定这在大屏幕上会是什么样子。 这个解决方案的问题是你必须为屏幕大小和文本大小的所有排列(包括本地化和动态类型)求解 maxWidth。【参考方案3】:

您可以为此使用kontiki 的geometry reader hack:

struct Column: View 
    @State private var height: CGFloat = 0
    @State var text = ""
    let spacing: CGFloat = 8

    var body: some View 
        HStack 
            VStack(alignment: .leading, spacing: spacing) 
                Group 
                    Text("Hello world")
                    Text("Hello Two")
                    Text("Hello Three")
                .frame(height: height)
            .fixedSize(horizontal: true, vertical: false)
            VStack(spacing: spacing) 
                TextField("label", text: $text).bindHeight(to: $height)
                TextField("label 2", text: $text)
                TextField("label 3", text: $text)
            .textFieldStyle(RoundedBorderTextFieldStyle())
        .fixedSize().padding()
    


extension View 
    func bindHeight(to binding: Binding<CGFloat>) -> some View 
        func spacer(with geometry: GeometryProxy) -> some View 
            DispatchQueue.main.async  binding.value = geometry.size.height 
            return Spacer()
        
        return background(GeometryReader(content: spacer))
    

我们这里只读取第一个 TextField 的高度,并在三个不同的 Text View 上应用 3 次,假设所有 TextField 具有相同的高度。如果您的三个 TextField 具有不同的高度或出现/消失的验证标签会影响各个高度,您可以使用相同的技术,但使用三个不同的高度绑定。

为什么这有点骇人听闻?

因为此解决方案将始终首先呈现没有标签的 TextField。在此渲染阶段,它将设置文本标签的高度并触发另一个渲染。在一个布局阶段渲染所有内容会更理想。

【讨论】:

这可能是一种 hack,但它似乎是唯一不需要常量的,并且实际上提供了所请求的布局。【参考方案4】:

这里不是专家,但我设法通过 (1) 选择 2-VStacks-in-a-HStack 替代方案来实现所需的布局,(2) 构建外部标签,(3) 释放通过分配 maxHeight = .infinity 和 (4) 固定 HStack 的高度,它们可以摆脱默认的垂直扩展约束

struct ContentView: View 
    @State var text = ""
    let labels = ["Username", "Email", "Password"]

    var body: some View 
        HStack 
            VStack(alignment: .leading) 
                ForEach(labels, id: \.self)  label in
                    Text(label)
                        .frame(maxHeight: .infinity)
                        .padding(.bottom, 4)
                
            

            VStack 
                ForEach(labels, id: \.self)  label in
                    TextField(label, text: self.$text)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                
            
            .padding(.leading)
        
        .padding(.horizontal)
        .fixedSize(horizontal: false, vertical: true)
    

这是结果预览:

为了解决外部和内部标签的未对齐基线(与此特定布局无关的附带问题 - 参见例如 this discussion),我手动添加了填充

感谢this website 在理解的道路上启发我 SwiftUI 布局技巧

【讨论】:

这个解决方案看起来不错。唯一的“hack”是使文本与 TextField 对齐的填充。 由于某种原因,右侧 VStack 使用默认间距 - 看起来可能是 8 点,但左侧 VStack 的间距为 0。当我添加间距时,文本的对齐方式到 TextField 更近了。 补充一点,如果文本字段的高度超过一行,我认为这不会给您带来预期的效果。使用此解决方案,左侧标签中心不会与右侧标签中心对齐。 @tetotechy 网站链接报告未找到。【参考方案5】:

您需要添加固定宽度和前导对齐。我在 Xcode 11.1 中测试过,没问题。

struct TextInputWithLabelDemo: View 
    @State var text = ""
    let labels = ["Username", "Email", "Password"]

    var body: some View 
        VStack 
            ForEach(labels, id: \.self)  label in
                HStack 
                    Text(label).frame(width: 100, alignment: .leading)
                    TextField(label, text: self.$text)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                
            
            .padding(.horizontal)
            .fixedSize(horizontal: false, vertical: true)
        
    

您可以在下面看到当我们对TextTextField 使用不同的VStack 时会出现什么问题。查看更多信息here

2019 年 10 月 16 日更新

仔细观察TextsTextFields,你会发现它们有不同的高度,这会影响Texts相对于TextFields的位置,正如你在屏幕截图右侧看到的@987654336 @ 相对于Password TextField 高于Username Text 相对于Username TextField。 我给出了解决这个问题的三种方法here

【讨论】:

设置文字宽度为100的问题是不够灵活。如果您有更长的文本(用于翻译)或更大的字体(使用动态类型),100 可能还不够。上面接受的答案解决了这些问题。感谢您花时间回答! 我添加了当您使用不同的VStackTextTextField 时的情况 如果您需要灵活的宽度,请查看here 几何阅读器的答案只是将宽度设置为父级的 1/3。这不考虑更长的文本、动态类型和不同的设备。上面接受的答案与调整足够接近,是我找到的最简单的解决方案。谢谢。 @Jerry 我已经测试过了,即使是最小的屏幕 100 也是完美的。事实上,即使是这个.extraExtraExtraLargeand iPhone SE(最小屏幕)宽度为 100 也能胜任【参考方案6】:
    HStack
        Image(model.image)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 10, alignment: .leading)
        VStack(alignment: .leading) 
            Text("Second column ")
            Text("Second column -")
        
        Spacer()
        Text("3rd column")
    

1- 第一列 - 图片

2- 第二列 - 两个文本

3- 浮点值

Spacer() - 使用 Spacer() -> 上面的示例图像和 vstack 保持所有行的垂直对齐,只需将间隔用于您想要在另一个垂直对齐/列中执行的视图 VStack(alignment: .leading. - 这对于开始对齐很重要

【讨论】:

以上是关于SwiftUI 中的布局与水平和垂直对齐的主要内容,如果未能解决你的问题,请参考以下文章

css基础 CSS 布局 – OverflowFloat 浮动CSS 布局 – 水平 垂直居中对齐

CSS:包含与水平规则对齐的块的多列布局

SwiftUI之HStack和VStack的切换

SwiftUI之HStack和VStack的切换

HTML CSS中如何实现DIV中的图片水平垂直居中对齐

Swift之深入解析SwiftUI布局如何自定义AlignmentGuides