How to get environment object in UIViewController? - ios

I write SwiftUI module, but there is a feature that uses UIKit functionality. So in my SwiftUI view there is a UIViewRepresentable wrapper, that makes UIKit controller. In its turn it makes a collection view where view is SwiftUI view.
So I have a chain like this: View -> ViewRepresentable -> CollectionView -> View
And the problem is that I have #State variable in the first View, and I want to access and change it in the last view. How can I do it?
FirstView.swift
...
#ObservedObject var viewModel: ViewModel
struct FirstView: View {
var body: some View {
CollectionWrapper()
.environmentObject(viewModel)
}
}
...
Wrapper.swift
...
struct CollectionWrapper: UIViewRepresentable {
#EnvironmentObject var viewModel: Model
func makeUIView(context: Context) -> CollectionView {
CollectionView() // how I can pass the variable? DI is not actually working right.
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
...
CollectionView.swift
...
class CollectionView: UICollectionView {
...
func makeView() -> UIView {
// Make hosting controller etc.
hostingContoller.view = ModelView() // and here I somehow should pass my view model
}
...
}
...
I have already tried to do DI through the wrapper in makeUIView method, but my app is crashing because .environmentObject doesn't work in hosting controller for some reason.

Related

How can i show UIViewControllerRepresentable screen on button tap from UIViewRepresentable

I am working on a SwiftUI project, in my app i have to show a map, i am able to show map using UIViewRepresentable, now that map view will have a button and on tapping on that button mapview will needs to show controller (full screen direction) view and i know i can do this using UIViewControllerRepresentable.
I need help in which how can i show the UIViewControllerRepresentable from button click of UIViewRepresentable
Till now what i have done is, adding my code :
On button click i am calling showDetailMap function which calls the MapDetailViewController , but some how MapDetailViewController is not opening as model.
private func showDetailMap() {
MapDetailViewController()
}
struct MapDetailViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MapDetailViewController {
let viewController: MapDetailViewController = MapDetailViewController()
return viewController
}
func updateUIViewController(_ uiViewController: MapDetailViewController, context: Context) {}
}
Please help me with this.
struct ContentView: View {
#State var isPresentVC = false
var body: some View {
ZStack {
Button("OpenMapVC") {
isPresentVC = true
}
}.fullScreenCover(isPresented: $isPresentVC, content: {
MapDetailViewController()
})
}
}

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

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

Send tapAction from SwiftUI button action to UIView function

I'm trying to find a way to trigger an action that will call a function in my UIView when a button gets tapped inside swiftUI.
Here's my setup:
foo()(UIView) needs to run when Button(SwiftUI) gets tapped
My custom UIView class making use of AVFoundation frameworks
class SomeView: UIView {
func foo() {}
}
To use my UIView inside swiftUI I have to wrap it in UIViewRepresentable
struct SomeViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> CaptureView {
SomeView()
}
func updateUIView(_ uiView: CaptureView, context: Context) {
}
}
SwiftUI View that hosts my UIView()
struct ContentView : View {
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable()
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
}) {
Text("Tap Here")
}
}
}
}
}
You can store an instance of your custom UIView in your representable struct (SomeViewRepresentable here) and call its methods on tap actions:
struct SomeViewRepresentable: UIViewRepresentable {
let someView = SomeView() // add this instance
func makeUIView(context: Context) -> SomeView { // changed your CaptureView to SomeView to make it compile
someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
func callFoo() {
someView.foo()
}
}
And your view body will look like this:
let someView = SomeViewRepresentable()
var body: some View {
VStack(alignment: .center, spacing: 24) {
someView
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
self.someView.callFoo()
}) {
Text("Tap Here")
}
}
}
}
To test it I added a print to the foo() method:
class SomeView: UIView {
func foo() {
print("foo called!")
}
}
Now tapping on your button will trigger foo() and the print statement will be shown.
M Reza's solution works for simple situations, however if your parent SwiftUI view has state changes, every time when it refreshes, it will cause your UIViewRepresentable to create new instance of UIView because of this: let someView = SomeView() // add this instance. Therefore someView.foo() is calling the action on the previous instance of SomeView you created, which is already outdated upon refreshing, so you might not see any updates of your UIViewRepresentable appear on your parent view.
See: https://medium.com/zendesk-engineering/swiftui-uiview-a-simple-mistake-b794bd8c5678
A better practice would be to avoid creating and referencing that instance of UIView when calling its function.
My adaption to M Reza's solution would be calling the function indirectly through parent view's state change, which triggers updateUIView :
var body: some View {
#State var buttonPressed: Bool = false
VStack(alignment: .center, spacing: 24) {
//pass in the #State variable which triggers actions in updateUIVIew
SomeViewRepresentable(buttonPressed: $buttonPressed)
.background(Color.gray)
HStack {
Button(action: {
buttonPressed = true
}) {
Text("Tap Here")
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#Binding var buttonPressed: Bool
func makeUIView(context: Context) -> SomeView {
return SomeView()
}
//called every time buttonPressed is updated
func updateUIView(_ uiView: SomeView, context: Context) {
if buttonPressed {
//called on that instance of SomeView that you see in the parent view
uiView.foo()
buttonPressed = false
}
}
}
Here's another way to do it using a bridging class.
//SwiftUI
struct SomeView: View{
var bridge: BridgeStuff?
var body: some View{
Button("Click Me"){
bridge?.yo()
}
}
}
//UIKit or AppKit (use NS instead of UI)
class BridgeStuff{
var yo:() -> Void = {}
}
class YourViewController: UIViewController{
override func viewDidLoad(){
let bridge = BridgeStuff()
let view = UIHostingController(rootView: SomeView(bridge: bridge))
bridge.yo = { [weak self] in
print("Yo")
self?.howdy()
}
}
func howdy(){
print("Howdy")
}
}
Here is yet another solution! Communicate between the superview and the UIViewRepresentable using a closure:
struct ContentView: View {
/// This closure will be initialized in our subview
#State var closure: (() -> Void)?
var body: some View {
SomeViewRepresentable(closure: $closure)
Button("Tap here!") {
closure?()
}
}
}
Then initialize the closure in the UIViewRepresentable:
struct SomeViewRepresentable: UIViewRepresentable {
// This is the same closure that our superview will call
#Binding var closure: (() -> Void)?
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// Since `closure` is part of our state, we can only set it on the main thread
DispatchQueue.main.async {
closure = {
// Perform some action on our UIView
}
}
return uiView
}
}
#ada10086 has a great answer. Just thought I'd provide an alternative solution that would be more convenient if you want to send many different actions to your UIView.
The key is to use PassthroughSubject from Combine to send messages from the superview to the UIViewRepresentable.
struct ContentView: View {
/// This will act as a messenger to our subview
private var messenger = PassthroughSubject<String, Never>()
var body: some View {
SomeViewRepresentable(messenger: messenger) // Pass the messenger to our subview
Button("Tap here!") {
// Send a message
messenger.send("button-tapped")
}
}
}
Then we monitor the PassthroughSubject in our subview:
struct SomeViewRepresentable: UIViewRepresentable {
let messenger = PassthroughSubject<String, Never>()
#State private var subscriptions: Set<AnyCancellable> = []
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// This must be run on the main thread
DispatchQueue.main.async {
// Subscribe to messages
messenger.sink { message in
switch message {
// Call funcs in `uiView` depending on which message we received
}
}
.store(in: &subscriptions)
}
return uiView
}
}
This approach is nice because you can send any string to the subview, so you can design a whole messaging scheme.
My solution is to create an intermediary SomeViewModel object. The object stores an optional closure, which is assigned an action when SomeView is created.
struct ContentView: View {
// parent view holds the state object
#StateObject var someViewModel = SomeViewModel()
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable(model: someViewModel)
.background(Color.gray)
HStack {
Button {
someViewModel.foo?()
} label: {
Text("Tap Here")
}
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#ObservedObject var model: SomeViewModel
func makeUIView(context: Context) -> SomeView {
let someView = SomeView()
// we don't want the model to hold on to a reference to 'someView', so we capture it with the 'weak' keyword
model.foo = { [weak someView] in
someView?.foo()
}
return someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
}
class SomeViewModel: ObservableObject {
var foo: (() -> Void)? = nil
}
Three benefits doing it this way:
We avoid the original problem that #ada10086 identified with #m-reza's solution; creating the view only within the makeUIView function, as per the guidance from Apple Docs, which state that we "must implement this method and use it to create your view object."
We avoid the problem that #orschaef identified with #ada10086's alternative solution; we're not modifying state during a view update.
By using ObservableObject for the model, we can add #Published properties to the model and communicate state changes from the UIView object. For instance, if SomeView uses KVO for some of its properties, we can create an observer that will update some #Published properties, which will be propagated to any interested SwiftUI views.

Resources