SwiftUI登录页面布局 [英] SwiftUI Login Page Layout
问题描述
我正在尝试构建登录视图时正在探索SwiftUI,现在我遇到了问题
I am exploring SwiftUI as I am trying to build a login view and now I am facing a problem
这是我要实现的目标:
如您所见,我已经达到了这一点,但是我不喜欢我的实现方式
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()
}
}
因为要使用户名和密码文本在文本字段的中间准确对齐,所以我不得不在VStack
中放置文字间距值20
,我不喜欢,因为很可能会这样.不能在不同尺寸的设备上使用.
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?
谢谢
推荐答案
我们将实现两个新的View
修饰符方法,以便我们可以编写此代码:
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)
}
}
两个新的修饰符是equalSizedLabel(width:alignment:)
和storeMaxLabelWidth(in:)
.
equalSizedLabel(width:alignment)
修饰符有两件事:
- 它将
width
和alignment
应用于其内容(Text(User:)
和Text(Password:)
视图). - 它测量内容的宽度,并将其传递到任何需要它的祖先视图.
- 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.
storeMaxLabelWidth(in:)
修饰符接收由equalSizedLabel
测量的宽度,并将最大宽度存储在我们传递给它的$labelWidth
绑定中.
The storeMaxLabelWidth(in:)
modifier receives those widths measured by equalSizedLabel
and stores the maximum width in the $labelWidth
binding we pass to it.
那么,我们如何实现这些修饰符?我们如何将值从后代视图传递到祖先?在SwiftUI中,我们使用(当前未记录的)首选项"系统执行此操作.
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.
要定义新的首选项,我们定义符合PreferenceKey
的类型.为了符合PreferenceKey
,我们必须为首选项定义默认值,并且必须定义如何组合多个子视图的首选项.我们希望我们的首选项是所有标签的最大宽度,因此默认值是零,我们通过采用最大值来组合首选项.这是我们要使用的PreferenceKey
:
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())
}
}
preference
修饰符函数设置首选项,因此我们可以说.preference(key: MaxLabelWidth.self, value: width)
来设置我们的首选项,但是我们必须知道要设置什么width
.我们需要使用GeometryReader
来获取宽度,正确地进行操作有些棘手,因此我们将其包裹在ViewModifier
中,如下所示:
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)
})
}
}
上面发生的事情是我们在内容上附加了背景View
,因为背景的大小总是与其所附加内容的大小相同.后台View
是GeometryReader
,它(通过proxy
)提供对其自身大小的访问.我们必须给GeometryReader
自己的内容.由于我们实际上并不希望在原始内容后面显示背景,因此我们将Color.clear
用作GeometryReader
的内容.最后,我们使用preference
修饰符将宽度存储为MaxLabelWidth
首选项.
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.
现在可以定义equalSizedLabel(width:alignment:)
和storeMaxLabelWidth(in:)
修饰符方法:
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
}
}
}
结果如下:
这篇关于SwiftUI登录页面布局的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!