Modifying an object of another view controller - ios

I would like to know is it possible to, let say, change or modify an object like a button of another view controller from AppDelegate.swift.
Here is what I tried to begin with but somehow stucked.
func application(_ application: UIApplication,didReceiveRemoteNotification userInfo: [AnyHashable : Any],fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if application.applicationState == .active {
if self.window?.rootViewController is homeViewController {
//modify a button exist in homeViewController
}
}
}
Any help is appreciated. Thanks in advance.

You can use the NotificationCenter to send and receive internal notifications (note they are different from local and remote notifications).
First create your notification doing something like this:
extension Notification.Name {
static let remoteNotificationReceived = Notification.Name("uk.co.company.app.remoteNotificationReceived")
}
Then in your view controller that is to respond do something like this:
class TestViewController: UIViewController {
var remoteNotificationReceivedObserver: NSObjectProtocol?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
remoteNotificationReceivedObserver = NotificationCenter.default.addObserver(forName: Notification.Name.remoteNotificationReceived, object: nil, queue: nil, using: { (notification) in
DispatchQueue.main.async { // because the notification won't be received on the main queue and you want to update the UI which must be done on the main queue.
// Put the code to change the button here
}
})
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if let remoteNotificationReceivedObserver = remoteNotificationReceivedObserver {
NotificationCenter.default.removeObserver(remoteNotificationReceivedObserver)
}
}
}
Then elsewhere in you app you post the notification like this:
NotificationCenter.default.post(name: Notification.Name.remoteNotificationReceived, object: nil)

You can use NotificationCenter! add observer in your view controller and remove it while you go back from that view controller(custom notification). and when you receive notification post it!
If you don't have idea that how to deal with NotificationCenter then refer This SO post !

The only place you should really be interacting with another vew controller like that is during segues (if you are using Storyboards). Even then, you should let the view functions for that controller be responsible for changing the state of its buttons and just pass some variable to the controller or perhaps better set up the controller to listen for notifications. Then your app delegate, or other controller can just post notifications that your home controller listens for.

It is possible, but addressing directly members of another ViewController breaks responsibility. It is a good practice to define Interface protocols for internal interactions. In this particular case, it is a good idea to create protcol RemoteNotificationReciverInterface (or kind of RemoteNotificationReciveable according to some modern coding styles advice, although I found it difficult to find appropriate adjective in this case) :
protocol RemoteNotificationReciverInterface: class {
func didReciveNotification(info : [AnyHashable : Any])
}
Then extent your ViewController( and any view controllers that had to react on Notifications when they are topmost)
extension HomeViewController: RemoteNotificationReciverInterface {
func didReciveNotification(info : [AnyHashable : Any]) {
// Chnage you button, ignore para,eters
}
}
You can adopt UINavigationContoroller, UITabBarConroller etc. to forward notifications to their topmost controllers, like:
extension UINavigationController: RemoteNotificationReciverInterface {
func didReciveNotification(info : [AnyHashable : Any]) {
(topViewController as? RemoteNotificationReciverInterface)?.didReciveNotification(info: info)
}
}
And the easily forward it from app delegate.
func application(_ application: UIApplication,didReceiveRemoteNotification userInfo: [AnyHashable : Any],fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if application.applicationState == .active {
(window?.rootViewController as? RemoteNotificationReciverInterface).didReciveNotification(info: userInfo)
}
}

Related

App Delegate : Service call coming back from background

I want to call a web service whenever application coming back in the foreground. I am calling it from didBecomeActive().
What's the best way to handle it and pass data to Root view controller?
Since the data you want to pass is always going to the same view controller you should instead set the observer in that view controller instead of app delegate. This way you won't need to pass any data in the first place.
class YourViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default
.addObserver(self, selector: #selector(activityHandler(_:)),
name: UIApplication.didBecomeActiveNotification, object: nil)
}
#objc func activityHandler(_ notification: Notification) {
//Call your web service here
}
}
You have two choices. Get rootViewController and pass the data, handle it.
func applicationDidBecomeActive(_ application: UIApplication) {
// 1
let rootVC1 = self.window?.rootViewController
// 2
let rooVC2 = application.windows.first?.rootViewController
...
/*
pass data to rootVC1 or rootVC2
*/
}

Change UILabel text from appdelegate

