Show/Hide Password - How can I add this feature? - swift

I've looked through the forums but I'm seeing mixed answers especially ones from an old Xcode version.
I only decided to add this after already typing up the code I have in this:
How could I go about doing that? I was wanting the 'Eyeball' toggle implemented on the password field.

You can simply use this view instead of SecureField. It has the eye icon inside, so for most cases you don't need to care about anything.
struct SecureInputView: View {
#Binding private var text: String
#State private var isSecured: Bool = true
private var title: String
init(_ title: String, text: Binding<String>) {
self.title = title
self._text = text
}
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecured {
SecureField(title, text: $text)
} else {
TextField(title, text: $text)
}
}.padding(.trailing, 32)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.accentColor(.gray)
}
}
}
}
Copy paste this view into your app, and instead of SecureField just use SecureInputView.
Example: SecureInputView("Password", text: $viewModel.password)

The possible approach is to show either TextField or SecureField joined to one storage, like in below demo:
Updated: Xcode 13.4 / iOS 15.5
with FocusState, now it is possible to change fields without having the keyboard disappear
Main part:
if showPassword {
TextField("Placeholer", text: $password)
.focused($inFocus, equals: .plain)
} else {
SecureField("Placeholder", text: $password)
.focused($inFocus, equals: .secure)
}
Button("toggle") {
self.showPassword.toggle()
inFocus = showPassword ? .plain : .secure
}
Test module in project is here
Old:
struct DemoShowPassword: View {
#State private var showPassword: Bool = false
#State private var password = "demo"
var body: some View {
VStack {
if showPassword {
TextField("Placeholer", text: $password)
} else {
SecureField("Placeholder", text: $password)
}
Button("toggle") {
self.showPassword.toggle()
}
}
}
}

For those still looking for a simple solution to this issue (requires iOS 15 for swiftUI 3):
With the new #FocusState introduced in swiftUI 3, it's possible to keep focus and keyboard open while changing State.
By using the opacity modifier instead of conditionally changing between SecureField and TextField, the focus can jump between the two without issues with the keyboard.
This allows you to toggle between revealing and hiding the password with the the eye button included in the ZStack.
import SwiftUI
struct SecureTextFieldWithReveal: View {
#FocusState var focus1: Bool
#FocusState var focus2: Bool
#State var showPassword: Bool = false
#State var text: String = ""
var body: some View {
HStack {
ZStack(alignment: .trailing) {
TextField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus1)
.opacity(showPassword ? 1 : 0)
SecureField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus2)
.opacity(showPassword ? 0 : 1)
Button(action: {
showPassword.toggle()
if showPassword { focus1 = true } else { focus2 = true }
}, label: {
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill").font(.system(size: 16, weight: .regular))
.padding()
})
}
}
}
}
Password field hidden
Password field revealed
This is the code in LoginModifier:
import SwiftUI
struct LoginModifier: ViewModifier {
var borderColor: Color = Color.gray
func body(content: Content) -> some View {
content
.disableAutocorrection(true)
.autocapitalization(.none)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(borderColor, lineWidth: 1))
}
}
The only issue I've had with this method is that on regaining focus SecureField will automatically clear any text already entered if you start typing. This seems to be a design choice by Apple.

I am using this approach for now in my current application. I would like to say that it works flawlessly.
#ViewBuilder
func secureField() -> some View {
if self.showPassword {
TextField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
} else {
SecureField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
}
}
Use:
HStack{
Image(systemName: "lock.fill")
.foregroundColor(passwordText.isEmpty ? .secondary : .primary)
.font(.system(size: 18, weight: .medium, design: .default))
.frame(width: 18, height: 18, alignment: .center)
secureField()
if !passwordText.isEmpty {
Button(action: {
self.showPassword.toggle()
}, label: {
ZStack(alignment: .trailing){
Color.clear
.frame(maxWidth: 29, maxHeight: 60, alignment: .center)
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill")
.font(.system(size: 18, weight: .medium))
.foregroundColor(Color.init(red: 160.0/255.0, green: 160.0/255.0, blue: 160.0/255.0))
}
})
}
}
.padding(.horizontal, 15)
.background(Color.primary.opacity(0.05).cornerRadius(10))
.padding(.horizontal, 15)

