问题
I am exploring SwiftUI as I am trying to build a login view and now I am facing a problem
This is what I am trying to achieve:
As you can see I already reached this point but I don't like my implementation
struct ContentView : View {
@State var username: String = ""
var body: some View {
VStack(alignment: .leading) {
Text("Login")
.font(.title)
.multilineTextAlignment(.center)
.lineLimit(nil)
Text("Please")
.font(.subheadline)
HStack {
VStack (alignment: .leading, spacing: 20) {
Text("Username: ")
Text("Password: ")
}
VStack {
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
}
}
}.padding()
}
}
Because in order to make the username and password text aligned exactly in the middle of the textfield, I had to put literal spacing value of 20
in the VStack
which I don't like because most probably It won't work on different device sizes.
Anyone sees a better way to achieve the same result?
Thanks
回答1:
We're going to implement two new View
modifier methods so that we can write this:
struct ContentView: View {
@State var labelWidth: CGFloat? = nil
@State var username = ""
@State var password = ""
var body: some View {
VStack {
HStack {
Text("User:")
.equalSizedLabel(width: labelWidth, alignment: .trailing)
TextField("User", text: $username)
}
HStack {
Text("Password:")
.equalSizedLabel(width: labelWidth, alignment: .trailing)
SecureField("Password", text: $password)
}
}
.padding()
.textFieldStyle(.roundedBorder)
.storeMaxLabelWidth(in: $labelWidth)
}
}
The two new modifiers are equalSizedLabel(width:alignment:)
and storeMaxLabelWidth(in:)
.
The equalSizedLabel(width:alignment)
modifier does two things:
- It applies the
width
andalignment
to its content (theText(“User:”)
andText(“Password:”)
views). - It measures the width of its content and passes that up to any ancestor view that wants it.
The storeMaxLabelWidth(in:)
modifier receives those widths measured by equalSizedLabel
and stores the maximum width in the $labelWidth
binding we pass to it.
So, how do we implement these modifiers? How do we pass a value from a descendant view up to an ancestor? In SwiftUI, we do this using the (currently undocumented) “preference” system.
To define a new preference, we define a type conforming to PreferenceKey
. To conform to PreferenceKey
, we have to define the default value for our preference, and we have to define how to combine the preferences of multiple subviews. We want our preference to be the maximum width of all the labels, so the default value is zero and we combine preferences by taking the maximum. Here's the PreferenceKey
we'll use:
struct MaxLabelWidth: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = max(value, nextValue())
}
}
The preference
modifier function sets a preference, so we can say .preference(key: MaxLabelWidth.self, value: width)
to set our preference, but we have to know what width
to set. We need to use a GeometryReader
to get the width, and it's a little tricky to do properly, so we'll wrap it up in a ViewModifier
like this:
extension MaxLabelWidth: ViewModifier {
func body(content: Content) -> some View {
return content
.background(GeometryReader { proxy in
Color.clear
.preference(key: Self.self, value: proxy.size.width)
})
}
}
What's happening above is we attach a background View
to the content, because a background is always the same size as the content it's attached to. The background View
is a GeometryReader
, which (via the proxy
) provides access to its own size. We have to give the GeometryReader
its own content. Since we don't actually want to show a background behind the original content, we use Color.clear
as the GeometryReader
's content. Finally, we use the preference
modifier to store the width as the MaxLabelWidth
preference.
Now have can define the equalSizedLabel(width:alignment:)
and storeMaxLabelWidth(in:)
modifier methods:
extension View {
func equalSizedLabel(width: CGFloat?, alignment: Alignment) -> some View {
return self
.modifier(MaxLabelWidth())
.frame(width: width, alignment: alignment)
}
}
extension View {
func storeMaxLabelWidth(in binding: Binding<CGFloat?>) -> some View {
return self.onPreferenceChange(MaxLabelWidth.self) {
binding.value = $0
}
}
}
Here's the result:
回答2:
You can use Spacer
s alongside with fixedSize
modifier for height. You should set set heights of any row's object in order to achieve exact table style
view:
struct ContentView : View {
private let height: Length = 32
@State var username: String = ""
var body: some View {
VStack(alignment: .leading) {
Text("Login")
.font(.title)
.multilineTextAlignment(.center)
.lineLimit(nil)
Text("Please")
.font(.subheadline)
HStack {
VStack (alignment: .leading) {
Text("Username: ") .frame(height: height)
Spacer()
Text("Password: ") .frame(height: height)
}
VStack {
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
.frame(height: height)
Spacer()
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
.frame(height: height)
}
}
.fixedSize(horizontal: false, vertical: true)
}
.padding()
}
}
Note that setting height on TextField
does not effect it's height directly, but it will just set the height of it's content text's height.
来源:https://stackoverflow.com/questions/56623310/swiftui-login-page-layout