Swift: Can't dismiss view controller - ios

I have a SwiftUI project in which I'm presenting a view controller to display an advertisement via MoPub.
Everything works as expected except one thing: when I tap the ad's close button, the ad itself closes but the black screen behind the ad continues to show. I guess the view controller is not being dismissed (but the completion block for dismiss does run).
Here's my code:
class InterstitialAds: UIViewController, MPInterstitialAdControllerDelegate {
var moPubView: MPInterstitialAdController?
func viewControllerForPresentingModalView() -> UIViewController! {
return self
}
//Called when you tap the ad's close button:
func interstitialWillDismiss(_ interstitial: MPInterstitialAdController) {
dismissControllerWithoutReward()
}
func showAd() {
let topViewController = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController
self.modalPresentationStyle = .fullScreen
topViewController?.present(self, animated: true) {}
}
func dismissControllerWithoutReward() {
self.dismiss(animated: true) {
print("dismissControllerWithoutReward()") //Successfully prints to console
}
}
override func viewDidLoad() {
let adId = "4f117153f5c24fa6a3a92b818a5eb630" //Test ad unit
self.moPubView = MPInterstitialAdController(forAdUnitId: adId)
if let v = self.moPubView {
v.delegate = self
v.loadAd()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
v.show(from: self)
}
}
super.viewDidLoad()
}
}
Question:
Why isn't the view controller being dismissed, despite the successful call to dismiss?
Thank you!
Edit:
Interestingly, if I wait 0.5 seconds before trying to dismiss the view controller, it dismisses as desired. So, now I've got this code in interstitialWillDismiss(_:) (but I still want to know why this is happening):
func interstitialWillDismiss(_ interstitial: MPInterstitialAdController) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.dismissControllerWithoutReward()
}
}

For getting clear implementation and expected behavior you should wrap InterstitialAds controller in UIViewControllerRepresentable, then connect you SwiftUI side with some InterstitialAdsView which implements from UIViewControllerRepresentable via some isPresented flag binding.
Short example:
class InterstitialAds: UIViewController {
let onFlowCompleted: () -> Void
init(onFlowCompleted: #escaping () -> Void) {
self.onFlowCompleted = onFlowCompleted
// ...
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
onFlowCompleted()
// or somewhere else ...
}
}
struct HomeView: View {
#State var shouldShowInterstitialView = false
var body: some View {
Button {
shouldShowInterstitialView = true
} label: {
Text("Show Ad")
}
.fullScreenCover(isPresented: $shouldShowInterstitialView) {
InterstitialAdsView {
shouldShowInterstitialView = false
}
}
}
}
struct InterstitialAdsView: UIViewControllerRepresentable {
// #Environment(\.presentationMode) var presentationMode
// or
// #Binding var isPresented: Bool
// or
let onFlowCompleted: () -> Void
func makeUIViewController(context: Context) -> InterstitialAds {
InterstitialAds(onFlowCompleted: onFlowCompleted)
}
func updateUIViewController(_ uiViewController: InterstitialAds, context: Context) {
// update if needed
}
}

Related

Callback is not working when button tapped

