How to add placeholder text to TextEditor in SwiftUI? - ios

When using SwiftUI's new TextEditor, you can modify its content directly using a #State. However, I haven't see a way to add a placeholder text to it. Is it doable right now?
I added an example that Apple used in their own translator app. Which appears to be a multiple lines text editor view that supports a placeholder text.

It is not possible out of the box but you can achieve this effect with ZStack or the .overlay property.
What you should do is check the property holding your state. If it is empty display your placeholder text. If it's not then display the inputted text instead.
And here is a code example:
ZStack(alignment: .leading) {
if email.isEmpty {
Text(Translation.email)
.font(.custom("Helvetica", size: 24))
.padding(.all)
}
TextEditor(text: $email)
.font(.custom("Helvetica", size: 24))
.padding(.all)
}
Note: I have purposely left the .font and .padding styling for you to see that it should match on both the TextEditor and the Text.
EDIT: Having in mind the two problems mentioned in Legolas Wang's comment here is how the alignment and opacity issues could be handled:
In order to make the Text start at the left of the view simply wrap it in HStack and append Spacer immediately after it like this:
HStack {
Text("Some placeholder text")
Spacer()
}
In order to solve the opaque problem you could play with conditional opacity - the simplest way would be using the ternary operator like this:
TextEditor(text: stringProperty)
.opacity(stringProperty.isEmpty ? 0.25 : 1)
Of course this solution is just a silly workaround until support gets added for TextEditors.

You can use a ZStack with a disabled TextEditor containing your placeholder text behind. For example:
ZStack {
if self.content.isEmpty {
TextEditor(text:$placeholderText)
.font(.body)
.foregroundColor(.gray)
.disabled(true)
.padding()
}
TextEditor(text: $content)
.font(.body)
.opacity(self.content.isEmpty ? 0.25 : 1)
.padding()
}

Until we have some API support, an option would be to use the binding string as placeholder and onTapGesture to remove it
TextEditor(text: self.$note)
.padding(.top, 20)
.foregroundColor(self.note == placeholderString ? .gray : .primary)
.onTapGesture {
if self.note == placeholderString {
self.note = ""
}
}

I built a custom view that can be used like this (until TextEditor officially supports it - maybe next year)
TextArea("This is my placeholder", text: $text)
Full solution below:
struct TextArea: View {
private let placeholder: String
#Binding var text: String
init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
}
var body: some View {
TextEditor(text: $text)
.background(
HStack(alignment: .top) {
text.isBlank ? Text(placeholder) : Text("")
Spacer()
}
.foregroundColor(Color.primary.opacity(0.25))
.padding(EdgeInsets(top: 0, leading: 4, bottom: 7, trailing: 0))
)
}
}
extension String {
var isBlank: Bool {
return allSatisfy({ $0.isWhitespace })
}
}
I'm using the default padding of the TextEditor here, but feel free to adjust to your preference.

I modified #bde.dev solution and here is the code sample and a screenshot..
struct TextEditorWithPlaceholder: View {
#Binding var text: String
var body: some View {
ZStack(alignment: .leading) {
if text.isEmpty {
VStack {
Text("Write something...")
.padding(.top, 10)
.padding(.leading, 6)
.opacity(0.6)
Spacer()
}
}
VStack {
TextEditor(text: $text)
.frame(minHeight: 150, maxHeight: 300)
.opacity(text.isEmpty ? 0.85 : 1)
Spacer()
}
}
}
}
And I used it in my view like:
struct UplodePostView: View {
#State private var text: String = ""
var body: some View {
NavigationView {
Form {
Section {
TextEditorWithPlaceholder(text: $text)
}
}
}
}
}

There are some good answers here, but I wanted to bring up a special case. When a TextEditor is placed in a Form, there are a few issues, primarily with spacing.
TextEditor does not horizontally align with other form elements (e.g. TextField)
The placeholder text does not horizontally align with the TextEditor cursor.
When there is whitespace or carriage return/newline are added, the placeholder re-positions to the vertical-middle (optional).
Adding leading spaces causes the placeholder to disappear (optional).
One way to fix these issues:
Form {
TextField("Text Field", text: $text)
ZStack(alignment: .topLeading) {
if comments.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Text("Long Text Field").foregroundColor(Color(UIColor.placeholderText)).padding(.top, 8)
}
TextEditor(text: $comments).padding(.leading, -3)
}
}