I am afraid most answers here fail to mention that switching from SecureField to TextField reduces security. SecureField is essentially, per Apple documentation, simply a TextField where user input is masked [1]. However, SecureField also does one other job - it prevents using third-party keyboards (keyboard extensions) and thus protects user's security and privacy.
Ideal solution would be to have input field that is both "secure" and has mask()/unmask() methods. Unfortunately, the only advice I found is when you want to implement unmasking as other answers suggested, at least block third-party keyboards from your application entirely [2]:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
return extensionPointIdentifier != UIApplication.ExtensionPointIdentifier.keyboard
}
}
#main
struct MyApplication: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Should also mention that UIApplicationDelegate is part of UIKit, not SwiftUI. There is no "native" SwiftUI for the same purpose as for now, although the above works fine for now.
https://developer.apple.com/documentation/swiftui/securefield
https://www.securing.pl/en/third-party-iphone-keyboards-vs-your-ios-application-security/

For those that do not want the keyboard disappearing while typing:
struct CustomSecureField: View {
#State var password: String = ""
#State var isShowingPassword: Bool = false
var body: some View {
VStack{
ZStack{
HStack{
SecureField(
isShowingPassword ? "" : "Password",
text: $password) {
}.opacity(isShowingPassword ? 0 : 1)
// show only one of these is not empty.
if(!password.isEmpty){
Image(systemName: isShowingPassword ? "eye.slash" : "eye")
.foregroundColor(.white)
.frame(width: 20, height: 20, alignment: .center)
.modifier(TouchDownUpEventModifier(changeState: { (buttonState) in
if buttonState == .pressed {
isShowingPassword = true
} else {
isShowingPassword = false
}
}))
}
}
if(isShowingPassword){
HStack{
Text(password)
.foregroundColor(.white)
.allowsHitTesting(false)
Spacer()
}
}
}
}.padding(10)
.background(Color.gray)
}
}
and the on tap and release modifier:
public enum ButtonState {
case pressed
case notPressed
}
/// ViewModifier allows us to get a view, then modify it and return it
public struct TouchDownUpEventModifier: ViewModifier {
/// Properties marked with `#GestureState` automatically resets when the gesture ends/is cancelled
/// for example, once the finger lifts up, this will reset to false
/// this functionality is handled inside the `.updating` modifier
#GestureState private var isPressed = false
/// this is the closure that will get passed around.
/// we will update the ButtonState every time your finger touches down or up.
let changeState: (ButtonState) -> Void
/// a required function for ViewModifier.
/// content is the body content of the caller view
public func body(content: Content) -> some View {
/// declare the drag gesture
let drag = DragGesture(minimumDistance: 0)
/// this is called whenever the gesture is happening
/// because we do this on a `DragGesture`, this is called when the finger is down
.updating($isPressed) { (value, gestureState, transaction) in
/// setting the gestureState will automatically set `$isPressed`
gestureState = true
}
return content
.gesture(drag) /// add the gesture
.onChange(of: isPressed, perform: { (pressed) in /// call `changeState` whenever the state changes
/// `onChange` is available in iOS 14 and higher.
if pressed {
self.changeState(.pressed)
} else {
self.changeState(.notPressed)
}
})
}
/// if you're on iPad Swift Playgrounds and you put all of this code in a seperate file,
/// you need to add a public init so that the compiler detects it.
public init(changeState: #escaping (ButtonState) -> Void) {
self.changeState = changeState
}
}
From what I have seen there is no easy way to keep the text showing unless you want to lose focus on your text.
Cheers!

