Receive delegate method calls from multiple view controllers - ios

I have an app with a SplitViewController. The MasterViewController is a UITableViewController. And the DetailViewController is a UIViewController with a MapView.
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.

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

Related

Networking in Swift Using WikipediaKit / Delegate Design Pattern

I'm trying to request basic info from Wikipedia - user selects tableView row and is taken to a new view that displays info associated with the row (album descriptions). Flow is:
tableView Cell passes user choice to network struct, performs segue to new view
network struct uses choice to query Wikipedia API
received info is passed via delegate method to new view
info is displayed in new view to user
Two issues I'm having:
Networking seems to not be carried out until after the new view is triggered and updated. Initially I didn't use delegate, and just had networking method return its result. But either way, I need networking to get its result back before we get to and update the new view. I've added Dispatch.main.async to the new view for the UI. I thought I may need a completion handler, but I don't fully understand this
Delegate protocol was introduced to fix problem 1, but it still doesn't work. For some reason nothing of the delegate method in the new view is triggering.
Here's the initial tableView trigger:
extension TopTenViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
albumManager.getAlbumInfo(albumTitle: topTenList[indexPath.row][1])
performSegue(withIdentifier: "topTenAlbumInfoSeg", sender: self)
}
Here is the delegate and all the networking:
protocol AlbumManagerDelegate {
func didUpdateAlbumInfo(albumInfo: String)
}
struct AlbumManager {
var newResult: String = ""
var delegate: AlbumManagerDelegate?
func getAlbumInfo(albumTitle: String) {
let wikipedia = Wikipedia.shared
let language = WikipediaLanguage("en")
let _ = wikipedia.requestArticle(language: language, title: albumTitle, imageWidth: 5) { result in
switch result {
case .success(let article):
print(article.displayTitle)
self.delegate?.didUpdateAlbumInfo(albumInfo: article.displayTitle)
print("delegate should be updated")
case .failure(let error):
print(error)
}
}
}
}
Here is the target view:
class AlbumInfoViewController: UIViewController, AlbumManagerDelegate {
#IBOutlet weak var albumDescriptionLabel: UILabel!
#IBOutlet weak var myWebView: WKWebView!
var albumDescription: String = ""
var albumManager = AlbumManager()
override func viewDidLoad() {
super.viewDidLoad()
albumManager.delegate = self
print(albumDescriptionLabel.text)
}
func didUpdateAlbumInfo(albumInfo: String) {
DispatchQueue.main.async {
print("This worked: \(albumInfo)")
self.albumDescriptionLabel.text = "Hi this is: \(albumInfo)"
}
}
}
Currently nothing in didUpdateAlbumInfo in the new view is being triggered. albumDescriptionLabel is not being updated. Could the issue be that the segue is performed too soon? Thanks in advance!

Am I implementing the tableviewdatasource correctly to get downloaded data to show?