With an overlay, you won't be able to allow touch on the placeholder text for the user to write in the textEditor.
You better work on the background, which is a view.
So, create it, while deactivating the default background:
struct PlaceholderBg: View {
let text: String?
init(text:String? = nil) {
UITextView.appearance().backgroundColor = .clear // necessary to remove the default bg
self.text = text
}
var body: some View {
VStack {
HStack{
Text(text!)
Spacer()
}
Spacer()
}
}
}
then, in your textEditor:
TextEditor(text: $yourVariable)
.frame(width: x, y)
.background(yourVariable.isEmpty ? PlaceholderBg(texte: "my placeholder text") : PlaceholderBG(texte:""))

Combined with the answer of #grey, but with white background coverage, you need to remove the background to have an effect
struct TextArea: View {
private let placeholder: String
#Binding var text: String
init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
// Remove the background color here
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
TextEditor(text: $text)
.background(
HStack(alignment: .top) {
text.isBlank ? Text(placeholder) : Text("")
Spacer()
}
.foregroundColor(Color.primary.opacity(0.25))
.padding(EdgeInsets(top: 0, leading: 4, bottom: 7, trailing: 0))
)
}
}
extension String {
var isBlank: Bool {
return allSatisfy({ $0.isWhitespace })
}
}

With iOS 15, you can use FocusState in order to manage the focus state of a TextEditor.
The following code shows how to use FocusState in order to show or hide the placeholder of a TextEditor:
struct ContentView: View {
#State private var note = ""
#FocusState private var isNoteFocused: Bool
var body: some View {
Form {
ZStack(alignment: .topLeading) {
TextEditor(text: $note)
.focused($isNoteFocused)
if !isNoteFocused && note.isEmpty {
Text("Note")
.foregroundColor(Color(uiColor: .placeholderText))
.padding(.top, 10)
.allowsHitTesting(false)
}
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
isNoteFocused = false
}
}
}
}
}

As I know, this is the best way to add a placeholder text to TextEditor in SwiftUI
struct ContentView: View {
#State var text = "Type here"
var body: some View {
TextEditor(text: self.$text)
// make the color of the placeholder gray
.foregroundColor(self.text == "Type here" ? .gray : .primary)
.onAppear {
// remove the placeholder text when keyboard appears
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if self.text == "Type here" {
self.text = ""
}
}
}
// put back the placeholder text if the user dismisses the keyboard without adding any text
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if self.text == "" {
self.text = "Type here"
}
}
}
}
}
}

I like Umayanga's approach but his code wasn't reusable.
Here's the code as a reusable view:
struct TextEditorPH: View {
private var placeholder: String
#Binding var text: String
init(placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
}
var body: some View {
TextEditor(text: self.$text)
// make the color of the placeholder gray
.foregroundColor(self.text == placeholder ? .gray : .primary)
.onAppear {
// create placeholder
self.text = placeholder
// remove the placeholder text when keyboard appears
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if self.text == placeholder {
self.text = ""
}
}
}
// put back the placeholder text if the user dismisses the keyboard without adding any text
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if self.text == "" {
self.text = placeholder
}
}
}
}
}
}

Here is how I solved it.
I used a Text for the placeholder together with the TextEditor in a ZStack.
The first problem was that since the Text is opaque, it would prevent the TextEditor from becoming focused if you tapped on the area covered by the Text. Tapping on any other area would make the TextEditor focused.
So I solved it by adding a tap gesture with the new iOS 15 #FocusState property wrapper.
The second problem was that the TextEditor was not properly aligned to the left of the placeholder so I added a negative .leading padding to solve that.
struct InputView: View {
#State var text: String = ""
#FocusState var isFocused: Bool
var body: some View {
ZStack(alignment: .leading) {
TextEditor(text: $text)
.font(.body)
.padding(.leading, -4)
.focused($isFocused, equals: true)
if text.isEmpty {
Text("Placeholder text...")
.font(.body)
.foregroundColor(Color(uiColor: .placeholderText))
.onTapGesture {
self.isFocused = true
}
}
}
}
}
Hopefully it is natively supported in the future.

