Dismiss a React Native RCTRootView back to a Native ViewController - ios

I'm creating an app to integrate react-native with an existing Swift app.
I've looked into similar issues:
React-Native: Dismiss/Exit React-Native View back to Native
How can I go back to native view controller from react-native page?
While following different tutorials:
React Native calling class methods on native Swift
Swift in React Native the ultimate guide
React Native tutorial integrating in an existing app*
And the official docs
The problem is: all of them are outdated (but the docs). Most of them use the legacy Navigation rather than Stack Navigator. One of the tutorials (the one with an asterisk) shows how to dismiss the React Native app back to the Native app using the rootTag of the app, but again, this was done with the legacy Navigation.
If I try to do the same, I'm not able to see the props from my app.
I have a single Storyboard with a Button inside that when clicked calls this UIViewController:
ButtonController
import Foundation
import UIKit
import React
class ButtonController: UIViewController {
#IBOutlet weak var button: UIButton!
#IBAction func buttonClicked(_ sender: Any) {
let data:[String : String] = ["onNavigationStateChange": "{handleNavigationChange}",
"uriPrefix":"/app"];
let rootView = MixerReactModule.sharedInstance.viewForModule("ReactNativeApp", initialProperties: data)
let viewController = UIViewController()
viewController.view = rootView
self.present(viewController, animated: true, completion: nil)
}
}
And when I start the app I can see this:
2019-10-23 10:29:30.021 [info][tid:com.facebook.react.JavaScript] Running "ReactNativeApp" with {"rootTag":1,"initialProps":{"uriPrefix":"/app","onNavigationStateChange":"{handleNavigationChange}"}}
But when I try to access the this.props property on React Native code I get undefined.
index.js
import HomeScreen from './components/HomeScreen'
import {AppRegistry} from 'react-native';
import {createAppContainer} from 'react-navigation';
import {createStackNavigator} from 'react-navigation-stack';
const MainNavigator = createStackNavigator({
Home: {screen: HomeScreen}
}, {
initialRouteName: 'Home'
})
const NavigationApp = createAppContainer(MainNavigator);
AppRegistry.registerComponent('ReactNativeApp', () => NavigationApp)
console.log("NANANA1", this)
console.log("NANANA2", this.routeName)
console.log("NANANA3", MainNavigator)
console.log("NANANA4", MainNavigator.props)
console.log("NANANA5", NavigationApp)
console.log("NANANA6", NavigationApp.props)
export default NavigationApp
HomeScreen.js
import React from 'react';
import {Text, View, Button, NativeModules} from 'react-native';
var RNBridge = NativeModules.RNBridge;
export default class HomeScreen extends React.Component {
static navigationOptions = {
headerTitle: () => (
<Text>'Welcome'</Text>
),
headerLeft: () => (
<Button title="Dismiss" onPress={() => {
console.log("WOLOLO: ", RNBridge)
console.log("ROGAN: ", this._reactInternalFiber.tag)
RNBridge.dismissPresentedViewController(this._reactInternalFiber.tag)
// RNBridge.dismissPresentedViewController(1)
}}/>
)
};
render() {
console.log('KIKIKI', this._reactInternalFiber.tag)
console.log("MMMMMMM: ", this.props)
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}
}
These are the 2 files that I use in RN to generate my View. I've tried many things to get the rootTag value, and the only one that seems to provide this value is (tag and rootTag on XCode are the same (1))
this._reactInternalFiber.tag
But I don't know how to send these values to my headerLeft method to use the same tag so that when I press the Dismiss button it calls the Swift code for dismissPresentedViewController.
How can I dismiss my VC effectively? Or at least get the rootTag being passed from Swift to my headerLeft() method?
I'm using these versions of react-native:
react-native-cli: 2.0.1
react-native: 0.61.2