#Derwrecked's answer really gave me some good inspirations: instead using two TextField, change SecureField opacity and show/hide a Text can avoid keyboard dismissing problem, but in his answer that long TouchDownUpEventModifier seems unnecessarily complicated, you can easily achieve the same effect using a Button with label.
So below is my approach, and the previews look like this
import SwiftUI
struct SecureInput: View {
let placeholder: String
#State private var showText: Bool = false
#State var text: String
var onCommit: (()->Void)?
var body: some View {
HStack {
ZStack {
SecureField(placeholder, text: $text, onCommit: {
onCommit?()
})
.opacity(showText ? 0 : 1)
if showText {
HStack {
Text(text)
.lineLimit(1)
Spacer()
}
}
}
Button(action: {
showText.toggle()
}, label: {
Image(systemName: showText ? "eye.slash.fill" : "eye.fill")
})
.accentColor(.secondary)
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary, lineWidth: 1)
.foregroundColor(.clear))
}
}
struct SecureInput_Previews: PreviewProvider {
static var previews: some View {
Group {
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.previewLayout(.fixed(width: 400, height: 100))
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.preferredColorScheme(.dark)
.previewLayout(.fixed(width: 400, height: 100))
}
}
}
An known issue for this approach: since when password is shown, SecureField has 0.0 opacity, so input cursor is not visible. But users can still keep typing without losing keyboard focus, so I find it acceptable, if anyone has a solution for this, please comment and share.

I've been looking for a nice solution for my use-case. I had to have an indicator which field is in focus. Successfully done that with onEditingChanged from TextField, but SecureField doesn't provide that closure. I tried stacking them both and disabling the SecureField so it only shows 'hidden' characters. That resulted in cursor sticking to the TextField text while SecureField text had different text width which made it seem buggy. Imagine a password with a lot of I's in it. The idea is to have a main binding with two side bindings that update the main one and sync each other.
struct CustomSecureField : View {
var label : String
#Binding var text : String
#State var isEditing = false
#State var isHidden = true
var body : some View {
let showPasswordBinding = Binding<String> {
self.text
} set: {
self.text = $0
}
let hidePasswordBinding = Binding<String> {
String.init(repeating: "●", count: self.text.count)
} set: { newValue in
if(newValue.count < self.text.count) {
self.text = ""
} else {
self.text.append(contentsOf: newValue.suffix(newValue.count - self.text.count) )
}
}
return ZStack(alignment: .trailing) {
TextField(
label,
text: isHidden ? hidePasswordBinding : showPasswordBinding,
onEditingChanged: { editingChanged in
isEditing = editingChanged
}
)
Image("eye").frame(width: 50, height: 50).onTapGesture {
isHidden.toggle()
}
}
}
}
}

Crazy (AKA don't use in production) and very breakable solution here (but working at the time of writing):
extension TextField {
public func secure(_ secure: Bool = true) -> TextField {
if secure {
var secureField = self
withUnsafeMutablePointer(to: &secureField) { pointer in
let offset = 32
let valuePointer = UnsafeMutableRawPointer(mutating: pointer)
.assumingMemoryBound(to: Bool.self)
.advanced(by: offset)
valuePointer.pointee = true
}
return secureField
} else {
return self
}
}
}
Usage
#State securing = true
...
TextField(...)
.secure(securing)

#Vahagn Gevorgyan's answer was almost correct but some people were struggling with maintaining state... this is because the field is using a binding which should ideally be held in a parent view. Therefore just update the bindings to state variables like this
struct SecureInputView: View {
let placeholder: String
#State var text: String
#State var isSecure: Bool = true
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecure {
SecureField(placeholder, text: $text)
} else {
TextField(placeholder, text: $text)
}
}.padding(.trailing, 32)
Button {
isSecure.toggle()
} label: {
Image(systemName: isSecure ? "lock.fill" : "lock.open")
}
}
}
}

#State private var isPasswordVisible = false
ZStack {
TextField("", text: $password)
.opacity(isPasswordVisible ? 1 : 0)
SecureField("", text: $password)
.opacity(isPasswordVisible ? 0 : 1)
}
It doesn't need #Focus from iOS 15
Keyboard will not disappear/appear on changing isPasswordVisible
Password will not cleared on changing from visible to invisible then typing
Good Luck

I made a custom text field that combine SecureField and TextField.
This is an example where I used my custom field for both email and pwd.
This is my solution:
struct CustomTextField: View {
let imageName: String
let placeholderText: String
var isSecureInput: Bool = false ///< define if this text field is secured and require eye button
#State private var isSecured: Bool
#Binding var text: String
init(image: String,
placeholder: String,
text: Binding<String>,
isSecureInput: Bool) {
imageName = image
placeholderText = placeholder
self._text = text
self.isSecureInput = isSecureInput
isSecured = isSecureInput
}
var body: some View {
VStack {
HStack {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
if isSecureInput {
Group {
if isSecured {
SecureField(placeholderText, text: $text)
}
else {
TextField(text, text: $text)
}
}
.disableAutocorrection(true)
.autocapitalization(.none)
.textContentType(.password)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
}
}
else {
TextField(placeholderText, text: $text)
}
}
Divider()
}
}
}

