3D Touch(Quick Actions) leading to UIViewController - ios

Is a way to open a specific View Controller from a Quick Action. I have the following code below in the App Delegate to open a specific View Controller. My app has the same design as the snapchat app i.e. with different View Controllers embedded in a scroll view and I would like to target a specific View Controller e.g. Sunday View Controller.
The idea is to present either the first, second or third Items as selected from the quick action items.
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
var handled = false
guard ShortcutIdentifier(fullType: shortcutItem.type) != nil else {
return false
}
guard let shortcutType = shortcutItem.type as String? else {
return false
}
switch (shortcutType) {
case ShortcutIdentifier.First.type:
//Present the Add View Controller
handled = true
let mondayVC : MondayViewController = MondayViewController(nibName: "MondayViewController", bundle: nil)
self.window?.rootViewController?.presentViewController(mondayVC, animated: true, completion: nil)
break
case ShortcutIdentifier.Second.type:
//Present the View Controller related to this Screen
handled = true
let fridayVC : FridayViewController = FridayViewController(nibName: "FridayViewController", bundle: nil)
self.window?.rootViewController?.presentViewController(fridayVC, animated: true, completion: nil)
break
case ShortcutIdentifier.Third.type:
//Present the View Controller related to this Screen
handled = true
let sundayVC : SundayViewController = SundayViewController(nibName: "SundayViewController", bundle: nil)
self.window?.rootViewController?.presentViewController(sundayVC, animated: true, completion: nil)
break
default:
break
}
return handled
}
The add delegate code to run this function is:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
let handledShortcutItem = self.handleShortCutItem(shortcutItem)
completionHandler(handledShortcutItem)
}

Related

How do I animate a view when presenting a UIViewController?

I have a custom alert which is presented as a UIView on a UIViewController. So when I want to present the alert I present the UIViewController which brings up the alert. The problem that I'm having is that I want to do a simple animation on the alert when it's shown. But as it is presented as a UIViewController and not just shown with a UIView overlay, I'm a bit confused as how to accomplish this. Here's the relevant code:
static func standardMessageAlert(title: String, msg: String, action: ((_ text: String?) -> Void)? = nil) -> CustomAlertViewController? {
let alert = CustomAlertViewController.create(title: title, message: msg)
let okAction = CustomAlertAction(title: "OK", type: .normal, handler: action)
alert?.addAction(okAction)
return alert
}
And the code for CustomAlertViewController, which is pretty big, but the create method looks like this:
static func create(title: String?, message: String?, addon: Addon? = nil, alertType: AlertType? = nil) -> CustomAlertViewController? {
let storyboard = UIStoryboard(name: "Overlay", bundle: nil)
guard let viewController = storyboard.instantiateViewController(withIdentifier: "CustomAlertViewController") as? CustomAlertViewController else {
return nil
}
viewController.modalPresentationStyle = .overCurrentContext
viewController.modalTransitionStyle = .crossDissolve
viewController.alertType = alertType
viewController.addon = addon
viewController.alertTitle = title
viewController.alertMessage = message
return viewController
}
...
The alert is presented like this:
func present(animated: Bool, onView: UIViewController? = UIApplication.rootViewController(), completion: (() -> Void)? = nil) {
onView?.present(self, animated: animated, completion: nil)
}
The animation would be something basic like this:
func animateView() {
alertView.alpha = 0
UIView.animate(withDuration: 1, animations: { [weak self] () -> Void in
guard let self = self else { return }
self.alertView.alpha = 1.0
})
}
alertView is the outlet for the alert UIView that is inside the UIViewController. I guess this is what I should animate, but where do I put that code? How do I implement it when the UIViewController is presented? I have also tried using onView.view to animate it. The problem that I see is that if I put the animation code before presenting it's too early, and if it's after presenting then its too late.
If you want to keep this logic then I can suggest you to setup initial view state in viewDidLoad and run animation after viewWillAppear.
But I recommend to use custom transitions

How to test if view controller is dismissed or popped