If your are looking to dismiss the native swift view controller presented from RN View
self.dismiss(animated: true, completion: nil)
I present the swift view controller as below
let modelVC = ModelViewController() <-- sub class of UIViewController
DispatchQueue.main.async {
let navController = UINavigationController(rootViewController: modelVC)
navController.modalPresentationStyle = .fullScreen
let topController = UIApplication.topMostViewController()
topController?.present(navController, animated: true, completion: nil)
}
I have an extension on the UIApplication to find out what is the top most view controller which is used above
extension UIApplication {
class func topMostViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topMostViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topMostViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topMostViewController(controller: presented)
}
return controller
}
}

Related

Solution for the implementation of interstitial ads of admob (SwiftUI)

Starting with iOS16, there was a problem with the implementation of AdMob interstitial advertising. I am using the SwiftUI framework. I looked at many sources, but unfortunately I did not find a solution. Link to on a similar topic, but without a solution. On iOS 15 everything works fine, but on iOS 16 the app crashes with the following error:
SwiftUI/UIViewControllerRepresentable.swift:332: Fatal error:
UIViewControllerRepresentables must be value types: InterstitialAdView
2022-11-20 15:34:32.811071+0300 MyApp[38437:1156286]
SwiftUI/UIViewControllerRepresentable.swift:332: Fatal error:
UIViewControllerRepresentables must be value types: InterstitialAdView
As I understand, to implement the UIViewControllerRepresentable protocol, you must use a struct, but the NSObject and GADFullScreenContentDelegate protocols can only be applied in a class. Question: how can the code be modified to meet the above requirements? I am using the following code (original from here):
// InterstitialAdView
final class InterstitialAdView: NSObject, UIViewControllerRepresentable, GADFullScreenContentDelegate {
//Here's the Ad Object we just created
let interstitialAd = InterstitialAd.shared.interstitialAd
#Binding var isPresented: Bool
var adUnitId: String
init(isPresented: Binding<Bool>, adUnitId: String) {
self._isPresented = isPresented
self.adUnitId = adUnitId
super.init()
interstitialAd?.fullScreenContentDelegate = self //Set this view as the delegate for the ad
}
//Make's a SwiftUI View from a UIViewController
func makeUIViewController(context: Context) -> UIViewController {
let view = UIViewController()
//Show the ad after a slight delay to ensure the ad is loaded and ready to present
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) {
self.showAd(from: view)
}
return view
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
//Presents the ad if it can, otherwise dismisses so the user's experience is not interrupted
func showAd(from root: UIViewController) {
if let ad = interstitialAd {
ad.present(fromRootViewController: root)
} else {
self.isPresented.toggle()
}
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
//Prepares another ad for the next time view presented
InterstitialAd.shared.loadAd(withAdUnitId: adUnitId)
//Dismisses the view once ad dismissed
isPresented.toggle()
}
}

SwiftUI and Unity integration window hierarchy

I'm trying to make an Unity player integration inside a native SwiftUI app. The player should appear when taping a button then user can close the player from Unity and get back to the SwiftUI app.
For the moment I can manage to display the Unity player with this code
SwiftUI View from where I want to display the player
//Some SwiftUI View
NavigationView {
SomeView()
.fullScreenCover(isPresented: $applicationState.unityPlayerActive, onDismiss: {}, content: {
//This is a UIViewControllerRepresentable that call the unity.show() function from Unity Framework
UnityPlayer()
.ignoresSafeArea()
})
}
UnityPlayer.swift
struct UnityPlayer: UIViewControllerRepresentable {
func makeUIViewController(context _: Context) -> UIViewController {
let vc = UIViewController()
let unity = UnityBridge.getInstance()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
unity.show()
}
return vc
}
func updateUIViewController(_: UIViewController, context _: Context) {
// Empty.
}
}
UnityBridge.swift
...
/// Notifies the UnityFramework to show the window, and append the Unity view
/// to the given controller
///
/// - Parameter controller: Controller that will host the Unity view
public func show() {
ufw.showUnityWindow()
}
/// Triggered by Unity via `UnityFrameworkListener` when the framework unloaded
internal func unityDidUnload(_: Notification!) {
ufw.unregisterFrameworkListener(self)
UnityBridge.instance = nil
self.view?.removeFromSuperview()
NotificationCenter.default.post(name: Notification.Name(closeUnityPlayerNotification), object: nil)
}
...
This code works but when the player is closed the app become unresponsive, the player is still in foreground capturing user's inputs.
I don't have any error message.
Thanks for your help :)
For reference I have followed the work of David Peicho that explain well the basic logic

