Pass Data using Closures - ios

I know that there are multiple approaches to pass data back from one controller to another like Delegates, NSNotifications. I am using another way using Closures to pass data data back. I just want to know is it safe way how I pass any data using blocks like below or should I avoid this approach.
First ViewController (where I make object of Second ViewController)
#IBAction func push(sender: UIButton) {
let v2Obj = storyboard?.instantiateViewControllerWithIdentifier("v2ViewController") as! v2ViewController
v2Obj.completionBlock = {(dataReturned) -> ()in
//Data is returned **Do anything with it **
print(dataReturned)
}
navigationController?.pushViewController(v2Obj, animated: true)
}
Second ViewController (where data is passed back to First VC)
import UIKit
typealias v2CB = (infoToReturn :NSString) ->()
class v2ViewController: UIViewController {
var completionBlock:v2CB?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func returnFirstValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: returnFirstValue)
}
#IBAction func returnSecondValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: returnSecondValue)
}
}

That's a very good and reasonable approach and much better than notifications.
Looking at the evolution of Cocoa API you will notice that Apple has replaced more and more delegate API with blocks / closures over the years.

Related

Get Data from UIViewController to Another UIViewController

Suppose I have a storyboard like so:
Is it possible for me to get a flag or a boolean data from A back to B? I initially thought of using delegation but most of the tutorials about it talks about sending data between UIViewControllers that are part of 1 NavigationController. In my case, the UIViewController I need to get data is outside of the navigation controller. Is there a way for me to send data from A to B despite not being embedded in the same NavigationController?
If you don't want to use delegate between the classes . One possible way is to create separated file , saved in class and fetch required data any where in navigation .
Useful class for your case would be create singleton class FlowEngine . Use getter / setter method for saving and fetching of data. Code is attached for your reference .
class FlowEngine : NSObject{
private let static shared = FlowEngine()
private var data : String
private init(){
}
func savedData(text : String){
data = text
}
func fetchSavedData() -> String{
return data // add checsk for nil values
}
}
Delegation doesn't require the ViewControllers to be in same navigation stack. You can use the same for your case. However, if you choose to go with NotificationCenter, just remember to remove the observer when appropriate.
Other answers seem to accomplish your requirements but for the sake of completeness you could try to use KVC and KVO for modifying values in A and receiving its changes in B (or any other place)
You could see a detailed explanation of how to use them in here.
You have several ways to go, depending on your needs :
Delegation
Declare a protocol in A, and make B conform to it. Set the delegate of A to B. This could be cumbersome if the navigation stack has too many level, as you would need to pass the reference of B to each ViewController between A & B
Notification / KVO
B subscribe to a notification sent by A, no reference needed, thread safe. Don't forget to unsubscribe when done.
Proxy class
Use a proxy singleton class, that will hold your data. A will write to it, and B will read it in viewWillAppear.
UserDefaults
Same concept as a Proxy Class, but the data will persist during your app life cycle and even after killing the app. It's appropriate if you want to change a flag or a setting for your user, not if you have a lot of data to hold.
Cocoa Touch uses the target-action mechanism for communication between a control and another object. More here... If you would like to use it with UIControl objects like buttons, then you can set it in Interface Builder by sending an action to the FirstResponder object.
Target-Action will start searching a VC which responds to a given method from the current first responder and then will move to the next responder and will terminate a search in a current UIWindow. Once a controller which responds to a method signature is found, the search is terminated.
class AViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func configure(with dictionary: Dictionary<String, Any>) {
print(dictionary)
}
}
class BViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let a = self.targetViewController(forAction: #selector(ViewController.configure(with:)), sender: self) as? ViewController
a?.configure(with: ["firstName": "Alex", "lastName": "Toto"])
}
}
if your A viewController is not huge, In B viewController do this :
class B : UIViewController {
var a : A! = nil
func viewDidLoad() {
super.viewDidLoad()
a = storyboard?.instantiateViewController(withIdentifier: "StoryBoard ID") as? A
if a.booleanValue == true {
// use your booleanValue
a = nil // deallocate after using your value.
}
}
}
Update (better solution)
We've had to edit a few things to the functionality which presented me with the opportunity to refactor this. I used the NSNotification way, which was way cleaner than using closures.
ViewControllerB
override func viewDidLoad() {
super.viewDidLoad()
//Observe for notification from "myIdentifier"
NotificationCenter.default.addObserver(self, selector: #selector(self.processNotification(notification:)), name: Notification.Name("myIdentifier"), object: nil)
}
//function that gets called when notification is received
//the #objc annotation is required!
#objc func processNotification(notification: Notification) {
//Do something
}
ViewControllerA
#IBAction func didTapButton(_ sender: Any) {
//Process something
// ...
//
//Post a notification to those observing "myIdentifier"
NotificationCenter.default.post(name: Notification.Name("myIdentifier"), object: nil)
self.dismiss(animated: true, completion: nil)
}
Old (but working) solution
This might be an unpopular solution but I managed to solve this with callbacks. I was looking into another possible solution which was commented NSNotification but since someone from the team already had experience with using callbacks in this manner, we decided to ultimately use that.
How we made it work:
ViewControllerB is given the actual code implementation through prepare(for segue: UIStoryboardSegue, sender: Any?) while ViewControllerC (This is the middle UIViewController in the picture) has a callback property and ViewControllerA contains the value to pass when it's about to be dismissed.
ViewControllerB
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "secondSegue" {
let nvc: NavigationController = segue.destination as! NavigationController
let vc = nvc.viewControllers[0] as! ViewControllerC
vc.completion = { hasAgreed in
//Do Something
}
}
}
ViewControllerC
class ViewControllerC: UIViewController {
var completion: ((Bool) -> ())?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "thirdSegue" {
let nvc: NavigationController = segue.destination as! NavigationController
let vc = nvc.viewControllers[1] as! ViewControllerA
vc.middleController = self
}
}
ViewControllerA
class ViewControllerC: UIViewController {
var middleController: ViewControllerC?
#IBAction func didTapButton(_ sender: Any) {
self.dismiss(animated: true, completion: {
middleController?.completion(true)
})
}
}
With this, we got the data we needed from the diagram picture above.
Your best bet is to make use of NotificationCenter to achieve this.
Post notification like this:
NotificationCenter.default.post(name: Notification.Name("NotificationName"), object: nil, userInfo: ["somekey":"somevalue"])
Observe it like this:
NotificationCenter.default.addObserver(self, selector: #selector(self.dataReceived(notification:)), name: Notification.Name("NotificationName"), object: nil)
Use the following method:
#objc func dataReceived(notification: Notification) {}

how to deliver data to other ViewController?

#IBAction func hallOfFame(_ sender: UIButton) {
guard let hof = self.storyboard?.instantiateViewController(withIdentifier: "HOF") as? HallOfFameTableViewController else { return }
hof.hallOfFames = self.hallOfFames.sorted(by: <)
}
Self-view has hallOfFame and it is Array.
And hof-view also has hallOfFame.
So I Send self-view's hallOfFame -> how-view's hallOfFame.
but hof-view do not receive data.
what's wrong?
if you want send the data forward in the view controller stack, you can use perform segue and prepare for segue method
https://www.youtube.com/watch?v=guSYMPaXLaw&t=595s
but if you send the data backward, you can use protocol and delegate pattern : https://www.youtube.com/watch?v=KhqXUi1SF4s&t=198s

