Catch event in view from another class - ios

I have async task with request where i fetching products every 3 seconds in class Item.
class Item: NSManagedObject {
var is_fetching:Bool = false;
func fetchProducts(q: String) {
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
self.is_fetching = true;
//some code
if ((response as! NSHTTPURLResponse).statusCode == 202) {
sleep(3)
self.fetchProducts(q)
return
}
if ((response as! NSHTTPURLResponse).statusCode == 200) {
self.is_fetching = false;
}
})
task.resume()
}
}
And i have UITableViewController where i show data from response. How do i update my cells when status code is 200:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:
indexPath) as! CartTableViewCell
if item.is_fetching {
cell.fetchIndicator.startAnimating();
} else {
cell.fetchIndicator.stopAnimating();
cell.fetchIndicator.hidden = true;
}
}

You can do it in few ways.
NSNotificationCenter (simplest).
You can post notifications, that will trigger your controller's methods. Looks like this:
// if response.code == 200
NSNotificationCenter.defaultCenter().postNotificationName("kSomeConstatnString", object: nil)
...
// in viewDidLoad of your controller:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateTable", object: nil)
// you also need implement updateTable() func inside your controller
// or if you need just update table
NSNotificationCenter.defaultCenter().addObserver(self.tableView, selector: "reloadData", object: nil)
// do not forget to delete observer (for instance in -deinit method)
NSNotificationCenter.defaultCenter().removeObserver(self)
// or tableView. also you can specify for which selector, if you use couple of them.
Delegate pattern.
You can describe your protocol, make your controller implement this protocol and save it as instance in your model object. Then just call methods from delegate. Details here.
Block callbacks.
Create block for action and call it from your model. For example:
// inside controller
model.refreshCallback = { Void in
self.tableView.reloadData() // or whatever
}
// inside model
var refreshCallback: (() -> Void)?
...
// if result.code == 200
if let callback = refreshCallback {
callback()
}

Use one of the UITableView’s reload functions, perhaps:
func reloadRowsAtIndexPaths(_ indexPaths: [NSIndexPath],
withRowAnimation animation: UITableViewRowAnimation)
This will cause it to ask again for the cell in question. Make sure you do this on the main thread.

Related

How to display tableView only after data fetched from web? (Swift)

