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

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

Related

SwiftUI - How to focus a TextField within a sheet as it is appearing?

I have a search TextField within a View that is triggered to appear within a sheet on top of the ContentView.
I'm able to automatically focus this TextField when the sheet appears using #FocusState and onAppear, however, I'm finding that the sheet needs to fully appear before the TextField is focused and the on screen keyboard appears.
This feels quite slow and I've noticed in many other apps that they are able to trigger the on screen keyboard and the sheet appearing simultaneously.
Here is my code:
struct ContentView: View {
#State var showSearch = false
var body: some View {
Button {
showSearch = true
} label: {
Text("Search")
}
.sheet(isPresented: $showSearch) {
SearchView()
}
}
}
struct SearchView: View {
#State var searchTerm = ""
#FocusState private var searchFocus: Bool
var body: some View {
TextField("Search", text: $searchTerm)
.focused($searchFocus)
.onAppear() {
searchFocus = true
}
}
}
Is there a different way to do this that will make the keyboard appear as the sheet is appearing, making the overall experience feel more seamless?
Here is an approach with a custom sheet that brings in the keyboard somewhat earlier. Not sure if its worth the effort though:
struct ContentView: View {
#State var showSearch = false
var body: some View {
ZStack {
Button {
withAnimation {
showSearch = true
}
} label: {
Text("Search")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
if showSearch {
SearchView(isPresented: $showSearch)
.transition(.move(edge: .bottom))
}
}
// .sheet(isPresented: $showSearch) {
// SearchView()
// }
}
}
struct SearchView: View {
#Binding var isPresented: Bool
#State var searchTerm = ""
#FocusState private var searchFocus: Bool
var body: some View {
Form {
TextField("Search", text: $searchTerm)
.focused($searchFocus)
Button("Close") {
searchFocus = false
withAnimation {
isPresented = false
}
}
}
.onAppear() {
searchFocus = true
}
}
}

How to despawn a Button and spawn a scrollView xcode swiftui [duplicate]

How do I toggle the presence of a button to be hidden or not?
We have the non-conditional .hidden() property; but I need the conditional version.
Note: we do have the .disabled(bool) property available, but not the .hidden(bool).
struct ContentView: View {
var body: some View {
ZStack {
Color("SkyBlue")
VStack {
Button("Detect") {
self.imageDetectionVM.detect(self.selectedImage)
}
.padding()
.background(Color.orange)
.foreggroundColor(Color.white)
.cornerRadius(10)
.hidden() // ...I want this to be toggled.
}
}
}
}
I hope hidden modifier gets argument later, but since then, Set the alpha instead:
#State var shouldHide = false
var body: some View {
Button("Button") { self.shouldHide = true }
.opacity(shouldHide ? 0 : 1)
}
For me it worked perfectly to set the frame's height to zero when you do not want to see it. When you want to have the calculated size, just set it to nil:
SomeView
.frame(height: isVisible ? nil : 0)
If you want to disable it in addition to hiding it, you could set .disabled with the toggled boolean.
SomeView
.frame(height: isVisible ? nil : 0)
.disabled(!isVisible)
You can utilize SwiftUI's new two-way bindings and add an if-statement as:
struct ContentView: View {
#State var shouldHide = false
var body: some View {
ZStack {
Color("SkyBlue")
VStack {
if !self.$shouldHide.wrappedValue {
Button("Detect") {
self.imageDetectionVM.detect(self.selectedImage)
}
.padding()
.background(Color.orange)
.foregroundColor(Color.white)
.cornerRadius(10)
}
}
}
}
}
The benefit of doing this over setting the opacity to 0 is that it will remove the weird spacing/padding from your UI caused from the button still being in the view, just not visible (if the button is between other view components, that is).
all the answers here works specifically for a button to be hidden conditionally.
What i think might help is making a modifier itself conditionally e.g:
.hidden for button/view, or maybe .italic for text, etc..
Using extensions.
For text to be conditionally italic it is easy since .italic modifier returns Text:
extension Text {
func italicConditionally(isItalic: Bool) -> Text {
isItalic ? self.italic() : self
}
}
then applying conditional italic like this:
#State private var toggle = false
Text("My Text")
.italicConditionally(isItalic: toggle)
However for Button it is tricky, since the .hidden modifier returns "some view":
extension View {
func hiddenConditionally(isHidden: Bool) -> some View {
isHidden ? AnyView(self.hidden()) : AnyView(self)
}
}
then applying conditional hidden like this:
#State private var toggle = false
Button("myButton", action: myAction)
.hiddenConditionally(isHidden: toggle)
You can easily hide a view in SwiftUI using a conditional statement.
struct TestView: View{
#State private var isVisible = false
var body: some View{
if !isVisible {
HStack{
Button(action: {
isVisible.toggle()
// after click you'r view will be hidden
}){
Text("any view")
}
}
}
}
}
It isn't always going to be a pretty solution, but in some cases, adding it conditionally may also work:
if shouldShowMyButton {
Button(action: {
self.imageDetectionVM.detect(self.selectedImage)
}) {
Text("Button")
}
}
There will be an issue of the empty space in the case when it isn't being shown, which may be more or less of an issue depending on the specific layout. That might be addressed by adding an else statement that alternatively adds an equivalently sized blank space.
#State private var isHidden = true
VStack / HStack
if isHidden {
Button {
if !loadVideo(),
let urlStr = drill?.videoURL as? String,
let url = URL(string: urlStr) {
player = VideoPlayerView(player: AVPlayer(), videoUrl: url)
playVideo.toggle()
}
} label: {
Image(playVideo ? "ic_close_blue" : "ic_video_attached")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50)
}
.buttonStyle(BorderlessButtonStyle())
}
.onAppear {
if shouldShowButton {
isHidden = false
} else {
isVideoButtonHidden = true
}
}