I want to trigger an action on button tap with my callback. Also I have presenter and coordinator. But nothing happenes. My code is not working in this closure:
startViewController.output = { [weak self] action in
switch action {
case .registrationButtonTapped:
self?.showRegistrationViewController()
case .loginButtonTapped:
self?.showLoginViewController()
}
}
In my ViewController I have enum:
enum StartViewControllerButton {
case registrationButtonTapped
case loginButtonTapped
}
callback:
var output: ((StartViewControllerButton) -> Void)?
and selectors:
#objc func registrationButtonPressed() {
startModulPresenter.openNextScreen()
self.output?(.registrationButtonTapped)
}
#objc func loginButtonPressed() {
startModulPresenter.openNextScreen()
self.output?(.loginButtonTapped)
}
My Presenter
class StartModulPresenter: StartModulPresenterProtocol {
var navigationController: UINavigationController
var coordinator: CoordinatorProtocol?
//Init
init(navigationController: UINavigationController) {
self.navigationController = navigationController
coordinator = AuthorizationCoordinator(navigationController: navigationController)
}
//Functions
func openNextScreen() {
coordinator?.start()
}
}
My Coordinator:
class AuthorizationCoordinator: RegistrationCoordinatorProtocol {
var presenter: PresenterProtocol?
var navigationController: UINavigationController
var childCoordinators: [CoordinatorProtocol] = []
//Init
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
presenter = StartModulPresenter(navigationController: navigationController)
let startViewController = StartViewController(startModulPresenter: presenter as! StartModulPresenter)
startViewController.output = { [weak self] action in
switch action {
case .registrationButtonTapped:
self?.showRegistrationViewController()
case .loginButtonTapped:
self?.showLoginViewController()
}
}
}
private func showRegistrationViewController() {
let registrationViewController = RegistrationViewController()
registrationViewController.view.backgroundColor = .orange
self.navigationController.pushViewController(registrationViewController, animated: true)
}
private func showLoginViewController() {
let loginViewController = LoginViewController()
loginViewController.view.backgroundColor = .orange
self.navigationController.pushViewController(loginViewController, animated: true)
}
}
Could you check if startViewController is pushed/presented or not?
func start() {
presenter = StartModulPresenter(navigationController: navigationController)
let startViewController = StartViewController(startModulPresenter: presenter as! StartModulPresenter)
startViewController.output = { [weak self] action in
switch action {
case .registrationButtonTapped:
self?.showRegistrationViewController()
case .loginButtonTapped:
self?.showLoginViewController()
}
}
}
And, is self.output is nil or not? If it is nil please check your assignment call, it needed to be called before you use this variable.
#objc func loginButtonPressed() {
startModulPresenter.openNextScreen()
self.output?(.loginButtonTapped)
}
Honestly, I don't recommend you to use this design pattern, just a simple thing but the real result is too complicated.
Just use protocol-based MVC. View communicate with Controller via protocol/closure or Reactive-based with Combine (PassthroughSubject/CurrentValueSubject)

Presenting UIDocumentInteractionController with UIViewControllerRepresentable in SwiftUI

I'm creating a new iOS app using SwiftUI where ever possible. However, I want to be able to generate a PDF with some data.
In a similar project without swiftUI I can do this
let docController = UIDocumentInteractionController.init(url: "PATH_TO_FILE")
docController.delegate = self
self.dismiss(animated: false, completion: {
docController.presentPreview(animated: true)
})
and as long as somewhere else in the view controller I have this:
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self
}
I'm good to go.
What I can't work out is how to apply this to a UIViewControllerRepresentable and have it working in SwiftUI. Should my UIViewControllerRepresentable be aiming to be a UIViewController? How do I then set the delegate and presentPreview? Will this overlay any view and display full screen over my SwiftUI app as it does for my standard iOS app?
Thanks
Here is possible approach to integrate UIDocumentInteractionController for usage from SwiftUI view.
Full-module code. Tested with Xcode 11.2 / iOS 13.2
import SwiftUI
import UIKit
struct DocumentPreview: UIViewControllerRepresentable {
private var isActive: Binding<Bool>
private let viewController = UIViewController()
private let docController: UIDocumentInteractionController
init(_ isActive: Binding<Bool>, url: URL) {
self.isActive = isActive
self.docController = UIDocumentInteractionController(url: url)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPreview>) -> UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<DocumentPreview>) {
if self.isActive.wrappedValue && docController.delegate == nil { // to not show twice
docController.delegate = context.coordinator
self.docController.presentPreview(animated: true)
}
}
func makeCoordinator() -> Coordintor {
return Coordintor(owner: self)
}
final class Coordintor: NSObject, UIDocumentInteractionControllerDelegate { // works as delegate
let owner: DocumentPreview
init(owner: DocumentPreview) {
self.owner = owner
}
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return owner.viewController
}
func documentInteractionControllerDidEndPreview(_ controller: UIDocumentInteractionController) {
controller.delegate = nil // done, so unlink self
owner.isActive.wrappedValue = false // notify external about done
}
}
}
// Demo of possible usage
struct DemoPDFPreview: View {
#State private var showPreview = false // state activating preview
var body: some View {
VStack {
Button("Show Preview") { self.showPreview = true }
.background(DocumentPreview($showPreview, // no matter where it is, because no content
url: Bundle.main.url(forResource: "example", withExtension: "pdf")!))
}
}
}
struct DemoPDFPreview_Previews: PreviewProvider {
static var previews: some View {
DemoPDFPreview()
}
}
I ended up doing something like the following as I wasn't able to get this working reliably with UIViewControllerRepresentable and the above answer. You might need to edit / extend this for your usecase.
class DocumentController: NSObject, ObservableObject, UIDocumentInteractionControllerDelegate {
let controller = UIDocumentInteractionController()
func presentDocument(url: URL) {
controller.delegate = self
controller.url = url
controller.presentPreview(animated: true)
}
func documentInteractionControllerViewControllerForPreview(_: UIDocumentInteractionController) -> UIViewController {
return UIApplication.shared.windows.first!.rootViewController!
}
}
Usage:
struct DocumentView: View {
#StateObject var documentController = DocumentController()
var body: some View {
Button(action: {
documentController.presentDocument(url: ...)
}, label: {
Text("Show Doc")
})
}
}
Using QLPreviewController
I know the question is about UIDocumentInteractionController, but if you want to present a PDF file (for example), you can use a QLPreviewController.
Local file
Presenting a local file:
import SwiftUI
struct DocView: View {
#State private var buttonPressed: Bool = false
var body: some View {
Button {
buttonPressed = true
} label: {
Text("Show PDF file")
}
.sheet(isPresented: $buttonPressed) {
let localURL = Bundle.main.url(forResource: "Example", withExtension: "pdf")!
PreviewController(url: localURL)
}
}
}
Remote file
Please see this gist if you need to present a remote file.
PreviewController
The UIViewControllerRepresentable for QLPreviewController.
import QuickLook
import SwiftUI
struct PreviewController: UIViewControllerRepresentable {
#Environment(\.dismiss) private var dismiss
let url: URL
func makeUIViewController(context: Context) -> UINavigationController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
controller.navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .done, target: context.coordinator,
action: #selector(context.coordinator.dismiss)
)
let navigationController = UINavigationController(rootViewController: controller)
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: QLPreviewControllerDataSource {
let parent: PreviewController
init(parent: PreviewController) {
self.parent = parent
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(
_ controller: QLPreviewController,
previewItemAt index: Int
) -> QLPreviewItem {
return parent.url as NSURL
}
#objc func dismiss() {
parent.dismiss()
}
}
}

