Good use case for singleton - ios

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
}
}

Related

Update tableView row from AppDelegate Swift 4

[![enter image description here][1]][1]
Hello. I have a tableview like in the picture above and I'm receiving some silent push notifications. Depending on them I need to reload a specific cell from the tableView. Since I'm getting the notification in the AppDelegate and there at the moment I'm reloading the whole tableView...but personally I don't find this the best solution since I only need to update a specific row.
Any hints please how can I update just a specific cell from appDelegate?
if userInfo["notification_type"] as? String == "update_conversation" {
if let rootVC = (self.window?.rootViewController as? UINavigationController)?.visibleViewController {
if rootVC is VoiceViewController {
let chatRoom = rootVC as! VoiceViewController
chatRoom.getConversations()
// the get Conversations method makes a call to api to get some data then I reload the whole tableView
}
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data{
self.conversations = data
self.onlineRecent = self.conversations
GlobalMainQueue.async {
self.mainTableView.reloadData()
}
}
}
}
}
This is my getConversation method which is used in VoiceViewController to populate my tableview
Have the app delegate broadcast an app-specific notification center notification (on the main thread). Have the view controller that contains your table view listen for that notification and update the cell in question as needed. That way you don't contaminate your app delegate. The app delegate should only deal with system level app stuff, not business logic.
You could get your row’s cell using self.mainTableView.cellForRow(at:IndexPath(…), and update it directly.
Or, I’ve found you save a load of time and your view controllers end up more reliable using ALTableViewHelper [commercial - available on Framework Central here]. It’s free to try.
The helper does the most of the work - you describe how the data connects to the UITableView. I’ve put together an example (on GitHub here), which I hope is something like what you’re trying to do.
import ALTableViewHelper
class VoiceViewController {
// #objc for the helper to see the var’s, and dynamic so it sees any changes to them
#obj dynamic var conversations: Any?
#obj dynamic var onlineRequest: Any?
func viewDidLoad() {
super.viewDidLoad()
tableView.setHelperString(“””
section
headertext "Conversation Status"
body
Conversation
$.viewWithTag(1).text <~ conversations[conversations.count-1]["title"]
$.viewWithTag(2).text <~ "At \\(dateFormat.stringFromDate(conversations[conversations.count-1]["update"]))"
UpdateButton
$.viewWithTag(1).isAnimating <~ FakeConversationGetter.singleton.busy
“””, context:self)
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data {
// change the data on the main thread as this causes the UI changes
GlobalMainQueue.async {
self.conversations = data
self.onlineRequest = self.conversations
}
}
}
}
}

Proper way to set up a protocol in Swift 3

