I have wrapped a UITextView in a UIViewRepresentable and included a Coordinator as UITextViewDelegate, but the events are not called. What am I doing wrong?
struct TextView : UIViewRepresentable {
typealias UIViewType = UITextView
var placeholder : String
#Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<TextView>) -> UITextView {
let textView = UITextView()
let placeholderLabel = UILabel()
textView.font = UIFont(name: "Helvetica", size: 16)
placeholderLabel.restorationIdentifier = "Placeholder"
placeholderLabel.text = self.placeholder
placeholderLabel.font = UIFont.italicSystemFont(ofSize: (textView.font?.pointSize)!)
placeholderLabel.sizeToFit()
textView.addSubview(placeholderLabel)
placeholderLabel.frame.origin = CGPoint(x: 25, y: (textView.font?.pointSize)! / 2 + 12)
placeholderLabel.textColor = UIColor.lightGray
placeholderLabel.isHidden = !textView.text.isEmpty
return textView
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<TextView>) {
uiView.text = self.text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
print("Not working")
return true
}
func textViewDidChange(_ textView: UITextView) {
print("Not working")
}
}
}
This is a working example in Xcode Version 11.0 beta 6:
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
Text("text is: \(text)")
TextView(
text: $text
)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
}
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.font = UIFont(name: "HelveticaNeue", size: 15)
myTextView.isScrollEnabled = true
myTextView.isEditable = true
myTextView.isUserInteractionEnabled = true
myTextView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
print("text now: \(String(describing: textView.text!))")
self.parent.text = textView.text
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You have to add textView.delegate = context.coordinator to makeUIView to register the coordinator to receive updates from UITextView.
Related
I want to paste an image in line with text like apps one note or notion do. I read somewhere that it's impossible with TextEditor and I had to use UITextView instead, so I did that but I do not know how to trigger the event PASTE to paste the picture.
Once I do that I will need to figure out how to save the whole shebang in CoreData (the text only as text, the imgs as images). But now I just need help to figure out how to paste an image into a TextEditor or UITextView. Thanks
struct itemDetail: View {
#Environment(\.managedObjectContext) var viewContext //it's like a environemtn
#Environment(\.presentationMode) var presentationMode
#State var taskItem: ToDoItem?
#State var textFieldText : String
#State var textStyle = UIFont.TextStyle.body
#State var textTop : String
#FocusState var isFocused: Bool
#State var textEditorHeight : CGFloat = 20
var body: some View {
VStack(alignment: .leading) {
ZStack(alignment: .leading) {
Text(textTop)
.font(.system(.title))
.foregroundColor(.clear)
.padding(14)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
TextEditor(text: $textTop)
.font(.system(.title))
.frame(height: max(40,textEditorHeight))
.cornerRadius(10.0)
.shadow(radius: 1.0)
}.onPreferenceChange(ViewHeightKey.self) { textEditorHeight = $0 }
///UI TEXTVIEW
TextView(text: $textFieldText, textStyle: $textStyle)
.padding(.horizontal)
Button {
//save to context
taskItem!.detail = textFieldText
taskItem!.desc = textTop
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
presentationMode.wrappedValue.dismiss()
} label: {
ZStack {
Rectangle().foregroundColor(.blue).frame(height: 40).cornerRadius(10)
Text("Save").font(.subheadline).foregroundColor(.white)
}
}
Spacer()
}
}
}
//override func paste(_ sender: Any?) {
// let textAttachment = NSTextAttachment()
// textAttachment.image = UIPasteboard.general.image
// textFieldText = NSAttributedString(attachment: textAttachment)
//}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
//textview wrpaped uikit
struct TextView: UIViewRepresentable {
#Binding var text: String
#Binding var textStyle: UIFont.TextStyle
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = UIFont.preferredFont(forTextStyle: textStyle)
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
}
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
init(_ text: Binding<String>) {
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.text
}
}
}
Would like to have the toolbar show all the time.
Any help is greatly appreciated as this is a real drag for the user experience.
I've added a toolbar to the keyboard for the TextView as shown below.
However the toolbar only shows after the app has run once. Meaning the toolbar does not show the first time the app is run. the app works every time after the initial load.
This is on IOS 14.3, Xcode 12.3, Swift 5, iMessage extension app. Fails on simulator or real device.
struct CustomTextEditor: UIViewRepresentable {
#Binding var text: String
private var returnType: UIReturnKeyType
private var keyType: UIKeyboardType
private var displayDoneBar: Bool
private var commitHandler: (()->Void)?
init(text: Binding<String>,
returnType: UIReturnKeyType = .done,
keyboardType: UIKeyboardType,
displayDoneBar: Bool,
onCommit: (()->Void)?) {
self._text = text
self.returnType = returnType
self.keyType = keyboardType
self.displayDoneBar = displayDoneBar
self.commitHandler = onCommit
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.keyboardType = keyType
textView.returnKeyType = returnType
textView.backgroundColor = .clear
textView.font = UIFont.systemFont(ofSize: 20, weight: .regular)
textView.isEditable = true
textView.delegate = context.coordinator
if self.displayDoneBar {
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace,
target: self,
action: nil)
let doneButton = UIBarButtonItem(title: "Close Keyboard",
style: .done,
target: self,
action: #selector(textView.doneButtonPressed(button:)))
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
toolBar.items = [flexibleSpace, doneButton, flexibleSpace]
toolBar.setItems([flexibleSpace, doneButton, flexibleSpace], animated: true)
toolBar.sizeToFit()
textView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
textView.translatesAutoresizingMaskIntoConstraints = true
textView.inputAccessoryView = toolBar
}
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: CustomTextEditor
init(_ textView: CustomTextEditor) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
}
func textViewDidEndEditing(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
parent.commitHandler?()
}
}
}
extension UITextView {
#objc func doneButtonPressed(button:UIBarButtonItem) -> Void {
self.resignFirstResponder()
}
}
This is how it's called...
import SwiftUI
final class ContentViewHostController: UIHostingController<ContentView> {
weak var myWindow: UIWindow?
init() {
super.init(rootView: ContentView())
}
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: ContentView())
}
}
let kTextColor = Color(hex: "3E484F")
let kOverlayRadius: CGFloat = 10
let kOverlayWidth: CGFloat = 2
let kOverlayColor = kTextColor
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
Spacer()
CustomTextEditor(text: $text, returnType: .default, keyboardType: .default, displayDoneBar: true, onCommit: nil)
.foregroundColor(kTextColor)
.overlay(
RoundedRectangle(cornerRadius: kOverlayRadius)
.stroke(kOverlayColor, lineWidth: kOverlayWidth)
)
.frame(width: 200, height: 100, alignment: .center)
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
From MessagesViewController...
override func willBecomeActive(with conversation: MSConversation) {
let childViewCtrl = ContentViewHostController()
childViewCtrl.view.layoutIfNeeded() // avoids snapshot warning?
if let window = self.view.window {
childViewCtrl.myWindow = window
window.rootViewController = childViewCtrl
}
}
I have created a SwiftUI TextView based on a UITextView using UIViewRepresentable (s. code below). Displaying text in Swiftui works OK.
But now I need to access internal functions of UITextView from my model. How do I call e.g. UITextView.scrollRangeToVisible(_:) or access properties like UITextView.isEditable ?
My model needs to do these modifications based on internal model states.
Any ideas ? Thanks
(p.s. I am aware of TextEditor in SwiftUI, but I need support for iOS 13!)
struct TextView: UIViewRepresentable {
#ObservedObject var config: ConfigModel = .shared
#Binding var text: String
#State var isEditable: Bool
var borderColor: UIColor
var borderWidth: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = uiView.font?.withSize(CGFloat(config.textsize))
uiView.text = text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
}
}
You can use something like configurator callback pattern, like
struct TextView: UIViewRepresentable {
#ObservedObject var config: ConfigModel = .shared
#Binding var text: String
#State var isEditable: Bool
var borderColor: UIColor
var borderWidth: CGFloat
var configurator: ((UITextView) -> ())? // << here !!
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = uiView.font?.withSize(CGFloat(config.textsize))
uiView.text = text
// alternat is to call this function in makeUIView, which is called once,
// and the store externally to send methods directly.
configurator?(myTextView) // << here !!
}
// ... other code
}
and use it in your SwiftUI view like
TextView(...) { uiText in
uiText.isEditing = some
}
Note: depending on your scenarios it might be additional conditions need to avoid update cycling, not sure.
How can I change the background color of the area that is white?
I used UIViewControllerRepresentable but I don't know how to change the color of UIViewControllers.
I guess I need to change the background color in the makeUIViewController function?
I don't know much English, I hope I could explain my problem.
OnboardingView:
struct OnboardingView: View {
#State var currentPageIndex = 0
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var subviews = [
UIHostingController(rootView: SubView(imageString: "1")),
UIHostingController(rootView: SubView(imageString: "1")),
UIHostingController(rootView: SubView(imageString: "1"))
]
var titles = ["Take some time out", "Conquer personal hindrances", "Create a peaceful mind"]
var captions = ["Take your time out and bring awareness into your everyday life", "Meditating helps you dealing with anxiety and other psychic problems", "Regular medidation sessions creates a peaceful inner mind"]
var body: some View {
VStack(alignment: .leading) {
Group {
Text(titles[currentPageIndex])
.font(.title)
Text(captions[currentPageIndex])
.font(.subheadline)
.frame(width: 300, height: 50, alignment: .center)
.lineLimit(nil)
}
.padding()
PageViewController(currentPageIndex: $currentPageIndex, viewControllers: subviews)
.frame(height: 600)
.background(Color.yellow)
...
}
}
}
PageViewController:
struct PageViewController: UIViewControllerRepresentable {
#Binding var currentPageIndex: Int
var viewControllers: [UIViewController]
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[viewControllers[currentPageIndex]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.viewControllers.last
}
return parent.viewControllers[index - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.viewControllers.count {
return parent.viewControllers.first
}
return parent.viewControllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.viewControllers.firstIndex(of: visibleViewController)
{
parent.currentPageIndex = index
}
}
}
}
You need to set the backgroundColor of your subviews.
You can do it either in OnboardingView or in PageViewController:
func makeUIViewController(context: Context) -> UIPageViewController {
...
// make the subviews transparent
viewControllers.forEach {
$0.view.backgroundColor = .clear
}
...
}
When I click the return button on the keyboard, the application freezes and never becomes responsive again. I am using a UIViewRepresentable that returns a UITextField because I am unable to create an outline on the text using SwiftUI. I'm running this on Xcode beta 5 on and iPhone XR simulator.
import SwiftUI
import UIKit
struct CustomTextField: View {
#State var text: String
#State var pos: CGPoint
var body: some View {
StrokeTextLabel(text: text)
// .frame(width: UIScreen.main.bounds.width - 16, height: 40, alignment: .center)
.position(pos)
.gesture(dragGesture)
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
struct StrokeTextLabel: UIViewRepresentable {
var text: String
func makeUIView(context: Context) -> UITextField {
let attributedStringParagraphStyle = NSMutableParagraphStyle()
attributedStringParagraphStyle.alignment = NSTextAlignment.center
let attributedString = NSAttributedString(
string: text,
attributes:[
NSAttributedString.Key.strokeColor : UIColor.white,
NSAttributedString.Key.foregroundColor : UIColor.black,
NSAttributedString.Key.strokeWidth : -4.0,
NSAttributedString.Key.font: UIFont(name: "impact", size: 50.0)!
]
)
let strokeLabel = UITextField(frame: CGRect.zero)
strokeLabel.attributedText = attributedString
strokeLabel.backgroundColor = UIColor.clear
strokeLabel.sizeToFit()
strokeLabel.center = CGPoint.init(x: 0.0, y: 0.0)
strokeLabel.sizeToFit()
return strokeLabel
}
func updateUIView(_ uiView: UITextField, context: Context) {}
}
#if DEBUG
struct CustomTextField_Previews: PreviewProvider {
static var previews: some View {
CustomTextField(text: "test text", pos: CGPoint(x: 300, y: 500))
}
}
#endif
I expect the application to be close the keyboard when the return button is clicked but instead the app freezes.
Credits to this answer goes to https://github.com/valvoline/SATextField/blob/master/SATextField/ContentView.swift. This answer wouldn't be here without the above source.
Nothing major changed.
struct CustomTextField: View {
#State var text: String
#State var pos: CGPoint
var body: some View {
StrokeTextLabel(text: $text, changeHandler: { newString in
self.text = newString
}, onCommitHandler: {
print("commitHandler")
})
.position(pos)
.gesture(dragGesture)
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
Added UITextField and UITextFieldDelegate to use their functions
class WrappableTextField: UITextField, UITextFieldDelegate {
var textFieldChangedHandler: ((String)->Void)?
var onCommitHandler: (()->Void)?
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let currentValue = textField.text as NSString? {
let proposedValue = currentValue.replacingCharacters(in: range, with: string)
textFieldChangedHandler?(proposedValue as String)
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
onCommitHandler?()
}
}
Use the delegate as an object and add the attributes. Also made the string a Binding
struct StrokeTextLabel: UIViewRepresentable {
private let tmpView = WrappableTextField()
#Binding var text: String
//var exposed to SwiftUI object init
var changeHandler:((String)->Void)?
var onCommitHandler:(()->Void)?
func makeUIView(context: Context) -> UITextField {
tmpView.delegate = tmpView
tmpView.onCommitHandler = onCommitHandler
tmpView.textFieldChangedHandler = changeHandler
let attributedStringParagraphStyle = NSMutableParagraphStyle()
attributedStringParagraphStyle.alignment = NSTextAlignment.center
let attributedString = NSAttributedString(
string: text,
attributes:[
NSAttributedString.Key.strokeColor : UIColor.white,
NSAttributedString.Key.foregroundColor : UIColor.black,
NSAttributedString.Key.strokeWidth : -4.0,
NSAttributedString.Key.font: UIFont(name: "impact", size: 50.0)!
]
)
tmpView.attributedText = attributedString
tmpView.autocorrectionType = .no
tmpView.keyboardType = .default
return tmpView
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
}