SwiftUI TextEditor does not yet have support for a placeholder. As a result, we have to "fake" it.
Other solutions had problems like bad alignment or color issues. This is the closest I got to simulating a real placeholder. This solution "overlays" a TextField over the TextEditor. The TextField contains the placeholder. The TextField gets hidden as soon as a character is inputted into the TextEditor.
import SwiftUI
struct Testing: View {
#State private var textEditorText = ""
#State private var textFieldText = ""
var body: some View {
VStack {
Text("Testing Placeholder Example")
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
TextEditor(text: $textEditorText)
.padding(EdgeInsets(top: -7, leading: -4, bottom: -7, trailing: -4)) // fix padding not aligning with TextField
if textEditorText.isEmpty {
TextField("Placeholder text here", text: $textFieldText)
.disabled(true) // don't allow for it to be tapped
}
}
}
}
}
struct Testing_Previews: PreviewProvider {
static var previews: some View {
Testing()
}
}

I've read all the comments above (and in the Internet at all), combined some of them and decided to come to this solution:
Create custom Binding wrapper
Create TextEditor and Text with this binding
Add some modifications to make all this pixel-perfect.
Let's start with creating wrapper:
extension Binding where Value: Equatable {
init(_ source: Binding<Value?>, replacingNilWith nilProxy: Value) {
self.init(
get: { source.wrappedValue ?? nilProxy },
set: { newValue in
if newValue == nilProxy {
source.wrappedValue = nil
} else {
source.wrappedValue = newValue
}
})
}
}
Next step is to initialize our binding as usual:
#State private var yourTextVariable: String?
After that put TextEditor and Text in the ZStack:
ZStack(alignment: .topLeading) {
Text(YOUR_HINT_TEXT)
.padding(EdgeInsets(top: 6, leading: 4, bottom: 0, trailing: 0))
.foregroundColor(.black)
.opacity(yourTextVariable == nil ? 1 : 0)
TextEditor(text: Binding($yourTextVariable, replacingNilWith: ""))
.padding(.all, 0)
.opacity(yourTextVariable != nil ? 1 : 0.8)
}
And this will give us pixel-perfect UI with needed functionality:
https://youtu.be/T1TcSWo-Mtc

We can create a custom view to add placeholder text in the TextEditor.
Here is my solution:
AppTextEditor.swift
import SwiftUI
// MARK: - AppTextEditor
struct AppTextEditor: View {
#Binding var message: String
let placeholder: LocalizedStringKey
var body: some View {
ZStack(alignment: .topLeading) {
if message.isEmpty {
Text(placeholder)
.padding(8)
.font(.body)
.foregroundColor(Color.placeholderColor)
}
TextEditor(text: $message)
.frame(height: 100)
.opacity(message.isEmpty ? 0.25 : 1)
}
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.placeholderColor, lineWidth: 0.5))
}
}
// MARK: - AppTextEditor_Previews
struct AppTextEditor_Previews: PreviewProvider {
static var previews: some View {
AppTextEditor(message: .constant(""), placeholder: "Your Message")
.padding()
}
}
Color+Extensions.swift
extension Color {
static let placeholderColor = Color(UIColor.placeholderText)
}
Usage:
struct YourView: View {
#State var message = ""
var body: some View {
AppTextEditor(message: $message, placeholder: "Your message")
.padding()
}
}

I did it this way:
TextEditor(text: $bindingVar)
.font(.title2)
.onTapGesture{
placeholderText = true
}
.frame(height: 150)
.overlay(
VStack(alignment: .leading){
HStack {
if !placeholderText {
Text("Your placeholdergoeshere")
.font(.title2)
.foregroundColor(.gray)
}
Spacer()
}
Spacer()
})

