Swift - How to get text formatting in a text editor like in the notes app? SwiftUI - swift

I currently have a TextEditor, and I want to be able to add text formatting like in the notes app like this:
I've tried with UITextView.allowsEditingTextAttributes = true but it didn't seem to work.
Any ideas will be highly appreciated.
:)

You can create a custom TextView UIViewRepresentable and set allowsEditingTextAttributes to true there:
create a new Swift file called TextView.swift
import SwiftUI
struct TextView: UIViewRepresentable {
#Binding var attributedText: NSMutableAttributedString
#State var allowsEditingTextAttributes: Bool = false
#State var font: UIFont?
func makeUIView(context: Context) -> UITextView {
UITextView()
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = attributedText
uiView.allowsEditingTextAttributes = allowsEditingTextAttributes
uiView.font = font
}
}
Then you can add it to your content view:
import SwiftUI
struct ContentView: View {
#State var attributedText = NSMutableAttributedString(string: "")
var body: some View {
TextView(attributedText: $attributedText, allowsEditingTextAttributes: true, font: .systemFont(ofSize: 32))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

How to make a text appear on the screen after pressing the button?

I want to make the text display on the screen according to the different scenes after pressing the button. For example, if model A is displayed, text "A" will be appeared on the screen. Similarly, if model B is displayed, text "B" will also be appeared. I am currently creating Augmented Reality app using SwiftUI interface and RealityKit but not sure what to do in the next step.
Here is my code:
import SwiftUI
import RealityKit
struct ContentView : View {
#State var arView = ARView(frame: .zero)
var body: some View {
ZStack {
ARViewContainer(arView: $arView)
HStack {
Spacer()
Button("information") {
print(self.arView.scene.name)
print(arView.scene.anchors.startIndex)
print(arView.scene.anchors.endIndex)
}
Spacer()
Button("remove") {
stop()
}
Spacer()
}
} .edgesIgnoringSafeArea(.all)
}
func stop() {
arView.scene.anchors.removeAll()
}
}
struct ARViewContainer: UIViewRepresentable {
#Binding var arView: ARView
func makeUIView(context: Context) -> ARView {
let boxAnchor = try! Experience1.loadBox()
let crownAnchor = try! Experience1.loadCrown()
arView.scene.anchors.append(boxAnchor)
arView.scene.anchors.append(crownAnchor)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) { }
}
From the code above, if boxAnchor and crownAnchor and displayed, text "Box" and "Crown" will be appeared on the screen respectively. Anyone who knows how to do that please guide me or suggest a tutorial that I can use to study.
Sorry if I use the wrong technical terms. Thank you
Use Combine's reactive subscriber and MVVM's bindings to update string values for Text views.
import SwiftUI
import RealityKit
import Combine
struct ContentView : View {
#State private var arView = ARView(frame: .zero)
#State private var str01: String = "...some text..."
#State private var str02: String = "...some text..."
var body: some View {
ZStack {
ARViewContainer(arView: $arView, str01: $str01, str02: $str02)
.ignoresSafeArea()
VStack {
Spacer()
Text(str01)
.foregroundColor(.white)
.font(.largeTitle)
Divider()
Text(str02)
.foregroundColor(.white)
.font(.largeTitle)
Spacer()
}
}
}
}
The miracle happens in the escaping closure of subscribe(to:) instance method. What will be the conditions in the if-statements is up to you.
struct ARViewContainer: UIViewRepresentable {
#Binding var arView: ARView
#Binding var str01: String
#Binding var str02: String
#State var subs: [AnyCancellable] = []
func makeUIView(context: Context) -> ARView {
let boxAnchor = try! Experience.loadBox()
let crownAnchor = try! Experience.loadCrown()
print(arView.scene.anchors.count)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
arView.scene.anchors.append(boxAnchor)
arView.scene.anchors.append(crownAnchor)
print(arView.scene.anchors.count)
}
return arView
}
func updateUIView(_ view: ARView, context: Context) {
DispatchQueue.main.async {
_ = view.scene.subscribe(to: SceneEvents.DidAddEntity.self) { _ in
if view.scene.anchors.count > 0 {
if view.scene.anchors[0].isAnchored {
str01 = "Crown"
str02 = "Cube"
}
}
}.store(in: &subs)
}
}
}

Adding UITextView to SwiftUI

i tried to add UITextView to swiftUI because there are things that TextEditor isn't capable of doing. Here's how I built it
struct TextViewSwift : UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.text = "Testing UITextView"
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
typealias UIViewType = UITextView
}
So I test it out with UILabel (because I want to make sure that the code is working when doing with other UIkit component)
and turns out when I debug the view, not even the UITextView appear, is it a bug within the SwiftUI it self or am I missing something? Thank you
Only UILabel appear
Just need a Text view? Try this.
import SwiftUI
struct TextArea: View {
#Binding var text: String
let placeholder: String
init(_ placeholder: String, text: Binding<String>) {
self._text = text
self.placeholder = placeholder
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack(alignment: .topLeading) {
if text.isEmpty {
Text(placeholder)
.foregroundColor(Color(.placeholderText))
.padding(.horizontal, 8)
.padding(.vertical, 12)
}
TextEditor(text: $text)
.padding(4)
}.font(.body)
}
}

SwiftUI: Showing HTML text inside a ScrollView

