iOS Swift: Passing data between views and completion handlers - ios

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.

Related

How to access callback function in adjacent Swift class method

I'm building an app that prompts the users for their contacts. I'm brand new to Swift but making decent progress.
I'm invoking this open function from a parent call site and want to receive the response value back from the contactPicker function. I'm a bit unfamiliar with the implied nature of the contactPicker invocation, and was expecting to have to invoke it directly from within the presentContactPicker function, but the documentation seemed to suggest this approach is preferable.
As it stands now, this implementation correctly prompts the Contact Picker UI, and prints the selected contacts to the console. But I need assistance actually passing the contacts to the callback function.
The issue that I have is that I'm not able to access the callback function from inside the contactPicker. I have an intuition that maybe I could attach the callback to the parent class with a technique like self.callback = callback in the open function and then call self.callback() in the contactPicker itself, but it didn't work.
import Foundation
import UIKit
import ContactsUI
#objc(ContactsPicker)
class ContactsPicker : NSObject, CNContactPickerDelegate {
#objc static func requiresMainQueueSetup() -> Bool {
return false
}
#objc func open(_ options: NSDictionary, callback: RCTResponseSenderBlock) -> Void {
DispatchQueue.main.async {
self._presentContactPicker(options: options)
}
}
func _presentContactPicker(options: NSDictionary) -> Void {
let contactPickerVC = CNContactPickerViewController()
contactPickerVC.delegate = self
let controller = RCTPresentedViewController()
controller?.present(contactPickerVC, animated: true)
}
func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
print("+++++++++++++++++++++++++++")
print(contacts)
print("+++++++++++++++++++++++++++")
### But how can I access the callback from the exposed open method?
}
}
maybe I could attach the callback to the parent class with a technique like self.callback = callback in the open function and then call self.callback() in the contactPicker itself
This is the right idea. You can declare a property to store the callback like this:
private var callback: RCTResponseSenderBlock?
#objc func open(_ options: NSDictionary, callback: #escaping RCTResponseSenderBlock) -> Void {
self.callback = callback
DispatchQueue.main.async {
self._presentContactPicker(options: options)
}
}
Note that the since the callback "escapes" into the class, you should mark the parameter as #escaping.
func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
callback(someParameters)
}

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

When using asynchronous calls/completion handlers in a class function whats the best way to update variables in my controller?

My app uses Parse for the backend and I have lots of asynchronous calls with completion handlers all over my app. Right now my app is a bit of a mess because I have repeated functions everywhere to get/set values in Parse, this is because I can't figure out how to put the function in my class and then be able to update whatever I'm doing in my controller.
Example: I have a User class and the user can have several associated Car objects. Ideally I think I would like a class method like "User.getCars" that I can use all over my app but instead I declare the below function in several different places.
var cars = [ParseCar]()
func getCar(){
let query = PFQuery(className:"Car")
query.whereKey("owner", equalTo: PFUser.currentUser()!)
query.findObjectsInBackgroundWithBlock
{
(objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
print("Successfully retrieved \(objects!.count) cars.")
for object in objects! {
self.cars.append(object as! ParseCar)
self.cars = objects! as! [ParseCar]
print("cars array is: \(self.cars)")
self.checkNumberOfCars()
}
} else {
showAlertPopup(self, titleText: "Error", messageText: "Network request to get users car failed")
print("Error: \(error!) \(error!.userInfo)")
}
}
}
So I have 2 specific questions...
My "cars" array is in the view controller and I want to update it with the results from Parse. If I were to put the "getCars" function in my "User" class I could do User.getCars which would call the getCars method but how do I append the results back to my "cars" array? I can't return anything from the completion handler so there's no way to pass it back, is there? and I can't set a variable at the beginning of "getCars" which gets updated in the completion handler then return it because the return will happen before the completion handler runs.
If I get a set of data back I have a function in my controller "checkNumberofCars" which counts the objects in the "cars" array and if there are multiple it triggers a popup so the user can select which car they want to work with. Obviously I can't call that function from inside the class method since its declared in the controller so how do I communicate back to my controller that once I have the set of cars I then want to run the "checkNumberofCars" function?
Any other advice on handling this general situation would be awesome!
One of the simplest ways to provide data from an asynchronous operation is to use a closure. This is just a function which your code calls when it's done. For example:
func getCar(completion: (ParseCar) -> Void) {
....
let someCar: ParseCar = ....
completion(someCar)
}
To manage complexity, it is often effective to decompose the problem into simpler pieces. There is no right answer, here is one suggestion:
Encapsulate the web service code into a class.
Encapsulate the common view controller code into a class.
Call the encapsulated code from each of your view controllers where it's used.
Your API for fetching cars could look like this:
class CarsService {
func getCars(completion: ([ParseCar]?, NSError?) -> Void){
let query = PFQuery(className:"Car")
query.whereKey("owner", equalTo: PFUser.currentUser()!)
query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, error: NSError?) -> Void in
if let objects = objects {
let output = [ParseCar]()
for object in objects {
output.append(object as! ParseCar)
}
completion(output, nil)
} else {
completion(nil, error)
}
}
}
}
The API could be used like this:
let service = CarsService()
service.getCars() { cars, error in
if let cars = cars {
self.countCars(cars)
}
else if let error = error {
self.showCarsError(error)
}
}
You could go one step further and combine countCars and the error handling into another reusable controller:
class CarsController {
weak var viewController: UIViewController?
var service: CarsService
init(viewController: UIViewController, service: CarsService) {
self.viewController = viewController
self.service = service
}
func getCar(completion: (ParseCar?) -> Void) {
service.getCars() { cars, error in
if let cars = cars {
self.countCars(cars, completion: completion)
}
else if let error = error {
self.showError(error)
}
}
}
private func countCars(cars: [ParseCar], completion: (ParseCar?) -> Void) {
// Count cars and display prompt, e.g:
if cars.count == 0 {
completion(nil)
}
else if cars.count == 1 {
completion(cars.first)
}
else {
// Create UI to select car.
// Call completion callback with selected car:
completion(selectedCar)
}
}
private func showError(error: NSError) {
let alertViewController = // Create view controller...
viewController.presentViewController(alertViewController)
}
}
It would then be relatively easy to reuse this functionality in multiple view controllers:
let carsService = CarsService()
carsController = CarsController(viewController: self, service: service)
carsController.getCar() { car in
print("Selected car = \(car)")
}

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.