i want to write an unit test for my function, here is code:
func smartBack(animated: Bool = true) {
if isModal() {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: animated)
}
}
This method automatically chooses dismiss or pop. So, how i can check if viewcontroller popped or dismissed after this function? Thank you for help
You can check the view controller's isBeingDismissed property in either its viewWillAppear or viewDidAppear function.
See https://developer.apple.com/documentation/uikit/uiviewcontroller/2097562-isbeingdismissed.
func smartBack(animated: Bool = true) will be:
func smartBack(animated: Bool = true) {
if self.navigationController?.viewControllers.first == self {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: true)
}
}
You can use property self.isBeingPresented, will return true is view controller presented otherwise false if pushed.
You could check the viewControllers stack and see if your viewController is included or not, using:
self.navigationController.viewControllers
This will return a [UIViewController] contained in the navigationController stack.
Personally I would use Mocks to track when certain methods are called.
You can do this like so:
class MockNavigationController: UINavigationController {
var _popCalled: Bool = false
override func popViewController(animated: Bool) -> UIViewController? {
_popCalled = true
return self.viewControllers.first
}
}
Then anytime your code calls popViewController, the _popCalled value would be updated but it wouldn't actually pop anything. So you can assert the _popCalled value to make sure that the expected call happened.
This makes it easy to test that an expected thing happened and also prevents you running actual code in your tests. This method could easily be a service call, or db update, setting a flag etc, so can be much safer.
They can be difficult to understand at first though. I would suggest reading up on them before heavy use.
A full example in a playground:
import UIKit
import PlaygroundSupport
import MapKit
class ClassUnderTest: UIViewController {
var isModal: Bool = false
func smartBack(animated: Bool = true) {
if isModal {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: animated)
}
}
}
class MockNavigationController: UINavigationController {
var _popCalled: Bool = false
override func popViewController(animated: Bool) -> UIViewController? {
_popCalled = true
return self.viewControllers.first
}
}
class MockClassUnderTest: ClassUnderTest {
var _mockNavigationController = MockNavigationController()
override var navigationController: UINavigationController? {
return _mockNavigationController
}
var _dismissCalled: Bool = false
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
_dismissCalled = true
}
}
var subject = MockClassUnderTest()
subject.isModal = true
subject.smartBack();
var navigation = subject.navigationController as! MockNavigationController
print(subject._dismissCalled)
print(navigation._popCalled)
OUTPUT:
true
false
subject = MockClassUnderTest();
subject.isModal = false
subject.smartBack();
navigation = subject.navigationController as! MockNavigationController
print(subject._dismissCalled)
print(navigation._popCalled)
OUTPUT:
false
true
In this example, you are overriding the dismiss and pop methods which would be called in either case. In your unit test you would just assert the stubbed values (_popCalled) are true or false for your expectations.
I solved in this way. I have needed to test a simple method that contains this: dismiss(animated: true, completion: nil) and I made a temporal mock that simulates a viewController that do a push to my MainController which it is where I apply the dismissView.
func testValidatesTheDismissOfViewController() {
// Given
let mockViewController: UIViewController = UIViewController()
let navigationController = UINavigationController(rootViewController: mockViewController)
// Create instance of my controller that is going to dismiss.
let sut: HomeWireFrame = HomeWireFrame().instanceController()
navigationController.presentFullScreen(sut, animated: true)
// When
sut.loadViewIfNeeded()
sut.closeView()
// Create an expectation...
let expectation = XCTestExpectation(description: "Dismiss modal view: HomeViewController")
// ...then fulfill it asynchronously
DispatchQueue.main.async { expectation.fulfill() }
wait(for: [expectation], timeout: 1)
// Then - if its different of my home controller
XCTAssertTrue(!(navigationController.topViewController is HomeViewController))
}
I hope can help, I´m here to any doubt.
It is worked for me:
func smartBack(animated: Bool = true) {
if self.navigationController == nil {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: true)
}
}

Shortcut Items, Quick Actions 3D Touch swift

