Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,56 @@ struct AccountCreationFormFieldViewModel {
struct AccountCreationFormFieldView: View {
private let viewModel: AccountCreationFormFieldViewModel

/// Whether the text field is *shown* as secure.
/// When the field is secure, there is a button to show/hide the text field input.
@State private var showsSecureInput: Bool = true

// Tracks the scale of the view due to accessibility changes.
@ScaledMetric private var scale: CGFloat = 1.0

init(viewModel: AccountCreationFormFieldViewModel) {
self.viewModel = viewModel
self.showsSecureInput = viewModel.isSecure
}

var body: some View {
VStack(alignment: .leading, spacing: Layout.verticalSpacing) {
Text(viewModel.header)
.subheadlineStyle()
if viewModel.isSecure {
SecureField(viewModel.placeholder, text: viewModel.text)
.textFieldStyle(RoundedBorderTextFieldStyle(focused: viewModel.isFocused))
ZStack(alignment: .trailing) {
// Text field based on the `isTextFieldSecure` state.
Group {
if showsSecureInput {
SecureField(viewModel.placeholder, text: viewModel.text)
} else {
TextField(viewModel.placeholder, text: viewModel.text)
}
}
.font(.body)
.textFieldStyle(RoundedBorderTextFieldStyle(
focused: viewModel.isFocused,
// Custom insets to leave trailing space for the reveal button.
insets: .init(top: RoundedBorderTextFieldStyle.Defaults.insets.top,
leading: RoundedBorderTextFieldStyle.Defaults.insets.leading,
bottom: RoundedBorderTextFieldStyle.Defaults.insets.bottom,
trailing: Layout.secureFieldRevealButtonHorizontalPadding * 2 + Layout.secureFieldRevealButtonDimension * scale),
height: 44 * scale
))
.keyboardType(viewModel.keyboardType)

// Button to show/hide the text field content.
Button(action: {
showsSecureInput.toggle()
}) {
Image(systemName: showsSecureInput ? "eye.slash" : "eye")
.accentColor(Color(.textSubtle))
.frame(width: Layout.secureFieldRevealButtonDimension * scale,
height: Layout.secureFieldRevealButtonDimension * scale)
.padding(.leading, Layout.secureFieldRevealButtonHorizontalPadding)
.padding(.trailing, Layout.secureFieldRevealButtonHorizontalPadding)
}
}
} else {
TextField(viewModel.placeholder, text: viewModel.text)
.textFieldStyle(RoundedBorderTextFieldStyle(focused: viewModel.isFocused))
Expand All @@ -50,6 +88,8 @@ struct AccountCreationFormFieldView: View {
private extension AccountCreationFormFieldView {
enum Layout {
static let verticalSpacing: CGFloat = 8
static let secureFieldRevealButtonHorizontalPadding: CGFloat = 16
static let secureFieldRevealButtonDimension: CGFloat = 18
}
}

Expand All @@ -62,12 +102,24 @@ struct AccountCreationFormField_Previews: PreviewProvider {
isSecure: false,
errorMessage: nil,
isFocused: true))
AccountCreationFormFieldView(viewModel: .init(header: "Choose a password",
placeholder: "Password",
keyboardType: .default,
text: .constant("w"),
isSecure: true,
errorMessage: "Too simple",
isFocused: false))
VStack {
AccountCreationFormFieldView(viewModel: .init(header: "Choose a password",
placeholder: "Password",
keyboardType: .default,
text: .constant("wwwwwwwwwwwwwwwwwwwwwwww"),
isSecure: true,
errorMessage: "Too simple",
isFocused: false))
.environment(\.sizeCategory, .medium)

AccountCreationFormFieldView(viewModel: .init(header: "Choose a password",
placeholder: "Password",
keyboardType: .default,
text: .constant("wwwwwwwwwwwwwwwwwwwwwwww"),
isSecure: true,
errorMessage: "Too simple",
isFocused: false))
.environment(\.sizeCategory, .extraExtraExtraLarge)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,45 @@ struct RoundedBorderTextFieldStyle: TextFieldStyle {
private let focused: Bool
private let focusedBorderColor: Color
private let unfocusedBorderColor: Color
private let insets: EdgeInsets
private let height: CGFloat?

/// - Parameters:
/// - focused: Whether the field is focused or not.
/// - focusedBorderColor: The border color when the field is focused.
/// - unfocusedBorderColor: The border color when the field is not focused.
/// - insets: The insets between the background border and the text input.
/// - height: An optional fixed height for the field.
init(focused: Bool,
focusedBorderColor: Color = Defaults.focusedBorderColor,
unfocusedBorderColor: Color = Defaults.unfocusedBorderColor) {
unfocusedBorderColor: Color = Defaults.unfocusedBorderColor,
insets: EdgeInsets = Defaults.insets,
height: CGFloat? = nil) {
self.focused = focused
self.focusedBorderColor = focusedBorderColor
self.unfocusedBorderColor = unfocusedBorderColor
self.insets = insets
self.height = height
}

func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding(10)
.padding(insets)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the change of height when switching between field types is due to vertical padding and the content height difference of SecureField and TextField.

A quick fix would be to add a fixed height, we may need to take scale into consideration:

Suggested change
.padding(insets)
.padding(insets)
.frame(height: 44 * scale)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some testing, the insets aren't the cause of the mismatched height, the height is just slightly different between the TextField and SecureField. In a09e7e3, I passed an optional height parameter for AccountCreationFormFieldView to set 44 * scale.

.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.stroke(focused ? focusedBorderColor: unfocusedBorderColor,
lineWidth: focused ? 2: 1)
.frame(height: height)
)
.frame(height: height)
}
}

extension RoundedBorderTextFieldStyle {
enum Defaults {
static let focusedBorderColor: Color = .init(uiColor: .brand)
static let unfocusedBorderColor: Color = .gray
static let insets = EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
}
}

Expand All @@ -41,8 +56,26 @@ struct TextFieldStyles_Previews: PreviewProvider {
.textFieldStyle(RoundedBorderTextFieldStyle(focused: false))
TextField("placeholder", text: .constant("focused with a different color"))
.textFieldStyle(RoundedBorderTextFieldStyle(focused: true, focusedBorderColor: .orange))
.environment(\.sizeCategory, .extraExtraExtraLarge)
TextField("placeholder", text: .constant("unfocused with a different color"))
.textFieldStyle(RoundedBorderTextFieldStyle(focused: false, unfocusedBorderColor: .cyan))
TextField("placeholder", text: .constant("custom insets"))
.textFieldStyle(RoundedBorderTextFieldStyle(focused: false, insets: .init(top: 20, leading: 0, bottom: 10, trailing: 50)))
.frame(width: 150)
HStack {
TextField("placeholder", text: .constant("text field"))
.textFieldStyle(RoundedBorderTextFieldStyle(focused: true))
SecureField("placeholder", text: .constant("secure"))
.textFieldStyle(RoundedBorderTextFieldStyle(focused: true))
}
.environment(\.sizeCategory, .extraExtraExtraLarge)
HStack {
TextField("placeholder", text: .constant("text field"))
.textFieldStyle(RoundedBorderTextFieldStyle(focused: true, height: 100))
SecureField("placeholder", text: .constant("secure"))
.textFieldStyle(RoundedBorderTextFieldStyle(focused: true))
}
.environment(\.sizeCategory, .extraExtraExtraLarge)
}
.preferredColorScheme(.dark)
}
Expand Down