SwiftUI 中水平和垂直对齐的布局
Layout in SwiftUI with horizontal and vertical alignment
我正在尝试完成此布局
如果我尝试将 HStack 包裹在 VStack 中,我会得到:
如果我尝试将 VStack 包裹在 HStack 中,我会得到:
有没有办法将文本与文本字段基线对齐,并获得从最长标签到对齐文本字段开头的标准间距?
看起来这行得通:
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)
}
}
}
}
使用该代码,我可以实现此布局:
这里需要注意的是,我必须为 TextField
指定最大宽度。不受约束,我在评论中链接的 WWDC 谈话中描述的布局系统检索 TextField
prior 对齐发生的大小,导致电子邮件 TextField
延伸到其他两个的末端。我不确定如何以允许 TextField
s 扩展到包含视图的大小而不超过...
的方式解决这个问题
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)
}
}
您可以为此使用 kontiki's 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 上三次,假设所有 TextField 都具有相同的高度。如果您的三个 TextField 具有不同的高度或具有影响各个高度的 appearing/disappearing 验证标签,您可以使用相同的技术,但使用三个不同的高度绑定。
为什么这有点乱?
因为此解决方案将始终首先呈现没有标签的 TextFields。在此渲染阶段,它将设置文本标签的高度并触发另一次渲染。在一个布局阶段渲染所有内容会更理想。
我不是这里的专家,但我设法通过 (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)
}
}
这是预览结果:
为了解决外部和内部标签未对齐的基线(与此特定布局无关的附带问题 - 例如参见 [=16=])我手动添加了填充
感谢 this website 启发了我的理解之路
SwiftUI 布局技巧
您需要添加固定宽度和前导对齐方式。我在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)
}
}
}
下面您可以看到当我们对 Text
和 TextField
使用不同的 VStack
时出现了什么问题。查看更多信息
2019 年 10 月 16 日更新
仔细观察 Texts
和 TextFields
,您会发现它们的高度不同,这会影响 Texts
相对于 TextFields
的位置,如您所见在屏幕截图的右侧,Password Text
相对于 Password TextField
高于 Username Text
相对于 Username TextField
。
我给出了三种方法来解决这个问题
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 保持所有行的垂直对齐,只需将 spacer 用于您想要在另一个垂直对齐/列中执行的视图
VStack(alignment: .leading. - 这对于开始对齐很重要
我正在尝试完成此布局
如果我尝试将 HStack 包裹在 VStack 中,我会得到:
如果我尝试将 VStack 包裹在 HStack 中,我会得到:
有没有办法将文本与文本字段基线对齐,并获得从最长标签到对齐文本字段开头的标准间距?
看起来这行得通:
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)
}
}
}
}
使用该代码,我可以实现此布局:
这里需要注意的是,我必须为 TextField
指定最大宽度。不受约束,我在评论中链接的 WWDC 谈话中描述的布局系统检索 TextField
prior 对齐发生的大小,导致电子邮件 TextField
延伸到其他两个的末端。我不确定如何以允许 TextField
s 扩展到包含视图的大小而不超过...
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)
}
}
您可以为此使用 kontiki's 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 上三次,假设所有 TextField 都具有相同的高度。如果您的三个 TextField 具有不同的高度或具有影响各个高度的 appearing/disappearing 验证标签,您可以使用相同的技术,但使用三个不同的高度绑定。
为什么这有点乱?
因为此解决方案将始终首先呈现没有标签的 TextFields。在此渲染阶段,它将设置文本标签的高度并触发另一次渲染。在一个布局阶段渲染所有内容会更理想。
我不是这里的专家,但我设法通过 (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)
}
}
这是预览结果:
为了解决外部和内部标签未对齐的基线(与此特定布局无关的附带问题 - 例如参见 [=16=])我手动添加了填充
感谢 this website 启发了我的理解之路 SwiftUI 布局技巧
您需要添加固定宽度和前导对齐方式。我在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)
}
}
}
下面您可以看到当我们对 Text
和 TextField
使用不同的 VStack
时出现了什么问题。查看更多信息
2019 年 10 月 16 日更新
仔细观察 Texts
和 TextFields
,您会发现它们的高度不同,这会影响 Texts
相对于 TextFields
的位置,如您所见在屏幕截图的右侧,Password Text
相对于 Password TextField
高于 Username Text
相对于 Username TextField
。
我给出了三种方法来解决这个问题
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 保持所有行的垂直对齐,只需将 spacer 用于您想要在另一个垂直对齐/列中执行的视图 VStack(alignment: .leading. - 这对于开始对齐很重要