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()
}
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 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.
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 open the finder using Mac Catalyst and get images.
At first, I tried the below code, but Xcode says 'NSOpenPanel' is unavailable in Mac Catalyst .
private func selectFile() {
NSOpenPanel.openImage { (result) in
if case let .success(image) = result {
self.image = image
}
}
}
So I tried this solution, this time Compile was successful, but when I click the button to open finder I got this error message: Thread 1: EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0)
My Final Code is below
import SwiftUI
struct ContentView: View {
var body: some View {
VStack{
Button("Choose file") {
let picker = DocumentPickerViewController(
supportedTypes: ["log"],
onPick: { url in
print("url : \(url)")
},
onDismiss: {
print("dismiss")
}
)
UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class DocumentPickerViewController: UIDocumentPickerViewController {
private let onDismiss: () -> Void
private let onPick: (URL) -> ()
init(supportedTypes: [String], onPick: #escaping (URL) -> Void, onDismiss: #escaping () -> Void) {
self.onDismiss = onDismiss
self.onPick = onPick
super.init(documentTypes: supportedTypes, in: .open)
allowsMultipleSelection = false
delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension DocumentPickerViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
onPick(urls.first!)
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
onDismiss()
}
}
How can I solve this issue?
In my macCatalyst app, I use a UIViewControllerRepresentable to show a UIDocumentPickerViewController
#Binding to your main view so you can reference that data from the selected file
struct DocumentImportViewController: UIViewControllerRepresentable {
#Binding var imgData:Data! //This doesn't need to be a "Data"
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentImportViewController>) -> UIDocumentPickerViewController {
let vc = UIDocumentPickerViewController(forOpeningContentTypes: [.image], asCopy: true)
vc.delegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext<DocumentImportViewController>) {
}
}
Coordinator for delegates
extension DocumentImportViewController{
class Coordinator: NSObject, UIDocumentPickerDelegate {
var parent: DocumentImportViewController
init(_ parent: DocumentImportViewController) {
self.parent = parent
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
parent.imgData = //get image from url
}
}}
I have a SwiftUI view with a sheet that directs to a UIViewControllerRepresentable struct, which uses a Google Ad Manager extension. I want the UIKit extension to dismiss the sheet when it's done running using presentation mode, but using presentation mode through the environment isn't working. Is the code not running the way I've set it up, or is it not possible to dismiss the sheet from the Google Ads Manager?
Alternatively, when I try to dismiss the sheet using a Binding to update isPresented in AdManager, I get the error message: "Property 'self.isPresented' not initialized at super.init call". When I try to initialize it, however, it says I can't do it before super.init, and when I put it after super.init, it says I can't call it after super.init. Is there any way I can dismiss the sheet when the ad is done loading (the function rewardedAdDidDismiss)?
Edit: This post was originally marked as a duplicate, but the linked question asked about dismissing a SwiftUI modal from a UIViewController, not the other way around.
The SwiftUI view:
import SwiftUI
struct AdRevenue: View {
#State var playAd = false
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(action: {
self.playAd = true
})
{
Text("Play Ad")
}.sheet(isPresented: $playAd) {
Ads()}
}
}
This is my UIViewControllerRepresentable:
import GoogleMobileAds
import SwiftUI
import UIKit
struct Ads: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
return ViewController()
}
func updateUIViewController(_ uiView: UIViewController, context: Context) {
}
class ViewController: UIViewController, GADRewardedAdDelegate, AdManagerRewardDelegate {
var rewardedAd: GADRewardedAd?
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
override func viewDidLoad() {
super.viewDidLoad()
AdManager.shared.loadAndShowRewardAd(AdIds.rewarded.rawValue, viewController: self)
AdManager.shared.delegateReward = self
presentationMode.wrappedValue.dismiss()
}
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
print("Reward received: \(reward.type), amount \(reward.amount).")
}
}
}
This is my Google Ads Manager:
public protocol AdManagerRewardDelegate{
func rewardAdGiveRewardToUser(type:String, amount: NSDecimalNumber)
func rewardAdFailedToLoad()
func rewardAdDidReceive(
rewardViewController: UIViewController?,
rewardedAd: GADRewardedAd?,
delegate: AdManager
)
func rewardAdDidOpen()
func rewardAdDidClose()
func rewardAdFailedToPresent()
}
//default implementation AdManagerRewardDelegate
public extension AdManagerRewardDelegate{
func rewardAdGiveRewardToUser(type:String, amount: NSDecimalNumber) {}
func rewardAdFailedToLoad() {}
func rewardAdDidReceive(
rewardViewController: UIViewController?,
rewardedAd: GADRewardedAd?,
delegate: AdManager
) {
if rewardedAd?.isReady == true {
if let rewardViewController = rewardViewController {
rewardedAd?.present(fromRootViewController: rewardViewController, delegate: delegate)
}
}
}
func rewardAdDidOpen() {}
func rewardAdDidClose() {}
func rewardAdFailedToPresent() {}
}
public class AdManager: NSObject {
public static let shared = AdManager()
public var ADS_DISABLED = false
public var delegateReward: AdManagerRewardDelegate?
private var viewController:UIViewController?
private var rewardViewController:UIViewController?
private var testDevices:[String] = [""]
private var rewardedAd: GADRewardedAd?
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
let borderSizeBetweenBannerAndContent:CGFloat = 5
public override init() {
super.init()
}
public func configureWithApp(){
GADMobileAds.sharedInstance().requestConfiguration.testDeviceIdentifiers = testDevices
}
public func setTestDevics(testDevices: [String]){
self.testDevices = testDevices
self.testDevices += [kGADSimulatorID as! String ] //all simulator
}
private func getGADRequest() -> GADRequest{
let request = GADRequest()
return request
}
// MARK:- Reward Video Ads
public func loadAndShowRewardAd(_ adUnit: String, viewController: UIViewController){
self.rewardViewController = viewController
rewardedAd = GADRewardedAd(adUnitID: adUnit)
rewardedAd?.load(getGADRequest()) { error in
if let error = error {
// Handle ad failed to load case.
print("Reward based video ad failed to load. \(error.debugDescription)")
self.delegateReward?.rewardAdFailedToLoad()
} else {
// Ad successfully loaded.
print("Reward based video ad is received.")
self.delegateReward?.rewardAdDidReceive(
rewardViewController: self.rewardViewController,
rewardedAd: self.rewardedAd,
delegate: self
)
}
}
}
}
// MARK:- GADRewardBasedVideoAdDelegate
extension AdManager : GADRewardedAdDelegate {
public func rewardedAdDidPresent(_ rewardedAd: GADRewardedAd) {
print("Rewarded ad presented.")
delegateReward?.rewardAdDidOpen()
}
public func rewardedAd(_ rewardedAd: GADRewardedAd, didFailToPresentWithError error: Error) {
print("Rewarded ad failed to present.")
delegateReward?.rewardAdFailedToPresent()
}
public func rewardedAdDidDismiss(_ rewardedAd: GADRewardedAd) {
presentationMode.wrappedValue.dismiss()
print("Rewarded ad dismissed.")
delegateReward?.rewardAdDidClose()
}
public func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
print("Reward received with currency: \(reward.type), amount \(reward.amount).")
delegateReward?.rewardAdGiveRewardToUser(type: reward.type, amount: reward.amount)
}
}