I encountered difficulties when loading the Collection views nested in table view cells. The content inside cells would only show after scrolling the table a couple of times. My approach was to use DispatchGroup() in order to fetch the data in a background thread but it didn't work. What is there to do in order to show all the information at once without scrolling through table?
ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
_tableView.isHidden = true
_tableView.dataSource = nil
_tableView.delegate = nil
SVProgressHUD.show()
view.backgroundColor = UIColor.flatBlack()
getData()
dispatchGroup.notify(queue: .main) {
SVProgressHUD.dismiss()
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
}
}
UICollectionView and UITableView datasource / OtherMethods
func getData(){
dispatchGroup.enter()
backend.movieDelegate = self
backend.actorDelegate = self
backend.getMoviePopularList()
backend.getMovieTopRatedList()
backend.getMovieUpcomingList()
backend.getPopularActors()
backend.getMovieNowPlayingList()
dispatchGroup.leave()
}
func transferMovies(data: [String:[MovieModel]]) {
dispatchGroup.enter()
popularMovies = data
dispatchGroup.leave()
}
func transferActors(data: [ActorModel]) {
dispatchGroup.enter()
popularActors = data
dispatchGroup.leave()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "DiscoverCell") as? DiscoverViewCell else { return UITableViewCell()}
cell.categoryLabel.text = cell.categories[indexPath.item]
//categories[indexPath.item]
cell._collectionView.delegate = self
cell._collectionView.dataSource = self
cell._collectionView.tag = indexPath.row
cell._collectionView.reloadData()
self.setUpCell(cell)
return cell
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", for: indexPath) as? MovieCollectionViewCell else { return UICollectionViewCell()}
if collectionView.tag == 0{
if let movieDetails = popularMovies["Popular"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
} else if collectionView.tag == 1{
if let movieDetails = popularMovies["Top rated"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
} else if collectionView.tag == 2{
if let movieDetails = popularMovies["Upcoming"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
} else if collectionView.tag == 3{
cell.movieTitleLabel.text = popularActors?[indexPath.item].name ?? ""
cell.moviePicture.image = popularActors?[indexPath.item].poster
}
} else if collectionView.tag == 4{
if let movieDetails = popularMovies["Now playing"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
}
return cell
}
MovieCollectionViewCell
class MovieCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var moviePicture: UIImageView!
#IBOutlet weak var movieTitleLabel: UILabel!
func updateMovieCollectionCell(movie: MovieModel){
moviePicture.image = movie.poster
movieTitleLabel.text = movie.name
}
}
DiscoverViewCell
class DiscoverViewCell: UITableViewCell {
#IBOutlet weak var categoryLabel: UILabel!
#IBOutlet weak var _collectionView: UICollectionView!
let categories = ["Popular", "Top Rated", "Upcoming", "Popular People", "Now playing"]
#IBAction func seeMoreAction(_ sender: Any) {
}
}
My intention is to show a loading animation until all the data is fetched and the display the table view cells containing the collection views with fetched data from web.
The desired result should look like this when opening the app
From what I can tell, you're using dispatchGroup incorrectly.
To summarize notify():
it runs the associated block when all currently queued blocks in the group are complete
the block is only run once and then released
if the group's queue is empty, the block is run immediately
The issue I see is that with the way your fetch code is written, the group thinks its queue is empty when you call notify. So the notify() block is run immediately and then you only see the cells populate when they are reloaded during scrolling.
There are two ways to populate a dispatchGroup's queue:
call DispatchQueue.async() and pass the group to directly enqueue a block and associate it with the group
manually call enter() when a block begins and leave() when it ends, which increases/decreases an internal counter on the group
The first way is safer, since you don't have to keep track of the blocks yourself, but the second one is more flexible if you don't have control over what queue a block is run on for example.
Since you're using enter/leave, you need to make sure that you call enter() for each separate work item (in your case, the asynchronous calls to backend), and only call leave() when each one those work items completes. I'm not sure how you're using the delegate methods, but there doesn't seem to one for each backend call, since there are 5 different calls and only 2 delegate methods. It also doesn't look like the delegate methods would be called if an error happened in the backend call.
I would recommend changing the backend calls to use completion blocks instead, but if you want to stick with the delegate pattern, here's how you might do it:
func getData(){
backend.movieDelegate = self
backend.actorDelegate = self
dispatchGroup.enter()
backend.getMoviePopularList()
dispatchGroup.enter()
backend.getMovieTopRatedList()
dispatchGroup.enter()
backend.getMovieUpcomingList()
dispatchGroup.enter()
backend.getPopularActors()
dispatchGroup.enter()
backend.getMovieNowPlayingList()
dispatchGroup.notify(queue: .main) {
SVProgressHUD.dismiss()
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
}
}
func transferPopularMovies(data: [MovieModel]) {
popularMovies = data
dispatchGroup.leave()
}
func transferTopRatedMovies(data: [MovieModel]) {
topRatedMovies = data
dispatchGroup.leave()
}
func transferUpcomingMovies(data: [MovieModel]) {
upcomingMovies = data
dispatchGroup.leave()
}
func transferActors(data: [ActorModel]) {
popularActors = data
dispatchGroup.leave()
}
func transferNowPlayingMovies(data: [MovieModel]) {
nowPlayingMovies = data
dispatchGroup.leave()
}
Don't forget to call the delegate methods even if there is an error to make sure the enter/leave calls are balanced. If you call enter more often than leave, the notify block never runs. If you call leave more often, you crash.
Try this...After getting your data from backend and assigning to moviedetails, set your delegate and datasource to self and reload table.
Set this line into the bottom of getData() function and run
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
and remove from viewdidload()

How to reload tableview after adding new entry?

