Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Receive delegate method calls from multiple view controllers

I have an app with a SplitViewController. The MasterViewController is a UITableViewController. And the DetailViewController is a UIViewController with a MapView.

enter image description here

I'm calling an API to get a list of events. In the tableView, I want to display the names and the addresses of the events. And in the mapView, I want to mark the locations of those events.

I have a class called DataLoader in which I call the API, parse the JSON object and create Event objects and put them in an array successfully.

import Foundation
import SwiftyJSON

@objc protocol DataLoaderDelegate {
    func eventDataReceived(items: [Event]?, error: NSError?)
}

public class DataLoader {

    private let api = ApiClient()

    var delegate: DataLoaderDelegate?

    init() { }

    public func fetchEvents() {
        api.getEvents({ (data) -> Void in
            let json = JSON(data)
            self.processEventData(json["data"])
        }, failure: { (error) -> Void in
            println("Error fetching events: \(error?.localizedDescription)")
            self.delegate?.eventDataReceived(nil, error: error)
        })
    }

    private func processEventData(data: JSON) {
        var events = [Event]()

        if let eventsArray = data.array {
            for eventObj in eventsArray {
                let event = Event(
                    id: eventObj["id"].int!,
                    type: EventType(rawValue: eventObj["type"].int!)!,
                    location: eventObj["location"].string!,
                    status: EventStatus(rawValue: eventObj["status"].int!)!
                )
                event.allDay = eventObj["allDay"].int!
                event.title = eventObj["title"].string
                event.description = eventObj["description"].string
                event.latitude = eventObj["lat"].double
                event.longitude = eventObj["lng"].double
                event.startDate = NSDate(string: eventObj["start"].string!)
                event.endDate = NSDate(string: eventObj["end"].string!)
                event.createdAtDate = NSDate(string: eventObj["created_at"].string!)
                event.updatedAtDate = NSDate(string: eventObj["updated_at"].string!)
                event.creatorEmail = eventObj["creatorEmail"].string
                event.organizerEmail = eventObj["organizerEmail"].string
                event.googleCalendarId = eventObj["google_cal_id"].string!
                event.googleEventId = eventObj["google_event_id"].string!
                event.htmlLink = eventObj["htmlLink"].string!

                events.append(event)
            }

            // Order the events by the startDate value
            events.sort({ $0.startDate!.timeIntervalSince1970 < $1.startDate!.timeIntervalSince1970 })

            self.delegate?.eventDataReceived(events, error: nil)
        }
    }
}

I have a delegate called DataLoaderDelegate with a method called func eventDataReceived(items: [Event]?, error: NSError?) which is called after all the event objects are inserted into an array so I can pass it through that method.

I need this method implemented in both the ListViewController where I populate the tableView and MapViewController where I populate the mapView. So I have implemented the eventDataReceived delegate method in both view controllers in order to get the events objects.

ListViewController.swift

import UIKit

class ListViewController: UITableViewController, DataLoaderDelegate {

    let loader = DataLoader()

    private var events = [Event]()

    override func viewDidLoad() {
        super.viewDidLoad()

        loader.delegate = self
        loader.fetchEvents()
    }

    // MARK: - Table view data source
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return events.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell

        let event = events[indexPath.row]
        cell.textLabel?.text = event.title
        cell.detailTextLabel?.text = event.location

        return cell
    }

    // MARK: - DataLoaderDelegate
    func eventDataReceived(items: [Event]?, error: NSError?) {
        println("ListViewController got \(items?.count) events")
        if let events = items {
            self.events = events
            tableView.reloadData()
        }
    }
}

MapViewController.swift

import UIKit

class MapViewController: UIViewController, DataLoaderDelegate {

    let loader = DataLoader()

    override func viewDidLoad() {
        super.viewDidLoad()

        loader.delegate = self
    }

    // MARK: - DataLoaderDelegate
    func eventDataReceived(items: [Event]?, error: NSError?) {
        println("MapViewController got \(items?.count) events")
    }
}

But here's the problem. Only the implementation in the ListViewController gets executed. I think the reason is I create a new instance of DataLoader class in the MapViewController.

What should I do to have one instance of DataLoader across the entire application and receive its delegate method calls from all view controllers?

I've uploaded a demo project to my Dropbox if you need to run it to get a better idea.

Thanks.

like image 796
Isuru Avatar asked May 10 '15 09:05

Isuru


1 Answers

This is a best-practice case for a Singleton-pattern Your DataLoader should implement a static let holding the reference to the same object of self.

static let sharedInstance = DataLoader()

When you want the reference to constantly the same singleton-object. Call it with DataLoader.sharedInstance

Make sure to always use .sharedInstance then and do not forget to wipe any calls of the standard initializer of the class, because it will still make new instances unless you block this behavior programmatically.

Explained

Only inserting the singleton constant (shown above) will NOT make DataLoader class always return the singleton instance. The existing initializer calls like:

   var myDataLoader = DataLoader()

will still instanciate a new object, everytime they are called. The singleton instance is reached with:

   var mySingletonDataLoader = DataLoader.sharedInstance

You could change your standard (non singleton) initializers like:

init() {
NSLog("No instances allowed, please use .sharedInstance for a singleton")
}

Complete solution

The singleton is only one piece to solve the whole problem. The used delegate-pattern works great for sending information from one object to another and therefore delegating the work. The problem from the questioner needs another broadcasting mechanism from one object to many other object. Therefore he decided to implement the Observer-pattern with NSNotificationCenter

like image 188
gutenmorgenuhu Avatar answered Nov 03 '22 20:11

gutenmorgenuhu