I am developing a small app to connect to my site, download data via a PHP web service, and display it in a table view. To get started I was following a tutorial over on Medium by Jose Ortiz Costa (Article on Medium).
I tweaked his project and got it running to verify the Web service was working and able to get the data. Once I got that working, I started a new project and tried to pull in some of the code that I needed to do the networking and tried to get it to display in a tableview in the same scene instead of a popup scene like Jose's project.
This is where I am running into some issues, as I'm still rather new to the swift programming language (started a Udemy course and have been picking things up from that) getting it to display in the table view. I can see that the request is still being sent/received, but I cannot get it to appear in the table view (either using my custom XIB or a programmatically created cell). I thought I understood how the code was broken down, and even tried to convert it from a UITableViewController to a UITableviewDataSource via an extension of the Viewcontroller.
At this point, I'm pretty stumped and will continue to inspect the code and tweak what I think might be the root cause. Any pointers on how to fix would be really appreciated!
Main Storyboard Screenshot
Struct for decoding my data / Lead class:
import Foundation
struct Lead: Decodable {
var id: Int
var name: String
var program: String
var stage: String
var lastAction: String
}
class LeadModel {
weak var delegate: Downloadable?
let networkModel = Network()
func downloadLeads(parameters: [String: Any], url: String) {
let request = networkModel.request(parameters: parameters, url: url)
networkModel.response(request: request) { (data) in
let model = try! JSONDecoder().decode([Lead]?.self, from: data) as [Lead]?
self.delegate?.didReceiveData(data: model! as [Lead])
}
}
}
ViewController:
import UIKit
class LeadViewController: UIViewController {
// Buttons
#IBOutlet weak var newButton: UIButton!
#IBOutlet weak var firstContactButton: UIButton!
#IBOutlet weak var secondContactButton: UIButton!
#IBOutlet weak var leadTable: UITableView!
let model = LeadModel()
var models: [Lead]?
override func viewDidLoad() {
super.viewDidLoad()
//Make Buttons rounded
newButton.layer.cornerRadius = 10.0
firstContactButton.layer.cornerRadius = 10.0
secondContactButton.layer.cornerRadius = 10.0
//Delegate
model.delegate = self
}
//Send request to web service based off Buttons Name
#IBAction func findLeads(_ sender: UIButton) {
let new = sender.titleLabel?.text
let param = ["stage": new!]
print ("findLead hit")
model.downloadLeads(parameters: param, url: URLServices.leads)
}
}
extension LeadViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
print ("number of sections hit")
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
guard let _ = self.models else {
return 0
}
print ("tableView 1 hit")
return self.models!.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create an object from LeadCell
let cell = tableView.dequeueReusableCell(withIdentifier: "leadID", for: indexPath) as! LeadCell
// Lead selection
cell.leadName.text = self.models![indexPath.row].name
cell.actionName.text = self.models![indexPath.row].lastAction
cell.stageName.text = self.models![indexPath.row].stage
cell.progName.text = self.models![indexPath.row].program
print ("tableView 2 hit")
// Return the configured cell
return cell
}
}
extension LeadViewController: Downloadable {
func didReceiveData(data: Any) {
//Assign the data and refresh the table's data
DispatchQueue.main.async {
self.models = data as? [Lead]
self.leadTable.reloadData()
print ("LeadViewController Downloadable Hit")
}
}
}
EDIT
So with a little searching around (okay...A LOT of searching around), I finally found a piece that said I had to set the class as the datasource.
leadTable.dataSource = self
So that ended up working (well after I added a prototype cell with the identifier used in my code). I have a custom XIB that isn't working right now and that's my next tackle point.
You load the data, but don't use it. First, add the following statement to the end of the viewDidLoad method
model.delegate = self
Then add the following LeadViewController extension
extension LeadViewController: Downloadable {
func dicReceiveData(data: [Lead]) {
DispatchQueue.main.async {
self.models = data
self.tableView.reloadData()
}
}
}
And a couple of suggestions:
It is not a good practice to use the button title as a network request parameter:
let new = sender.titleLabel?.text
let param = ["stage": new!]
It is better to separate UI and logic. You can use the tag attribute for buttons (you can configure it in the storyboard or programmatically) to check what button is tapped.
You also have several unnecessary type casts in the LeadModel class. You can change
let model = try! JSONDecoder().decode([Lead]?.self, from: data) as [Lead]?
self.delegate?.didReceiveData(data: model! as [Lead])
to
do {
let model = try JSONDecoder().decode([Lead].self, from: data)
self.delegate?.didReceiveData(data: model)
}
catch {}

Swift 3 : Back to last ViewController with sending data [duplicate]