I'm trying to show some HTML text in an app I'm making, but I can't get it to work properly.
The problem is that the view that contains the text is always shown with the height of only one line, even tho it is wrapped in a ScrollView. In the screenshot, you guys can see that the ScrollView works, but the original size is very small.
The code is:
ScrollView {
GeometryReader { proxy in
AttributedText(htmlContent: job.description)
.frame(height: proxy.size.height, alignment: .center)
}
}
struct AttributedText: UIViewRepresentable {
let htmlContent: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}
}
struct AttributedText_Previews: PreviewProvider {
static var previews: some View {
AttributedText(htmlContent: "<h1>This is HTML String</h1>")
}
}
I have also tried to use the GeometryReader to establish the size of the ScrollView, but no luck either.
Is there any way to make this look normal and nice, scrollable, and with a proper text size?
Thanks a lot in advance!
Edit: After #RajaKishan's answer, this is how it looks like (where you can see that the content is cut-off):
As WKWebView has already scroll and you are also wrapped inside the scroll view, so parent scroll view not get the proper size.
You have to disable WKWebView scrolling. Also, bind the size with webview and update the frame of webview.
Here is the possible demo.
struct AttributedText: UIViewRepresentable {
let htmlContent: String
#Binding var size: CGSize
private let webView = WKWebView()
var sizeObserver: NSKeyValueObservation?
func makeUIView(context: Context) -> WKWebView {
webView.scrollView.isScrollEnabled = false //<-- Here
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: AttributedText
var sizeObserver: NSKeyValueObservation?
init(parent: AttributedText) {
self.parent = parent
sizeObserver = parent.webView.scrollView.observe(\.contentSize, options: [.new], changeHandler: { (object, change) in
parent.size = change.newValue ?? .zero
})
}
}
}
For view
#State private var size: CGSize = .zero
var body: some View{
ScrollView {
AttributedText(htmlContent: "<h1>This is HTML String</h1>", size: $size)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: size.height, maxHeight: .infinity)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}

Undo/redo text input w/ SwiftUI TextEditor

Admittedly this is a broad question, but is it possible to undo or redo text input (via iOS's UndoManager?) when using a SwiftUI TextEditor control? I've looked everywhere and was unable to find any resource focusing on this workflow combination (SwiftUI + TextEditor + UndoManager). I'm wondering given the relative immaturity of TextEditor that either this isn't possible at all, or requires some plumbing work to facilitate. Any guidance will be greatly appreciated!
Admittedly, this is a bit of a hack and non very SwiftUI-y, but it does work. Basically declare a binding in your UITextView:UIViewRepresentable to an UndoManager. Your UIViewRepresentable will set that binding to the UndoManager provided by the UITextView. Then your parent View has access to the internal UndoManager. Here's some sample code. Redo works as well although not shown here.
struct MyTextView: UIViewRepresentable {
/// The underlying UITextView. This is a binding so that a parent view can access it. You do not assign this value. It is created automatically.
#Binding var undoManager: UndoManager?
func makeUIView(context: Context) -> UITextView {
let uiTextView = UITextView()
// Expose the UndoManager to the caller. This is performed asynchronously to avoid modifying the view at an inappropriate time.
DispatchQueue.main.async {
undoManager = uiTextView.undoManager
}
return uiTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
struct ContentView: View {
/// The underlying UndoManager. Even though it looks like we are creating one here, ultimately, MyTextView will set it to its internal UndoManager.
#State private var undoManager: UndoManager? = UndoManager()
var body: some View {
NavigationView {
MyTextView(undoManager: $undoManager)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
undoManager?.undo()
} label: {
Image(systemName: "arrow.uturn.left.circle")
}
Button {
undoManager?.redo()
} label: {
Image(systemName: "arrow.uturn.right.circle")
}
}
}
}
}
}
In respect to using UIViewRepresentable as a TextView or TextField…. this approach works for undo, but not for redo it seems.
The redo button condition undoManager.canRedo seems to change appropriately. However, it doesn’t return any undone text into either the textfield or TextView
I’m now wondering is this a bug or something I’m missing in the logic?
import SwiftUI
import PlaygroundSupport
class Model: ObservableObject {
#Published var active = ""
func registerUndo(_ newValue: String, in undoManager: UndoManager?) {
let oldValue = active
undoManager?.registerUndo(withTarget: self) { target in
target.active = oldValue
}
active = newValue
}
}
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
struct ContentView: View {
#ObservedObject private var model = Model()
#Environment(\.undoManager) var undoManager
#State var text: String = ""
var body: some View {
ZStack (alignment: .bottomTrailing) {
// Testing TextView for undo & redo functionality
TextView(text: Binding<String>(
get: { self.model.active },
set: { self.model.registerUndo($0, in: self.undoManager) }))
HStack{
// Testing TextField for undo & redo functionality
TextField("Enter Text...", text: Binding<String>(
get: { self.model.active },
set: { self.model.registerUndo($0, in: self.undoManager) })).padding()
Button("Undo") {
withAnimation {
self.undoManager?.undo()
}
}.disabled(!(undoManager?.canUndo ?? false)).padding()
Button("Redo") {
withAnimation {
self.undoManager?.redo()
}
}.disabled(!(undoManager?.canRedo ?? false)).padding()
}.background(Color(UIColor.init(displayP3Red: 0.1, green: 0.3, blue: 0.3, alpha: 0.3)))
}.frame(width: 400, height: 400, alignment: .center).border(Color.black)
}
}
PlaygroundPage.current.setLiveView(ContentView())

Change background color of TextEditor in SwiftUI

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