I have a UIViewcontroller let's say "DialerViewController" which has a UILabel
#IBOutlet weak var statusText: UILabel!
,
which has a default value of "pending", how can I change the value of statusText using an app delegate, let's assume the app delegate downloads a text from the server and needs to update the statusText after completion.
I am new to swift development, what is the best way to go around this?
If the DialerViewController is the only view controller in your app you can address it like this...
(window?.rootViewController as? DialerViewController)?.statusText?.text = "YOURTEXT"
Another option would be to make the DialerViewController instance observe some specific notification and post this notification in the app delegate when the text was downloaded from the server.
// create an extension for your own notification
extension Notification.Name {
static let textWasDownloadedNotification = Notification.Name("textWasDownloadedNotification")
}
class DialerViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// make your dialer view controller listen to your new notification
NotificationCenter.default.addObserver(self, selector: #selector(updateLabel), name: .textWasDownloadedNotification, object: nil)
}
// function that gets called when a notification is received
#objc func updateLabel(_ notification: Notification) {
// get the new text from the notification's `userInfo` dictionary
let text = notification.userInfo?["text"] as? String
// update the label
statusText.text = text
}
}
// somewhere in your app delegate...
// prepare the `userInfo` dictionary with the information that is needed
let userInfo = ["text": "This is the new text."]
// post the notification
NotificationCenter.default.post(name: .textWasDownloadedNotification,
object: nil,
userInfo: userInfo)
See https://developer.apple.com/documentation/foundation/notificationcenter.
I think you can't do that because AppDelegate methods are called at specifics state of your application and the one that could be good is this one :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
But when it's called, your viewController isn't yet loaded.
Loading from network it’s not AppDelegate responsibility, add new network service class and inject it to the view controller. Just get knowledges about layers architecture, solid. It’s very powerful for new devs, good luck.

Good use case for singleton

In my app, I have a like feature where is are displayed users which liked your profile. Because the user receive push notification when another user like him, I decided to create a singleton ENFeedLikeManager to store my likes array and add object when a notification is coming:
ENFeedLikeManager.swift
let ENFeedLikeInstance = ENFeedLikeManager.sharedInstance
class ENFeedLikeManager: NSObject {
var likeViewController: ENLikeViewController?
var likes = [ENFeedLike]()
static let sharedInstance = ENFeedLikeManager()
override init() {
super.init()
}
func addNotificationLike(like: ENFeedLike) {
guard let likeViewController = likeViewController else {
return
}
likes.insert(like, at: 0)
likeViewController.likeTableView.insertRows(at: [IndexPath.init(row: 0, section: 0)], with: .automatic)
}
}
When the app is launched, I fetch the like data from the server and I store the result in the ENFeedLikeInstance.likes and I works from this array for further operations like displaying the tableView...
AppDelegate.swift (application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) when it's a new like notification)
let newLikeData = parseLikeNotification(notification: dataDict)
if let user = newLikeData {
ENFeedLikeInstance.addNotificationLike(like: user)
}
I am afraid that by using this kind of singleton, I have some problems of deinitialization.
Have you some advice or another to accomplish that?
You will have issues indeed.
These kind of things should be handle/stored on the server side, and then retrieve via a request every time you need to display the list of like.
If you really want to store them locally, a singleton is not the way to go, cause every time your app is quitted, your singleton will disappear with everything in it. You should store that in:
a sqlite database,
or a coredata,
or a NSUserDefault
If you really want to use a singleton, then you need to make a request every time it is init to grab the list of likes from your server.
Note:
If you choose to store your data locally you can have issues, especially with a list of like that could change quite often, you can end up with a list that is not up to date.
Edit:
Now I understand your concern. So yes it's a bad practice to manage the logic of your controller in your singleton. If you want to do the way you are doing (which is not that bad at the end, but be careful, I would not rely heavily on notifications to display the right list, they are many cases where notifications are not received), your singleton should only keep updated the list of like, and notify your controller about any change done to that list. Then your controller will update the tableview.
So all the logic of the tableview should go in your controller. You could create a delegate and your controller respond to the protocol of that delegate.
You would end up with something like that:
let ENFeedLikeInstance = ENFeedLikeManager.sharedInstance
protocol ENFeedLikeManagerDelegate: class {
func didUpdateLikeList(_ list: [ENFeedLike])
}
class ENFeedLikeManager: NSObject {
weak var delegate: ENFeedLikeManagerDelegate?
var likes = [ENFeedLike]() {
didSet {
delegate?. didUpdateLikeList(likes)
}
}
static let sharedInstance = ENFeedLikeManager()
override init() {
super.init()
}
func addNotificationLike(like: ENFeedLike) {
guard let likeViewController = likeViewController else {
return
}
likes.insert(like, at: 0)
}
}
then you just have to use that delegate in your controller like so:
extension yourViewController: ENFeedLikeManagerDelegate {
func didUpdateLikeList(_ list: [ENFeedLike]) {
// update your tableview here with the new list of like
}
}

