In my application, the first view that is launched is controlled by RootUIViewController. Now the user can tap on any of the buttons on this view and then segue to a view controlled by LeafUIViewController. Each button points to this same view, with different value for some of the arguments.
Now I have implemented the 3D Touch shortcut menu which correctly calls the following function in the AppDelegate:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void)
In this function, I want to go to the LeafUIViewController such that the user can still navigate back to the RootViewController.
What is the right way to do this so that the Root is correctly instantiated, pushed on stack and then view navigates to the Leaf?
I suggest against doing any launch actions specific segues from that callback. I usually set a global state and handle it in all my relevant viewcontrollers. They usually pop to the main viewcontroller. In there I do programatically do the action just as would the user normally do.
The advantage is that the viewcontroller hierarchy is initialised in a normal way. The state can be then handled by the first viewcontroller that's going to be displayed on screen which isn't necessarily the first viewcontroller of the app. The application could be already initialised when the user triggers the action. So there could be a random viewcontroller in the hierarchy.
I use something like:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if #available(iOS 9.0, *) {
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem {
handleShortcutItem(shortcutItem)
}
}
return true
}
enum ShortcutType: String {
case myAction1 = "myAction1"
case myAction2 = "myAction2"
}
#available(iOS 9.0, *)
func handleShortcutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
if let shortcutType = ShortcutType.init(rawValue: shortcutItem.type) {
switch shortcutType {
case .myAction1:
MyHelper.sharedInstance().actionMode = 1
return true
case .myAction2:
MyHelper.sharedInstance().actionMode = 2
return true
default:
return false
}
}
return false
}
and then in main viewController (such as main menu) handle the action somehow:
override func viewDidAppear() {
super.viewDidAppear()
switch MyHelper.sharedInstance().actionMode {
case 1:
// react on 1 somehow - such as segue to vc1
case 2:
// react on 2 somehow - such as segue to vc2
default:
break
}
// clear the mode
MyHelper.sharedInstance().actionMode = 0
}
And in other vc's:
override func viewDidLoad() {
super viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "reloadView", name: UIApplicationWillEnterForegroundNotification, object: nil)
}
func reloadView() {
if MyHelper.sharedInstance().actionMode {
self.navigationController.popToRootViewControllerAnimated(false)
}
}
This might not be the best if you are using several vc's. If there is something better, I'love to learn that :)
Related
Background
I have an app that uses AVFoundation in order to have a custom camera. This happens in the OCRViewController. When I take a picture I send the captured picture to a different view ImagePreviewViewController.
I am using Xcode 10.2.1 (10E1001) with Swift 5
The Goal
What I would like to achieve is to lock the orientation of the ImagePreviewViewController to the original orientation of the image. I already know how to get the orientation of the image but I am not able to lock the orientation of the view.
I get the image rotation as such: let imageOri = capturedImage?.imageOrientation
What did I try?
I tried the accepted answers at and several other sources:
How to lock orientation just for one view controller?
How to lock orientation of one view controller to portrait mode only in Swift
https://www.hackingwithswift.com/example-code/uikit/how-to-lock-a-view-controllers-orientation-using-supportedinterfaceorientations
Reading the documentation at https://developer.apple.com/documentation/uikit/uiviewcontroller#//apple_ref/occ/instm/UIViewController/supportedInterfaceOrientations under Handling View Rotation the following is stated:
I also tried the many suggested solutions while writing this query, however, the majority appears to use the following approach (or a variation of it), and it does not work for me.
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
}
override func shouldAutorotate() -> Bool{
return false
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return UIInterfaceOrientation.Portrait
}
As of iOS 8, all rotation-related methods are deprecated. Instead, rotations are treated as a change in the size of the view controller’s view and are therefore reported using the viewWillTransition(to:with:) method.
However, I am not sure how to progress from here.
Interesting code snippets
The following method is in my OCRViewController, here I instantiate the ImagePreviewViewController and attach the captured image.
func displayCapturedPhoto(capturedPhoto : UIImage) {
let imagePreviewViewController = storyboard?.instantiateViewController(withIdentifier: "ImagePreviewViewController") as! ImagePreviewViewController
imagePreviewViewController.capturedImage = capturedPhoto
navigationController?.pushViewController(imagePreviewViewController, animated: true)
}
Using the below override function inside my ImagePreviewViewController I am able to detect the orientation of the view controller.
override func viewDidAppear(_ animated: Bool) {
if UIDevice.current.orientation.isLandscape {
print("Landscape")
} else {
print("Portrait")
}
}
To restrict the rotation of one screen, use this.
In AppDelegate
var restrictRotation = Bool()
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if !restrictRotation {
return .portrait
} else {
return .all
}
}
In your viewcontroller add the function,
func restrictRotation(restrict : Bool) -> Void {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
appDelegate?.restrictRotation = restrict
}
In the ViewDidload() method, call the function to disable rotation.
self.restrictRotation(restrict: false)
in viewWillDisappear() method, call the function to enable rotation.
self.restrictRotation(restrict: true)
I have a class with properties updated in viewController. I wanted to save the properties when the app goes into background or quit using AppDelegate. I used the following codes but it appears that the properties were not passed to the AppDelegate. Furthermore the applicationWillTerminate codes did not seem to get executed.
// testClass is defined and the properties are updated in viewController, e.g
testClass.status = true // default is false
// I want to save testClass.status when the app goes into background or being terminated using the following:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var vc = ViewController()
func applicationDidEnterBackground(_ application: UIApplication) {
print(vc.testClass.status) // prints false
//codes to save
}
// save before App is terminated
func applicationWillTerminate(_ application: UIApplication) {
print(vc.testClass.status) // this code did not get executed?
//codes to save
}
}
applicationWillTerminate is called only when a user terminates the app without switching it to background mode.
When the app is active, double press on Home button and terminate the app.
But if you switch the app to the background, and then try to terminate the app, applicationWillTerminate will not be called.
And you are creating an instance of ViewController in AppDelegate
var vc = ViewController()
If you change the testClass property in another ViewController class instance, you won't get that value here. So create a singleton class like this
class TestClass: NSObject {
static let shared = TestClass()
private override init() {
super.init()
}
var status = false
}
Now update the value in any view controller from the singleton class
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
TestClass.shared.status = true
}
}
In AppDelegate save and retrieve the value from UserDefaults
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
TestClass.shared.status = UserDefaults.standard.bool(forKey: "Status")
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
UserDefaults.standard.set(TestClass.shared.status, forKey: "Status")
}
func applicationWillTerminate(_ application: UIApplication) {
UserDefaults.standard.set(TestClass.shared.status, forKey: "Status")
}
}
Or create a computed property to save the value in UserDefaults whenever it is changed.
class TestClass: NSObject {
static let shared = TestClass()
private override init() {
super.init()
}
var status: Bool {
get {
return UserDefaults.standard.bool(forKey: "Status")
}
set {
UserDefaults.standard.set(newValue, forKey: "Status")
}
}
}
As already mentioned by others you can ignore applicationWillTerminate.
To get notified when the app goes into the background just add an observer in the view controller.
However rather than didEnterBackground I'd recommend to observe willResignActive.
Add the observer in viewDidLoad once
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { notification in
// save the properties
}
}
Or if you are using multiple view controllers you can add the observer in viewWillAppear and remove it in viewDidDisappear
Side note:
Never create a view controller with the default initializer ViewController() if you are using storyboard. You'll get a brand new instance which is not the storyboard instance.
I trying to make a calling app for my project and I want to add a function that keeps checking if someone if calling. My app uses Firebase where I have a key for each users to check if he made a call or not.
There's two problem I am facing here, the first one is, as I said, that I want my function to keep checking anywhere in the app for an incoming call. The other problem is that i have a viewcontroller that I want to pop up when someone is calling. I have found this code on github but it uses navigationcontroller which I am not using in my app :
extension UIViewController{
func presentViewControllerFromVisibleViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let navigationController = self as? UINavigationController, let topViewController = navigationController.topViewController {
topViewController.presentViewControllerFromVisibleViewController(viewControllerToPresent: viewControllerToPresent, animated: true, completion: completion)
} else if (presentedViewController != nil) {
presentedViewController!.presentViewControllerFromVisibleViewController(viewControllerToPresent: viewControllerToPresent, animated: true, completion: completion)
} else {
present(viewControllerToPresent, animated: true, completion: completion)
}
}
}
For your question on monitoring when incoming calls occur and to be called as a result, see this answer. It's probably what you need (I've never tried it, however). The example shows creating a CXCallObserver and setting your AppDelegate as delegate.
For your second question, I'd first try this answer which leverages the window.rootViewController so you can do this from your AppDelegate. Generally, the root VC is your friend when trying to do UI your AppDelegate. :)
A better answer based on Alex's added comments:
I'd first look at how to set up an observer to your Firebase model so that you can get a callback. If you don't have a way to do that, I'd use KVO on the Firebase model property. But to do exactly as you're requesting, and to do so lazily from AppDelegate (rather than from a singleton), see this code:
// In AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool
{
self.timerToCheckForCalls = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true)
}
func timerFired()
{
let didCall = // TODO: query your Firebase model...
guard didCall == true else
{
return
}
self.displayCallerView()
}
func displayCallerView()
{
// See below link.
}
See this answer for how to present your view controller, even when your app might be showing an action sheet, alert, etc... which I think you'd especially value since you need to display the caller regardless of what your app is doing.
Note while user is scrolling a UITextView, the timer won't fire yet. There may be other situations where the timer could be delayed too. So it really would be best to observe your Firebase model or receive a KVO callback than to use a timer.
If you want to make a function that can be called from anywhere, use a singleton pattern. You can also use that to store your special view controller.
Bear in mind that this code SHOULD NOT considered fully functioning code and will need to be customized by you to suit your needs.
class MyClass {
let shared = MyClass()
var viewController: SpecialViewController?
func checkForCall() {
// do function stuff
}
func getSpecialViewController() {
let storyBoard = UIStoryboard.init(name: "main", bundle: nil)
// keep it so we don't have to instantiate it every time
if viewController == nil {
viewController = storyBoard.instantiateViewController(withIdentifier: "SomeViewController")
}
return viewController
}
}
// Make an extension for UIViewController so that they can all
// use this function
extension UIViewController {
func presentSpecialViewController() {
let vc = MyClass.shared.getSpecialViewController()
present(vc, animated: false, completion: nil)
}
}
Somewhere in your code:
// in some function
MyClass.shared.checkForCall()
Somewhere else in code:
presentSpecialViewController()
I have Tap Bar Controller with 2 paths. One is settingController Other one is loginController and contactListController.
When i run the program the entry pint is set tocontactListControllerand if login is false apps shownloginController`. After login value is set on true and loginController is dismiss. On bottom i have Tab Bar Controller: ContactList | Settings
When i go to settings i have a LOGOUT button, i would like to do when i tap this how to set value login on false ? i have no segue between ContactList and SettingController
This is my ContactListController
class ContactsTableViewController: UITableViewController, SettingsControllerDelegate {
let settingsController: SettingsController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("settingsController") as! SettingsController
override func viewDidLoad() {
super.viewDidLoad()
settingsController.delegate = self
}
func didLogoutSuccessfully() {
loggedIn = false
}
}
This is settings controller
protocol SettingsControllerDelegate {
func didLogoutSuccessfully()
}
class SettingsController: UITableViewController {
var delegate: SettingsControllerDelegate?
fun tapButton() {
self.delegate?.didLogoutSuccessfully() // Set login as false
}
if i added
override func viewDidLoad() {
super.viewDidLoad(
settingsController.delegate = self
presentViewController(settingsController, animated: true, completion: nil)
}
my controller setting is appear first. How can i change this value in other way?
UPDATE
in contact list i have
var loggedIn: Bool = false {
didSet {
if loggedIn == true {
self.configureView()
}
}
}
override func viewDidAppear(animated: Bool) {
if loggedIn == false {
performSegueWithIdentifier("showLogin", sender: nil)
}
//tableView.reloadData()
}
Consider either 1: using notifications (to which all interested controllers are registered as observers) to react to session state changes, 2: moving your session state to something "higher up the chain" (like in or "hanging off of" your app delegate), or 3: making a singleton session controller.
1 can be used with 2 and 3 and either 2 or 3 make accessing the current state from anywhere in your app easier. I'd go with a mix of 1 and 3 myself.
This approach in general relieves you from having to walk and inspect the controller hierarchy to find and set the same thig on all other controllers (which is icky because it's so tightly coupled; changing the hierarchy and/or reusing VCs elsewhere would probably break things).
You can pass data between the tabs using UITabViewControllers.viewControllers method which returns array of the view controllers in the tab
//in SettingsVC
func viewWillDisapear(){
//assuming its in the second index of tabBar
let contactVC = self.tabBarController.viewControllers[1] as ContactsTableViewController
contactVC.delegate = self
contactVC.loggedIn = true //or false as you wish
super.viewWillDisapear()
}
How to launch and navigate to a particular view controller using short cut items?
I am using UINavigationController in my app. The issue is when I click on the short cut items, I have to navigate directly to last view controller. In the last view controller, I will have Back button to navigate back to previous screens. Is this possible?
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) {
let handledShortCutItem = handleShortCutItem(shortcutItem)
completionHandler(handledShortCutItem)
}
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
var handled = false
// Verify that the provided `shortcutItem`'s `type` is one handled by the application.
guard ShortcutIdentifier(fullType: shortcutItem.type) != nil else { return false }
guard let shortCutType = shortcutItem.type as String? else { return false }
switch (shortCutType) {
case ShortcutIdentifier.LastViewController.type:
print("Inside Agent **********")
**// Navigate to view controller**
handled = true
break
default:
break
}