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)
}
}
}
Related
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)
}
I have a CustomSearchBar view that looks like this
However, when I wrap it with NavigationLink, the placeholder text will be centered. And user inputs will be centered too.
How do I maintain the leading alignment while using NavigationLink?
My code structure looks like this:
enum Tab {
case social
}
struct MainAppView: View {
#State var selection: Tab = .social
var body: some View {
TabView(selection: $selection) {
ZStack{
CustomButton()
NavigationView { SocialView() }
}.tabItem{Image(systemName: "person.2")}.tag(Tab.social)
// other tabs....
}
struct SocialView: View {
// ...
var body: some View {
GeometryReader{ geometry in
VStack{
NavigationLink(destination: Text("test")) {
CustomSearchBar()
//...
}.navigationBarHidden(true)
.navigationBarTitle(Text(""))
}
}
}
}
struct CustomSearchBar: View {
var body: some View {
VStack{
HStack {
SearchBarSymbols(// some binding arguments)
CustomTextField(// some binding arguments)
CancelButton(// some binding arguments)
}
.padding(.vertical, 8.0)
.padding(.horizontal, 10.0)
.background(Color("SearchBarBackgroundColor"))
.clipShape(Capsule())
}
.padding(.horizontal)
}
}
struct CustomTextField: View {
var body: some View {
TextField("friend name", text: $searchText)
.frame(alignment: .leading)
.onTapGesture {
// some actions
}
.foregroundColor(Color("SearchBarSymbolColor"))
.accentColor(Color("SearchBarSymbolColor"))
.disableAutocorrection(true)
}
}
The issues with your code are:
Your navigation view contains the search field. This means that any new view that gets pushed will cover the search field.
Your search field is inside of the navigation link. There are conflicting interactions here as it effectively turns the field into a button, ie tapping the search field vs tapping the navigation link.
Solution:
Move the navigation view below the text field, so that the new view will appear without covering it. Then change the navigation link so that it is activated via a binding that gets triggered when the search field is editing:
struct SocialView: View {
#State private var text: String = ""
#State private var isActive: Bool = false
var body: some View {
GeometryReader{ geometry in
VStack {
CustomTextField(searchText: $text, isActive: $isActive)
.padding(.vertical, 8.0)
.padding(.horizontal, 10.0)
.background(Color("SearchBarBackgroundColor"))
.clipShape(Capsule())
NavigationView {
NavigationLink(isActive: $isActive, destination: { Text("test") }, label: { EmptyView() })
}
}
}
}
}
struct CustomTextField: View {
#Binding var searchText: String
#Binding var isActive: Bool
var body: some View {
TextField("friend name", text: $searchText) { editing in
self.isActive = editing
} onCommit: {
}
.frame(alignment: .leading)
.disableAutocorrection(true)
}
}
I'm trying to add login form to white color view but textfield placeholder can't see well, however on preview it shows up.
I tried to change placeholder color SwiftUI. How to change the placeholder color of the TextField? but it still doesn't shows up well. Could you give me some tip how can I solve this problem?
struct CardView: View {
var body: some View {
ZStack {
Rectangle()
.fill(Color(UIColor.white))
.frame(height:300)
.cornerRadius(10)
.padding(16)
LoginForm()
}
}
}
struct LoginForm: View {
#State var username: String = ""
var body: some View {
VStack(alignment:.center) {
UsernameTextField(userNumber: $username)
.padding(50)
LoginButton()
}.padding()
}
}
struct UsernameTextField: View {
#Binding var userNumber: String
var body: some View {
TextField("Phone number", text: $userNumber)
.padding(50)
.onChange(of: userNumber, perform: { value in
userNumber = formatNumberTextField(pattern: "+X(XXX) XXX XX XX", phoneNumber: userNumber)
})
.frame(height: 48)
//.textFieldStyle(DefaultTextFieldStyle())
.cornerRadius(16)
.foregroundColor(.black)
.accentColor(.black)
.fixedSize(horizontal: true, vertical: false)
.padding([.leading, .trailing], 10)
.underlineTextField()
}
I would like to limit the tappable area of the picker in a form to only it's visible area of text (i.e. black rectangle in the picture below). Currently whole row can be tappable.
Screen capture of the program
Here is the code:
import SwiftUI
struct ContentView: View {
#State var rate: String = ""
#State var units = ["mL/day","mL/hour"]
#State var unit: Int = 0
var body: some View {
NavigationView{
Form{
HStack{
Text("Rate")
TextField("0", text: $rate)
Picker(selection: $unit, label: Text(""), content: {
ForEach(0..<units.count, content: { unit in
Text(units[unit])
})
})
.frame(width: 100.0)
.compositingGroup()
.clipped()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I read through this question and try to add .clipped() and .compositingGroup() but seems not useful.
You are going to have to redesign the UI a bit. I would recommend this:
struct ContentView: View {
#State var rate: String = ""
#State var units = ["mL/day","mL/hour"]
#State var unit: Int = 0
var body: some View {
NavigationView{
Form{
Section(header: Text("Rate")) { // Do This
HStack{
TextField("0", text: $rate)
.background(Color.red)
Picker(selection: $unit, label: Text(""), content: {
ForEach(0..<units.count, content: { unit in
Text(units[unit])
})
})
.frame(width: 100)
.clipped()
.background(Color.green)
}
}
}
}
}
}
I made colored backgrounds for each of the subviews. The issue is not that the whole row is selectable. It is that when you have a Picker() in the row, any view that is not itself selectable, in this case the Text() triggers the Picker(). If you use your code and put a colored background behind the text, you will see what happens. If you tap on the Picker() it triggers. If you tap on the TextField() you get an insertion point there. But if you tap on the Text() it triggers the Picker(). The .compositingGroup doesn't affect this behavior at all.
I have this exact design in an app, and the solution is to put the text into Section with a header, or put the Text() in the row above.
I have a navigation link which has a custom ButtonStyle:
NavigationLink(destination: NextScreen()) {
Text("Next")
}
.buttonStyle(CustomButtonStyle(disabled: !isValidPassword))
And my CustomButtonStyle looks like this:
#State var disabled = false
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding(15)
.foregroundColor(.white)
.background(disabled ? .black : .gray)
.cornerRadius(40)
.disabled(disabled) // this has no effect when inside a NavigationLink
}
The UI updates correctly as the user types in the password.
You can see that I disable the button inside the ButtonStyle, but this doesn't prevent the user from still tapping the NavigationLink to go to NextScreen().
To fix this I end up doing this:
NavigationLink(destination: SignupStepBirthdayView()) {
Text("Next")
}
.buttonStyle(BobbleUpButtonStyle(disabled: !isValidPassword))
.disabled(!isValidPassword)
Which seems inefficient as I'm passing a disabled state to the button style to update the UI, and then having to disable the actual NavigationLink.
Is there a better way to do this?
I will show you simple way, no needed to using #Bingding or #State
First, create your button style:
struct CustomButtonStyle: ButtonStyle {
public func makeBody(configuration: ButtonStyle.Configuration) -> some View {
MyButton(configuration: configuration)
}
struct MyButton: View {
let configuration: ButtonStyle.Configuration
#Environment(\.isEnabled) private var isEnabled: Bool
var body: some View {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding(15)
.foregroundColor(.white)
.background(isEnabled ? Color.blue : Color.gray)
.cornerRadius(40)
.disabled(!isEnabled)
}
}
}
Then, disable it as any SwiftUI component:
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter your text", text: $text)
NavigationLink(destination: NextScreen()) {
Text("Next")
}
.buttonStyle(CustomButtonStyle())
.disabled(text.isEmpty)
}
}
}
}
You have to use Binding instead of State in CustomButtonStyle
#Binding var disabled : Bool
And this changes in your ContentView
#State var isValidPassword: Bool = true
also:
.buttonStyle(BobbleUpButtonStyle(disabled: $isValidPassword))