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

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)
)
}
}
}

Related

Why is my Text Editor not updating to my selected background color? [duplicate]

TextEditor seems to have a default white background. So the following is not working and it displayed as white instead of defined red:
var body: some View {
TextEditor(text: .constant("Placeholder"))
.background(Color.red)
}
Is it possible to change the color to a custom one?
iOS 16
You should hide the default background to see your desired one:
TextEditor(text: .constant("Placeholder"))
.scrollContentBackground(.hidden) // <- Hide it
.background(.red) // To see this
iOS 15 and below
TextEditor is backed by UITextView. So you need to get rid of the UITextView's backgroundColor first and then you can set any View to the background.
struct ContentView: View {
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
List {
TextEditor(text: .constant("Placeholder"))
.background(.red)
}
}
}
Demo
You can find my simple trick for growing TextEditor here in this answer
Pure SwiftUI solution on iOS and macOS
colorMultiply is your friend.
struct ContentView: View {
#State private var editingText: String = ""
var body: some View {
TextEditor(text: $editingText)
.frame(width: 400, height: 100, alignment: .center)
.cornerRadius(3.0)
.colorMultiply(.gray)
}
}
Update iOS 16 / SwiftUI 4.0
You need to use .scrollContentBackground(.hidden) instead of UITextView.appearance().backgroundColor = .clear
https://twitter.com/StuFFmc/status/1556561422431174656
Warning: This is an iOS 16 only so you'll probably need some if #available and potentially two different TextEditor component.
extension View {
/// Layers the given views behind this ``TextEditor``.
func textEditorBackground<V>(#ViewBuilder _ content: () -> V) -> some View where V : View {
self
.onAppear {
UITextView.appearance().backgroundColor = .clear
}
.background(content())
}
}
Custom Background color with SwiftUI on macOS
On macOS, unfortunately, you have to fallback to AppKit and wrap NSTextView.
You need to declare a view that conforms to NSViewRepresentable
This should give you pretty much the same behaviour as SwiftUI's TextEditor-View and since the wrapped NSTextView does not draw its background, you can use the .background-ViewModifier to change the background
struct CustomizableTextEditor: View {
#Binding var text: String
var body: some View {
GeometryReader { geometry in
NSScrollableTextViewRepresentable(text: $text, size: geometry.size)
}
}
}
struct NSScrollableTextViewRepresentable: NSViewRepresentable {
typealias Representable = Self
// Hook this binding up with the parent View
#Binding var text: String
var size: CGSize
// Get the UndoManager
#Environment(\.undoManager) var undoManger
// create an NSTextView
func makeNSView(context: Context) -> NSScrollView {
// create NSTextView inside NSScrollView
let scrollView = NSTextView.scrollableTextView()
let nsTextView = scrollView.documentView as! NSTextView
// use SwiftUI Coordinator as the delegate
nsTextView.delegate = context.coordinator
// set drawsBackground to false (=> clear Background)
// use .background-modifier later with SwiftUI-View
nsTextView.drawsBackground = false
// allow undo/redo
nsTextView.allowsUndo = true
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
// get wrapped nsTextView
guard let nsTextView = scrollView.documentView as? NSTextView else {
return
}
// fill entire given size
nsTextView.minSize = size
// set NSTextView string from SwiftUI-Binding
nsTextView.string = text
}
// Create Coordinator for this View
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// Declare nested Coordinator class which conforms to NSTextViewDelegate
class Coordinator: NSObject, NSTextViewDelegate {
var parent: Representable // store reference to parent
init(_ textEditor: Representable) {
self.parent = textEditor
}
// delegate method to retrieve changed text
func textDidChange(_ notification: Notification) {
// check that Notification.name is of expected notification
// cast Notification.object as NSTextView
guard notification.name == NSText.didChangeNotification,
let nsTextView = notification.object as? NSTextView else {
return
}
// set SwiftUI-Binding
parent.text = nsTextView.string
}
// Pass SwiftUI UndoManager to NSTextView
func undoManager(for view: NSTextView) -> UndoManager? {
parent.undoManger
}
// feel free to implement more delegate methods...
}
}
Usage
ContenView: View {
#State private var text: String
var body: some View {
VStack {
Text("Enter your text here:")
CustomizableTextEditor(text: $text)
.background(Color.red)
}
.frame(minWidth: 600, minHeight: 400)
}
}
Edit:
Pass reference to SwiftUI UndoManager so that default undo/redo actions are available.
Wrap NSTextView in NSScrollView so that it is scrollable. Set minSize property of NSTextView to enclosing SwiftUIView-Size so that it fills the entire allowed space.
Caveat: Only first line of this custom TextEditor is clickable to enable text editing.
This works for me on macOS
extension NSTextView {
open override var frame: CGRect {
didSet {
backgroundColor = .clear
drawsBackground = true
}
}
}
struct ContentView: View {
#State var text = ""
var body: some View {
TextEditor(text: $text)
.background(Color.red)
}
Reference this answer
To achieve this visual design here is the code I used.
iOS 16
TextField(
"free_form",
text: $comment,
prompt: Text("Type your feedback..."),
axis: .vertical
)
.lineSpacing(10.0)
.lineLimit(10...)
.padding(16)
.background(Color.themeSeashell)
.cornerRadius(16)
iOS 15
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 16)
.foregroundColor(.gray)
TextEditor(text: $comment)
.padding()
.focused($isFocused)
if !isFocused {
Text("Type your feedback...")
.padding()
}
}
.frame(height: 132)
.onAppear() {
UITextView.appearance().backgroundColor = .clear
}
You can use Mojtaba's answer (the approved answer). It works in most cases. However, if you run into this error:
"Return from initializer without initializing all stored properties"
when trying to use the init{ ... } method, try adding UITextView.appearance().backgroundColor = .clear to .onAppear{ ... } instead.
Example:
var body: some View {
VStack(alignment: .leading) {
...
}
.onAppear {
UITextView.appearance().backgroundColor = .clear
}
}
Using the Introspect library, you can use .introspectTextView for changing the background color.
TextEditor(text: .constant("Placeholder"))
.cornerRadius(8)
.frame(height: 100)
.introspectTextView { textView in
textView.backgroundColor = UIColor(Color.red)
}
Result
import SwiftUI
struct AddCommentView: View {
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
VStack {
if #available(iOS 16.0, *) {
TextEditor(text: $viewModel.commentText)
.scrollContentBackground(.hidden)
} else {
TextEditor(text: $viewModel.commentText)
}
}
.background(Color.blue)
.frame(height: UIScreen.main.bounds.width / 2)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.red, lineWidth: 1)
)
}
}
It appears the UITextView.appearance().backgroundColor = .clear trick in IOS 16,
only works for the first time you open the view and the effect disappear when the second time it loads.
So we need to provide both ways in the app. Answer from StuFF mc works.
var body: some View {
if #available(iOS 16.0, *) {
mainView.scrollContentBackground(.hidden)
} else {
mainView.onAppear {
UITextView.appearance().backgroundColor = .clear
}
}
}
// rename body to mainView
var mainView: some View {
TextEditor(text: $notes).background(Color.red)
}