ReplayKit with SwiftUI

I want to record the screen with ReplayKit.
I have researched the method with UIKit. But my project used SwiftUI, so I want to use the ReplayKit to record the screen with SwiftUI.
How I record the screen with SwiftUI?
-
When I use the stopRecording function, the function will have previewViewController. But I cannot call present function to present previewViewController.
Note: This is not a very practical answer.
In most cases, "SwiftUI.View" runs on top of "UIHostingController".
You need to grab this to present the "RPPreviewViewController".
You can find one of them by following the "UIApplication".
let scene = UIApplication.shared.connectedScenes.first as! UIWindowScene
let viewController = scene.windows.last!.rootViewController
viewController.present(previewViewController, animated: true, completion:nil)
I have just open-sourced a simple ReplayKit application, which uses SwiftUI.
https://github.com/snakajima/ReplayStartUpKit
Please take a look at how it presents a RPBroadcastActivityViewController from SwiftUI.
It first stores the pointer to the controller in a property bavController, then set #Pubilshed property activePopup to initiate SwiftUI change.
RPBroadcastActivityViewController.load { controller, error in
if let controller = controller {
self.bavController = controller
controller.delegate = self
self.activePopup = .broadCast
}
}
In SwiftUI (MainUIView.swift), the following view is activated when the property activePopup becomes .broadCast.
.sheet(item: $state.activePopup) { item in
switch(item) {
case .broadCast:
BroadcastActivityController(controller: state.bavController!)
}
}
BroadcastActivityController is bit long because of a work-around for iPad, but it is just a wrapper of RPBroadcastActivityController.
struct BroadcastActivityController: UIViewControllerRepresentable {
let controller: RPBroadcastActivityViewController
func makeUIViewController(context: Context) -> RPBroadcastActivityViewController {
return controller
}
func updateUIViewController(_ uiViewController: RPBroadcastActivityViewController, context: Context) {
// Hack to work around iPad issue
if UIDevice.current.userInterfaceIdiom == .pad {
guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate,
let vc = sceneDelegate.uiWindow?.rootViewController,
let view = vc.view else {
print("somethign is really wrong")
return
}
controller.modalPresentationStyle = .popover
if let popover = controller.popoverPresentationController {
popover.sourceRect = CGRect(origin: .zero, size: CGSize(width: 10, height: 10))
popover.sourceView = view
popover.permittedArrowDirections = []
}
}
}
typealias UIViewControllerType = RPBroadcastActivityViewController
}
You need to do something very similar to previewViewController.
Try This
import SwiftUI
import ReplayKit
struct ContentView: View {
let recorder = RPScreenRecorder.shared()
#State var isBool = false
#State var rp: RPPreviewView!
#State var isRecording = false
#State var isShowPreviewVideo = false
var body: some View {
ZStack {
VStack {
Button(action: {
if !self.isRecording {
self.startRecord()
} else {
self.stopRecord()
}
}) {
Image(systemName: isRecording ? "stop.circle" : "video.circle")
.resizable()
.frame(width: 100, height: 100)
}
}
if isShowPreviewVideo {
rp
.transition(.move(edge: .bottom))
.edgesIgnoringSafeArea(.all)
}
}
}
func startRecord() {
guard recorder.isAvailable else {
print("Recording is not available at this time.")
return
}
if !recorder.isRecording {
recorder.startRecording { (error) in
guard error == nil else {
print("There was an error starting the recording.")
return
}
print("Started Recording Successfully")
self.isRecording = true
}
}
}
func stopRecord() {
recorder.stopRecording { (preview, error) in
print("Stopped recording")
self.isRecording = false
guard let preview = preview else {
print("Preview controller is not available.")
return
}
self.rp = RPPreviewView(rpPreviewViewController: preview, isShow: self.$isShowPreviewVideo)
withAnimation {
self.isShowPreviewVideo = true
}
}
}
}
struct RPPreviewView: UIViewControllerRepresentable {
let rpPreviewViewController: RPPreviewViewController
#Binding var isShow: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> RPPreviewViewController {
rpPreviewViewController.previewControllerDelegate = context.coordinator
rpPreviewViewController.modalPresentationStyle = .fullScreen
return rpPreviewViewController
}
func updateUIViewController(_ uiViewController: RPPreviewViewController, context: Context) { }
class Coordinator: NSObject, RPPreviewViewControllerDelegate {
var parent: RPPreviewView
init(_ parent: RPPreviewView) {
self.parent = parent
}
func previewControllerDidFinish(_ previewController: RPPreviewViewController) {
withAnimation {
parent.isShow = false
}
}
}
}

