在 SwiftUI 中让 VStack 填满屏幕宽度

Make a VStack fill the width of the screen in SwiftUI

鉴于此代码:

import SwiftUI

struct ContentView: View {
  var body: some View {
    VStack(alignment: .leading) {
      Text("Title")
        .font(.title)

      Text("Content")
        .lineLimit(nil)
        .font(.body)

      Spacer()
    }
    .background(Color.red)
  }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
#endif

结果是这个界面:

即使 labels/text 组件不需要全宽,我怎样才能让 VStack 填满屏幕的宽度?


我发现的一个技巧是在结构中插入一个 empty HStack,如下所示:

VStack(alignment: .leading) {
  HStack {
    Spacer()
  }
  Text("Title")
    .font(.title)

  Text("Content")
    .lineLimit(nil)
    .font(.body)

  Spacer()
}

产生所需的设计:

有没有更好的方法?

尝试使用带有以下选项的 .frame 修饰符:

.frame(
      minWidth: 0,
      maxWidth: .infinity,
      minHeight: 0,
      maxHeight: .infinity,
      alignment: .topLeading
    )
struct ContentView: View {
  var body: some View {
    VStack(alignment: .leading) {
      Text("Hello World")
        .font(.title)
      Text("Another")
        .font(.body)
      Spacer()
    }
    .frame(
      minWidth: 0,
      maxWidth: .infinity,
      minHeight: 0,
      maxHeight: .infinity,
      alignment: .topLeading
    )
    .background(Color.red)
  }
}

这被描述为一个灵活的框架 (see the documentation),它将伸展以填满整个屏幕,当它有额外的 space 时,它会将其内容居中。

有更好的方法!

要使 VStack 填充其父级的宽度,您可以使用 GeometryReader 并设置框架。 (.relativeWidth(1.0) 应该可以,但现在显然不行)

struct ContentView : View {
    var body: some View {
        GeometryReader { geometry in
            VStack {
                Text("test")
            }
                .frame(width: geometry.size.width,
                       height: nil,
                       alignment: .topLeading)
        }
    }
}

要使 VStack 成为实际屏幕的宽度,您可以在设置框架时使用 UIScreen.main.bounds.width 而不是使用 GeometryReader,但我想您可能想要宽度父视图。

此外,这种方式还有一个额外的好处,即不在 VStack 中添加间距,如果您添加 HStackSpacer() 可能会发生这种情况(如果您有间距) VStack.

的内容

更新 - 没有更好的方法!

查看接受的答案后,我意识到接受的答案实际上不起作用!乍一看似乎可以工作,但如果您将 VStack 更新为绿色背景,您会注意到 VStack 的宽度仍然相同。

struct ContentView : View {
    var body: some View {
        NavigationView {
            VStack(alignment: .leading) {
                Text("Hello World")
                    .font(.title)
                Text("Another")
                    .font(.body)
                Spacer()
                }
                .background(Color.green)
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
                .background(Color.red)
        }
    }
}

这是因为 .frame(...) 实际上是在向视图层次结构中添加另一个视图,并且该视图最终填满了屏幕。但是,VStack还是不行。

这个问题在我的回答中似乎也一样,可以使用与上述相同的方法进行检查(在 .frame(...) 前后放置不同的背景颜色)。唯一看起来实际的方法加宽VStack就是用垫片:

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            HStack{
                Text("Title")
                    .font(.title)
                Spacer()
            }
            Text("Content")
                .lineLimit(nil)
                .font(.body)
            Spacer()
        }
            .background(Color.green)
    }
}

您可以使用 GeometryReader

GeometryReader

代码:

struct ContentView : View {
    var body: some View {
        GeometryReader { geometry in
            VStack {
               Text("Turtle Rock").frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading).background(Color.red)
            }
        }
    }
}

你的输出像:

另一种选择是将其中一个子视图放在 HStack 中,然后在其后放置 Spacer()

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {

            HStack {
                Text("Title")
                    .font(.title)
                    .background(Color.yellow)
                Spacer()
            }

            Text("Content")
                .lineLimit(nil)
                .font(.body)
                .background(Color.blue)

            Spacer()
            }

            .background(Color.red)
    }
}

导致:

另一种有效且可能更直观的堆叠排列如下:

struct ContentView: View {
    var body: some View {
        HStack() {
            VStack(alignment: .leading) {
                Text("Hello World")
                    .font(.title)
                Text("Another")
                    .font(.body)
                Spacer()
            }
            Spacer()
        }.background(Color.red)
    }
}

内容也可以很容易地 re-positioned,如有必要,只需删除 Spacer()

这对我有用(ScrollView(可选)所以如果需要可以添加更多内容,加上居中的内容):

import SwiftUI

struct SomeView: View {
    var body: some View {
        GeometryReader { geometry in
            ScrollView(Axis.Set.horizontal) {
                HStack(alignment: .center) {
                    ForEach(0..<8) { _ in
                        Text("")
                    }
                }.frame(width: geometry.size.width, height: 50)
            }
        }
    }
}

// MARK: - Preview

