Interacting with a SwiftUI view below a UIKit PKCanvasView - ios

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!

Related

UITextView inputAccessoryView not behaving properly on safe area phones?

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

Update Binding in UIViewRepresentable

I have a swiftUI view that calls a UIViewRepresentable view. In the SwiftUI view I am toggling the state of #State boolean value.
In my UIViewRepresentable view I have created a binding that gets past from the main SwiftUI view. The problem is the binding never gets update or at least the updateView function is not getting called in the UIViewRepresentable view. I fell like I must be doing something wrong but I am just overlooking it. Here is an example of what I am trying to do.
import Foundation
import SwiftUI
struct BindingTest: UIViewRepresentable {
#Binding var status: Bool
func makeUIView(context: Context) -> UIActivityIndicatorView {
let activityIndicator = UIActivityIndicatorView()
activityIndicator.style = .large
return activityIndicator
}
func updateUIView(_ uiView: UIViewType, context: Context) {
print("Hello")
}
}
import SwiftUI
struct ChartView: View {
#State var status = false
var body: some View {
VStack{
Spacer()
Button(action: {
status = !status
}) {
Text("Change")
}
BindingTest(status: $status)
}
}
}
I am using Xcode 12.5 and Swift 5.4
Looks like SwiftUI is doing some cleverness under the hood that isn't immediately obvious to us. Because you don't actually use your binding in updateUIView, it's not actually getting called.
However, if you update your code to the following:
func updateUIView(_ uiView: UIViewType, context: Context) {
print("Hello \(status)")
}
then you'll see that it does, in fact, get called.
PS - you can use status.toggle() instead of status = !status

Send data changes from UIKit, Wrapped inside UIViewRepresentable, to SwiftUI, and Rich Text Editor problem

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

SIGABRT when navigating from ARView with cameraMode = `nonAR` to a regular AR mode ARView

I'm using SwiftUI and RealityKit to make an AR app. I am trying to transition from a nonAR camera mode ARView to a regular ARView using a NavigationLink, but I'm running into a SIGABRT and see the following error whenever I select the link:
validateTextureDimensions, line 1227: error 'MTLTextureDescriptor has width (4294967295) greater than the maximum allowed size of 16384.'
I've reproduced this behavior in a fresh RealityKit app with these simple views:
// ContentView (nonAR ARView)
import SwiftUI
import RealityKit
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
ARViewContainer().edgesIgnoringSafeArea(.all)
NavigationLink(destination: ContentView2()) {
Text("Go!")
}
}
}
}
}
struct ARViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: true)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
// ContentView2 (AR ARView)
import SwiftUI
import RealityKit
struct ContentView2: View {
var body: some View {
return ARViewContainer2().edgesIgnoringSafeArea(.all)
}
}
struct ARViewContainer2: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
It seems like some cleanup needs to be performed with the first nonAR ARView before navigating but I'm not sure what the best way to manage that is. I saw the answer in this post and I tried adding a #Binding to ARViewContainer that I set in the parent view to flag to that the uiview should be removed in updateUIView before navigating, but I'm still hitting the crash :/ Any help here would be greatly appreciated!
I ended up resolving this for the time being by using a SceneKit view in place of the nonAR AR view.

SwiftUI passing touch events from UIViewControllerRepresentable to View behind

I'm working on a project which uses a mixture of UIKit and SwiftUI. I currently have a ZStack which I hold my SwiftUI content in, I need to display a UIViewController over top of that content. So the last item in my ZStack is a UIViewControllerRepresentable. ie:
ZStack {
...
SwiftUIContent
...
MyUIViewControllerRepresentative()
}
My overlaid UIViewControllerRepresentative is a container for other UIViewControllers. Those child view controllers don't always need to take up the full screen, so the UIViewControllerRepresentative has a transparent background so the user can interact with the SwiftUI content behind.
The problem I'm facing is that the UIViewControllerRepresentative blocks all touch events from reaching the SwiftUI content.
I've tried overriding the UIViewControllers views hit test like so:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// return nil if the touch event should propagate to the SwiftUI content
}
I've also event tried completely removing the touch events on the view controllers view with:
view.isUserInteractionEnabled = false
Even that doesn't work.
Any help would be really appreciated.
SOLUTION:
I managed to come up with a solution that works.
Instead of putting the UIViewControllerRepresentable inside the ZStack, I created a custom ViewModifier which passes the content value into the UIViewControllerRepresentable. That content is then embedded within my UIViewController by wrapping it inside of a UIHostingController and adding it as a child view controller.
ZStack {
... SwiftUI content ...
}.modifier(MyUIViewControllerRepresentative)
You could use allowsHitTesting() modifier on the UIViewControllerRepresentable.
As Paul Hudson states on his website: https://www.hackingwithswift.com/quick-start/swiftui/how-to-disable-taps-for-a-view-using-allowshittesting
"If hit testing is disallowed for a view, any taps automatically continue through the view on to whatever is behind."
ZStack {
...
SwiftUIContent
...
MyUIViewControllerRepresentative()
.allowsHitTesting(false)
}
The solution I've found was to pass my SwiftUI view down to the overlaid UIViewControllerRepresentable.
What makes it possible is to make all entities involved generic and using #ViewBuilder in your representable.
It's possible to make this even cleaner by using a View Modifier instead of wrapping your content in the representable.
ContentView
struct ContentView: View {
var body: some View {
ViewControllerRepresentable {
Text("SwiftUI Content")
}
}
}
UIViewControllerRepresentable
struct ViewControllerRepresentable<Content: View>: UIViewControllerRepresentable {
#ViewBuilder let content: Content
typealias UIViewControllerType = ViewController<Content>
func makeUIViewController(context: Context) -> BottomSheetViewController<Content> {
ViewController<Content>(content: UIHostingController(rootView: content))
}
func updateUIViewController(_ uiViewController: ViewController<Content>, context: Context) {
}
}
UIViewController
import SwiftUI
import UIKit
final class ViewController<Content: View>: UIViewController {
private let content: UIHostingController<Content>
init(content: UIHostingController<Content>) {
self.content = content
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
return nil
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
setupLayout()
}
private func setupLayout() {
addChild(content)
view.addSubview(content.view)
content.didMove(toParent: self)
// Here you can add UIKit views above your SwiftUI content while keeping it interactive
NSLayoutConstraint.activate([
content.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
content.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
content.view.topAnchor.constraint(equalTo: view.topAnchor),
content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}

Resources