Related

How can I make a smoothed custom Picker on SwiftUI?

I would like to replicate this picker in swiftUI. In particular, I have a button on the bottom left of the screen and when I click it I would like to show different icons (similar to the image below, but vertically). As soon as I click on one of the choices the button should shrink back to the initial form (circle) with the chosen icon.
When closed:
When open:
I am new to this language and to app in general, I tried with a Pop Up menu, but it is not the desired result, for now I have an horizontal segmented Picker.
You can't do this with the built-in Picker, because it doesn't offer a style like that and PickerStyle doesn't let you create custom styles (as of the 2022 releases).
You can create your own implementation out of other SwiftUI views instead. Here's what my brief attempt looks like:
Here's the code:
enum SoundOption {
case none
case alertsOnly
case all
}
struct SoundOptionPicker: View {
#Binding var option: SoundOption
#State private var isExpanded = false
var body: some View {
HStack(spacing: 0) {
button(for: .none, label: "volume.slash")
.foregroundColor(.red)
button(for: .alertsOnly, label: "speaker.badge.exclamationmark")
.foregroundColor(.white)
button(for: .all, label: "volume.2")
.foregroundColor(.white)
}
.buttonStyle(.plain)
.background {
Capsule(style: .continuous).foregroundColor(.black)
}
}
#ViewBuilder
private func button(for option: SoundOption, label: String) -> some View {
Button {
withAnimation(.easeOut) {
if isExpanded {
self.option = option
isExpanded = false
} else {
isExpanded = true
}
}
} label: {
Image(systemName: label)
.fontWeight(.bold)
.padding(10)
}
.frame(width: shouldShow(option) ? buttonSize : 0, height: buttonSize)
.opacity(shouldShow(option) ? 1 : 0)
.clipped()
}
private var buttonSize: CGFloat { 44 }
private func shouldShow(_ option: SoundOption) -> Bool {
return isExpanded || option == self.option
}
}
struct ContentView: View {
#State var option = SoundOption.none
var body: some View {
ZStack {
Color(hue: 0.6, saturation: 1, brightness: 0.2)
SoundOptionPicker(option: $option)
.shadow(color: .gray, radius: 3)
.frame(width: 200, alignment: .trailing)
}
}
}

Remove padding of TextEditor

I am currently doing a sort of an edition view and as TextEditor doesn't have a place holder, I am faking one by putting an overlay with a Text("Description") when the TextEditor is empty.
The problem is as you can see in the screen, TextEditor adds a small padding and I would like to know if you can remove it
TextEditor(text: $description)
.overlay(alignment: .topLeading) {
if description.isEmpty {
Text("Description")
.foregroundStyle(.tertiary)
.allowsHitTesting(false)
}
}
The white bar is where the TextEditor lets us type.
And the post Remove padding on TextEditor didn't help me at all.
EDIT: For now, I have used a simple .offset(x: -5) but as it's a really good solution, I'll leave this open if someone has a better solution :)
I have faced the same issue so I have make my own text editor with palceholder. Please have a loot at this simple code. You just need to Initialised it with binding variables for use.
import SwiftUI
struct MLBTextEditor: View {
var title: String
var text: Binding<String>
var placeHolder: String
#FocusState private var detailIsFocused: Bool
init(title: String, text: Binding<String>, placeholder: String) {
UITextView.appearance().backgroundColor = .clear
self.title = title
self.text = text
self.placeHolder = placeholder
}
var body: some View {
VStack(alignment: .leading, spacing: 7) {
if !title.isEmpty {
Text(title)
.font(.custom(Nunito.Medium.rawValue, size: 13))
}
ZStack(alignment: .topLeading) {
if text.wrappedValue.isEmpty {
Text(placeHolder)
.padding(.all)
.foregroundColor(.gray)
.font(.custom(Roboto.Regular.rawValue, size: 14))
}
TextEditor(text: text)
.font(.custom(Roboto.Regular.rawValue, size: 14))
.padding(15)
.focused($detailIsFocused)
.opacity(text.wrappedValue.isEmpty ? 0.25 : 1)
.foregroundColor(self.text.wrappedValue == placeHolder ? .gray : .primary)
// this onchange method is use to close the keyboard
.onChange(of: text.wrappedValue) { _ in
if !text.wrappedValue.filter({ $0.isNewline }).isEmpty {
detailIsFocused = false
}
}
}
.onTapGesture {
if text.wrappedValue.isEmpty {
text.wrappedValue = " "
}
}
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(.gray, lineWidth: 1)
)
}
}
}
struct MLBTextEditor_Previews: PreviewProvider {
static var previews: some View {
MLBTextEditor(title: "", text: .constant(""), placeholder: "Name please")
}
}