Is there a SwiftUI equivalent for viewWillDisappear(_:) or detect when a view is about to be removed?

In SwiftUI, I'm trying to find a way to detect that a view is about to be removed only when using the default navigationBackButton. Then perform some action.
Using onDisappear(perform:) acts like viewDidDisappear(_:), and the action performs after another view appears.
Or, I was thinking the above problem might be solved by detecting when the default navigationBarBackButton is pressed. But I've found no way to detect that.
Is there any solution to perform some action before another view appears?
(I already know it is possible to do that by creating a custom navigation back button to dismiss a view)
Here is approach that works for me, it is not pure-SwiftUI but I assume worth posting
Usage:
SomeView()
.onDisappear {
print("x Default disappear")
}
.onWillDisappear { // << order does NOT matter
print(">>> going to disappear")
}
Code:
struct WillDisappearHandler: UIViewControllerRepresentable {
func makeCoordinator() -> WillDisappearHandler.Coordinator {
Coordinator(onWillDisappear: onWillDisappear)
}
let onWillDisappear: () -> Void
func makeUIViewController(context: UIViewControllerRepresentableContext<WillDisappearHandler>) -> UIViewController {
context.coordinator
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<WillDisappearHandler>) {
}
typealias UIViewControllerType = UIViewController
class Coordinator: UIViewController {
let onWillDisappear: () -> Void
init(onWillDisappear: #escaping () -> Void) {
self.onWillDisappear = onWillDisappear
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onWillDisappear()
}
}
}
struct WillDisappearModifier: ViewModifier {
let callback: () -> Void
func body(content: Content) -> some View {
content
.background(WillDisappearHandler(onWillDisappear: callback))
}
}
extension View {
func onWillDisappear(_ perform: #escaping () -> Void) -> some View {
self.modifier(WillDisappearModifier(callback: perform))
}
}
You can bind the visibility of the child view to some state, and monitor that state for changes.
When the child view is pushed, the onChange block is called with show == true. When the child view is popped, the same block is called with show == false:
struct ParentView: View {
#State childViewShown: Bool = false
var body: some View {
NavigationLink(destination: Text("child view"),
isActive: self.$childViewShown) {
Text("show child view")
}
.onChange(of: self.childViewShown) { show in
if show {
// child view is appearing
} else {
// child view is disappearing
}
}
}
}
Here's a slightly more succinct version of the accepted answer:
private struct WillDisappearHandler: UIViewControllerRepresentable {
let onWillDisappear: () -> Void
func makeUIViewController(context: Context) -> UIViewController {
ViewWillDisappearViewController(onWillDisappear: onWillDisappear)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
private class ViewWillDisappearViewController: UIViewController {
let onWillDisappear: () -> Void
init(onWillDisappear: #escaping () -> Void) {
self.onWillDisappear = onWillDisappear
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onWillDisappear()
}
}
}
extension View {
func onWillDisappear(_ perform: #escaping () -> Void) -> some View {
background(WillDisappearHandler(onWillDisappear: perform))
}
}
you have a couple of actions for each object that you want to show on the screen
func onDisappear(perform action: (() -> Void)? = nil) -> some View
//Adds an action to perform when this view disappears.
func onAppear(perform action: (() -> Void)? = nil) -> some View
//Adds an action to perform when this view appears.
you can use the like the sample ( in this sample it affects on the VStack):
import SwiftUI
struct TestView: View {
#State var textObject: String
var body: some View {
VStack {
Text(textObject)
}
.onAppear {
textObject = "Vertical stack is appeared"
}
.onDisappear {
textObject = ""
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TestView()
}
}
}
You can trigger the change of the #Environment .scenePhase like this :
struct YourView: View {
#Environment(\.scenePhase) var scenePhase
var body: Some View {
VStack {
// Your View code
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
print("active")
case .inactive:
print("inactive")
case .background:
print("background")
#unknown default:
print("?")
}
}
}
}