viewDidLoad() is running before Alamofire request in prepareforsegue has a chance to finish

I have two viewControllers that are giving me trouble. One is called notificationAccessViewController.swift and the other is called classChooseViewController.swift. I have a button in the notificationAccessViewController that triggers a segue to the classChooseViewController. What I need to do is within the prepareForSegue function, perform an Alamofire request and then pass the response to the classChooseViewController. That works! BUT, not fast enough.
Below is my prepareForSegue function within the notificationAccessViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let DestViewController = segue.destination as! classChooseViewController
Alamofire.request("MYURL.com").responseJSON { (response) in
if let JSON : NSDictionary = response.result.value as! NSDictionary?{
let classNameArray = JSON["className"] as! NSArray
print("---")
print(classNameArray)
DestViewController.classNameArray = classNameArray
}
}
}
I have it printing out classNameArray to the console, which it does SUCCESSFULLY. Although, in my classChooseViewController.swift, I also print out the classNameArray to the console and it prints with nothing inside of it. This is because the viewDidLoad() function of the classChooseViewController.swift is running before the prepareForSegue function has finished. I want to know what I can do to ensure that the classChooseViewController does not load until the prepareForSegue function has FINISHED running.
Below is my code for classChooseViewController
class classChooseViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var classNameArray: NSArray = []
override func viewDidLoad() {
super.viewDidLoad()
print(classNameArray)
}
}
I have attached a picture of what the console reads. You can see that the classNameArray prints from the classChooseViewControllers viewDidLoad() function BEFORE the prepareForSegue() function because the "---" prints out after.
Your design is wrong.
The Alamofire request function is asynchronous. It returns immediately, before the results are ready. Then it invokes the completion handler that you pass in.
I tend to agree with Paul that you should make the request in the destination view controller, not in the current view controller.
If you DO want to fetch the data in Alamofire before you segue to the other view controller then you'll need to write a method that starts the request, then invokes the segue to the other view controller from the Alamofire.request() call's completion handler.
If you're invoking the new view controller from a button press, you need to not link the button directly to a segue, but instead write an IBAction method that triggers the AlamoFire request call, then invokes the segue (or instantiates the view controller directly and pushes/presents it manually) from the completion handler.
Something like this:
#IBAction buttonAction(sender: UIButton) {
Alamofire.request("MYURL.com").responseJSON {
(response) in
if let JSON : NSDictionary = response.result.value as! NSDictionary? {
let classNameArray = JSON["className"] as! NSArray
print("---")
print(classNameArray)
let theClassChooseViewController = storyboard.instantiateViewController(withIdentifier:"ClassChooseViewController" as ClassChooseViewController
theClassChooseViewController.classNameArray = classNameArray
presentViewController(theClassChooseViewController,
animated: true,
completion: nil)
}
}
}
By the way, class names and types should start with an upper case letter, and variable names should start with a lower-case letter. This is a very strong convention in both Swift and Objective-C, and you'll confuse the hell out of other iOS/Mac developers unless you follow the convention.