How do I programmatically set secure text field and normal text field in swiftUI

SwiftUI has two different forms of text fields, one is SecureField which hides input and TextField which doesn't hide input. Instead of creating two separate views, is there a way to create a single view that takes in a parameter to create both types while repeating as little code as possible?
You just make a View with all the code you want for the SecureTextField and the TextField then all you have to do is call the HybridTextField where ever you need it.
import SwiftUI
struct HybridTextFieldUsageView: View {
#State var password: String = "password"
var body: some View {
//Use this anywhere in your code
HybridTextField(text: $password, titleKey: "password")
}
}
///Contains all the code for the Secure and regular TextFields
struct HybridTextField: View {
#Binding var text: String
#State var isSecure: Bool = true
var titleKey: String
var body: some View {
HStack{
Group{
if isSecure{
SecureField(titleKey, text: $text)
}else{
TextField(titleKey, text: $text)
}
}.textFieldStyle(.roundedBorder)
.animation(.easeInOut(duration: 0.2), value: isSecure)
//Add any common modifiers here so they dont have to be repeated for each Field
Button(action: {
isSecure.toggle()
}, label: {
Image(systemName: !isSecure ? "eye.slash" : "eye" )
})
}//Add any modifiers shared by the Button and the Fields here
}
}
struct HybridTextField_Previews: PreviewProvider {
static var previews: some View {
HybridTextFieldUsageView()
}
}
I create a custom view for PasswordTextField. May be this code will help. I don't know either it helps you, though it fulfilled my requirement. That's why sharing it to you. This is the output of my code
struct PasswordTextField: View {
#Binding var isPasswordVisible: Bool
var hint: String
#Binding var text: String
var isTextChanged: (Bool) -> Void
var body: some View {
HStack {
if isPasswordVisible {
TextFieldView(
hint: hint,
text: $text,
isTextChanged: isTextChanged
)
} else {
SecuredTextFieldView(
hint: hint,
text: $text
)
}
}.overlay(alignment: .trailing) {
Image(systemName: isPasswordVisible ? "eye.fill" : "eye.slash.fill")
.padding()
.onTapGesture {
isPasswordVisible.toggle()
}
}
}
}
struct TextFieldView: View {
var hint: String
#Binding var text: String
var isTextChanged: (Bool) -> Void
var body: some View {
TextField(
hint,
text: $text,
onEditingChanged: isTextChanged
)
.padding()
.overlay(
Rectangle().strokeBorder(
.gray.opacity(0.2),
style: StrokeStyle(lineWidth: 2.0)
)
)
}
}
struct SecuredTextFieldView: View {
var hint: String
#Binding var text: String
var body: some View {
SecureField(
hint,
text: $text
)
.padding()
.overlay(
Rectangle().strokeBorder(
.gray.opacity(0.2),
style: StrokeStyle(lineWidth: 2.0)
)
)
}
}
and call the custom view in your actual view
struct PasswordView: View {
#State var password: String = ""
#State var confirmPassword: String = ""
#State var isPasswordVisible: Bool = false
#State var isConfirmPasswordVisible: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 15) {
Text("New Password")
.font(.headline)
.fontWeight(.regular)
.padding(.top, 30)
PasswordTextField(
isPasswordVisible: $isPasswordVisible,
hint: "Password having 8 charecture",
text: $password,
isTextChanged: { (changed) in
}
)
Text("Confirm New Password")
.font(.headline)
.fontWeight(.regular)
.padding(.top, 10)
PasswordTextField(
isPasswordVisible: $isConfirmPasswordVisible,
hint: "Password having 8 charecture",
text: $confirmPassword,
isTextChanged: { (changed) in
}
)
Spacer()
}.padding(.horizontal, 25)
}
}
In your view's body you can use a ternary to create the right textfield as needed without using a giant if/else block:
(self.isSecure ? AnyView(SecureField(placeholder, text: $value)) : AnyView(TextField(placeholder, text: $value)))
This will return a view that you can use operators on, which is useful if you're creating a custom text input. For example, the following would be painful if we had to do it twice for each kind of text field. Using a ternary in the actual view body keeps you from having two giant if/else blocks.
VStack {
ZStack(alignment: .leading) {
Text(placeholder)
.foregroundColor(Color(.placeholderText))
.offset(y: $value.wrappedValue.isEmpty ? 0 : -25)
.scaleEffect($value.wrappedValue.isEmpty ? 1 : 0.8, anchor: .leading)
(self.isSecure ? AnyView(SecureField(placeholder, text: $value)) : AnyView(TextField(placeholder, text: $value)))
.onChange(of: self.value) { newValue in
if self.onChange(newValue) != true {
self.value = previousValue
}
DispatchQueue.main.async {
self.previousValue = newValue
}
}
}
.padding(.top, 15)
.animation(.easeInOut(duration: 0.2))
Divider()
.frame(height: 1)
.padding(.horizontal, 30)
.background(Color.black)
}