This question already has answers here:
Passing data between view controllers
(45 answers)
Closed 5 years ago.
I'm trying to go back to my las viewController with sending data, but it doesn't work.
When I just use popViewController, I can go back to the page, but I can't move my datas from B to A.
Here is my code :
func goToLastViewController() {
let vc = self.navigationController?.viewControllers[4] as! OnaylarimTableViewController
vc.onayCode.userId = taskInfo.userId
vc.onayCode.systemCode = taskInfo.systemCode
self.navigationController?.popToViewController(vc, animated: true)
}
To pass data from Child to parent Controller, you have to pass data using Delegate pattern.
Steps to implement delegation pattern, Suppose A is Parent viewController and B is Child viewController.
Create protocol, and create delegate variable in B
Extend protocol in A
pass reference to B of A when Push or Present viewcontroller
Define delegate Method in A, receive action.
After that, According to your condition you can call delegate method from B.
You should do it using delegate protocol
class MyClass: NSUserNotificationCenterDelegate
The implementation will be like following:
func userDidSomeAction() {
//implementation
}
And ofcourse you have to implement delegete in your parent class like
childView.delegate = self
Check this for more information
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html
You have to send back to last ViewController with 2 options.
1. Unwind segue. (With use of storyboard)
You can refer this link.
2. Use of delegate/protocol.
You can refer this link.
Also this link will be useful for you.
You can use Coordinator Pattern
For example, I have 2 screens. The first displays information about the user, and from there, he goes to the screen for selecting his city. Information about the changed city should be displayed on the first screen.
final class CitiesViewController: UITableViewController {
// MARK: - Output -
var onCitySelected: ((City) -> Void)?
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
onCitySelected?(cities[indexPath.row])
}
...
}
UserEditViewController:
final class UserEditViewController: UIViewController, UpdateableWithUser {
// MARK: - Input -
var user: User? { didSet { updateView() } }
#IBOutlet private weak var userLabel: UILabel?
private func updateView() {
userLabel?.text = "User: \(user?.name ?? ""), \n"
+ "City: \(user?.city?.name ?? "")"
}
}
And Coordinator:
protocol UpdateableWithUser: class {
var user: User? { get set }
}
final class UserEditCoordinator {
// MARK: - Properties
private var user: User { didSet { updateInterfaces() } }
private weak var navigationController: UINavigationController?
// MARK: - Init
init(user: User, navigationController: UINavigationController) {
self.user = user
self.navigationController = navigationController
}
func start() {
showUserEditScreen()
}
// MARK: - Private implementation
private func showUserEditScreen() {
let controller = UIStoryboard.makeUserEditController()
controller.user = user
controller.onSelectCity = { [weak self] in
self?.showCitiesScreen()
}
navigationController?.pushViewController(controller, animated: false)
}
private func showCitiesScreen() {
let controller = UIStoryboard.makeCitiesController()
controller.onCitySelected = { [weak self] city in
self?.user.city = city
_ = self?.navigationController?.popViewController(animated: true)
}
navigationController?.pushViewController(controller, animated: true)
}
private func updateInterfaces() {
navigationController?.viewControllers.forEach {
($0 as? UpdateableWithUser)?.user = user
}
}
}
Then we just need to start coordinator:
coordinator = UserEditCoordinator(user: user, navigationController: navigationController)
coordinator.start()

REST API, Swift, Automatic Update