Swift - Dynamic cast class unconditional?

It doesn't seem like I can cast a generic type to another? Swift is throwing DynamicCastClassException.
Basically here is the problem:
// T is defined as T: NSObject
let oebj1 = NetworkResponse<User>()
let oebj2 = oebj1 as NetworkResponse<NSObject>
Here is why I need to do this casting
class BaseViewController: UIViewController {
// Not allowed to make a generic viewController and therefore have to cast the generic down to NSObject
func fetchData(completion: (NetworkResponse<NSObject>)->()) {
fatalError("You have to implement fetchData method")
}
}
class UsersViewController: BaseViewController {
override func fetchData(completion: (NetworkResponse<NSObject>)->()) {
userNetworkManager.fetchUsers { networkUSerResponse in
completion(networkUSerResponse as NetworkResponse<NSObject>)
}
}
}
class UserNetworkManager {
func fetchUsers(completion: (NetworkResponse<User>)->()) {
// Do stuff
}
}
In general, there doesn't seem to be a way to do this. The basic problem is that NetworkResponse<NSObject> and NetworkResponse<User> are essentially completely unrelated types that happen to have identical functionality and similar naming.
In this specific case, it really isn't necessary since you're throwing away the known Userness of the result anyway, meaning that if you really want to treat it as a User later you'll have to do a conditional cast back. Just remove the generic from NetworkResponse and it will all work as expected. The major drawback is that within UserVC.fetchData you won't have access to the returned User result without a (conditional) cast.
The alternative solution would be to separate out whatever additional information is in NetworkResponse from the payload type (User/NSObject) using a wrapper of some sort (assuming there's significant sideband data there). That way you could pass the NetworkResponse to super without mutilation and down-cast the payload object as needed.
Something like this:
class User : NSObject {
}
class Transaction {
let request:NSURLRequest?
let response:NSURLResponse?
let data:NSData?
}
class Response<T:NSObject> {
let transaction:Transaction
let payload:T
init(transaction:Transaction, payload:T) {
self.transaction = transaction
self.payload = payload
}
}
class UserNetworkManager {
func fetchUsers(completion: (Response<User>) -> ()) {
completion(Response(transaction:Transaction(), payload:User()))
}
}
let userNetworkManager = UserNetworkManager();
class BaseVC {
func fetchData(completion: (Response<NSObject>) -> ()) {
fatalError("Gotta implement fetchData")
}
}
class UserVC : BaseVC {
override func fetchData(completion: (Response<NSObject>) -> ()) {
userNetworkManager.fetchUsers { response -> () in
completion(Response(transaction: response.transaction, payload: response.payload))
}
}
}
Although at that point, you're probably better off just separating the transaction information and payload information into separate arguments to the callback.

Resources