SwiftUI: Send email using MFMailComposeViewController

I am currently trying to implement a "send email" button in my SwiftUI app, using SwiftUI lifecycle and targeting iOS 14.
I know there are quite some solutions presented online - here on stack overflow and elsewhere. However, I have-not been able to make anything work so far in simulator/on device.
My current solution looks like this (based on this question on stackoverflow:
import SwiftUI
import MessageUI
import Foundation
struct ContentView: View {
class MailComposeViewController: UIViewController, MFMailComposeViewControllerDelegate {
static let shared = MailComposeViewController()
func sendEmail() {
if MFMailComposeViewController.canSendMail() {
let mail = MFMailComposeViewController()
mail.mailComposeDelegate = self
mail.setToRecipients(["test#test.com"])
UIApplication.shared.windows.last?.rootViewController?.present(mail, animated: true, completion: nil)
} else {
// Alert
}
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
}
var body: some View {
Button(action: {
MailComposeViewController.shared.sendEmail()
}, label: {
Text("Send")
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The simulator does show the button and doesn't give me any errors. However, upon clicking the button, nothing happens at all - same thing when testing on device.
Any idea what might be wrong here?
Thanks!
Building up on the code snippet shared in my original question:
Based on the answer from #Arjun this is my current workaround to account for the edge case that someone might have deleted the Apple Mail app and is using another email app:
Button(action: {
if MailComposeViewController.shared.canSendMail() {
MailComposeViewController.shared.sendEmail()
} else {
openURL(URL(string: "mailto:someone#example.com?subject=This%20is%20the%20subject")!)
}
}, label: {
Text("Send")
})
It opens the in-app sheet as long as the user has set up apple mail and otherwise switches to any other email app using a mailto: link.
Your code works fine, the problem is that the iOS Simulator does not have the Mail app, thus MFMailComposeViewController.canSendMail() returns false. Try it on a physical device, it works. The reason you didn't see any errors is because of this block of code:
if MFMailComposeViewController.canSendMail() {
let mail = MFMailComposeViewController()
mail.mailComposeDelegate = self
mail.setToRecipients(["test#test.com"])
UIApplication.shared.windows.last?.rootViewController?.present(mail, animated: true, completion: nil)
} else {
// Alert
}
Inside the else block, instead of temporarily commenting // Alert, you should print something instead while debugging, it'll make your life a lot easier.

Error creating the CFMessagePort needed to communicate with PPT

I try to present a CNContactPickerViewController inside a SwiftUI application using the UIViewControllerRepresentable protocol. As I already read, there seems to be a known issue for this not working, but I got it working quite ok using the workaround described here.
However, whenever the CNContactPickerViewController gets presented or dismissed resp., I get the following error in my output log:
[PPT] Error creating the CFMessagePort needed to communicate with PPT.
I tried to find explanations on this, but there seems to be no answer anywhere on the internet. Does someone know where this error comes from and what PPT is? Could this error have something to do with the CNContactPickerViewController not working properly with SwiftUI?
I noticed the error for the first time in the iOS 14 beta together with the Xcode 12 beta, and it is still present in iOS 14.2 with Xcode 12.2.
I don't know if the error appears on iOS 13 as well.
I already issued a feedback report about this.
I wrote a workaround using a hosting UINavigationController and here is my code:
import SwiftUI
import ContactsUI
struct ContactPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
func makeUIViewController(context: Context) -> UINavigationController {
let navController = UINavigationController()
let controller = CNContactPickerViewController()
controller.delegate = context.coordinator
navController.present(controller, animated: false, completion: nil)
return navController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
print("Updating the contacts controller!")
}
// MARK: ViewController Representable delegate methods
func makeCoordinator() -> ContactsCoordinator {
return ContactsCoordinator(self)
}
class ContactsCoordinator : NSObject, UINavigationControllerDelegate, CNContactPickerDelegate {
let parent: ContactPickerView
public init(_ parent: ContactPickerView) {
self.parent = parent
}
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
print("Contact picked cancelled!")
parent.presentationMode.wrappedValue.dismiss()
}
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
print("Selected a contact")
parent.presentationMode.wrappedValue.dismiss()
}
}
}
And I use it like:
Button("Select a contact") {
openSelectContact.toggle()
}
.sheet(isPresented: $openSelectContact, onDismiss: nil) {
ContactPickerView()
}

Getting a Snapshotting Error when leaving app after Keyboard was shown (requires afterScreenUpdates:YES)

I am currently starting a new test project for importing a client certificate via a separate (modal) view. The App always starts with a View "Main View" (Main View), where I have some dummy elements to test if the connection works. Once a user adds a file to my app (e.g., opening it in mail or drag&drop in simulator), the "Certificate Import" View is shown via a Segue. When the user taps the "Enter Certificate Password" textfield, the keyboard pops up. On return .resignFirstResponder() is called and the app via label if the entered password is correct for the imported p12 client cert. I want my "Certificate Import" View (Certificate Import View) to get dismissed when the app is closed or multitasking is activated. I realised this by calling dismiss(animated:completion:) in the Application delegate's applicationWillResignActive(application:) method on the CertificateImportViewController.
Now, my problem occurs when I close my app or switch to multitasking ONLY after the keyboard was shown in the "Certificate Import View".
When I close/multitask the app in the following states, I get the respective entry in debugger console:
When the keyboard is/was shown and I am in Certificate Import View:
[Snapshotting] Snapshotting a view (0x7fa91506b200, UIKeyboardImpl) that is not in a visible window requires afterScreenUpdates:YES.
Even when I left Certificate Import View and am back in the "Main View": [Snapshotting] Snapshotting a view (0x7fa91506b200, UIKeyboardImpl) that is not in a visible window requires afterScreenUpdates:YES.
After I had the keyboard shown in Certificate Import View, left the app, open the app again and then close/multitask the app:[Snapshotting] Snapshotting a view (0x7f99e3821600, UIKeyboardImpl) that has not been rendered at least once requires afterScreenUpdates:YES.
I tried to find out what makes the OS think it has to take a snapshot including the keyboard even though the Main view never does show a keyboard. I also tried to find out why the keyboard believes it has to snapshot the keyboard after it has been dismissed (resignFirstResponder()). I am not sure how to debug what keeps my Certificate Import View on the stack (which I believe could be a reason).
AppDelegate.swift
func applicationWillResignActive(_ application: UIApplication) {
guard let rv = window?.rootViewController as? UINavigationController else {
print("No Navigation Controller")
return
}
if let r = rv.presentedViewController as? UINavigationController, let c = r.topViewController as? CertificateImportViewController {
c.dismiss(animated: true, completion: nil)
}
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
self.fileBlobURL = url
guard let rv = window?.rootViewController as? UINavigationController else {
print("No Navigation Controller")
return false
}
guard let myEntryViewController = rv.topViewController, myEntryViewController.title == "MainView" else {
print("Wrong View Controller")
return false
}
myEntryViewController.performSegue(withIdentifier: "ShowCertificateImport", sender: myEntryViewController)
return true
}
CertificateImportViewController.swift
class CertificateImportViewController: UIViewController {
var fileURL: URL?
var credential: URLCredential?
#IBOutlet weak var certpwTextField: UITextField!
#IBOutlet weak var certResult: UILabel!
#IBOutlet weak var saveButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
certpwTextField.delegate = self
let appdelegate = UIApplication.shared.delegate as! AppDelegate
guard let u = appdelegate.fileBlobURL else {
print("No file blob path found!")
return
}
self.fileURL = u
}
#IBAction func cancel(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
certpwTextField.resignFirstResponder()
certpwTextField.delegate = nil
}
}
After some more research including lots of trial and error I realised that:
this is not the way I should handle "hiding sensitive information" when my app goes to background, see Apple's Q&A 1838
this might also be a bug as proposed in this Apple developer forum post
However, if anyone in the future finds a solution to fix this, out of curiousity I would be highly interested in how it was done.

Resources