Proper way to set up a protocol in Swift 3 - ios

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

Related

Delegate becomes nil in Operation of urlSession. How to keep delegate variable in separate thread?

I'm using an OperationQueue to upload files one by one to a remote server using URLSession.dataTask. A delegate is used to update a progress bar but after implementing OperationQueue my delegate becomes nil. It worked without OperationQueues. Looking at the stack while the program is running I don't see my progress bar's view controller. It's been a few days and I still can't quite figure it out. I'm guessing the view controller is getting deallocated but I'm not sure how to prevent it from getting deallocated. Thank you.
I have my delegate set to self in NetWorkViewController but inside my NetworkManager class's urlSession(didSendBodyData), the delegate becomes nil. The delegate is not weak and is a class variable.
However, my delegate becomes none-nil again within the completion block of my BlockOperation. This works for dismissing the ViewController via delegation. But the delegate is nil when I'm trying to update inside urlSession(didSendBodyData)...
UPDATE 10/30/2018
It seems that my urlSessions delegates are on a separate thread and is enqueued to the main thread when called but I lose reference to my custom delegate that updates the UI. I'm trying to read more about multithreading but any help would be appreciated!
UPDATE 2 10/30/2018
Solution found The issue was that I was creating another instance of NetworkManager inside each operation. This causes the delegate to be nil because a new instance of NetworkManager is being created for each operation. The fix is to pass self from the original NetworkManager so the delegate is retained.
uploadFiles
func uploadFiles(item: LocalEntry) {
let mainOperation = UploadMainFileOperation(file: item)
// This is where I need to give the operation its
// networkManager so the proper delegate is transferred.
mainOperation.networkManager = self
mainOperation.onDidUpload = { (uploadResult) in
if let result = uploadResult {
self.result.append(result)
}
}
if let lastOp = queue.operations.last {
mainOperation.addDependency(lastOp)
}
queue.addOperation(mainOperation)
....
....
let finishOperation = BlockOperation { [unowned self] in
self.dismissProgressController()
for result in self.result {
print(result)
}
self.delegate?.popToRootController()
}
if let lastOp = queue.operations.last {
finishOperation.addDependency(lastOp)
}
queue.addOperation(finishOperation)
queue.isSuspended = false
}
UploadMainFileOperation
class UploadMainFileOperation: NetworkOperation {
let file: LocalEntry
// First issue is here. I re-declared another NetworkManager that doesn't have its delegate properly set.
private let networkManager = NetworkManager()
// I have since have this class receive the original networkManager after it's declared.
var networkManager: NetworkManager?
var onDidUpload: ((_ uploadResult: String?) -> Void)!
init(file: LocalEntry) {
self.file = file
}
override func execute() {
uploadFile()
}
private func uploadFile() {
networkManager.uploadMainFile(item: file) {
(httpResult) in
self.onDidUpload(httpResult)
self.finished(error: "upload main")
}
}
}
urlSession(didSendBodyData)
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
// This is wrong.
let uploadProgress: Float = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
updateDelegateWith(progress: uploadProgress)
// This is the correct way for my situation.
// Because each operation on the queue is on a separate thread. I need to update the UI from the main thread.
DispatchQueue.main.async {
let uploadProgress: Float = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
self.updateDelegateWith(progress: uploadProgress)
}
}
updateDelegateWith(progress: Float)
func updateDelegateWith(progress: Float) {
delegate?.uploadProgressWith(progress: progress)
}
NetworkManagerViewController where the progress bar lives
class NetworkViewController: UIViewController, NetWorkManagerDelegate {
var localEntry: LocalEntry?
var progressBackground = UIView()
var progressBar = UIProgressView()
func uploadProgressWith(progress: Float) {
progressBar.progress = progress
view.layoutSubviews()
}
deinit {
print("deallocate")
}
override func viewDidLoad() {
super.viewDidLoad()
let networkManager = NetworkManager()
networkManager.delegate = self
networkManager.uploadFiles(item: self.localEntry!)
....
....
}
}
With the latest code shared, i would suggest to keep NetworkManager instance at the class level instead of a function level scope as this will ensure the networkManager instance is not deallocated.
class NetworkViewController: UIViewController, NetWorkManagerDelegate {
var localEntry: LocalEntry?
var progressBackground = UIView()
var progressBar = UIProgressView()
let networkManager = NetworkManager()
func uploadProgressWith(progress: Float) {
progressBar.progress = progress
view.layoutSubviews()
}
deinit {
print("deallocate")
}
override func viewDidLoad() {
super.viewDidLoad()
networkManager.delegate = self
networkManager.uploadFiles(item: self.localEntry!)
}
...
Also, you need to be careful for retain-cycles that cause memory leaks. To avoid retain cycles, you need to declare your delegate variable as weak.
As user #Kamran pointed out, I was creating a class level instance of networkManager inside UploadMainFileOperation. The issue has been fixed by changed that variable to an Optional and giving it an instance of NetworkManager, as self ,that was queueing up the operations. The code blocks as been updated with comments of the correct code along with the incorrect code.
If you set a delegate and later it becomes nil, this means your delegate has been deallocated.
I would recommend to create an (empty) deinit in your delegate class and set a breakpoint for the debugger in that method. This will help you find out where you're losing the reference to said delegate.
You can probably avoid this by assigning your delegate to a property of one of your classes or make it a strong reference in one of your completion blocks.

Using a delegate to save values to core data

Beginner here, this is my first shot at using a delegate and I'm pretty confused - I'm trying to pass data between two controllers, the first of which is a tableview displaying some products, and the other is a modal view which allows the user to enter a new product to be displayed on that tableview. When the user hits "Save" in the modal view, I want to save the new product into core data and have it be displayed on the tableview.
The user enters information in three text fields in AddProductController (the modal view) and then hits save which calls handleSave:
func handleSave() {
guard let newProductUrl = self.urlTextField.text else {
print("error getting text from product url field")
return
}
guard let newProductName = self.nameTextField.text else {
print("error getting text from product name field")
return
}
guard let newProductImage = self.logoTextField.text else {
print("error getting text from product logo field")
return
}
DispatchQueue.main.async {
self.productSaveDelegate?.save(name: newProductName, url: newProductUrl, image: newProductImage)
let companyController = CompanyController()
self.navigationController?.pushViewController(companyController, animated: true)
}
}
Which in turn calls save in ProductController (the tableview):
func save(name: String, url: String, image: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let entity =
NSEntityDescription.entity(forEntityName: "Product",
in: managedContext)!
let product = NSManagedObject(entity: entity,
insertInto: managedContext)
product.setValue(name, forKey: "name")
product.setValue(url, forKey: "url")
product.setValue(image, forKey: "image")
do {
try managedContext.save()
products.append(product)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
tableView.reloadData()
}
If I'm understanding correctly, I'm using the delegate as a sort of link between the two so that I can pass the user-entered values directly into my save function? Correct me if I'm wrong I'm pretty new. But I create the delegate outside the class scope at the top of ProductController (the tableview controller) like so:
protocol ProductSaveDelegate {
func save(name: String, url: String, image: String)
}
Then in AddProductController (the modal view where the user enters the new product information) I initialize the delegate near the top of the class:
var productSaveDelegate: ProductSaveDelegate?
And then use it to call the save function in handleSave() as seen above.
When I try to add ProductSaveDelegate to the class definition of AddProductController I get an error saying that AddProductController does not conform to the protocol.
What can I change here to make the user-entered product save to core data properly? Thanks in advance for any help!
Delegates are really cool and powerful, but sometimes, yeah, they can be confusing. You need to initialize the delegate inside of the ProductController, not the AddProductController.
This is a great image which shows how a delegate and protocol setup works:
Image from Andrew Bancroft's website
In this instance, your Delegate is your ProductController and your Delegator is your AddProductController
MAKE SURE YOU HAVE SUBSCRIBED THE DELEGATE
Do NOT forget -
classObject.delegate = self
I suppose you are already familiar with the delegates,
You're get this error because, either you have not implemented the delegate method from the AddProductController(which in your case is false, as i can see it implemented), or you forgot to subscribe the delegate, to do so you need to make sure -
Subscribing the delegate, i.e. before transitioning to the next controller set the delegate to self like -
// addProductController is the AddProductController Class's Object
addProductController.productSaveDelegate = self
self.navigationController?.present(addProductController, animated: true, completion: nil)
also in the same class you need to implement the method defined in the protocol, i.e func save(name: String, url: String, image: String) in your case(already implemented)
You can also check a small demo, i have impleted here for the protocols.

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.

Losing api response when delegate is changed

I am facing an issue using the Protocol in my app, I have created a control to manage api calls, and return the response to the delegate view controller. but my concern is for example:
I was at ViewController1 and requested an api to get admin profile for example, and assign api delegate to self meaning (APIClient.delegate = self) : ViewController1 will receive the response because it implemented the delegate of api client to have the response back
Now before the response comes back of get admin profile. i went to ViewController2 and assigned the APIClient.delegate = self and requested another api call here. Now the response of get admin profile came but it will be discard because the delegate is not equal to ViewController1 who did the request or not implementing the method to handle profile response!
Here is some of my code:
#objc protocol APIClientDelegate : NSObjectProtocol {
#objc optional func getAdminProfileFinishedWithResponse(_ dictionary:NSMutableDictionary)
#objc optional func getAdminProfileFailedWithError(_ error:NSError)
}
#objc class APIClient: APIClientDelegate {
weak var delegate : APIClientDelegate?
func getAdminProfile(_ postDictionary:NSMutableDictionary){
self.get(getUserProfilePath, parameters: postDictionary, progress: nil, success: { (task, response) in
if self.delegate != nil && self.delegate!.responds(to: #selector(APIClientDelegate.getAdminProfileFinishedWithResponse(_:))){
self.delegate!.getAdminProfileFinishedWithResponse!((response as! NSDictionary ).mutableCopy() as! NSMutableDictionary)
}
}) { (task, error) in
if self.delegate != nil && self.delegate!.responds(to: #selector(APIClientDelegate.getAdminProfileFailedWithError(_:))){
self.delegate!.getAdminProfileFailedWithError!(error as NSError)
}
}
}
}
If you get my point, if ViewController1 requested an api to be sure that the response doesn't get lost if the delegate is changed. Any idea of how to solve it dynamically, in background..etc?
To achieve what you want, you have create an object of object of APIClient each time you make an API call.
e.g.
let objClient = APIClient()
objClient.delegate = self
objClient.getAdminProfile(.....
by this way, the reference of each call will be contained and API calls won't overlap.

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