Run a function N time anywhere in the app in Swift - swift

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()

Related

How can I make completion handler execute after a child UIViewController dismisses itself? [duplicate]

This question already has answers here:
Detecting sheet was dismissed on iOS 13
(13 answers)
Closed 8 months ago.
Is there a way to place a completion handler in the parent UIViewController that gets called after its child UIViewController dismisses itself?
For my current project a UIViewController might have to create and present a child UIViewController to get missing data.
My thought was that the completion handler in the present method wouldn't be called till the child UIViewController dismisses itself.
Obviously I was wrong, the completion handler is called immediately after the child UIViewController is presented, yet still exists.
This is an extremely simplified code, just so I can see when the completion handler is being called within the debugger.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
...determine if data is missing...
if *data is missing* {
let myUp = UploadInv()
myUpload.modalPresentationStyle = .fullScreen
myUpload.modalTransitionStyle = .flipHorizontal
x += 1
self.present(myUpload, animated: true, completion: { [self] in print ("\(x)");})
}}
That completion block is called when your UploadInv was presented. If you want to handle dismissing action from child controller you have to define it inside UploadInv
class UploadInv: UIViewController {
....
var onDismiss: (() -> ())?
func onDismissAction() {
self.dismiss(animated: true) {
onDismiss?()
}
}
}
Then use it whenever create an UploadInv instance if needed
let myUp = UploadInv()
myUp.onDismiss = { [weak self] in
//TODO: - Do something
}

Swift calling a ViewController function from the AppDelegate [duplicate]

I am building an iOS app using the new language Swift. Now it is an HTML5 app, that displays HTML content using the UIWebView. The app has local notifications, and what i want to do is trigger a specific javascript method in the UIWebView when the app enters foreground by clicking (touching) the local notification.
I have had a look at this question, but it does not seem to solve my problem. I have also come across this question which tells me about using UIApplicationState, which is good as that would help me know the the app enters foreground from a notification. But when the app resumes and how do i invoke a method in the viewController of the view that gets displayed when the app resumes?
What i would like to do is get an instance of my ViewController and set a property in it to true. Something as follows
class FirstViewController: UIViewController,UIWebViewDelegate {
var execute:Bool = false;
#IBOutlet var tasksView: UIWebView!
}
And in my AppDelegate i have the method
func applicationWillEnterForeground(application: UIApplication!) {
let viewController = self.window!.rootViewController;
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var setViewController = mainStoryboard.instantiateViewControllerWithIdentifier("FirstView") as FirstViewController
setViewController.execute = true;
}
so what i would like to do is when the app enters foreground again, i want to look at the execute variable and run the method as follows,
if execute{
tasksView.stringByEvaluatingJavaScriptFromString("document.getElementById('sample').click()");
}
Where should i put the code for the logic to trigger the javascript from the webview? would it be on viewDidLoad method, or one of the webView delegate methods? i have tried to put that code in the viewDidLoad method but the value of the boolean execute is set to its initial value and not the value set in the delegate when the app enters foreground.
If I want a view controller to be notified when the app is brought back to the foreground, I might just register for the UIApplication.willEnterForegroundNotification notification (bypassing the app delegate method entirely):
class ViewController: UIViewController {
private var observer: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [unowned self] notification in
// do whatever you want when the app is brought back to the foreground
}
}
deinit {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
}
}
Note, in the completion closure, I include [unowned self] to avoid strong reference cycle that prevents the view controller from being deallocated if you happen to reference self inside the block (which you presumably will need to do if you're going to be updating a class variable or do practically anything interesting).
Also note that I remove the observer even though a casual reading of the removeObserver documentation might lead one to conclude is unnecessary:
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.
But, when using this block-based rendition, you really do need to remove the notification center observer. As the documentation for addObserver(forName:object:queue:using:) says:
To unregister observations, you pass the object returned by this method to removeObserver(_:). You must invoke removeObserver(_:) or removeObserver(_:name:object:) before any object specified by addObserver(forName:object:queue:using:) is deallocated.
I like to use the Publisher initializer of NotificationCenter. Using that you can subscribe to any NSNotification using Combine.
import UIKit
import Combine
class MyFunkyViewController: UIViewController {
/// The cancel bag containing all the subscriptions.
private var cancelBag: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
addSubscribers()
}
/// Adds all the subscribers.
private func addSubscribers() {
NotificationCenter
.Publisher(center: .default,
name: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
self?.doSomething()
}
.store(in: &cancelBag)
}
/// Called when entering foreground.
private func doSomething() {
print("Hello foreground!")
}
}
Add Below Code in ViewController
override func viewDidLoad() {
super.viewDidLoad()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector:#selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func appMovedToForeground() {
print("App moved to foreground!")
}
In Swift 3, it replaces and generates the following.
override func viewDidLoad() {
super.viewDidLoad()
foregroundNotification = NotificationCenter.default.addObserver(forName:
NSNotification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) {
[unowned self] notification in
// do whatever you want when the app is brought back to the foreground
}

How to get Stripe's STPPaymentCardTextField Data programmatically?

I've successfully set my first view controller to STPAddCardViewController. I now need to get the user information in the STPPaymentCardTextField. Problem is, I'm used to using the storyboard to make outlets. How do I detect the STPPaymentCardTextField programmatically?
I've tried:
class ViewController: STPAddCardViewController, STPPaymentCardTextFieldDelegate {
let paymentCardTextField = STPPaymentCardTextField()
func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) {
print(paymentCardTextField.cardNumber)
//ERROR: printing nil in the console
}
}
But I'm getting nil as an output. Any help?
You should use either STPAddCardViewController, or STPPaymentCardTextField, not both. The SDK's ViewControllers are not designed to be extended. The intended use is:
class MyVC : STPAddCardViewControllerDelegate {
override func viewDidLoad() {
…
let addCardView = STPAddCardViewController()
addCardView.delegate = self
// Start the addCardView
self.navigationController.pushViewController(addCardView, animated: true)
}
…
func addCardViewController(_ addCardViewController: STPAddCardViewController, didCreatePaymentMethod paymentMethod: STPPaymentMethod, completion: #escaping STPErrorBlock) {
// TODO: do something with paymentMethod
// Always call completion() to dismiss the view
completion()
}
func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) {
// TODO: handle cancel
}
}
But rather than my partial example I'd recommend reading these docs and trying out this example iOS code. Best wishes!