None of the suggested answers was helpful for me, When the user taps the TextEditor, it should hide the placeholder. Also there's a nasty bug from Apple that doesn't allow you to properly change the TextEditor's background color (iOS 15.5 time of writing this) I provided my refined code here.
Make sure add this code at the app initialization point:
#main
struct MyApplication1: App {
let persistenceController = PersistenceController.shared
init(){
UITextView.appearance().backgroundColor = .clear // <-- Make sure to add this line
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
struct PlaceHolderTextEditor: View {
let cornerRadius:CGFloat = 8
let backgroundColor:Color = .gray
let placeholder: String
#Binding var text: String
#FocusState private var isFocused: Bool
var body: some View {
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) {
TextEditor(text: $text)
.focused($isFocused)
.onChange(of: isFocused) { isFocused in
self.isFocused = isFocused
}
.opacity((text.isEmpty && !isFocused) ? 0.02 : 1)
.foregroundColor(.white)
.frame(height:150)
.background(backgroundColor)
if text.isEmpty && !isFocused {
Text(placeholder)
.padding(.top, 8)
.padding(.leading,8)
}
}.cornerRadius(cornerRadius)
}
}

textEditor{...}.onTapGesture {
if text == placeholder {
self.text = ""
}
}.onAppear {
text = placeholder
}
Button {
text = placeholder
isFocused = false
}....

Fighting TextEditor recently I use this as an approximate and simple solution
TextEditor(text: dvbEventText)
.overlay(alignment:.topLeading)
{
Text(dvbEventText.wrappedValue.count == 0 ? "Enter Event Text":"")
.foregroundColor(Color.lightGray)
.disabled(true)
}
As soon as you start typing the hint goes away and the prompt text is where you type.
FWIW

Related

SwiftUI bottom safe area does not shrink after keyboard was shown

If after activating TextField I press Open link button, NavigationLink will be opened. After that if I return back to previous screen, VStack with TextField will stay in the middle of the screen, because bottom SafeArea will be expanded by keyboard. This happening if first view in ZStack is ScrollView. It should go back to bottom after keyboard is disabled. How can I fix that?
struct ContentView: View {
#State private var text = ""
var body: some View {
NavigationStack {
ZStack(alignment: .bottom) {
ScrollView {
Color.green.opacity(0.2)
.frame(height: 1000)
}
.ignoresSafeArea(.keyboard)
VStack {
TextField("", text: $text, prompt: Text("Input"))
.textFieldStyle(.roundedBorder)
.padding()
NavigationLink("Open link") {
Text("Details view")
}
}
.background { Color.red }
.background(ignoresSafeAreaEdges: .bottom)
}
}
}
}
You can try using the #FocusState property wrapper. Add 3 following command lines:
//1
#FocusState private var nameIsFocused: Bool
//2
.focused($nameIsFocused)
//3
.simultaneousGesture(TapGesture().onEnded({ _ in
nameIsFocused = false
}))
The code you wrote looks like this:
struct ContentView: View {
#State private var text = ""
//1
#FocusState private var nameIsFocused: Bool
var body: some View {
NavigationStack {
ZStack(alignment: .bottom) {
ScrollView {
Color.green.opacity(0.2)
.frame(height: 1000)
}
.ignoresSafeArea(.keyboard)
VStack {
TextField("", text: $text, prompt: Text("Input"))
//2
.focused($nameIsFocused)
.textFieldStyle(.roundedBorder)
.padding()
NavigationLink("Open link") {
Text("Details view")
}
//3
.simultaneousGesture(TapGesture().onEnded({ _ in
nameIsFocused = false
}))
}
.background { Color.red }
.background(ignoresSafeAreaEdges: .bottom)
}
}
}
}
Result:
Hope it is useful for you!

How to move text and change it's value at the same time in SwiftUI?

For example, this is what happening right now
struct ContentView: View {
#State var titleLable = "This is basic text"
#State var isTextAnimated: Bool = false
var body: some View {
VStack {
Text(titleLable)
.offset(y: isTextAnimated ? 300 : 0)
.animation(.linear)
Button {
isTextAnimated.toggle()
if isTextAnimated {
titleLable = "New text appeared"
} else {
titleLable = "This is basic text"
}
} label: {
Text("Press")
}
}
.padding()
}
The code above leads to this in Live Preview:
click there
This happens if text doesn't change its value ( I need this behaviour with changing ):
click there
One of the simplest way to achieve this animation is to embed two Text inside a ZStackand modify their opacity, and modify the ZStack's offset rather than the individual Texts. in this way both the offset and the change between two texts will get animated. here is my code:
struct HomeScreen: View {
#State var isTextAnimated: Bool = false
var body: some View {
ZStack{
Text("Hello")
.opacity(isTextAnimated ? 1 : 0)
Text("World")
.opacity(isTextAnimated ? 0 : 1)
}
.offset(y: isTextAnimated ? 150 : 0)
Button(action: {withAnimation{isTextAnimated.toggle()}}){
Text("Press")
}
}
}
To animate the position and the content of the Text label, you can use matchedGeometryEffect, as follows:
struct ContentView: View {
#State var isTextAnimated: Bool = false
#Namespace var namespace
var body: some View {
VStack {
if isTextAnimated {
Text("New text appeared")
.matchedGeometryEffect(id: "title", in: namespace)
.offset(y: 300)
} else {
Text("This is basic text")
.matchedGeometryEffect(id: "title", in: namespace)
}
Button {
withAnimation {
isTextAnimated.toggle()
}
} label: {
Text("Press")
}
}
.padding()
}
}
edit: I forgot to animate the text change
struct AnimationsView: View {
#State private var buttonWasToggled = false
#Namespace private var titleAnimationNamespace
var body: some View {
VStack {
if !buttonWasToggled {
Text("This is some text")
.matchedGeometryEffect(id: "text", in: titleAnimationNamespace)
.transition(.opacity)
} else {
Text("Another text")
.matchedGeometryEffect(id: "text", in: titleAnimationNamespace)
.transition(.opacity)
.offset(y: 300)
}
Button("Press me") {
withAnimation {
buttonWasToggled.toggle()
}
}
}
}
}
A good way to animate such change is to animate the offset value rather than toggle a boolean:
struct AnimationsView: View {
#State private var title = "This is basic text"
#State private var offset: CGFloat = 0
var body: some View {
VStack {
Text("Some text")
.offset(y: offset)
Button("Press me") {
withAnimation {
// If we already have an offset, jump back to the previous position
offset = offset == 0 ? 300 : 0
}
}
}
}
}
or by using a boolean value:
struct AnimationsView: View {
#State private var title = "This is basic text"
#State private var animated = false
var body: some View {
VStack {
Text("Some text")
.offset(y: animated ? 300 : 0)
Button("Press me") {
withAnimation {
animated.toggle()
}
}
}
}
}
Note the important withAnimation that indicates to SwiftUI that you want to animate the changes made in the block. You can find the documentation here
The .animation(...) is optional and used if you want to change the behavior of the animation, such as using a spring, changing the speed, adding a delay etc... If you don't specify one, SwiftUI will use a default value. In a similar fashion, if you don't want a view to animate, you can use add the .animation(nil) modifier to prevent SwiftUI from animating said view.
Both solutions provided result in the following behavior : https://imgur.com/sOOsFJ0
As an alternative to .matchedGeometryEffect to animate moving and changing value of Text view you can "rasterize" text using .drawingGroup() modifier for Text. This makes text behave like shape, therefore animating smoothly. Additionally it's not necessary to define separate with linked with .machtedGeometryEffect modifier which can be impossible in certain situation. For example when new string value and position is not known beforehand.
Example
struct TextAnimation: View {
var titleLabel: String {
if self.isTextAnimated {
return "New text appeared"
} else {
return "This is basic text"
}
}
#State var isTextAnimated: Bool = false
var body: some View {
VStack {
Text(titleLabel)
.drawingGroup() // ⬅️ It makes text behave like shape.
.offset(y: isTextAnimated ? 100 : 0)
.animation(.easeInOut, value: self.isTextAnimated)
Button {
isTextAnimated.toggle()
} label: {
Text("Press")
}
}
.padding()
}
}
More informations
Apple's documentation about .drawingGroup modifier

Swift - Updating Binding<String> stored value programmatically from View extension

So my goal is to have a more convenient method for adding a placeholder text value on SwiftUI's TextEditor, since there doesn't appear to be one. The approach I'm trying has uncovered something I really don't understand around Binding<> wrapped types. (Maybe this is a red flag that I'm doing something not recommended?)
Anyway, on to my question: are we able to programmatically update the underlying values on Bindings? If I accept some Binding<String> value, can I update it from within my method here? If so, will the updated value be referenced by the #State originator? The below example places my placeholder value in as text where I'm trying to type when you click into it, and does not even attempt it again if I clear it out.
Imported this code from other posts I found some time ago to make it display a placeholder if the body is empty.
import Foundation
import SwiftUI
struct TextEditorViewThing: View {
#State private var noteText = ""
var body: some View {
VStack{
TextEditor(text: $noteText)
.textPlaceholder(placeholder: "PLACEHOLDER", text: $noteText)
.padding()
}
}
}
extension TextEditor {
#ViewBuilder func textPlaceholder(placeholder: String, text: Binding<String>) -> some View {
self.onAppear {
// remove the placeholder text when keyboard appears
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if text.wrappedValue == placeholder {
text.wrappedValue = placeholder
}
}
}
// put back the placeholder text if the user dismisses the keyboard without adding any text
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if text.wrappedValue == "" {
text.wrappedValue = placeholder
}
}
}
}
}
}
Customize this setup as per your requirement:
struct ContentView: View {
#State private var text: String = ""
var body: some View {
VStack {
ZStack(alignment: .leading) {
if self.text.isEmpty {
VStack {
Text("Placeholder Text")
.multilineTextAlignment(.leading)
.padding(.leading, 25)
.padding(.top, 8)
.opacity(0.5)
Spacer()
}
}
TextEditor(text: $text)
.padding(.leading, 20)
.opacity(self.text.isEmpty ? 0.5 : 1)
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height/2)
.overlay(
Rectangle().stroke()
.foregroundColor(Color.black)
.padding(.horizontal, 15)
)
}
}
}