I am creating a cloudkit tableview. I load the app and my tableview appears with my entries from cloud kit.
I then use my add method insertNewObject which adds the record to cloud kit but this does not show up in my tableview. It will only show up on my next run of the app.
func insertNewObject(sender: AnyObject) {
let record = CKRecord(recordType: "CloudNote")
record.setObject("New Note", forKey: "Notes")
MyClipManager.SaveMethod(Database!, myRecord:record)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
This is my add method. I am calling tableview reload as you can see but nothing is happening.
My tableview creation code:
// Tableview stuff --- Done
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
/////// Get number of rows
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
//// FIll the table
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let object = objects[indexPath.row]
cell.textLabel!.text = object.objectForKey("Notes") as? String
return cell
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
As requested: Method that saves to CloudDB
func SaveMethod(publicDatabase: CKDatabase, myRecord: CKRecord ) -> CKRecord {
publicDatabase.saveRecord(myRecord, completionHandler:
({returnRecord, error in
if let err = error {
self.notifyUser("Save Error", message:
err.localizedDescription)
} else {
dispatch_async(dispatch_get_main_queue()) {
self.notifyUser("Success",
message: "Record saved successfully")
}
}
}))
return myRecord
}
My viewdidload method in masterview:
override func viewDidLoad() {
super.viewDidLoad()
// Database loading on runtime
Database = container.privateCloudDatabase
///Build Query
let query = CKQuery(recordType: "CloudNote", predicate: NSPredicate(format: "TRUEPREDICATE"))
///Perform query on DB
Database!.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
if (error != nil) {
NSLog("Error performing query. \(error.debugDescription)")
return
}
self.objects = records!
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
You should not reload your entire tableView when you insert a single object. Only do that when you know ALL the data has changed.
To do what you want, this is the order:
Insert a new data object into your datasource (self.objects). Make sure you get the index of where it ends up in the array.
Call insertRowAtIndexPath: with the correct indexPath on your tableView. This will make sure your data and tableView are in sync again, and tableView:cellForRowAtIndexPath: is called for at least your new data object (and possible others, as certain cells might now be reused to display other data).
Note that the order is always: update your data first, then update your UI (the only place I know of that his is hairy is when using a UISwitch).

Building table view while getting number of rows from completion block

I am trying to retrieve the calendar events in swift 2, and I can not solve this: to build the table view I need to know the number of cells, which I can get from a method like this (for the sake of simplicity array is String):
func fetchCalendarEvents () -> [String] {
var arrayW = [String]()
let eventStore : EKEventStore = EKEventStore()
eventStore.requestAccessToEntityType(EKEntityType.Event, completion: {
granted, error in
if (granted) && (error == nil) {
print("access granted: \(granted)")
//do stuff...
}
else {
print("error: access not granted \(error)")
}
})
return arrayW
}
I am calling this method in viewDidLoad, and save the array to var eventArray. Immediately the following method gets called:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return eventArray.count
}
The problem is that the completion block of the fetchCalendarEvents is not complete at this point, and therefore returns 0 (while there are events in the calendar).
My question: how can I handle building the table view from array that I get from method, that has completion block, and takes some time to be completed?
Add aBlockSelf.tableView.reloadData() in your calendar completion block to reload your table with fetched data.
Here aBlockSelf is a weak reference to self because its being passed to a block.
EDIT: Post OP comment - Try this:
weak var aBlockSelf = self
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (eventArray.count == 0) { //or unwrap value, depends on your code
return 0 // or 1, if you want add a 'Loading' cell
} else {
return eventArray.count
}
}
And, when you get eventArray, just reload table with
//without animation
tableView.reloadData()
//or with, but it can get little tricky
tableView.insertRowsAtIndexPaths(indexArray, withRowAnimation:UITableViewRowAnimationRight];
Make 2 cells type. One for events and one which just say "Loading...". While your block in progress show only 1 cell with "Loading...". When an events will retrieve hide this cell and reload table with events.
Cheers.
Ok, all seemed to answer relatively correct, but not exactly in the way that worked straight. What worked for me, is that instead of returning array, I just need to reload the table view after the for loop, where array is built up:
func fetchCalendarEvents () {
let eventStore : EKEventStore = EKEventStore()
eventStore.requestAccessToEntityType(EKEntityType.Event, completion: {
granted, error in
if (granted) && (error == nil) {
print("access granted: \(granted)")
let startDate=NSDate().dateByAddingTimeInterval(-60*60*24)
let endDate=NSDate().dateByAddingTimeInterval(60*60*24*3)
let predicate2 = eventStore.predicateForEventsWithStartDate(startDate, endDate: endDate, calendars: nil)
print("startDate:\(startDate) endDate:\(endDate)")
let events = eventStore.eventsMatchingPredicate(predicate2) as [EKEvent]!
if events != nil {
var arrayOfEvents = [CalendarEventObject]()
for event in events {
var eventObject: CalendarEventObject
// (ಠ_ಠ) HARDCODEDE
eventObject = CalendarEventObject(id: 0, title: event.title, location: event.location!, notes: event.notes!, startTime: event.startDate, endTime: event.endDate, host: "host", origin: "origin", numbers: ["0611111111", "0611111112"], passcodes: ["123456", "123457"], hostcodes: ["123458", "123459"], selectedNumber: "0611111113", selectedPasscode: "123457", selectedHostcode: "123459", scheduled: true, parsed: false, update: true, preferences: [], eventUrl: "www.youtube.com", attendees: [])
arrayOfEvents.append(eventObject)
}
self.eventArray = arrayOfEvents
//reload data after getting the array
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
}
else {
print("error: access not granted \(error)")
}
})
}

Pull to Refresh: data refresh is delayed

I've got Pull to Refresh working great, except when the table reloads there is a split second delay before the data in the table reloads.
Do I just have some small thing out of place? Any ideas?
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
handleRefresh for Pull to Refresh:
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
refreshControl.endRefreshing()
})
}
Need the data in two places, so created a function for it getCloudKit:
func getCloudKit() {
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
for play in results! {
let newPlay = Play()
newPlay.color = play["Color"] as! String
self.objects.append(newPlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
tableView:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = objects[indexPath.row]
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}
This is how you should do this:
In your handleRefresh function, add a bool to track the refresh operation in process - say isLoading.
In your getCloudKit function just before reloading the table view call endRefreshing function if isLoading was true.
Reset isLoading to false.
Importantly - Do not remove your model data before refresh operation is even instantiated. What if there is error in fetching the data? Delete it only after you get response back in getCloudKit function.
Also, as a side note, if I would you, I would implement a timestamp based approach where I would pass my last service data timestamp (time at which last update was taken from server) to server and server side would return me complete data only there were changes post that timestamp else I would expect them to tell me no change. In such a case I would simple call endRefreshing function and would not reload data on table. Trust me - this saves a lot and gives a good end user experience as most of time there is no change in data!

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

Resources