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!
When opening a context menu, the keyboard disappears and the active textfield will become unfocused. Is there a way to keep the keyboard open and the active textfield focused?
This short example demonstrates the issue:
struct ContentView: View {
#State private var text: String = ""
var body: some View {
TextField(
"Enter text",
text: $text
)
.contextMenu {
Button(action: {
print("Hello")
}) {
Text("Hello")
}
}
}
}
Before opening the context menu the keyboard is open and the text field is focused:
After opening the context menu the keyboard is closed and the textfield becomes unfocused:
You need to put FocusState in a onAppear inside of a DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {}
This solves the problem:
struct ContentView: View {
#FocusState private var focusOn: Bool
#State private var text = ""
var body: some View {
TextField("Enter text", text: $text)
.disableAutocorrection(true)
.keyboardType(.default)
.focused($focusOn)
.padding()
.contextMenu {
Button {
print("Hello")
} label: { Text("Hello") }
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
focusOn = true
}
}
.submitLabel(.go)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button { hideKeyboard() } label: {
Image(systemName: "keyboard.chevron.compact.down")
.padding()
}
}
}
}
public func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
I'm trying to show a view with a loader in full screen. I want also to overlay the TabBar, but I don't know how to do it. Let me show my code.
This is ProgressViewModifier.
// MARK: - View - Extension
extension View {
/// Show a loader binded to `isShowing` parameter.
/// - Parameters:
/// - isShowing: `Bool` value to indicate if the loader is to be shown or not.
/// - text: An optional text to show below the spinning circle.
/// - color: The color of the spinning circle.
/// - Returns: The loader view.
func progressView(
isShowing: Binding <Bool>,
backgroundColor: Color = .black,
dimBackground: Bool = false,
text : String? = nil,
loaderColor : Color = .white,
scale: Float = 1,
blur: Bool = false) -> some View {
self.modifier(ProgressViewModifier(
isShowing: isShowing,
backgroundColor: backgroundColor,
dimBackground: dimBackground,
text: text,
loaderColor: loaderColor,
scale: scale,
blur: blur)
)
}
}
// MARK: - ProgressViewModifier
struct ProgressViewModifier : ViewModifier {
#Binding var isShowing : Bool
var backgroundColor: Color
var dimBackground: Bool
var text : String?
var loaderColor : Color
var scale: Float
var blur: Bool
func body(content: Content) -> some View {
ZStack { content
if isShowing {
withAnimation {
showProgressView()
}
}
}
}
}
// MARK: - Private methods
extension ProgressViewModifier {
private func showProgressView() -> some View {
ZStack {
Rectangle()
.fill(backgroundColor.opacity(0.7))
.ignoresSafeArea()
.background(.ultraThinMaterial)
VStack (spacing : 20) {
if isShowing {
ProgressView()
.tint(loaderColor)
.scaleEffect(CGFloat(scale))
if text != nil {
Text(text!)
.foregroundColor(.black)
.font(.headline)
}
}
}
.background(.clear)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
This is the RootTabView, the one containing the TabBar.
struct RootTabView: View {
var body: some View {
TabView {
AddEverydayExpense()
.tabItem {
Label("First", systemImage: "1.circle")
}
AddInvestment()
.tabItem {
Label("Second", systemImage: "2.circle")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
RootTabView()
}
}
This is my view.
struct AddEverydayExpense: View {
#ObservedObject private var model = AddEverydayExpenseVM()
#State private var description: String = ""
#State private var cost: String = ""
#State private var date: Date = Date()
#State private var essential: Bool = false
#State private var month: Month?
#State private var category: Category?
private var isButtonDisabled: Bool {
return description.isEmpty ||
cost.isEmpty ||
month == nil ||
category == nil
}
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("", text: $description, prompt: Text("Descrizione"))
TextField("", text: $cost, prompt: Text("10€"))
.keyboardType(.numbersAndPunctuation)
DatePicker(date.string(withFormat: "EEEE"), selection: $date)
HStack {
CheckboxView(checked: $essential)
Text("È considerata una spesa essenziale?")
}
.onTapGesture {
essential.toggle()
}
}
Section {
Picker(month?.name ?? "Mese di riferimento", selection: $month) {
ForEach(model.months) { month in
Text(month.name).tag(month as? Month)
}
}
Picker(category?.name ?? "Categoria", selection: $category) {
ForEach(model.categories) { category in
Text(category.name).tag(category as? Category)
}
}
}
Section {
Button("Invia".uppercased()) { print("Button") }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.font(.headline)
.listRowBackground(isButtonDisabled ? Color.gray.opacity(0.5) : Color.blue)
.foregroundColor(Color.white.opacity(isButtonDisabled ? 0.5 : 1))
.disabled(!isButtonDisabled)
}
}
Spacer()
}
.navigationTitle("Aggiungi Spesa")
}
.progressView(isShowing: $model.isFetching, blur: true)
}
}
As you can see, there is the line .progressView(isShowing: $model.isFetching, blur: true) that does the magic. The problem is that the loader is only shown on the current view, but not on the tab. .
How can I achieve the result?
If you want the progress view to cover the entire view (including the tab bar), it has to be in the view hierarchy at or above the TabBar. Right now, it's below the TabBar in the child views.
Because the state will need to be passed up to the parent (the owner of the TabBar), you'll need some sort of state management that you can pass down to the children. This could mean just passing a Binding to a #State. I've chosen to show how to achieve this with an ObservableObject passed down the hierarchy using an #EnvironmentObject so that you don't have to explicitly pass the dependency.
class ProgressManager : ObservableObject {
#Published var inProgress = false
}
struct ContentView : View {
#StateObject private var progressManager = ProgressManager()
var body: some View {
TabView {
AddEverydayExpense()
.tabItem {
Label("First", systemImage: "1.circle")
}
AddInvestment()
.tabItem {
Label("Second", systemImage: "2.circle")
}
}
.environmentObject(progressManager)
.progressView(isShowing: $progressManager.inProgress) //<-- Note that this is outside of the `TabBar`
}
}
struct AddEverydayExpense : View {
#EnvironmentObject private var progressManager : ProgressManager
var body: some View {
Button("Progress") {
progressManager.inProgress = true
}
}
}
I have a ScrollView with a ForEach loop, each rendering a View. In the View I have 3 renders of the below ActionItem (a button that displays a sheet). The sheet does not show up with ScrollView but does with List. I'd normally attach the .sheet at the ScrollView layer however, with each button rendering a different view it seems more appropriate to nest it.
How I could get this to work with ScrollView? I'm using Xcode 12
struct ActionItem<Content>: View where Content : View {
public var text: String
public var icon: String
public var content: Content
#State var isPresented = false
init(text: String, icon: String, #ViewBuilder content: () -> Content) {
self.text = text
self.icon = icon
self.content = content()
}
var body: some View {
Button (action: {
DispatchQueue.main.async {
withAnimation {
self.isPresented = true
}
}
}) {
HStack(spacing: 2) {
Image(systemName: icon).font(.system(size: 14, weight: .semibold))
Text(text).fontWeight(.semibold)
}.padding([.top, .bottom], Dimensions.spacing)
.padding([.leading, .trailing], Dimensions.spacingMedium)
}.foregroundColor(Color.gray).font(.subheadline).background(Color.grayWhiteTer)
.cornerRadius(Dimensions.spacing)
.sheet(isPresented: $isPresented) {
self.content
}
}
}
In the View I'd render ActionItem such as Text, this also occurs if the View is ignored and the ActionItem is just directly in the ForEach. Same issue, sheet does not appear.
ActionItem(text: "", icon: "pencil") {
Text("ok")
}
The list looks like this
import SwiftUI
struct ItemsList: View {
#ObservedObject var itemModel: ItemModel
var body: some View {
VStack(alignment: .center, spacing: 0) {
ScrollView {
VStack {
ForEach(itemModel.items, id: \.self) { item in
ItemView(item: item)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}}
Suggested callback update
struct ActionItem<Content>: View where Content : View {
public var text: String
public var icon: String
public var content: () -> Content
#State var isPresented = false
init(text: String, icon: String, #ViewBuilder content: #escaping () -> Content) {
self.text = text
self.icon = icon
self.content = content
}
var body: some View {
Button (action: {
DispatchQueue.main.async {
withAnimation {
self.isPresented = true
}
}
}) {
HStack(spacing: 2) {
Image(systemName: icon).font(.system(size: 14, weight: .semibold))
Text(text).fontWeight(.semibold)
}.padding([.top, .bottom], Dimensions.spacing)
.padding([.leading, .trailing], Dimensions.spacingMedium)
}.foregroundColor(Color.gray).font(.subheadline).background(Color.grayWhiteTer)
.cornerRadius(Dimensions.spacing)
.sheet(isPresented: $isPresented) {
self.content()
}
}
}
Try saving content as a callback (i.e. () -> Content) and call it in the sheet method instead of calling it in the initializer.. This will change when the view is created.
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