I have a view controller that will show images from Flickr. I put Flickr API request methods in a separate class "FlickrAPIRequests", and now I need to update the image in the view controller when I get the data.
I choose to go with that using a protocol to eliminate the coupling of the two classes. How would I achieve that?
You could define a protocol and set up your FlickrAPIRequests class to take a delegate. I suggest another approach.
Set up your FlickrAPIRequests to have a method that takes a completion handler. It might look like this:
func downloadFileAtURL(_ url: URL, completion: #escaping DataClosure)
The FlickrAPIRequests function would take a URL to the file to download, as well as a block of code to execute once the file has downloaded.
You might use that function like this (In this example the class is called DownloadManager)
DownloadManager.downloadManager.downloadFileAtURL(
url,
//This is the code to execute when the data is available
//(or the network request fails)
completion: {
[weak self] //Create a capture group for self to avoid a retain cycle.
data, error in
//If self is not nil, unwrap it as "strongSelf". If self IS nil, bail out.
guard let strongSelf = self else {
return
}
if let error = error {
print("download failed. message = \(error.localizedDescription)")
strongSelf.downloadingInProgress = false
return
}
guard let data = data else {
print("Data is nil!")
strongSelf.downloadingInProgress = false
return
}
guard let image = UIImage(data: data) else {
print("Unable to load image from data")
strongSelf.downloadingInProgress = false
return
}
//Install the newly downloaded image into the image view.
strongSelf.imageView.image = image
}
)
I have a sample project on Github called Asyc_demo (link) that has uses a simple Download Manager as I've outlined above.
Using a completion handler lets you put the code that handles the completed download right in the call to start the download, and that code has access to the current scope so it can know where to put the image data once it's been downloaded.
With the delegation pattern you have to set up state in your view controller so that it remembers the downloads that it has in progress and knows what to do with them once they are complete.
Write a protocol like this one, or similar:
protocol FlickrImageDelegate: class {
func displayDownloadedImage(flickrImage: UIImage)
}
Your view controller should conform to that protocol, aka use protocol method(s) (there can be optional methods) like this:
class ViewController:UIViewController, FlickrImageDelegate {
displayDownloadedImage(flickrImage: UIImage) {
//handle image
}
}
Then in FlickrAPIRequests, you need to have a delegate property like this:
weak var flickrDelegate: FlickrImageDelegate? = nil
This is used in view controller when instantiating FlickrAPIRequests, set its instance flickrDelegate property to view controller, and in image downloading method,when you download the image, you call this:
self.flickrDelegate.displayDownloadedImage(flickrImage: downloadedImage)
You might consider using callback blocks (closures) in FlickrAPIRequests, and after you chew that up, look into FRP, promises etc :)
Hope this helps
I followed silentBob answer, and it was great except that it didn't work for me. I needed to set FlickrAPIRequests class as a singleton, so here is what I did:
protocol FlickrAPIRequestDelegate{
func showFlickrPhoto(photo: UIImage) }
class FlickrAPIRequests{
private static let _instance = FlickrAPIRequests()
static var Instance: FlickrAPIRequests{
return _instance
}
var flickrDelegate: FlickrAPIRequestDelegate? = nil
// Make the Flickr API call
func flickrAPIRequest(_ params: [String: AnyObject], page: Int){
Then in the view controller when I press the search button:
// Set flickrDelegate protocol to self
FlickrAPIRequests.Instance.flickrDelegate = self
// Call the method for api requests
FlickrAPIRequests.Instance.flickrAPIRequest(paramsDictionary as [String : AnyObject], page: 0)
And Here is the view controller's extension to conform to the protocol:
extension FlickrViewController: FlickrAPIRequestDelegate{
func showFlickrPhoto(photo: UIImage){
self.imgView.image = photo
self.prepFiltersAndViews()
self.dismissAlert()
}
}
When the api method returns a photo I call the protocol method in the main thread:
// Perform this code in the main UI
DispatchQueue.main.async { [unowned self] in
let img = UIImage(data: photoData as Data)
self.flickrDelegate?.showFlickrPhoto(photo: img!)
self.flickrDelegate?.setPhotoTitle(photoTitle: photoTitle)
}

Updating label if value in singleton changes

I am getting some user information from Firebase and storing it into singleton. After that every time the value changes I want that the label changes also but it doesn't until I terminate the app and come back in.
How should I update label if value changes in singleton?
I have tab views. In first tab I assign values and in second tab I try to put the values to label.
This is my singleton:
class CurrentUser: NSObject
{
var generalDetails: User = User()/// Consecutive declarations on a line must be separated by ';'
static let sharedInstance = CurrentUser()
fileprivate override init(){
super.init()
}
}
And like this I assign values:
self.databaseRef.child("users").child(user.uid).observeSingleEvent(of: .value) { (snapshot:FIRDataSnapshot) in
guard let firebaseValue = snapshot.value as? [String:AnyObject], let userName = firebaseValue["username"] as? String, let email = firebaseValue["email"] as? String, let reputation = firebaseValue["reputation"] as? Int, let profilePicURL = firebaseValue["profileImageUrl"] as? String
else
{
print("Error with FrB snapshot")//Here
return
}
//Set values
self.currentUser.generalDetails = User(userName: userName, email: email, reputation: reputation, profileImageURL: profilePicURL, uid: user.uid)
}
And if I want to put the value to the label I simply do this(This reputation is the only thing that can change often):
self.reputationLabel.text = String(self.currentUser.generalDetails.reputation)
You can do either of these:-
Communicate between the singleton and your class with delegate-protocol method , fire the delegate method in the class whenever your repo changes and update your label.
Open a different thread in your network link for the user's reputation in the viewController itself:-
override func viewDidLoad() {
super.viewDidLoad()
FIRDatabase.database().reference().child("users").child(FIRAuth.auth()!.currentUser!.uid).child("reputation").observe(.childChanged, with: {(Snapshot) in
print(Snapshot.value!)
//Update yor label
})
which will get called every time the value of reputation changes.
I like Dravidian's answer and I would like to offer an alternative: KVO
We use Key-Value Observing to monitor if our app is disconnected from the Firebase server. Here's the overview.
We have a singleton which stores a boolean variable isConnected, and that variable is set by observing a special node in Firebase
var isConnected = rootRef.child(".info/connected")
When connected/disconnected, the isConnected var changes state.
We have a little icon on our views that indicates to the user the connected state; when connected it's green, when disconnected it's red with a line through it.
That icon is a class and within each class we have code that observes the isConnected variable; when it's state changes all of the icons change automatically.
It takes very little code, is reusable, is clean and easily maintained.
Here's a code snippet from the Apple documentation
//define a class that you want to observe
class MyObjectToObserve: NSObject {
dynamic var myDate = NSDate()
func updateDate() {
myDate = NSDate()
}
}
//Create a global context variable.
private var myContext = 0
//create a class that observes the myDate var
// and will be notified when that var changes
class MyObserver: NSObject {
var objectToObserve = MyObjectToObserve()
objectToObserve.addObserver(self,
forKeyPath: "myDate",
options: .new,
context: &myContext)
There will be more to it but that's it at a 10k foot level.
The Apple documentation is here
Using Swift with Cocoa and Obj-c 3.01: Design Patterns
and scroll down the the Key-Value Observing Section. It's a good read and very powerful. It follows the same design pattern as Firebase (or vice-versa) - observe a node (variable) and tell me when it changes.

iOS Swift: Passing data between views and completion handlers

I have two viewControllers: LoginViewController and NextViewController. Now when the app runs the LoginViewController runs a completion handler that takes care of the authentication with HTTP request, and after it's done, it calls another closure that gets the necessary user data.
I don't know how to pass that data that i get back from the callback function into the NextViewController to display it because I have no way of knowing when the data becomes available as it is running an HTTP request in the background.
So how should I present the data when it becomes available?
I know I can just call the second callback method for getting the user information inside the nextViewController, but that makes the app slower.
Here's example code:
class Methods: NSObject {
//Singleton
class func sharedInstance() -> Methods {
struct Singleton {
static var sharedInstance = Methods()
}
return Singleton.sharedInstance
}
private func GETMethod(callBackMethod: (Success: Bool) -> Void) {
//Do the authentication
}
private func retriveUserData(callBackMethod: (data: String, Success: Bool) -> Void) {
//Gets the data and passes processed data back in a callBackMethod
}
func doAuthentication(callBackMethod: (Success: Bool) -> Void){
GETMethod { (Success) in
if Success {
self.retriveUserData({ (data, Success) in
data // <- HOW DO I GET THIS INTO LOGIN VIEW CONTROLLER?
callBackMethod(Success: true)
})
}
}
}
}
class LoginViewController: UIViewController {
func loginButtonPressed(){
Methods.sharedInstance().doAuthentication { (Success) in
}
}
}
class NextViewController: UIViewController {
//Present data when it becomes available
}
Consider using a MVC pattern, especially the "model" part. Create an object that serves as the shared data model for your application. Update it when you have new data. Depending on the timing of updates vs. controller loading, it can either send notifications when data changes or provide an API that the interested objects (controllers) can query to find out the current state.

Working with Model async data and TableView

I'm creating an app and I have all the logic done, but I want to do a Code refactoring and create MVC pattern. But I dealing with some asynchronous informations, that came from API.
/MenuViewController
Alamofire.request(.GET, Urls.menu).responseJSON { request in
if let json = request.result.value {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
let data = JSON(json)
var product: [Product] = []
for (_, subJson): (String, JSON) in data {
product += [Product(id: subJson["id"].int!, name: subJson["name"].string!, description: subJson["description"].string!, price: subJson["price"].doubleValue)]
}
dispatch_async(dispatch_get_main_queue()) {
self.products += product
self.tableView.reloadData()
}
}
}
}
This is my code, already working. But I want to create a Model that will handle this and just return the array of Products to my MenuViewController.
Model/Menu
class Menu {
var products: [Product] = []
init() {
Alamofire.request(.GET, Urls.menu).responseJSON { request in
if let json = request.result.value {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
let data = JSON(json)
var product: [Product] = []
for (_, subJson): (String, JSON) in data {
product += [Product(id: subJson["id"].int!, name: subJson["name"].string!, description: subJson["description"].string!, price: subJson["price"].doubleValue)]
}
dispatch_async(dispatch_get_main_queue()) {
self.products += product
}
}
}
}
}
func totalOfProducts() -> Int {
return self.products.count
}
func getProducts() -> [Product]? {
return self.products
}
func getProductFromIndex(index: Int) -> Product {
return self.products[index]
}
}
But I got my self thinking, how I gonna get the main_queue to another class?
So I tried something like this:
class MenuViewControlvar: UITableViewController {
var products: [Product] = []
let menu: Menu = Menu()
// MARK: View Controller Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
if let products = menu.getProducts() {
self.tableView.reloadData()
}
// rest of the code
But didn't worked. My TableView is never updated.
I was wondering if I can do this, or I've to keep my Alamofire code in my viewDidLoad() from my MenuViewController
Thank you.
I am just giving you a direction with the step I would follow (Not writing the code thinking you can work it out):
First, write a networking class that accepts network request along with a competition block. Completion block shall be executed as soon as networking is done. This is a wrapper class and can be used across classes.
Second, write a model class that has all the parameters necessary for view controller's functionalities/view drawing.
Third, from view controller, call the networking class. In completion block, pass the model setting, table reload code and any code to remove loading overlay/indicator. This block should get executed on main queue.
Fourth, add code to show loading overlay/indicator before you trigger networking.
Delegation is an ideal solution for this problem of updating your model data and your view based on an asynchronous network call and it’s pretty much the same technique that is implemented throughout the iOS SDK to solve the same problem. There are many benefits of delegation over observation, another viable solution.
First, move your networking code to a separate class
class NetworkingController {
Create a protocol that view controllers can conform to. This provides the loose coupling between your network operations and your views to effectively maintain separation between the MVC layers.
#protocol NetworkingControllerDelegate: class {
func menuDataDidUpdate()
}
Have the networking controller support a property for its delegate
weak var delegate: NetworkingControllerDelegate?
In summary your networking class now looks something like this:
#protocol NetworkingControllerDelegate: class {
func menuDataDidUpdate()
}
class NetworkingController {
weak var delegate: NetworkingControllerDelegate?
// Insert networking functions here.
}
Then, have your view controller conform to this protocol like so
class MenuViewController: NetworkingControllerDelegate {
and create a new network controller in your view controller
var myNetworkController = NetworkController()
and set the delegate of your network controller instance to be your view controller
myNetworkController.delegate = self
Then in your networking code, when the network request has completed and your model has been updated, make a call to the networking controller's delegate.
delegate.menuDidUpdate()
Create the implementation for this method in your view controller since it is now the delegate for your networking code.
func menuDidUpdate() {
// Update your menu.
}
This makes your view controller look something like:
class MenuViewController: NetworkingControllerDelegate {
var myNetworkController = NetworkController()
override func viewDidLoad() {
myNetworkController.delegate = self
}
// MARK: NetworkingControllerDelegate
func menuDidUpdate() {
// Update your menu.
}
}
This is just the outline of the implementation to give you the necessary information about how to proceed. Fully adapting this to your problem is up to you.

Resources