How do you send values to other classes using ObservableObject in SwiftUI

Im trying to create a simple tip Calculator but i am having a problem once a user has added in the information in the app instead of calculating the tip, the function is returning as if there is an issue somewhere.
Im trying to make it so that when a user inputs a number in the text field and selects a percentage the total tip is displayed underneath.
How do i fix this problem?
Ive added a print statement to print the word "returning" and it keeps printing this word so i think the problem is somewhere in the calculateTip function:
This is my Code for my ContentView class:
struct ContentView: View {
//MARK: - PROPERTIES
#ObservedObject public var tipVM = TipViewModel()
//MARK: - FUNCTIONS
private func endEditing() {
hideKeyboard()
}
//MARK: - BODY
var body: some View {
Background {
NavigationView {
ZStack {
VStack {
HStack(spacing: 10) {
Text("Tip Calculator")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.heavy)
.padding(.leading, 4)
.foregroundColor(.blue)
Spacer()
Button(action: {
// tipVM.clearFields()
}, label: {
Text("Clear")
.font(.system(size: 16, weight: .semibold, design: .rounded))
.padding(.horizontal, 10)
.frame(minWidth: 70, minHeight: 24)
.background(
Capsule().stroke(lineWidth: 2)
)
.foregroundColor(.blue)
}) //: BUTTON
} //: HSTACK
.padding()
Spacer(minLength: 80)
TextField("Enter Amount: ", text: $tipVM.amount)
.padding()
.background(Color.secondary)
.foregroundColor(.white)
.font(.system(.title3, design: .rounded))
Picker(selection: $tipVM.tipPercentage, label: Text("Picker"), content: {
ForEach(0 ..< tipVM.tipChoices.count) { index in
Text("\(self.tipVM.tipChoices[index])%").tag(index)
.font(.system(.body, design: .rounded)).padding()
}.padding()
.background(subtitleColor)
.foregroundColor(.white)
}).onTapGesture(perform: {
tipVM.calculateTip()
})
.pickerStyle(SegmentedPickerStyle())
Text(tipVM.tip == nil ? "£0" : "\(tipVM.tip!)")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.bold)
.foregroundColor(.blue)
.padding()
} //: VSTACK
} //: ZSTACK
.navigationBarHidden(true)
} //: NAVIGATION VIEW
.navigationViewStyle(StackNavigationViewStyle())
}.onTapGesture {
self.endEditing()
}
.ignoresSafeArea(.keyboard, edges: .all)
} //: BACKGROUND
}
And here is my code for my TipViewModel Class:
import Foundation
import SwiftUI
import Combine
class TipViewModel: ObservableObject {
var amount: String = ""
var tipPercentage: Int = 0
var tip: Double?
let tipChoices = [10,15,20,25,30]
let didChange = PassthroughSubject<TipViewModel, Never>()
func calculateTip() {
guard let amount = Double(amount) else {
print("returning")
return
}
self.tip = amount * (Double(tipPercentage)/100)
self.didChange.send(self)
}
}
I would appreciate any help thanks.
The usual way is to mark the properties with #Published whose changes are going to be monitored. The extra Combine subject is not needed.
And declare tip as non-optional
import Foundation
import SwiftUI
// import Combine
class TipViewModel: ObservableObject {
var amount: String = ""
var tipPercentage: Int = 0
#Published var tip = 0.0
let tipChoices = [10,15,20,25,30]
func calculateTip() {
guard let numAmount = Double(amount) else {
print("returning")
return
}
self.tip = numAmount * (Double(tipPercentage)/100)
}
}
Secondly if the current view struct creates (aka owns) the observable object use always #StateObject rather than #ObservedObject. The latter is for objects which are initialized at higher levels in the view hierarchy and just passed through.
struct ContentView: View {
//MARK: - PROPERTIES
#StatedObject private var tipVM = TipViewModel()
...
Text("£\(tipVM.tip)")

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

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
}
}
}