I'm currently struggling to find an easy-to-use programming approach/design pattern, which solves the following problem:
I've got an REST API where the iOS app can request the required data. The data is needed in different ViewControllers. But the problem is, that the data should "always" be up to date. So I need to set up a timer which triggers a request every 5-20 seconds, or sth like that. Everytime the data changes, the view needs to be updated (at the current viewcontroller, which is displayed).
I tried some stuff with delegation and MVC Pattern, but it's kind a messy. How is it done the right way?
In my current implementation I only can update the whole UICollectionView, not some specific cells, because I don't know how the data changed. My controller keeps track of the data from the api and updates only if the hash has changed (if data changed on the server). My models always holds the last fetched data.
It's not the perfect solution, in my opinion..
I also thought about models, that keep themselves up to date, to abstract or virtualise my Rest-API. In this case, my controller doesn't even know, that it isn't directly accessible data.
Maybe someone can help me out with some kind of programming model, designpattern or anything else. I'm happy about anything!
UPDATE: current implementation
The Controller, which handles all the data
import Foundation
import SwiftyJSON
import SwiftyTimer
class OverviewController {
static let sharedInstance = OverviewController()
let interval = 5.seconds
var delegate : OverviewControllerUpdateable?
var model : OverviewModel?
var timer : NSTimer!
func startFetching() -> Void {
self.fetchData()
timer = NSTimer.new(every: interval) {
self.fetchData()
}
timer.start(modes: NSRunLoopCommonModes)
}
func stopFetching() -> Void {
timer.invalidate()
}
func getConnections() -> [Connection]? {
return model?.getConnections()
}
func getConnectionsSlave() -> [Connection]? {
return model?.getConnectionsSlave()
}
func getUser() -> User? {
return model?.getUser()
}
func countConnections() -> Int {
if let count = model?.getConnections().count {
return count
}
return 0
}
func countConnectionsSlave() -> Int {
if let count = model?.getConnectionsSlave().count {
return count
}
return 0
}
func fetchData() {
ApiCaller.doCall(OverviewRoute(), completionHandler: { (data, hash) in
if let actModel = self.model {
if (actModel.getHash() == hash) {
//no update required
return
}
}
var connections : [Connection] = []
var connectionsSlave : [Connection] = []
for (_,connection):(String, JSON) in data["connections"] {
let connectionObj = Connection(json: connection)
if (connectionObj.isMaster == true) {
connections.append(connectionObj)
} else {
connectionsSlave.append(connectionObj)
}
}
let user = User(json: data["user"])
//model needs update
let model = OverviewModel()
model.setUser(user)
model.setConnections(connections)
model.setConnectionsSlave(connectionsSlave)
model.setHash(hash)
self.model = model
//prevent unexpectedly found nil exception
if (self.delegate != nil) {
self.delegate!.reloadView()
}
}, errorHandler: { (errors) in
}) { (progress) in
}
}
}
protocol OverviewControllerUpdateable {
func reloadView()
}
The model, which holds the data:
class OverviewModel {
var user : User!
var connections : [Connection]!
var connectionsSlave : [Connection]!
var connectionRequests : [ConnectionRequest]!
var hash : String!
...
}
And in the ViewController, I use it like this:
class OverviewVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, OverviewControllerUpdateable {
let controller = OverviewController.sharedInstance
override func viewDidLoad() {
super.viewDidLoad()
self.controller.delegate = self
self.controller.startFetching()
}
//INSIDE THE UICOLLECTIONVIEW DELEGATE METHODS
...
if let user : User = controller.getUser() {
cell.intervalTime = interval
cell.nameLabel.text = "Ihr Profil"
}
...
func reloadView() {
self.userCollectionView.reloadData()
}
}
You could use a Singleton object to fetch your data periodically, then post notifications (using NSNotificationCenter) when the data is updated. Each view controller dependent on the data would listen for these notifications, then reload UI based on the updated data.

pass data between content and menucontroller

I'm having a menuViewController and a ContentViewController using https://github.com/romaonthego/RESideMenu. The MenuViewController contain a list of different leagues retrieved from local database. This contain a league object with following vars leagueId and name. When a league is selected it should send the data to the ContentViewController. However the problem is that the MenuViewController is not presenting the viewController it is just hiding the menuViwController and therefore i can't pass data from menuViewController to contentViewController when a cell with a league is selected. i've therefore tried to save the leagueId to a NSUserDefault key, however this creates a problem when the app is exited, since it wont reset the NSUserDefaults. What is the best approach for such issue? should i rethink it?
pressing cell in menuViewController
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
NSUserDefaults.standardUserDefaults().setInteger(menuArray![indexPath.row].id, forKey: "leagueId")
self.sideMenuViewController.hideMenuViewController()
}
You can achieve this by creating a global class, First create a global class, :-
import Foundation
class User {
class var sharedInstance: User {
struct Static {
static var instance: User?
static var token: dispatch_once_t = 0
}
dispatch_once(&Static.token) {
Static.instance = User()
}
return Static.instance!
}
var leagueId: Int?
var name:String?
}
then store data in the class that you want
class ViewController: UIViewController {
let user = User.sharedInstance
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetails" {
self.user.leagueId = Id
self.user.name = Name
}
}
}
Then retrieve data :-
class ViewController: UIViewController {
let user = User.sharedInstance
mylabel.text = self.user.leagueId
mylabael2.text = self.user.name
}

Resources