How to pass data to a ViewController from push notification through the storyboard?

This is a fragment of my iOS app:
where the Tab Bar Controller is my root controller followed by a Navigation Controller followed by a Table View Controller followed by a final View Controller. When a push notification arrives, I need to pass some data to this final view controller (in order to fill those blank UITextView and a UITextField).
This is the code I have written up to now but I don't know how to go on:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
//let tab :UITabBarController = self.window?.rootViewController as! UITabBarController
//tab.selectedViewController = tab.viewControllers![3]
print("NOTIFICA \n")
print(userInfo)
let tab :UITabBarController = self.window?.rootViewController as! UITabBarController
tab.selectedViewController = tab.viewControllers![2]
}
This code is supposed to let me access to the 3-rd view controller of the tab bar controller but I need to go beyond it to the final view controller and pass userInfoto it. How shall I do? Thanks to all.
UPDATE
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
//let tab :UITabBarController = self.window?.rootViewController as! UITabBarController
//tab.selectedViewController = tab.viewControllers![3]
print("NOTIFICA \n")
print(userInfo)
application.applicationIconBadgeNumber = 0
application.cancelAllLocalNotifications()
NSNotificationCenter.defaultCenter().postNotificationName("MyNotificationReceived", object: userInfo)
}
class DynamicEventsViewController:UIViewController {
#IBOutlet weak var dynamicEventTitle:UITextField!
#IBOutlet weak var dynamicEventDescription:UITextView!
var eventTitle:String!
var eventDescription:String!
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.titleView = UIImageView(image: UIImage(named: "icons/bar.png"))
self.navigationController?.navigationBar.barTintColor = UIColor.whiteColor()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(DynamicEventsViewController.notificatonArrived(_:)), name: "MyNotificationReceived", object: nil)
self.dynamicEventTitle.text = self.eventTitle.uppercaseString
self.dynamicEventDescription.text = self.eventDescription
}
func notificatonArrived(notification: NSNotification) {
let userInfo:[NSObject : AnyObject] = notification.userInfo!
self.eventTitle = userInfo["aps"]!["alert"] as! String
self.eventDescription = userInfo["aps"]!["description"] as! String
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}
I think there are two questions to this post.
How to take the user to the final view controller upon receiving a push notification?
How to pass the payload of the push notification to the final view controller?
For #1, you will have to first determine how the user would normally go to the final view controller. Based on the storyboard he would switch to the third tab, select an item in the table view which would then load the final view controller. So you will have to do this programatically by switching the Tab controller to the third view controller, then getting the navigation controller and then simulating the selection of an item in the table view which will then load the final view controller.
For #2 notifications may not work properly because the final view controller may not be loaded by the time the notification is sent. Its hard to sync this. In this case you can save the push notification payload in user defaults using NSStandardUserDefaults. Then in the final view controller you can check if there is any prefs already stored as a part of this flow and if so load it and populate the text field and text view with the values and delete the prefs so that next time it is loaded the values are not populated.
This kind of decouples the push notification and saving of the data to be preloaded in the final view controller from the actual loading of this data in the final view controller.
Another option is to just load the final view controller as a modal view controller and pass the data to that controller at the time of receiving the notification itself. Of course this is applicable only if your final view controller's data model doesn't require it to always comes from the table view controller.
You can use NSNotificationClass, you can read a tutorial here. Register the notification in (say) FinalViewController
class FinalViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(FinalViewController.notificatonArrived(_:)), name: "MyNotificationReceived", object: nil)
}
func notificatonArrived(notification: NSNotification) {
//do you updation of labels here
let userInfo = notification.userInfo
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}
And when you receive the notification the do this
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
print("NOTIFICA \n")
print(userInfo)
NSNotificationCenter.defaultCenter().postNotificationName("MyNotificationReceived", object: userInfo)
}

ios - Dynamically edit 3d touch shortcut list

