可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm trying to accomplish this layout
If I try HStack wrapped in VStack, I get this:
If I try VStack wrapped in HStack, I get this:
Is there a way to baseline align the text with the textfield and get standard spacing from the longest label to the start of the aligned textfields?
回答1:
not an expert here, but I managed to achieve the desired layout by (1) opting for the 2-VStacks
-in-a-HStack
alternative, (2) framing the external labels, (3) freeing them from their default vertical expansion constraint by assigning their maxHeight = .infinity
and (4) fixing the height of the 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)
}
}
Here is the resulting preview:
in order to account for the misaligned baselines of the external and internal labels (a collateral issue that is not related to this specific layout – see for instance this discussion) I manually added the padding
credits to this website for enlightening me on the path to understanding
SwiftUI layout trickeries
回答2:
Looks like this will work:
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)
}
}
}
}
With that code, I am able to achieve this layout:
The caveat here is that I had to specify a max width for the TextField
s. Left unconstrained, the layout system described in the WWDC talk I linked in the comments retrieves a size for the TextField
prior to alignment happening, causing the TextField
for email to extend past the end of the other two. I'm not sure how to address this in a way that will allow the TextField
s to expand to the size of the containing view without going over...
回答3:
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)
}
}
回答4:
You could use kontiki's geometry reader hack for this:
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))
}
}
We are only reading the height of the first TextField here and applying it three times on the three different Text Views, assuming that all TextFields have the same height. If your three TextFields have different heights or have appearing/disappearing verification labels that affect the individual heights, you can use the same technique but with three different height bindings instead.
Why is this a bit of a hack?
Because this solution will always first render the TextFields without the labels. During this render phase it will set the height of the Text labels and trigger another render. It would be more ideal to render everything in one layout phase.
回答5:
You need to add fixed width and leading alignment. I've tested in Xcode 11.1 it's ok.
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)
}
}
}
Below You can see what the issue when we use different VStack
for Text
and TextField
. See more info here
Updated 16 Oct 2019
A closer inspection of Texts
and TextFields
you can notice that they have different heights and it effects the positions of Texts
relative to TextFields
as you can see on the right side of the screenshot that Password Text
is higher relative to Password TextField
than the Username Text
relative to Username TextField
.
I gave three ways to resolve this issue here