I try to catch an event that occurs in a UIViewController in the SwiftUI view.
the architecture is in SwiftUI with an element in Swift:
The View
struct PlayGame: View {
var body: some View {
if stateProgress.stateOfGame == 0 {
CardsDeckRepresentable()
}
}
}
The UIViewControllerRepresentable
struct CardsDeckRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = CardsDeckViewController
func makeUIViewController(context: Context) -> CardsDeckViewController {
let deck = CardsDeckViewController()
return deck
}
func updateUIViewController(_ uiViewController: CardsDeckViewController, context: Context) {
}
}
The UIViewController
class CardsDeckViewController : UIViewController, iCarouselDelegate, iCarouselDataSource{
... some code ...
func moveCardChoosen(number:Int) {
self.view.addSubview(cardMobile)
UIView.animate(withDuration: 0.5, delay: 0.0, options:[], animations: {
self.cardMobile.center.y = self.cardPlace5.center.y
}, completion: { finished in
if finished {
self.cardMobile.isHidden = true
}
})
}
}
At the end of the animation, I want to tel the swiftUIView to do update.
I tried with some ObservableObject or using the updateUIViewController function.
I can pass data from the SwiftUI View to the UIViewController through the UIViewControllerRepresentable.
But how to revive the change from UIViewController? The updateUIViewController don't seems not be called.
Related
I'm using Pulley a maps drawer library which is written in UIKit in a SwiftUI project. I have a SwiftUI ListView that I'm using in the project via a UIHostingController but I want to disable scrolling when the drawers position is not open and to do that I'm pretty sure I need to use one of the delegate functions Pulley provides (drawerPositionDidChange) but I'm not sure how to use the delegate in the Coordinator or if I should even try to use the delegate, maybe I just need to use some type of state variable?
Delegate in the view controller
#objc public protocol PulleyDelegate: AnyObject {
/** This is called after size changes, so if you care about the bottomSafeArea property for custom UI layout, you can use this value.
* NOTE: It's not called *during* the transition between sizes (such as in an animation coordinator), but rather after the resize is complete.
*/
#objc optional func drawerPositionDidChange(drawer: PulleyViewController, bottomSafeArea: CGFloat)
}
This is the UIViewRepresentable where I'm trying to use the delegate.
import SwiftUI
struct DrawerPosition: UIViewControllerRepresentable {
#Binding var bottomSafeArea: CGFloat?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some UIViewController {
let vc = PulleyViewController()
vc.delegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
// Updates the state of the specified view controller with new information from SwiftUI.
}
class Coordinator: NSObject, PulleyDrawerViewControllerDelegate {
var parent: DrawerPosition
init (_ parent: DrawerPosition) {
self.parent = parent
}
func drawerPositionDidChange(drawer: PulleyViewController, bottomSafeArea: CGFloat){
self.parent.bottomSafeArea = bottomSafeArea
}
}
}
the ListView where I want to disable the scroll.
import SwiftUI
struct ListView: View {
#State private var bottomSafeArea: CGFloat?
var body: some View {
ScrollViewReader { proxy in
VStack {
Button("Jump to #50") {
proxy.scrollTo(50)
}
List(0..<100, id: \.self) { i in
Text("Example")
.id(i)
}.scrollDisabled(bottomSafeArea == 0 ? true : false)
}
}
}
}
class ListViewVHC: UIHostingController<ListView> {
required init?(coder: NSCoder) {
super.init (coder: coder, rootView: ListView())
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Here is the correct way to set up a Coordinator:
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIViewController(context: Context) -> PullyViewController {
context.coordinator.pullyViewController
}
func updateUIViewController(_ uiViewController: PullyViewController, context: Context) {
// Updates the state of the specified view controller with new information from SwiftUI.
context.coordinator.bottomSafeAreaChanged = { bottomSafeArea in
self.bottomSafeArea = bottomSafeArea
}
}
class Coordinator: NSObject, PulleyDrawerViewControllerDelegate {
lazy var pullyViewController: PulleyViewController = {
let vc = PulleyViewController()
vc.delegate = self
return vc
}()
var bottomSafeAreaChanged: ((CGFloat) -> Void)?
func drawerPositionDidChange(drawer: PulleyViewController, bottomSafeArea: CGFloat){
bottomSafeAreaChanged?(bottomSafeArea)
}
I'm using a PageViewController from this tutorial
Since the pages are being added as controllers, here:
controllers = parent.pages.map { UIHostingController(rootView: $0) }
Whenever they're updated in my SwiftUI view, the user can't see any updates, since (i'm guessing) the init method doesn't refresh.
So how am I able to use the current pages instead of instantiating a controllers array?
Here is my code:
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
var onLast: () -> AnyView
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .vertical)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
var controllers = [UIViewController]()
init(_ pageViewController: PageViewController) {
parent = pageViewController
controllers = parent.pages.map { UIHostingController(rootView: $0) }
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController?
{
guard let index = controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return controllers.last
}
return controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController?
{
guard let index = controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == controllers.count {
return controllers.first
}
return controllers[index + 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = controllers.firstIndex(of: visibleViewController) {
if index + 1 == controllers.count {
print("index: \(index)")
print(controllers.count)
controllers.append(UIHostingController(rootView: parent.onLast()))
}
parent.currentPage = index
}
}
}
}
struct PageView<Page: View>: View, Equatable {
static func == (lhs: PageView<Page>, rhs: PageView<Page>) -> Bool {
return true
}
var pages: [Page]
var onLast: () -> AnyView
#Binding var currentPage: Int
var body: some View {
PageViewController(pages: pages, onLast: onLast, currentPage: $currentPage)
}
}
SwiftUI View
import SwiftUI
struct Test: View {
#State var color: Color = Color.blue
var body: some View {
PageView(
pages: [
AnyView(
color
),
AnyView(
Color.purple
),
AnyView(Color.green),
AnyView(Color.orange),
],
onLast: {
color = Color.red
},
currentPage: $currentPage
)
.equatable()
}
}
Well, UIKit views should live in own world, SwiftUI views in own. Actually it is expected here that parent state will not update something deep inside bridged UIKit, even if it looks like it is in same stack, but actually it is just a function and it is called god-damn-known-when.
Anyway the approach is to solve it is simple - use SwiftUI tools to update SwiftUI views. Here it looks appropriate to use ObservableObject view model with explicit view observed it and work with that view model, because it is a reference-type.
Here is a sketch:
class ViewModel: ObservableObject {
#Published var color = Color.blue
}
struct Test: View {
#StateObject var vm = ViewModel() // << owner !!
var body: some View {
PageView(
pages: [
AnyView(
NativePath(vm: vm) // << inject
),
AnyView(
Color.purple
),
AnyView(Color.green),
AnyView(Color.orange),
],
onLast: {
vm.color = Color.red // << update by ref
},
currentPage: $currentPage
)
.equatable()
}
struct NativePath: View {
#ObservedObject var vm: ViewModel // << observe explicitly
var body: some View {
vm.color // << updated
}
}
}
I am building a camera app with all the UI in SwiftUI (parent) holding a UIKit Controller that contains all the recording functionalities. The UI is pretty complex, so would like if possible to remain with this structure for the project.
The UIKit Class has some functions like startRecord() stopRecord() which I would like to be triggered from the SwiftUI view. For that reason, I would like to 'call' the UIKit functions from my SwiftUI view.
I am experimenting with UIViewControllerRepresentable, being able to perform updates on a global variable change, but I am still not able to call the individual functions I want to trigger from the SwiftUI parent.
Here its the SwiftUI file:
init(metalView: MetalViewController?) {
self.metalView = MetalViewController(appStatus: appStatus)
}
var body: some View {
ZStack {
// - Camera view
metalView
.edgesIgnoringSafeArea(.top)
.padding(.bottom, 54)
VStack {
LateralMenuView(appStatus: appStatus, filterTooltipShowing: $_filterTooltipShowing)
Button("RECORD", action: {
print("record button pressed")
metalView?.myMetalDelegate.switchRecording(). // <-- Not sure about this
})
Here is the MetalViewController:
protocol MetalViewControllerDelegate {
func switchRecording()
}
// MARK: - The secret sauce for loading the MetalView (UIKit -> SwiftUI)
struct MetalViewController: UIViewControllerRepresentable {
var appStatus: AppStatus
typealias UIViewControllerType = MetalController
var myMetalDelegate: MetalViewControllerDelegate!
func makeCoordinator() -> Coordinator {
Coordinator(metalViewController: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MetalViewController>) -> MetalController {
let controller = MetalController(appStatus: appStatus)
return controller
}
func updateUIViewController(_ controller: MetalController, context: UIViewControllerRepresentableContext<MetalViewController>) {
controller.changeFilter()
}
class Coordinator: NSObject, MetalViewControllerDelegate {
var controller: MetalViewController
init(metalViewController: MetalViewController) {
controller = metalViewController
}
func switchRecording() {
print("just testing")
}
}
}
and the UIKit Controller...
class MetalController: UIViewController {
var _mydelegate: MetalViewControllerDelegate?
...
override func viewDidLoad() {
...
self._mydelegate = self
}
extension MetalController: MetalViewControllerDelegate {
func switchRecording() {
print("THIS SHOULD BE WORKING, BUT ITS NOT")
}
}
I like to use Combine to pass messages through an ObservableObject to the UIKit views. That way, I can call them imperatively. Rather than trying to parse your code, I made a little example of the concept:
import SwiftUI
import Combine
enum MessageBridgeMessage {
case myMessage(parameter: Int)
}
class MessageBridge : ObservableObject {
#Published var result = 0
var messagePassthrough = PassthroughSubject<MessageBridgeMessage, Never>()
}
struct ContentView : View {
#StateObject private var messageBridge = MessageBridge()
var body: some View {
VStack {
Text("Result: \(messageBridge.result)")
Button("Add 2") {
messageBridge.messagePassthrough.send(.myMessage(parameter: messageBridge.result))
}
VCRepresented(messageBridge: messageBridge)
}
}
}
struct VCRepresented : UIViewControllerRepresentable {
var messageBridge : MessageBridge
func makeUIViewController(context: Context) -> CustomVC {
let vc = CustomVC()
context.coordinator.connect(vc: vc, bridge: messageBridge)
return vc
}
func updateUIViewController(_ uiViewController: CustomVC, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
class Coordinator {
private var cancellable : AnyCancellable?
func connect(vc: CustomVC, bridge: MessageBridge) {
cancellable = bridge.messagePassthrough.sink(receiveValue: { (message) in
switch message {
case .myMessage(let parameter):
bridge.result = vc.addTwo(input: parameter)
}
})
}
}
}
class CustomVC : UIViewController {
func addTwo(input: Int) -> Int {
return input + 2
}
}
In the example, MessageBridge has a PassthroughSubject that can be subscribed to from the UIKit view (or in this case, UIViewController). It's owned by ContentView and passed by parameter to VCRepresented.
In VCRepresented, there's a method on the Coordinator to subscribe to the publisher (messagePassthrough) and act on the messages. You can pass parameters via the associated properties on the enum (MessageBridgeMessage). Return values can be stored on #Published properties on the MessageBridge if you need them (or, you could setup another publisher to go the opposite direction).
It's a little verbose, but seems to be a pretty solid pattern for communication to any level of the tree you need (SwiftUI view, representable view, UIKit view, etc).
I’m trying to integrate a Unity view in SwiftUI, I have the below code, but when I run the app I get no output, I know SpriteKit and SceneKit are possible and my unity view runs in a standard swift app, I’m wondering if swiftUI is possible.
struct ContentView: View {
var body: some View {
UnityUIView()
}
}
struct UnityUIView : UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
appDelegate.startUnity()
return UnityGetGLView()!
}
func updateUIView(_ view: UIView, context: Context) {
}
}
I've tried to create a UIViewControllerRepresentable but get the same thing, The screen flashes once and then disappears, I think it's the splash screen as I changed the colour for debugging, no dice.
struct ContentView: View {
var body: some View {
TestUnityViewController()
}
}
struct TestUnityViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.startUnity()
let unityView = UnityGetGLView()!
vc.view.backgroundColor = .red
vc.view!.addSubview(unityView)
return vc
}
func updateUIViewController(_ viewController: UIViewController, context: Context) {
}
}
If I add a delay to the UIViewControllerRepresentable, it works....interesting
struct TestUnityViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
vc.view.backgroundColor = .red
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.startUnity()
let unityView = UnityGetGLView()!
vc.view!.addSubview(unityView)
}
return vc
}
func updateUIViewController(_ viewController: UIViewController, context: Context) {
}
}
For people still having the issue. I haven't investigated yet, but if you are using the new Unity example to integrate Unity as a framework, the delay indeed fixed the issue with SwiftUI.
You can create a SwiftUI View to which the Unity view will be added:
import SwiftUI
struct TestUnityViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
UnityBridge.showUnity()
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
let unityView = UnityBridge.getAppController().rootView!
vc.view!.addSubview(unityView)
}
return vc
}
func updateUIViewController(_ viewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
TestUnityViewController()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here, UnityBridge is a wrapper around the Unity framework instanciation, similar to the Objective C version from the repository.
The methods:
showUnity() creates the UnityFramework instance, and then calls the showUnityWindow method
getAppController calls the appController method from he UnityFramework object
In the meantime I find a better solution, this will do. I imagine there is a better way to do that, maybe an event triggered to know when Unity's view is fully ready.
EDIT:
I created an example repository to show how to integrate Unity in a SwiftUI project: https://github.com/DavidPeicho/unity-swiftui-example
When using VisionKit's VNDocumentCameraViewController for scanning documents the camera hangs after some seconds. The scan is implemented in a ViewController, which is used in SwiftUI.
The implementation of a DocumentScannerViewController:
import UIKit
import VisionKit
import SwiftUI
final class DocumentScannerViewController: UIViewController, VNDocumentCameraViewControllerDelegate, UIViewControllerRepresentable {
public typealias UIViewControllerType = DocumentScannerViewController
public func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentScannerViewController>) -> DocumentScannerViewController {
return DocumentScannerViewController()
}
public func updateUIViewController(_ uiViewController: DocumentScannerViewController, context: UIViewControllerRepresentableContext<DocumentScannerViewController>) {
}
override func viewDidLoad() {
super.viewDidLoad()
let scannerViewController = VNDocumentCameraViewController()
scannerViewController.delegate = self as VNDocumentCameraViewControllerDelegate
view.addSubview(scannerViewController.view)
}
func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
}
func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
}
func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) {
}
}
And the implementation of the ContentView:
import SwiftUI
struct ContentView: View {
var body: some View {
DocumentScannerViewController()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The document scan camera launches and works for a short period of time. Then camera just stops moving.
Any idea what causes this behavior?
Apple does provide a way to use ViewControllers inside SwiftUI view with UIViewControllerRepresentable which you have to implement this way.
First declare your View as follows:
import SwiftUI
import UIKit
import Vision
import VisionKit
struct ScanningVNDocumentView: UIViewControllerRepresentable {
// implement your custom init() in case ..
typealias UIViewControllerType = VNDocumentCameraViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<ScanningVNDocumentView>) -> VNDocumentCameraViewController {
let viewController = VNDocumentCameraViewController()
viewController.delegate = context.coordinator
return viewController
}
func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: UIViewControllerRepresentableContext<ScanningVNDocumentView>) {
}
func makeCoordinator() -> Coordinator {
//Coordinator is Apple bridge between SwiftUI and ViewController
return Coordinator() `// this basically call init of the UIViewControllerRepresentable above`
}
final class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
#Environment(\.presentationMode) var presentationMode
init() {
}
// implement VNDocumentCameraViewControllerDelegate methods where you can dismiss the ViewController
func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
print("user did press save with scanned docs numbers \(scan.pageCount) ")
}
func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
print("Did press cancel")
}
func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) {
print("Document camera view controller did finish with error ", error)
}
}
}
You can now call your view this way:
var body: some View {
ScanningVNDocumentView()
}