Text() in front of TextField() blocking editing in SwiftUI

So I'd like my textfield to have a customizable placeholder text so I decided to put a Text() element in a ZStack in front of the text field. The only problem is, this Text() item blocks the selection of the textfield that is behind it (AKA when I click the placeholder I want the TextField to be clicked). Unfortunately, this Text() element blocks the click. I tried using the .allowsHitTesting() property as seen below but that also didn't work, and I'm not sure why.
struct ContentView: View {
#State var text: String = ""
var body: some View {
ZStack {
TextField("", text: self.$text)
.background(Color.red)
.foregroundColor(Color.white)
if text.isEmpty {
Text("Placeholder")
.allowsHitTesting(false)
}
}
}
}
It can be done with custom text field style.
Here is a demo of solution (or parameters can be tuned). Tested with Xcode 12 / iOS 14 (border is just for visibility)
struct PlaceholderStyle: TextFieldStyle {
let isActive: Bool
var placeholder = "Placeholder"
var color = Color.white
var backgrond = Color.red
func _body(configuration: TextField<_Label>) -> some View {
Text("\(isActive ? placeholder : "")")
.foregroundColor(isActive ? color : .clear)
.background(isActive ? backgrond : .clear)
.frame(maxWidth: .infinity, alignment: .leading)
.overlay(configuration)
}
}
struct DemoView: View {
#State private var text = ""
var body: some View {
TextField("", text: $text)
.border(Color.gray).padding(.horizontal)
.textFieldStyle(PlaceholderStyle(isActive: text.isEmpty))
}
}
See if this fits your needs:
struct ContentView: View {
#State var text = ""
var body: some View {
ZStack(alignment: .leading) {
if text.isEmpty { Text("Placeholder")
.foregroundColor(.red)
.background(Color.yellow)
}
TextField("", text: $text)
.background(text.isEmpty ? Color.clear : Color.yellow)
}
}
}

Changing the color of a button in SwiftUI based on disabled or not