Why does this swiftui view not update?

Consider the following code block:
import SwiftUI
struct MeasurementReading: View, Equatable {
#ObservedObject var ble: BluetoothConnectionmanager
#GestureState var isDetectTap = false
#State var MyText:String = "Wait"
static func == (lhs: MeasurementReading, rhs: MeasurementReading)->Bool{
return lhs.MyText == rhs.MyText
}
var body: some View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
return HStack {
Spacer()
VStack{
Button(action:{
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText as NSString)")
}, label: {
Text(MyText)
.font(.system(size: 40))
.bold()
.foregroundColor(Color.black)
.padding(.trailing, 15)
.frame(height: 100)
})
Button(action: {
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText as NSString)")
}, label: {
Text(MyText)
.font(.system(size: 25))
.padding(.top, -20)
.padding(.bottom, 20)
.foregroundColor(Color.black)
})
}
}.onReceive(timer)
{ _ in // TIMER FUNCTIONALITY HERE
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText)")
}
}
}
struct MeasurementReading_Previews: PreviewProvider {
static var previews: some View {
MeasurementReading(ble: BluetoothConnectionmanager())
}
}
Every 1 second the correct value read from the BLE system is assigned to MyText and then MyText is printed to the debug output properly with the updated value.
The problem here is that view MeasurementReading does not update. Also, using a closure on any item also has the same behavior (variable is updated, it is output properly but no view update) ex .onTap{....} will have the same behavior or any other .onXXXX closure. The only way I could get the view to update at all with new values for the MyText state is to put the behavior in a Button.
My question is this: Why does the view not update even when the state variable changes via Timer or .onXXXX closure?
You need to be setting the ble value to the updated timer value:
Without testing this properly. I also think your BluetoothConnectionmanager needs to be a #State property for this to work.
#State var ble: BluetoothConnectionmanager
.onReceive(timer) { value in // value is the updated value
self.ble.value = value
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText)")
}
Take a look at this example to see how a timer works with a Date() object.

Resources