UITextField.text value won't change on GooglePlaceAutocomplete

I am working on a form app on iOS 11 using Swift 4. I'd like to put the return of Google's selector (PlaceAutocomplete) in the UITextField of one of the cells of my UITableView. The issue is that despite assigning new values, the text fields remain blank. After debugging for a while it seems that something is discarding the content of my UITextField when the location is being selected.
These are the GooglePlaceAutocomplete callbacks with the result value assignment :
extension MyUITableViewController, GMSAutocompleteViewControllerDelegate {
func viewController(_ viewController: GMSAutocompleteViewController, didAutocompleteWith place: GMSPlace) {
print("Place name: \(place.name)")
let indexPath4 = IndexPath(row: 4, section: 0)
let cell4 = tableView.cellForRow(at: indexPath4) as! InputPlacePicker
cell4.inputTextField.text = place.name
dismiss(animated: true, completion: nil)
}
func viewController(_ viewController: GMSAutocompleteViewController, didFailAutocompleteWithError error: Error) {
print("Error: ", error.localizedDescription)
}
func wasCancelled(_ viewController: GMSAutocompleteViewController) {
dismiss(animated: true, completion: nil)
}
}
And this is the call to GooglePlaceAutocomplete
#objc func pickPlace() {
let autocompleteController = GMSAutocompleteViewController()
autocompleteController.delegate = self
present(autocompleteController, animated: true, completion: nil)
}
Changing the cells directly is probably not a good idea, since everytime the UITableView is reloaded, cells get recycled and depending on your tableView(_:cellForRowAt:) implementation, your UITextFieldmight get overwritten or end up somewhere else.
It is a better practice to have some kind of model or state in your view controller, which holds the state for each cell in the different sections.
Change this state (for instance a list of place names) in your GooglePlacesAutocomplete delegate and call reloadData on your tableView to get the new data from the changed state.
In your tableView(_:cellForRowAt:) implementation, you set inputTextField.text = place.name before you return it, this way all cells should end up with the right place name from your internal state.
Okay! I understood what you said Kie and you're right. I'll implement something to save my field values.
But in my case, it is the GMSAutocompleteViewController which reloads my UITableView because it was called with present(). I changed that to add it to my navigationController and now my field persists.
Thanks

Navigating to a ViewController from AppDelegate triggered by UIApplicationShortcutItem

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 :)