#if DEBUG
struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        SomeView()
    }
}
#endif

Result

使用这个

.edgesIgnoringSafeArea(.all)

我设法解决问题的最简单方法是使用 ZStack + .edgesIgnoringSafeArea(.all)

struct TestView : View {

    var body: some View {
        ZStack() {
            Color.yellow.edgesIgnoringSafeArea(.all)
            VStack {
                Text("Hello World")
            }
        }
    }
}

我知道这并不适用于所有人,但我觉得有趣的是,只需添加一个分隔线就可以解决这个问题。

struct DividerTest: View {
var body: some View {
    VStack(alignment: .leading) {
        Text("Foo")
        Text("Bar")
        Divider()
    }.background(Color.red)
  }
}

这是一段有用的代码:

extension View {
    func expandable () -> some View {
        ZStack {
            Color.clear
            self
        }
    }
}

比较使用和不使用 .expandable() 修饰符的结果:

Text("hello")
    .background(Color.blue)

-

Text("hello")
    .expandable()
    .background(Color.blue)

您可以在方便的扩展中使用 GeometryReader 来填充父项

extension View {
    func fillParent(alignment:Alignment = .center) -> some View {
        return GeometryReader { geometry in
            self
                .frame(width: geometry.size.width,
                       height: geometry.size.height,
                       alignment: alignment)
        }
    }
}

所以使用请求的例子,你得到

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Title")
                .font(.title)

            Text("Content")
                .lineLimit(nil)
                .font(.body)
        }
        .fillParent(alignment:.topLeading)
        .background(Color.red)
    }
}

(注意不再需要垫片)

登录页面设计使用SwiftUI

import SwiftUI

struct ContentView: View {

    @State var email: String = "william@gmail.com"
    @State var password: String = ""
    @State static var labelTitle: String = ""


    var body: some View {
        VStack(alignment: .center){
            //Label
            Text("Login").font(.largeTitle).foregroundColor(.yellow).bold()
            //TextField
            TextField("Email", text: $email)
                .textContentType(.emailAddress)
                .foregroundColor(.blue)
                .frame(minHeight: 40)
                .background(RoundedRectangle(cornerRadius: 10).foregroundColor(Color.green))

            TextField("Password", text: $password) //Placeholder
                .textContentType(.newPassword)
                .frame(minHeight: 40)
                .foregroundColor(.blue) // Text color
                .background(RoundedRectangle(cornerRadius: 10).foregroundColor(Color.green))

            //Button
            Button(action: {

            }) {
                HStack {
                    Image(uiImage: UIImage(named: "Login")!)
                        .renderingMode(.original)
                        .font(.title)
                        .foregroundColor(.blue)
                    Text("Login")
                        .font(.title)
                        .foregroundColor(.white)
                }


                .font(.headline)
                .frame(minWidth: 0, maxWidth: .infinity)
                .background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
                .cornerRadius(40)
                .padding(.horizontal, 20)

                .frame(width: 200, height: 50, alignment: .center)
            }
            Spacer()

        }.padding(10)

            .frame(minWidth: 0, idealWidth: .infinity, maxWidth: .infinity, minHeight: 0, idealHeight: .infinity, maxHeight: .infinity, alignment: .top)
            .background(Color.gray)

    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

使用 Swift 5.2 和 iOS 13.4,根据您的需要,您可以使用以下示例之一将您的 VStack 与顶部前导约束和全尺寸框架对齐.

请注意,下面的所有代码片段都会导致相同的显示,但不保证 VStack 的有效框架,也不保证在调试视图层次结构时可能出现的 View 元素的数量.


1。使用frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)方法

最简单的方法是将 VStack 的框架设置为最大宽度和高度,并在 frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:):

中传递所需的对齐方式
struct ContentView: View {

    var body: some View {
        VStack(alignment: .leading) {
            Text("Title")
                .font(.title)
            Text("Content")
                .font(.body)
        }
        .frame(
            maxWidth: .infinity,
            maxHeight: .infinity,
            alignment: .topLeading
        )
        .background(Color.red)
    }

}

2。使用 Spacers 强制对齐

您可以将 VStack 嵌入全尺寸 HStack 并使用尾部和底部 Spacer 强制您的 VStack 顶部前导对齐:

struct ContentView: View {

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text("Title")
                    .font(.title)
                Text("Content")
                    .font(.body)

                Spacer() // VStack bottom spacer
            }

            Spacer() // HStack trailing spacer
        }
        .frame(
            maxWidth: .infinity,
            maxHeight: .infinity
        )
        .background(Color.red)
    }

}

3。使用 ZStack 和全尺寸背景 View

此示例展示了如何将 VStack 嵌入到具有顶部前导对齐的 ZStack 中。请注意 Color 视图如何用于设置最大宽度和高度:

struct ContentView: View {

    var body: some View {
        ZStack(alignment: .topLeading) {
            Color.red
                .frame(maxWidth: .infinity, maxHeight: .infinity)

            VStack(alignment: .leading) {
                Text("Title")
                    .font(.title)
                Text("Content")
                    .font(.body)
            }
        }
    }

}

