I am trying use UIKit's UITextView in an swiftUI project using UIViewRepresentable. Everything works great but I am facing some issue with inputAccessoryView on phones with safe area.
My inputAccessoryView is a swiftUI view wrapped in UIHostingController which lets me use swiftUI views as UIView.
I have attached my code and some images, To simplify my issue I am using simple swiftUI colorView as inputAccessoryView. As you can see the swiftUI view moves out of the frame of inputAccessoryView when it comes near bottom safe area. When you swipe down to close the keyboard it disappears weirdly. I have tried everything but could not find the solution. Can you please help me?
Thank You!
UIViewRepresentable, UITextView
struct UITextViewWrapper: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.text = text
textView.inputAccessoryView = makeKeyboardBar()
textView.keyboardDismissMode = .interactive
textView.alwaysBounceVertical = true
textView.delegate = context.coordinator
textView.becomeFirstResponder()
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
//
}
class Coordinator: NSObject, UITextViewDelegate {
let parent: UITextViewWrapper
init(_ parent: UITextViewWrapper) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeKeyboardBar() -> UIView {
let swiftUIView = ColorView()
let hostingController = UIHostingController(rootView: swiftUIView)
let uiView = hostingController.view!
uiView.frame = CGRect(origin: .zero, size: hostingController.sizeThatFits(in: uiView.frame.size))
uiView.layer.borderWidth = 1
uiView.layer.borderColor = UIColor.red.cgColor
return uiView
}
}
SwiftUI View to use as inputAccessoryView
struct ColorView: View {
var body: some View {
Color.yellow
.frame(height: 55)
}
}
ContentView
struct ContentView: View {
#State private var text = "Hello World"
var body: some View {
UITextViewWrapper(text: $text)
}
}
image
SwiftUI has own safe area tracking so you have to manage safe area behaviour explicitly inside SwiftUI view, like
var body: some View {
Color.yellow.edgesIgnoringSafeArea(.bottom) // << here !!
.frame(height: 55)
}
Related
I have an iOS app which needs to capture user's signature
UI should be like this
I tried this using pencilKit as follows but,
It shows all the tools I don't need them to be visible
SwiftUIView
struct HandSignatureView: View {
var body: some View {
VStack {
Text("Place Your Signature Here")
.font(.title2)
.foregroundColor(.blue)
SignaturePadView()
}
.preferredColorScheme(.light)
}
}
UIViewRepresentable
struct SignaturePadView : UIViewRepresentable {
var canvasView = PKCanvasView()
let picker = PKToolPicker.init()
func makeUIView(context: Context) -> PKCanvasView {
self.canvasView.tool = PKInkingTool(.pen, color: .black, width: 15)
self.canvasView.becomeFirstResponder()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
picker.addObserver(canvasView)
picker.setVisible(true, forFirstResponder: uiView)
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
This is the output I Got
picker.setVisible(false, forFirstResponder: uiView)
Hides tools but it draws nothing
If there's any good library I can use please be kind enough to share
Any Help will be appreciated Thank You !
I have a view Structure as follows:
ZStack {
UIKitView //PKCanvasView
.zIndex(1)
SwiftUIView //SwiftUI view which has a gesture recogniser
.zIndex(0)
}
I wanted to know what the best way was to allow simultaneous interaction to the SwiftUI view below the UIKit view.
This would work best if the SwiftUI view had a higher priority gesture over the UIKit view on top.
My UIKit representable view is as follows:
struct CanvasView: UIViewRepresentable {
#Binding var canvasView: PKCanvasView
#Binding var isFirstResponder: Bool
#State var toolPicker = PKToolPicker()
func makeUIView(context: Context) -> PKCanvasView {
canvasView.drawingPolicy = .pencilOnly
canvasView.backgroundColor = .clear
canvasView.isOpaque = false
canvasView.alwaysBounceVertical = true
canvasView.alwaysBounceHorizontal = true
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
//canvasView.becomeFirstResponder()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
if isFirstResponder != uiView.isFirstResponder {
if isFirstResponder {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
}
}
}
Ideally I would like to handle the layering of views in SwiftUI as in reality a have multiple SwiftUI views below the canvas view which when held & dragged (that is the compound gesture I am applying to them) should appear above it. (This is achieved using dynamic zIndexs). I'm not comfortable enough with UIKit to know how to do this there with subviews and a UIViewRepresentable structure.
Any help would be greatly appreciated!
I am working on a SwiftUI project, the functionalities it required is to make a Rich Text Editor on IOS.
The approach I am following is fairly simple, I used cbess/RichTextEditor link originally written in UIKit and import it into SwiftUI. To run the imported UIView, I wrap the view inside one UIViewRpresentable and add it into the ContentView struct of SwiftUI.
Now, I want to publish the data inside UIView and assign it to one of #state ContentView owns.
The code structure look similar to this:
For the ContentView (SwiftUI)
struct ContentView: View {
#State var textHtml: String = "" //I want all changes come from UIView be stored inside this
var body: some View {
VStack {
Cbess(
frameEditor: CGRect(x: 0, y: 40, width: 360, height: 400)
)
}
}
}
For the UiViewRepresentable
struct Cbess : UIViewRepresentable{
let frameEditor : CGRect
func makeUIView(context: Context) -> UIView {
let frameEditor = RichEditorView(frame: frameEditor)
let uiView : UIView = UIView()
uiView.addSubview(editorView)
return uiView
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
For the UiView(Simplified)
#objcMembers open class RichEditorView: UIView, {
var contentHTML : String // This variable get updated regularly
}
One additional question is that I want to make a Rich Text Editor by solely SwiftUI. How can I achieve it? Can you give me some keywords? Some Repo?
Any help is very appreciated! Thanks for read this whole question.
Use #Binding and delegate.
UIViewRepresentable view
struct Cbess : UIViewRepresentable {
#Binding var textHtml: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> RichEditorView {
let editorView = RichEditorView()
editorView.delegate = context.coordinator
return editorView
}
func updateUIView(_ uiView: RichEditorView, context: Context) {
}
class Coordinator: NSObject, RichEditorDelegate {
var parent: Cbess
init(_ parent: Cbess) {
self.parent = parent
}
// Use delegate here
func richEditor(_ editor: RichEditorView, contentDidChange content: String) {
self.parent.textHtml = content
print(content)
}
}
}
Your content view:
struct ContentView: View {
#State var textHtml: String = ""
var body: some View {
VStack {
Cbess(textHtml: $textHtml)
.frame(width: 360, height: 400)
Text("Print----\n\(textHtml)")
}
}
}
I’m depending on the TextView using UIViewRepresentable created here https://www.appcoda.com/swiftui-textview-uiviewrepresentable/.
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
}
}
}
struct ContentView: View {
#State private var message = ""
#State private var textStyle = UIFont.TextStyle.body
var body: some View {
ZStack(alignment: .topTrailing) {
TextView(text: $message, textStyle: $textStyle)
.padding(.horizontal)
Button(action: {
self.textStyle = (self.textStyle == .body) ? .title1 : .body
}) {
Image(systemName: "textformat")
.imageScale(.large)
.frame(width: 40, height: 40)
.foregroundColor(.white)
.background(Color.purple)
.clipShape(Circle())
}
.padding()
}
}
}
The problem I’m having is whenever I start a newline 1) before the final line + 2) after the last character on the line, the cursor always jumps to after the final character in the text. Any ideas?
UPDATE:
Leo’s response technically addresses this issue, but it doesn’t seem perfect as there’s undesired scroll behaviour whereby although the caret position is now correct that doesn’t stop an auto-scroll to the bottom. See below:
You can save the caret position (selectedRange) and set your text view selected range after setting the text property inside updateUIView method:
func updateUIView(_ uiView: UITextView, context: Context) {
let selectedRange = uiView.selectedRange
uiView.text = text
uiView.font = .preferredFont(forTextStyle: textStyle)
uiView.selectedRange = selectedRange
}
In addition to Leo answer, what I noticed is if you start editing with a 'new line' character your textView might also jump to the end. What works for me is adding
func updateUIView(_ uiView: UITextView, context: Context) {
//previous code
uiView.scrollRangeToVisible(selectedRange)
}
it happens because you set font every symbol change. Just try not to set style too often if it is not needed
Is it possible to make a UIViewRepresentable view which takes a ViewBuilder argument work with dynamic content such as ForEach loops?
I have the following UIViewRepresentable view which I’m using to drop down to UIKit and get some custom UIScrollView behaviour:
struct CustomScrollView<Content:View>: UIViewRepresentable {
private let content: UIView
private let scrollView = CustomUIScrollView()
init(#ViewBuilder content: () -> Content) {
self.content = UIHostingController(rootView: content()).view
self.content.backgroundColor = .clear
}
func makeUIView(context: Context) -> UIView {
scrollView.addSubview(content)
// ...
return scrollView
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
This works fine with static content as follows:
var body: some View {
CustomScrollView {
VStack {
ForEach(1..<50) { number in
Text(String(number))
}
}
}
}
But it fails with dynamic content, showing a blank view:
var body: some View {
CustomScrollView {
VStack {
ForEach(self.numbers) { number in
Text(String(number))
}
}
}
}
I understand that this is because when makeUIView() is called my dynamic data is empty, and it is later filled or updated. I evaluate my UIViewRepresentable’s content at init, and don’t update it in updateUIView().
How do you go about updating dynamic child content in updateUIView()? I tried capturing the #ViewBuilder parameter as an #escaping closure and evaluating it every time updateUIView() is called, which seems like the right solution (albeit inefficient?), but no luck so far.
Evaluation of #ViewBuilder fails because you mutating the wrong copy of the struct here
func updateUIView(_ uiView: UIView, context: Context) {}
You should mutate uiView's subviews directly with the content()
Updated Answer: Removed solution with a coordinator, because in some cases it works not as expected
The following might be helpful. It is not clear how absent CustomUIScrollView behaves (probably the issue is there), but using standard UIScrollView works with dynamic ForEach. Tested with Xcode 11.4 / iOS 13.4
struct CustomScrollView<Content:View>: UIViewRepresentable {
private let content: UIView
private let scrollView = UIScrollView()
init(#ViewBuilder content: () -> Content) {
self.content = UIHostingController(rootView: content()).view
self.content.backgroundColor = .clear
}
func makeUIView(context: Context) -> UIView {
content.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(content)
let constraints = [
content.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
content.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
content.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
content.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
content.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
]
scrollView.addConstraints(constraints)
return scrollView
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
struct TestCustomScrollView: View {
private var items = Array(repeating: "Test", count: 50)
var body: some View {
CustomScrollView {
VStack {
ForEach(Array(items.enumerated()), id: \.0) { i, item in
Text("\(item) - \(i)")
}
}
}
}
}