How to change body background color with if in SwiftUI

I'm creating a simple iOS app with SwiftUI, and I'd like to change my view's background color when switch toggle change.
My code
struct ContentView: View {
#State private var isOnLight: Bool = false
var body: some View {
VStack {
Toggle(isOn: $isOnLight) {
Text("Switch")
.font(.title)
.foregroundColor(.gray)
}
if isOnLight {
}
}.padding()
}
}
For background colors you can use the ZStack like this and with one line ifs then decide on the color
struct ContentView: View {
#State private var isOnLight: Bool = false
var body: some View {
ZStack {
isOnLight ? Color.blue : Color.red
VStack {
Toggle(isOn: $isOnLight) {
Text("Switch")
.font(.title)
.foregroundColor(.gray)
}
}
.padding()
}
}
}
To learn about how to use ternary operator in SwiftUI you can watch this video
You just need to embed your VStack inside a ZStack, where the back layer is a color that changes every time isOnLight changes.
Like this:
struct Example: View {
#State private var isOnLight: Bool = false
#State private var color: Color = .white
var body: some View {
ZStack {
color
.ignoresSafeArea()
VStack {
Toggle(isOn: $isOnLight) {
Text("Switch")
.font(.title)
.foregroundColor(.gray)
}
}
.padding()
}
.onChange(of: isOnLight) { value in
if value {
color = .yellow
} else {
color = .white
}
}
}
}

SwiftUI - Animate View expansion (show / hide)

I have a View that contains a HStack and a DatePicker. When you tap on the HStack, the DatePicker is shown / hidden. I want to animate this action like the animation of Starts and Ends row in iOS Calendar's New Event View.
struct TimePicker: View {
#Binding var startTime: Date
#State private var isDatePickerVisible: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack {
ListItemView(icon: "start-time",
leadingText: "Start Time",
trailingText: startTime.stringTime())
}
.onTapGesture {
withAnimation {
self.isDatePickerVisible.toggle()
}
}
Group {
if isDatePickerVisible {
DatePicker("", selection: $startTime, displayedComponents: [.hourAndMinute])
.datePickerStyle(WheelDatePickerStyle())
}
}
.background(Color.red)
.modifier(AnimatingCellHeight(height: isDatePickerVisible ? 300 : 0))
}
}
}
I have used the following code for animation. It almost works. The only problem is that HStack jumps. And I can not fix it.
https://stackoverflow.com/a/60873883/8292178
struct AnimatingCellHeight: AnimatableModifier {
var height: CGFloat = 0
var animatableData: CGFloat {
get { height }
set { height = newValue }
}
func body(content: Content) -> some View {
content.frame(height: height)
}
}
How to fix this issue? How to animate visibility of the DatePicker?
It's simple, you don't need extra ViewModifier
struct TimePicker: View {
#Binding var startTime: Date
#State private var isDatePickerVisible: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack {
ListItemView(icon: "start-time"
, leadingText: "Start Time"
, trailingText: startTime.stringTime())
}.onTapGesture {
isDatePickerVisible.toggle()
}
if isDatePickerVisible {
DatePicker(""
, selection: $model.startTime
, displayedComponents: [.hourAndMinute]
).datePickerStyle(WheelDatePickerStyle())
}
}.animation(.spring())
}
}

How to add placeholder text to TextEditor in SwiftUI?

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

Resources