I have a textfield with a send button that's a systemImage arrow. I want the foreground color of the image to change depending on whether the textField is empty or not. (I.e. the button is gray, and it is disabled if the textfield is empty. It's blue if the count of the textfield text is > 1).
I have a workaround that's not perfect:
if chatMessageIsValid {
Spacer()
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.padding(.leading, 10)
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
.foregroundColor(Color.blue)
.padding(.trailing, 10)
}.disabled(!chatMessageIsValid)
}
} else {
Spacer()
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.padding(.leading, 10)
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
.foregroundColor(Color.gray)
.padding(.trailing, 10)
}.disabled(!chatMessageIsValid)
}
}
This almost works, and it does change the color of the image if the text is > 1 in length. However, due to the change in state you're kicked out of editing the textfield after one character is typed, and you'll need to select the textfield again to continue typing. Is there a better way to do this with the .disabled modifier?
I guess you want this:
You can add a computed property for the button color, and pass the property to the button's foregroundColor modifier. You can also use a single padding modifier around the HStack instead of separate paddings on its subviews.
struct ContentView : View {
#State var chatMessage: String = ""
var body: some View {
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
.foregroundColor(buttonColor)
}
.disabled(!chatMessageIsValid)
}
.padding([.leading, .trailing], 10)
}
var chatMessageIsValid: Bool {
return !chatMessage.isEmpty
}
var buttonColor: Color {
return chatMessageIsValid ? .accentColor : .gray
}
func sendMessage() {
chatMessage = ""
}
}
However, you shouldn't use the foregroundColor modifier at all here. You should use the accentColor modifier. Using accentColor has two benefits:
The Image will automatically use the environment's accentColor when the Button is enabled, and gray when the Button is disabled. You don't have to compute the color at all.
You can set the accentColor in the environment high up in your View hierarchy, and it will trickle down to all descendants. This makes it easy to set a uniform accent color for your whole interface.
In the following example, I put the accentColor modifier on the HStack. In a real app, you would probably set it on the root view of your entire app:
struct ContentView : View {
#State var chatMessage: String = ""
var body: some View {
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
}
.disabled(!chatMessageIsValid)
}
.padding([.leading, .trailing], 10)
.accentColor(.orange)
}
var chatMessageIsValid: Bool {
return !chatMessage.isEmpty
}
func sendMessage() {
chatMessage = ""
}
}
Also, Matt's idea of extracting the send button into its own type is probably smart. It makes it easy to do nifty things like animating it when the user clicks it:
Here's the code:
struct ContentView : View {
#State var chatMessage: String = ""
var body: some View {
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.textFieldStyle(.roundedBorder)
SendButton(action: sendMessage, isDisabled: chatMessage.isEmpty)
}
.padding([.leading, .trailing], 10)
.accentColor(.orange)
}
func sendMessage() {
chatMessage = ""
}
}
struct SendButton: View {
let action: () -> ()
let isDisabled: Bool
var body: some View {
Button(action: {
withAnimation {
self.action()
self.clickCount += 1
}
}) {
Image(systemName: "arrow.up.circle")
.rotationEffect(.radians(2 * Double.pi * clickCount))
.animation(.basic(curve: .easeOut))
}
.disabled(isDisabled)
}
#State private var clickCount: Double = 0
}
With these various solutions, you can use the \.isEnabled environment property instead of creating custom button styles or passing in disabled booleans yourself.
#Environment(\.isEnabled) private var isEnabled
If you want to customize the button even further with disabled (or any other) state, you can add variables to your custom ButtonStyle struct.
Swift 5.1
struct CustomButtonStyle: ButtonStyle {
var disabled = false
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(disabled ? .red : .blue)
}
}
// SwiftUI
#State var isDisabled = true
var body: some View {
Button().buttonStyle(CustomButtonStyle(disabled: self.isDisabled))
}
Try this
Spacer()
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.padding(.leading, 10)
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
.foregroundColor(chatMessageIsValid ? Color.blue : Color.gray)
.padding(.trailing, 10)
}.disabled(!chatMessageIsValid)
}
I suppose, the reason TextField loses focus is that in your code the whole view hierarchy is changed depending on the value of chatMessageIsValid. SwiftUI doesn't understand that the view hierarchy in the then block is almost identical to the one in the else block, so it rebuilds it completely.
In this edited code it should see that the only thing that changes is the foregroundColor of the image and the disabled status of the button, leaving the TextField untouched. Apple had a similar examples in one of the WWDC videos, I believe.
All modifier arguments can be conditional:
.foregroundColor(condition ? .red : .yellow)
where the condition could be any true/false
var condition: Bool { chatMessage.isEmpty } // can be used inline
You can use any other condition you need