Who came first? IBAction or ViewDidLoad

I have a Button on First VC which is directed to two active states.
1) SecondVC
override func viewDidLoad() {
super.viewDidLoad()
subjectPickerView.dataSource = self
subjectPickerView.delegate = self
SwiftyRequest()
// Used the text from the First View Controller to set the label
}
func SwiftyRequest(){
print("SecondViewController METHOD BEGINS")
let jsonobj = UserDefaults.standard.object(forKey: "PostData")
let json = JSON(jsonobj as Any)
for i in 0 ..< json.count{
let arrayValue = json[i]["name"].stringValue
print(arrayValue)
self.subjects.append(arrayValue)
self.subjectPickerView.reloadAllComponents()
}
print(self.subjects)
}
2) IBAction of FirstVC
#IBAction func buttonPressed(_ sender: Any) {
Alamofire.request("http://localhost/AIT/attempt3.php",method: .post, parameters: ["something": semValue, "branch" : streamValue])
.responseJSON { response in
print(response.result)
if let JSON1 = response.result.value {
print("Did receive JSON data: \(JSON1)")
// JSONData.someData = JSON1 as AnyObject?
UserDefaults.standard.set(JSON1, forKey: "PostData")
UserDefaults.standard.synchronize()
}
else {
print("JSON data is nil.")
}
}
}
NOW, Whenever i pressed the button it calls the viewDidLoad of SecondVC before IBAction of FirstVC which is a bit problematic for my app! How can i decide the priority between these two function.
You have to think about what you want to happen. Clearly the Alamofire call is going to take some time. What do you want to do with the 2nd VC while that time elapses? What do you want to do if the call does not return at all?
This is a common problem when dependent on external resources. How do you manage the UI? Do you present the UI in a partial state? Do you put a popover saying something like "loading". Or do you wait for the resource to complete before presenting the 2nd VC at all?
We cannot make that decision for you, since it depends on your requirement. There are ways to implement each one, though. If the resource usually responds quickly you could show the VC in a partial state and then populate it on some kind of callback. Typically call backs are either (1) blocks (2) delegate methods or (3) notifications. There is also (less commonly) (4) KVO. You should probably research the pros and cons of each.

iOS Swift: best way to pass data to other VCs after query is completed

Context: iOS App written in Swift 3 powered by Firebase 3.0
Challenge: On my app, the user's currentScore is stored on Firebase. This user can complete/un-complete tasks (that will increase/decrease its currentScore) from several ViewControllers.
Overview of the App's architecture:
ProfileVC - where I fetch the currentUser's data from Firebase & display the currentScore.
⎿ ContainerView
⎿ CollectionViewController - users can update their score from here
⎿ DetailsVC - (when users tap on a collectionView cell) - again users can update their score from here.
Question: I need to pass the currentScore to the VCs where the score can be updated. I thought about using prepare(forSegue) in cascade but this doesn't work since it passes "nil" before the query on ProfileVC is finished.
I want to avoid having a global variable as I've been told it's bad practice.
Any thoughts would be much appreciated.
Why don't you create a function that will pull all data before you do anything else.
So in ViewDidLoad call...
pullFirebaseDataASYNC()
Function will look like below...
typealias CompletionHandler = (_ success: Bool) -> Void
func pullFirebaseDataASYNC() {
self.pullFirebaseDataFunction() { (success) -> Void in
if success {
// Perform all other functions and carry on as normal...
Firebase function may look like...
func pullFirebaseDataFunction(completionHandler: #escaping CompletionHandler) {
let refUserData = DBProvider.instance.userDataRef
refUserData.observeSingleEvent(of: .value, with: { snapshot in
if let dictionary = snapshot.value as? [String: AnyObject] {
self.userCurrentScore = dictionary["UserScore"] as! Int
completionHandler(true)
}
})
}
Then when you segue the information across...
In ProfileVC
Create 2 properties
var containerVC: ContainerVC!
var userCurrentScore = Int()
Include the below function in ProfileVC...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ProfileToContainerSegue" {
let destination = segue.destination as! ContainerVC
containerVC = destination
containerVC.userCurrentScore = userCurrentScore
}
}
In ContainerVC create a property
var userCurrentScore = Int()
Ways to improve could be an error message to make sure all the information is pulled from Firebase before the user can continue...
Then the information can be segued across the same way as above.
Try instantiation, first embed a navigation controller to your first storyboard, and then give a storyboardID to the VC you are going to show.
let feedVCScene = self.navigationController?.storyboard?.instantiateViewController(withIdentifier: "ViewControllerVC_ID") as! ViewController
feedVCScene.scoreToChange = current_Score // scoreToChange is your local variable in the class
// current_Score is the current score of the user.
self.navigationController?.pushViewController(feedVCScene, animated: true)
PS:- The reason why instantiation is much healthier than a modal segue storyboard transfer , it nearly removes the memory leaks that you have while navigating to and fro, also avoid's stacking of the VC's.

Resources