How to stop SwiftUI change the fixed padding when we switch button content between Text and Image?

So I have two buttons on the bottom of my screen, button A and button B, somewhere along the line I need to replace the image in the button with text, so we do so by changing the Bool in the top.
Although we apply the same modifiers, the padding of button B changes, and the UI moves around, it seems as if the text claims more space. Desired situation: button A and B, should not move around when changing the button Image to Text.
import SwiftUI
private var showImage: Bool = true
struct SwiftUIView: View {
var body: some View {
VStack {
Spacer()
Button(action: {
print("CLICK")
}) {
Image(systemName: "a.circle")
.modifier(TestButtonModifier())
}
.padding(10)
Button(action: {
print("CLICK")
}) {
if showImage {
Image(systemName: "b.circle")
.modifier(TestButtonModifier())
} else {
Text("B")
.modifier(TestButtonModifier())
}
}
.padding(10)
} //: VSTACK
}
}
struct TestButtonModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.system(size: 52, weight: .regular))
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 100)
.background(Color.black)
.padding(2)
.foregroundColor(Color.white)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
It is due to VStack spacing, which by default differs between different pairs of subviews, so specify some explicit (or remove at all, ie set to zero)
struct SwiftUIView: View {
var body: some View {
VStack(spacing: 0) { // << here !!
// .. other code

TextField SwiftUI Dismiss Keyboard

How can I dismiss the keyboard after the user clicks outside the TextField using SwiftUI?
I created a TextField using SwiftUI, but I couldn't find any solution for dismissing the keyboard if the user clicks outside the TextField. I took a look at all attributes of TextField and also the SwiftUI TextField documentation and I couldn't find anything related with dismissing keyboard.
This is my view's code:
struct InputView: View {
#State var inputValue : String = ""
var body: some View {
VStack(spacing: 10) {
TextField("$", text: $inputValue)
.keyboardType(.decimalPad)
}
}
}
This can be done with a view modifier.
Code
public extension View {
func dismissKeyboardOnTap() -> some View {
modifier(DismissKeyboardOnTap())
}
}
public struct DismissKeyboardOnTap: ViewModifier {
public func body(content: Content) -> some View {
#if os(macOS)
return content
#else
return content.gesture(tapGesture)
#endif
}
private var tapGesture: some Gesture {
TapGesture().onEnded(endEditing)
}
private func endEditing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
.first?.windows
.filter {$0.isKeyWindow}
.first?.endEditing(true)
}
}
Usage
backgroundView()
.dismissKeyboardOnTap()
Check out the demo here: https://github.com/youjinp/SwiftUIKit
here is the solution using DragGesture it's working.
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("My Text", text: $text)
.keyboardType(.decimalPad)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
.gesture(
TapGesture()
.onEnded { _ in
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
)
}
}
Add tap gesture to most outer view and call extension method inside tap gesture closure.
struct InputView: View {
#State var inputValue : String = ""
var body: some View {
VStack(spacing: 10) {
TextField("$", text: $inputValue)
.keyboardType(.decimalPad)
} .onTapGesture(perform: {
self.endTextEditing()
})
}
}
extension View {
func endTextEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
TextField("Phone Number", text: $no)
.keyboardType(.numbersAndPunctuation)
.padding()
.background(Color("4"))
.clipShape(RoundedRectangle(cornerRadius: 10))
.offset(y:-self.value).animation(.spring()).onAppear() {
NotificationCenter.default.addObserver(forName:UIResponder.keyboardWillShowNotification, object: nil, queue: .main){ (notif)in
let value = notif.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
let height = value.height
self.value = height
}
NotificationCenter.default.addObserver(forName:UIResponder.keyboardWillHideNotification, object: nil, queue: .main){ (notification)in
self.value = 0
}
}
In SwiftUI 3 #FocusState wrapper can be used to remove or switch focus from editable fields. When the focus is removed from field, keyboard dismisses. So in your case it is just a matter of giving space and gesture to the surrounding space of TextView.
struct ContentView: View {
#State var inputValue : String = ""
#FocusState private var inputIsFocused: Bool
var body: some View {
VStack(spacing: 10) {
TextField("$", text: $inputValue)
.keyboardType(.decimalPad)
.border(Color.green)
.focused($inputIsFocused)
}
.frame(maxHeight: .infinity) // If input is supposed to be in the center
.background(.yellow)
.onTapGesture {
inputIsFocused = false
}
}
}
But we can do more interesting things with #FocusState. How about switching from field to field in a form. And if you tap away, keyboard also dismisses.
struct ContentView: View {
enum Field {
case firstName
case lastName
case emailAddress
}
#State private var firstName = ""
#State private var lastName = ""
#State private var emailAddress = ""
#FocusState private var focusedField: Field?
var body: some View {
ZStack {
VStack {
TextField("Enter first name", text: $firstName)
.focused($focusedField, equals: .firstName)
.textContentType(.givenName)
.submitLabel(.next)
TextField("Enter last name", text: $lastName)
.focused($focusedField, equals: .lastName)
.textContentType(.familyName)
.submitLabel(.next)
TextField("Enter email address", text: $emailAddress)
.focused($focusedField, equals: .emailAddress)
.textContentType(.emailAddress)
.submitLabel(.join)
}
.onSubmit {
switch focusedField {
case .firstName:
focusedField = .lastName
case .lastName:
focusedField = .emailAddress
default:
print("Creating account…")
}
}
}
.textFieldStyle(.roundedBorder)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle()) // So ZStack becomes clickable
.onTapGesture {
focusedField = nil
}
}
}

Resources