I want to add a "Continue" shortcut to my game. But when user will finish my game completely I want this to be either removed or replaced by another shortcut. Is this possible? I know 3d touch is handled by ios system, but maybe there are still some options
There are two ways to create shortcuts - dynamic and static.
Static are added to the plist and never change.
Dynamic can be added and removed in code.
It sounds like you want a dynamic shortcut, so here's roughly how you would do that:
To add:
if #available(iOS 9.0, *) {
if (UIApplication.sharedApplication().shortcutItems?.filter({ $0.type == "com.app.myshortcut" }).first == nil) {
UIApplication.sharedApplication().shortcutItems?.append(UIMutableApplicationShortcutItem(type: "com.app.myshortcut", localizedTitle: "Shortcut Title"))
}
}
To remove:
if #available(iOS 9.0, *) {
if let shortcutItem = UIApplication.sharedApplication().shortcutItems?.filter({ $0.type == "com.app.myshortcut" }).first {
let index = UIApplication.sharedApplication().shortcutItems?.indexOf(shortcutItem)
UIApplication.sharedApplication().shortcutItems?.removeAtIndex(index!)
}
}
You can then handle the shortcut by checking for it in the app delegate method:
#available(iOS 9.0, *)
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
if shortcutItem.type == "com.app.myshortcut" {
// Do something
}
}
Don't forget to check for iOS9 and 3d Touch compatibility.
You can find Apple developer 3d touch pages here:
https://developer.apple.com/ios/3d-touch/
And specifically dynamic shortcuts here:
https://developer.apple.com/library/ios/samplecode/ApplicationShortcuts/Listings/ApplicationShortcuts_AppDelegate_swift.html#//apple_ref/doc/uid/TP40016545-ApplicationShortcuts_AppDelegate_swift-DontLinkElementID_3
Here's a handy class to trigger segues off of 3D touch off of your app icon. Of course you could trigger any action, but this is probably the most common. It then syncs itself when the app comes up or goes to the background. I'm using this to trigger a "My Projects" section only after the user has generated one (VisualizerProject.currentProject.images.count > 0).
class AppShortcut : UIMutableApplicationShortcutItem {
var segue:String
init(type:String, title:String, icon:String, segue:String) {
self.segue = segue
let translatedTitle = NSLocalizedString(title, comment:title)
let iconImage = UIApplicationShortcutIcon(templateImageName: icon)
super.init(type: type, localizedTitle:translatedTitle, localizedSubtitle:nil, icon:iconImage, userInfo:nil)
}
}
class AppShortcuts {
static var shortcuts:[AppShortcut] = []
class func sync() {
var newShortcuts:[AppShortcut] = []
//reverse order for display
newShortcuts.append(AppShortcut(type: "find-color", title: "Find Color", icon:"ic_settings_black_24px", segue: "showColorFinder"))
newShortcuts.append(AppShortcut(type: "samples", title: "Sample Rooms", icon:"ic_photo_black_24px", segue: "showSamples"))
//conditionally add an item like this:
if (VisualizerProject.currentProject.images.count > 0) {
newShortcuts.append(AppShortcut(type: "projects", title: "My Projects", icon:"ic_settings_black_24px", segue: "showProjects"))
}
newShortcuts.append(AppShortcut(type: "visualizer", title: "Paint Visualizer", icon:"ic_photo_camera_black_24px", segue: "showPainter"))
UIApplication.sharedApplication().shortcutItems = newShortcuts
shortcuts = newShortcuts
}
class func performShortcut(window:UIWindow, shortcut:UIApplicationShortcutItem) {
sync()
if let shortcutItem = shortcuts.filter({ $0.type == shortcut.type}).first {
if let rootNavigationViewController = window.rootViewController as? UINavigationController,
let landingViewController = rootNavigationViewController.viewControllers.first {
//Pop to root view controller so that approperiete segue can be performed
rootNavigationViewController.popToRootViewControllerAnimated(false)
landingViewController.performSegueWithIdentifier(shortcutItem.segue, sender: self)
}
}
}
}
Then in your app delegate, add the sync and perform shortcut calls
func applicationDidEnterBackground(application: UIApplication) {
AppShortcuts.sync()
}
func applicationDidBecomeActive(application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
AppShortcuts.sync()
}
#available(iOS 9.0, *)
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
if let window = self.window {
AppShortcuts.performShortcut(window, shortcut: shortcutItem)
}
}
I assume you're talking about the so-called Quick Actions a user can call by force-touching your app's icon on his home screen. You can dynamically create and update them right from your code. The best way to learn about all the possibilities is looking at Apple's sample code.
[UIApplication sharedApplication].shortcutItems = #[];
worked for me. the other answer that suggests removing something from the array didn't work as shortcutItems is not mutable.

Resources