I am for some reason having some troubles figuring out the Shortcut Items in my app. I have followed all the tutorials and I believe that I have done everything right; however, it is acting really weird. The problem is that when you completely close out that app and do the 3D Touch Shortcut item on the home screen, it takes you to the correct part of the app as it should; however, when the app is open in the background (like you just click the home button) it doesn't take you to the correct part of the app, it just opens it. This is really odd and I am not sure what to do because I have followed all the instructions. Thank you so much!
Code:
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
if shortcutItem.type == "com.myapp.newMessage" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "RecentVC") as! UITabBarController
vc.selectedIndex = 2 //this takes me to the contacts controller
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(vc, animated: false, completion: {
completionHandler(true)
})
} else if shortcutItem.type == "com.myapp.groups" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "RecentVC") as! UITabBarController
vc.selectedIndex = 1 //this takes me to the groups controller
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(vc, animated: false, completion: {
completionHandler(true)
})
} else {
//more short cut items I will add later
}
}
NOTE: these are static Shortcut Items, and I configured them in the info.plist file, This function that was presented is the only place in the app besides the info.plist file that have anything about the Shortcut Items.
EDIT: This seems like an issue with the RootViewController, because I have tried multiple different ways of doing this, and it still gives me the same warning message
2017-11-22 22:36:46.473195-0800 QuickChat2.0[3055:1068642] Warning: Attempt to present on whose view is not in the window hierarchy!
I just tried to accomplish my goals this way through this post but it gives me the exact same results as before.
You should set a variable in your performActionForShortcutItem that tells you the shortcut type and move the code you have in there into applicationDidBecomeActive. Your could should look like this
var shortcut: String?
func applicationDidBecomeActive(application: UIApplication) {
if let shortcutItem = shortcut {
shortcut = nil
if shortcutItem == "com.myapp.newMessage" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "RecentVC") as! UITabBarController
vc.selectedIndex = 2 //this takes me to the contacts controller
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(vc, animated: false, completion: nil)
} else if shortcutItem == "com.myapp.groups" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "RecentVC") as! UITabBarController
vc.selectedIndex = 1 //this takes me to the groups controller
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(vc, animated: false, completion: nil)
} else {
//more short cut items I will add later
}
}
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
shortcut = shortcutItem.type
completionHandler(true)
}
I think a cleaner method is to set your flags in the performActionFor method, then check for them once your VC loads. Using this notification will handle the shortcut every time the app becomes active, including launch.
override func viewDidLoad() {
super.viewDidLoad()
// Sends notification if active app is reentered to handle quick actions
NotificationCenter.default.addObserver(self, selector: #selector(handleQuickActions), name: UIApplication.didBecomeActiveNotification, object: nil)

Show two ViewController from AppDelegate

When APP is Launching - start SigninView - it's Okey. Next if success - I need showTripController(). Function work but nothing show? What's a problem?
func showSigninView() {
let controller = self.window?.rootViewController!.storyboard?.instantiateViewControllerWithIdentifier("DRVAuthorizationViewController")
self.window?.rootViewController!.presentViewController(controller!, animated: true, completion: nil)
}
func showTripController() {
let cv = self.window?.rootViewController!.storyboard?.instantiateViewControllerWithIdentifier("DRVTripTableViewController")
let nc = UINavigationController()
self.window?.rootViewController!.presentViewController(nc, animated:true, completion: nil)
nc.pushViewController(cv!, animated: true);
}
First of all you must add this before you use window :
self.window.makeKeyAndVisible()
Another thing to keep in mind is:
Sometimes keyWindow may have been replaced by window with nil rootViewController (showing UIAlertViews, UIActionSheets on iPhone, etc), in that case you should use UIView's window property.
So, instead of using rootViewController, use the top one presented by it:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
if let topController = UIApplication.topViewController() {
topController.presentViewController(vc, animated: true, completion: nil)
}
Replace last 3 lines of showTripController as below:
let nc = UINavigationController(rootViewController: cv));
self.window!.rootViewController = nc

3D touch quick actions preview view controller only one time

I have the following issue. When I run my app on my phone, 3d touch the icon and select the quick action it launches the app presenting the correct view controller but when I put the app in background and try to invoke the quick action it just opens the app where I have left it. So to make it work I have to kill my app every time.
Here is my code:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
if shortcutItem.type == "com.traning.Search" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let searchVC = sb.instantiateViewControllerWithIdentifier("searchVC") as! UINavigationController
let root = UIApplication.sharedApplication().keyWindow?.rootViewController
root?.presentViewController(searchVC, animated: false, completion: { () -> Void in
completionHandler(true)
})
}
}
Thanks in advance.
I'm guessing you're trying to present a view controller from a view controller that's not visible. You can use extensions like:
extension UIViewController {
func topMostViewController() -> UIViewController {
if self.presentedViewController == nil {
return self
}
if let navigation = self.presentedViewController as? UINavigationController {
return navigation.visibleViewController.topMostViewController()
}
if let tab = self.presentedViewController as? UITabBarController {
if let selectedTab = tab.selectedViewController {
return selectedTab.topMostViewController()
}
return tab.topMostViewController()
}
return self.presentedViewController!.topMostViewController()
}
}
extension UIApplication {
func topMostViewController() -> UIViewController? {
return self.keyWindow?.rootViewController?.topMostViewController()
}
}
You can place both of these in your app delegate.swift, above your app delegate class, to get the currently visible view controller. Then present the search view controller on that. For example:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
if shortcutItem.type == "com.traning.Search" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let searchVC = sb.instantiateViewControllerWithIdentifier("searchVC") as! UINavigationController
let topViewController = UIApplication.sharedApplication.topMostViewController()
topViewController.presentViewController(searchVC, animated: false, completion: { () -> Void in
completionHandler(true)
})
}
}

Resources