4。使用 GeometryReader

GeometryReader 具有以下 declaration:

A container view that defines its content as a function of its own size and coordinate space. [...] This view returns a flexible preferred size to its parent layout.

下面的代码片段显示了如何使用 GeometryReader 将您的 VStack 与顶部引导约束和全尺寸框架对齐:

struct ContentView : View {

    var body: some View {
        GeometryReader { geometryProxy in
            VStack(alignment: .leading) {
                Text("Title")
                    .font(.title)
                Text("Content")
                    .font(.body)
            }
            .frame(
                width: geometryProxy.size.width,
                height: geometryProxy.size.height,
                alignment: .topLeading
            )
        }
        .background(Color.red)
    }

}

5。使用 overlay(_:alignment:) 方法

如果您想在现有全尺寸 View 的基础上将您的 VStack 与顶部前导约束对齐,您可以使用 overlay(_:alignment:) 方法:

struct ContentView: View {

    var body: some View {
        Color.red
            .frame(
                maxWidth: .infinity,
                maxHeight: .infinity
            )
            .overlay(
                VStack(alignment: .leading) {
                    Text("Title")
                        .font(.title)
                    Text("Content")
                        .font(.body)
                },
                alignment: .topLeading
            )
    }

}

显示:

一个很好的解决方案,没有“装置”是被遗忘的 ZStack

ZStack(alignment: .top){
    Color.red
    VStack{
        Text("Hello World").font(.title)
        Text("Another").font(.body)
    }
}

结果:

这是另一种可以节省项目时间的方法:

与其他不可重用的答案相比,代码少得多且可重用!


extension View {
    
    var maxedOut: some View {
        
        return Color.clear
            .overlay(self, alignment: .center)

    }
    
    func maxedOut(color: Color = Color.clear, alignment: Alignment = Alignment.center) -> some View {
        
        return color
            .overlay(self, alignment: alignment)
        
    }
    
}

用例:

struct ContentView: View {

    var body: some View {

        Text("Hello, World!")
            .maxedOut
            .background(Color.blue)
        
        Text("Hello, World!")
            .maxedOut(color: Color.red)
  
    }
}

方式 1 -> 使用 MaxWidth 和 MaxHeight

import SwiftUI

struct SomeView: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.red)
    }
}

struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        SomeView()
    }
}

方式 2 -> 使用主屏幕边界

import SwiftUI

struct SomeView: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
        }
        .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height)
        .background(.red)
    }
}

struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        SomeView()
    }
}

方式 3 -> 使用几何 Reader

import SwiftUI

struct SomeView: View {
    var body: some View {
        GeometryReader { geometryReader in
            VStack {
                Text("Hello, World!")
            }
            .frame(maxWidth: geometryReader.size.width, maxHeight: geometryReader.size.height)
            .background(.red)
        }
    }
}

struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        SomeView()
    }
}

方式 4 -> 使用垫片

import SwiftUI

struct SomeView: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
            HStack{
                Spacer()
            }
            Spacer()
        }
        .background(.red)
    }
}

struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        SomeView()
    }
}

⚠️重要提示!

所有其他解决方案只是在周围添加一个框架内容!

✅ 但此解决方案改变了 实际框架 !


简单正确的扩展

你可以使用这个修饰符

.flexible(width: true, height: false)

演示

请注意内容如何完全按照您在原始堆栈中分配的方式对齐


这背后的代码(FlexibleViewModifier.swift

extension View {
    func flexible(width: Bool, height: Bool) -> some View {
        self.modifier(MatchingParentModifier(width: width, height: height))
    }
}

struct MatchingParentModifier: ViewModifier {
    @State private var intrinsicSize: CGSize = UIScreen.main.bounds.size
    private let intrinsicWidth:  Bool
    private let intrinsicHeight:  Bool

    init(width: Bool, height: Bool) {
        intrinsicWidth = !width
        intrinsicHeight = !height
    }

    func body(content: Content) -> some View {
        GeometryReader { _ in
            content.modifier(intrinsicSizeModifier(intrinsicSize: $intrinsicSize))
        }
        .frame(
            maxWidth: intrinsicWidth ? intrinsicSize.width : nil,
            maxHeight: intrinsicHeight ? intrinsicSize.height : nil
        )
    }
}

struct intrinsicSizeModifier: ViewModifier {
    @Binding var intrinsicSize: CGSize

    func body(content: Content) -> some View {
        content.readIntrinsicContentSize(to: $intrinsicSize)
    }
}

struct IntrinsicContentSizePreferenceKey: PreferenceKey {
    static let defaultValue: CGSize = .zero

    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

extension View {
    func readIntrinsicContentSize(to size: Binding<CGSize>) -> some View {
        background(
            GeometryReader {
                Color.clear.preference(
                    key: IntrinsicContentSizePreferenceKey.self,
                    value: [=11=].size
                )
            }
        )
        .onPreferenceChange(IntrinsicContentSizePreferenceKey.self) {
            size.wrappedValue = [=11=]
        }
    }
}