How to make initial view controller present again in iOS?

I have an app that uses a main screen (below) and then a barcode scanner framework to scan in a barcode and then I will code an action to perform. However, the problem I am having is that in the code it takes me back to the scanning screen. I have tried to use present(VC1, animated: true, completion: nil) and it does not know what VC1 is.
I assigned VC1 in Xcode under storyboard id (see below):
Below is my code here for view controller.swift. Now the code takes you back to the scanning screen, but I want to go back to VC1 the main view controller.
import UIKit
import BarcodeScanner
var presentedViewController: UIViewController?
final class ViewController: UIViewController {
#IBOutlet var pushScannerButton: UIButton!
//Present view and handle barcode scanning
#IBAction func handleScannerPush(_ sender: Any, forEvent event: UIEvent) {
let viewController = makeBarcodeScannerViewController()
viewController.title = "Barcode Scanner"
present(viewController, animated: true, completion: nil)
}
private func makeBarcodeScannerViewController() -> BarcodeScannerViewController {
let viewController = BarcodeScannerViewController()
viewController.codeDelegate = self
viewController.errorDelegate = self
viewController.dismissalDelegate = self
return viewController
}
}
// MARK: - BarcodeScannerCodeDelegate
extension ViewController: BarcodeScannerCodeDelegate {
func scanner(_ controller: BarcodeScannerViewController, didCaptureCode code: String, type: String) {
print("Barcode Data: \(code)")
print("Symbology Type: \(type)")
controller.dismiss(animated: true, completion: nil)
// DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
// controller.resetWithError()
// }
}
}
// MARK: - BarcodeScannerErrorDelegate
extension ViewController: BarcodeScannerErrorDelegate {
func scanner(_ controller: BarcodeScannerViewController, didReceiveError error: Error) {
print(error)
}
}
// MARK: - BarcodeScannerDismissalDelegate
extension ViewController: BarcodeScannerDismissalDelegate {
func scannerDidDismiss(_ controller: BarcodeScannerViewController) {
controller.dismiss(animated: true, completion: nil)
}
}

Resources