Building table view while getting number of rows from completion block - ios

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

Related

Is it possible for NSFetchResultsController to perform move and update operation, when we change the section of an item?

Currently, I have a UICollectionView which consists of 2 sections
Pinned
Normal
They looks as following.
Overview
== Pinned ===========
|------|
|NOTE0 |
|------|
== Normal ===========
|------| |------|
|NOTE1 | |NOTE2 |
|------| |------|
|------|
|NOTE3 |
|------|
NSManagedObject
This is the NSManagedObject
extension NSPlainNote {
#nonobjc public class func fetchRequest() -> NSFetchRequest<NSPlainNote> {
return NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote")
}
#NSManaged public var title: String?
#NSManaged public var body: String?
#NSManaged public var pinned: Bool
#NSManaged public var uuid: UUID
}
NSFetchResultsController
We use the Bool field, to decide whether an item should belong to Pinned section, or Normal section
This is how our NSFetchResultsController looks like
lazy var fetchedResultsController: NSFetchedResultsController<NSPlainNote> = {
// Create a fetch request for the Quake entity sorted by time.
let fetchRequest = NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "pinned", ascending: false)
]
// Create a fetched results controller and set its fetch request, context, and delegate.
let controller = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: CoreDataStack.INSTANCE.persistentContainer.viewContext,
sectionNameKeyPath: "pinned",
cacheName: nil
)
controller.delegate = fetchedResultsControllerDelegate
// Perform the fetch.
do {
try controller.performFetch()
} catch {
fatalError("Unresolved error \(error)")
}
return controller
}()
Move and update operation
We then perform the following operations
Either move the item from Normal section to Pinned section, or move the item from Pinned section to Normal section.
Update the content.
func updatePinned(_ objectID: NSManagedObjectID, _ pinned: Bool) {
let coreDataStack = CoreDataStack.INSTANCE
let backgroundContext = coreDataStack.backgroundContext
// TODO: Can we optimize the code, to avoid fetching the entire model object?
backgroundContext.perform {
let nsPlainNote = try! backgroundContext.existingObject(with: objectID) as! NSPlainNote
// This will trigger "move". The cell shall move to different section.
nsPlainNote.pinned = pinned
// Can we trigger "update" as well?
if nsPlainNote.pinned {
nsPlainNote.body = nsPlainNote.title! + "(Pinned)"
} else {
nsPlainNote.body = nsPlainNote.title
}
RepositoryUtils.saveContextIfPossible(backgroundContext)
}
}
NSFetchedResultsControllerDelegate
extension ViewController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == NSFetchedResultsChangeType.insert {
print("Insert Object: \(newIndexPath)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertItems(at: [newIndexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print("Update Object: \(indexPath)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadItems(at: [indexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.move {
print("Move Object: \(indexPath) to \(newIndexPath)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.moveItem(at: indexPath!, to: newIndexPath!)
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print("Delete Object: \(indexPath)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteItems(at: [indexPath!])
}
})
)
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.insert {
print("Insert Section: \(sectionIndex)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print("Update Section: \(sectionIndex)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print("Delete Section: \(sectionIndex)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
}
})
)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView!.performBatchUpdates({ () -> Void in
for operation: BlockOperation in self.blockOperations {
operation.start()
}
}, completion: { (finished) -> Void in
self.blockOperations.removeAll(keepingCapacity: false)
})
}
}
We want to move and update an item from Normal section to Pinned section.
We execute
updatePinned(objectId, true)
Only the following is printed
Move Object: Optional([1, 12]) to Optional([0, 0])
We expect besides NSFetchedResultsChangeType.move, NSFetchedResultsChangeType.update should happen too. But, it doesn't. Only NSFetchedResultsChangeType.move is happen.
Workaround (This is a wrong approach! Do NOT apply this!)
I try to reloadData after the end of animation.
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView!.performBatchUpdates({ () -> Void in
for operation: BlockOperation in self.blockOperations {
operation.start()
}
}, completion: { (finished) -> Void in
self.blockOperations.removeAll(keepingCapacity: false)
// Do not do this! As, it will cause NSFetchedResultsController malfuntion after some time.
// You will soon realize NSFetchedResultsController is wrongly placing a pinned
// note in normal section.
// Or even worst, it will issue didChange callback with wrong NSFetchedResultsChangeType value
self.collectionView.reloadData()
})
}
It looks like thing works fine at first sight. However, if you perform pin and unpin operation for several times, you will notice that NSFetchedResultsController is placing note in wrong section. It will place the pinned note in normal section, and a normal note in pinned section.
Or even worst, it will issue didChange callback with wrong NSFetchedResultsChangeType value
Demo
The following is the demo code to illustrate the mentioned problem.
https://github.com/yccheok/UICollectionView-02/tree/stackoverflow
As you can see, after move, update is not performed. We can observe
Blue pin icon is not drawn
Orange background color body text is not update
The update will only be performed, if we perform scrolling explicitly.
May I know, what is the correct way for me to change an item's section (So that there is move animation), and update item's content (So that cellForItemAt function will be called)?
(Crossporting my answer over at r/iOSProgramming so Stackoverflow users find it too)
NSFRC moves always imply an update. The only reason an object would “move” is if its value for the sort key changes, which means that object has been updated as well.
Edit:
I see, I think you've hit one of the common edge cases in UICollectionView reloading in tandem with NSFRCs. You shouldn't use reloadItems() here, regardless of how unintuitive that is. Instead, use cellForRow(at:) and create a method that updates the data for that cell. This is true for both UICollectionView and UITableView.
Check how everybody else does it:
JSQDataSourcesKit
CoreStore

Trying to reloadData() in viewWillAppear

I have a tabBarController and in one of the tabs I can select whatever document to be in my Favourites tab.
So when I go to the Favourites tab, the favourite documents should appear.
I call the reloading after fetching from CoreData the favourite documents:
override func viewWillAppear(_ animated: Bool) {
languageSelected = UserDefaults().string(forKey: "language")!
self.title = "favourites".localized(lang: languageSelected)
// Sets the search Bar in the navigationBar.
let search = UISearchController(searchResultsController: nil)
search.searchResultsUpdater = self
search.obscuresBackgroundDuringPresentation = false
search.searchBar.placeholder = "searchDocuments".localized(lang: languageSelected)
navigationItem.searchController = search
navigationItem.hidesSearchBarWhenScrolling = false
// Request the documents and reload the tableView.
fetchDocuments()
self.tableView.reloadData()
}
The fetchDocuments() function is as follows:
func fetchDocuments() {
print("fetchDocuments")
// We make the request to the context to get the documents we want.
do {
documentArray = try context.fetchMOs(requestedEntity, sortBy: requestedSortBy, predicate: requestedPredicate)
***print(documentArray) // To test it works ok.***
// Arrange the documentArray per year using the variable documentArrayPerSection.
let formatter = DateFormatter()
formatter.dateFormat = "yyyy"
for yearSection in IndexSections.sharedInstance.allSections[0].sections {
let documentsFilteredPerYear = documentArray.filter { document -> Bool in
return yearSection == formatter.string(from: document.date!)
}
documentArrayPerSection.append(documentsFilteredPerYear)
}
} catch {
print("Error fetching data from context \(error)")
}
}
From the statement print(documentArray) I see that the function updates the content. However there is no reload of documents in the tableView.
If I close the app and open it again, then it updates.
Don't know what am I doing wrong!!!
The problem is that you're always appending to documentArrayPerSection but never clearing it so I imagine the array was always getting bigger but only the start of the array which the data source of the tableView was requesting was being used. Been there myself a few times.
I assume that reloadData() is called before all data processing is done. To fix this you will have to call completion handler when fetching is done and only then update tableView.
func fetchDocuments(_ completion: #escaping () -> Void) {
do {
// Execute all the usual fetching logic
...
completion()
}
catch { ... }
}
And call it like that:
fetchDocuments() {
self.tableView.reloadData()
}
Good luck :)

How can I unit test that a block of code is run on DispatchQueue.main

Caveat - I read the few questions about testing threads but may have missed the answer so if the answer is there and I missed it, please point me in the right direction.
I want to test that a tableView call to reloadData is executed on the main queue.
This should code should result in a passing test:
var cats = [Cat]() {
didSet {
DispatchQueue.main.async { [weak self] in
tableView.reloadData()
}
}
}
This code should result in a failing test:
var cats = [Cat]() {
didSet {
tableView.reloadData()
}
}
What should the test look like?
Note to the testing haters: I know this is an easy thing to catch when you run the app but it's also an easy thing to miss when you're refactoring and adding layers of abstraction and multiple network calls and want to update the UI with some data but not other data etc etc... so please don't just answer with "Updates to UI go on the main thread" I know that already. Thanks!
Use dispatch_queue_set_specific function in order to associate a key-value pair with the main queue
Then use dispatch_queue_get_specific to check for the presence of key & value:
fileprivate let mainQueueKey = UnsafeMutablePointer<Void>.alloc(1)
fileprivate let mainQueueValue = UnsafeMutablePointer<Void>.alloc(1)
/* Associate a key-value pair with the Main Queue */
dispatch_queue_set_specific(
dispatch_get_main_queue(),
mainQueueKey,
mainQueueValue,
nil
)
func isMainQueue() -> Bool {
/* Checking for presence of key-value on current queue */
return (dispatch_get_specific(mainQueueKey) == mainQueueValue)
}
I wound up taking the more convoluted approach of adding an associated Bool value to UITableView, then swizzling UITableView to redirect reloadData()
fileprivate let reloadDataCalledOnMainThreadString = NSUUID().uuidString.cString(using: .utf8)!
fileprivate let reloadDataCalledOnMainThreadKey = UnsafeRawPointer(reloadDataCalledOnMainThreadString)
extension UITableView {
var reloadDataCalledOnMainThread: Bool? {
get {
let storedValue = objc_getAssociatedObject(self, reloadDataCalledOnMainThreadKey)
return storedValue as? Bool
}
set {
objc_setAssociatedObject(self, reloadDataCalledOnMainThreadKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
dynamic func _spyReloadData() {
reloadDataCalledOnMainThread = Thread.isMainThread
_spyReloadData()
}
//Then swizzle that with reloadData()
}
Then in the test I updated the cats on the background thread so I could check if they were reloaded on the main thread.
func testReloadDataIsCalledWhenCatsAreUpdated() {
// Checks for presence of another associated property that's set in the swizzled reloadData method
let reloadedPredicate = NSPredicate { [controller] _,_ in
controller.tableView.reloadDataWasCalled
}
expectation(for: reloadedPredicate, evaluatedWith: [:], handler: nil)
// Appends on the background queue to simulate an asynchronous call
DispatchQueue.global(qos: .background).async { [weak controller] in
let cat = Cat(name: "Test", identifier: 1)
controller?.cats.append(cat)
}
// 2 seconds seems excessive but NSPredicates only evaluate once per second
waitForExpectations(timeout: 2, handler: nil)
XCTAssert(controller.tableView.reloadDataCalledOnMainThread!,
"Reload data should be called on the main thread when cats are updated on a background thread")
}
Here is an updated version of the answer provided by Oleh Zayats that I am using in some tests of Combine publishers.
extension DispatchQueue {
func setAsExpectedQueue(isExpected: Bool = true) {
guard isExpected else {
setSpecific(key: .isExpectedQueueKey, value: nil)
return
}
setSpecific(key: .isExpectedQueueKey, value: true)
}
static func isExpectedQueue() -> Bool {
guard let isExpectedQueue = DispatchQueue.getSpecific(key: .isExpectedQueueKey) else {
return false
}
return isExpectedQueue
}
}
extension DispatchSpecificKey where T == Bool {
static let isExpectedQueueKey = DispatchSpecificKey<Bool>()
}
This is an example test using Dispatch and Combine to verify it is working as expected (you can see it fail if you remove the receive(on:) operator).:
final class IsExpectedQueueTests: XCTestCase {
func testIsExpectedQueue() {
DispatchQueue.main.setAsExpectedQueue()
let valueExpectation = expectation(description: "The value was received on the expected queue")
let completionExpectation = expectation(description: "The publisher completed on the expected queue")
defer {
waitForExpectations(timeout: 1)
DispatchQueue.main.setAsExpectedQueue(isExpected: false)
}
DispatchQueue.global().sync {
Just(())
.receive(on: DispatchQueue.main)
.sink { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
completionExpectation.fulfill()
} receiveValue: { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
valueExpectation.fulfill()
}.store(in: &cancellables)
}
}
override func tearDown() {
cancellables.removeAll()
super.tearDown()
}
var cancellables = Set<AnyCancellable>()
}

tableview's Datasource array's value is automatically changes to 0

var noteList:NSMutableArray = NSMutableArray() // declaration of `noteObjects`
query.findObjectsInBackgroundWithBlock { ( objects, error) -> Void in
if (error == nil) {
let temp: NSArray = objects as NSArray!
self.noteList = temp.mutableCopy() as! NSMutableArray
self.tableView.reloadData()
}else {
print(error!.userInfo)
}
}
//populating noteObjects
i have a tableView who's datasource is an Array 'noteObjects' its type is NSMutableArray , so the problem is that every time i open my tableView my 'noteObjects' array's Value is 0 but then it automatically changes to desired value , how can i say this ? i did this in different stages of my tableViewController -
i printed the noteObjects.count in ViewDidload
override func viewDidLoad() {
super.viewDidLoad()
print("\(noteObjects.count) viewDidlaod3") }
output :
0 viewDidlaod3
inside cellForRowAtindexPath i printed this
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("\(noteObjects.count) in cellForAtIndexPath")
return cell }
output :
18 in cellForAtIndexPath
and if i move to another View then again open this tableView it keeps giving the sae result , (note object.count = 0 at first)
i want to use that noteObjects.count so that i can confirm that if the tableView's datasource is empty so i can show a message , but when am using this then its always showing that my tableview is empty because at first noteObjects.count is 0
if details provided above is not enough then please let me know i'll fix it
The api call 'findeObjectsInBackground' is asynchronous meaning the closure is executed later in time when result is obtained in a different thread. So getting back to the main thread and reloading the table view when data is ready will solve the problem. You can read more about these type of closures as they are very common in iOS.
query.findObjectsInBackgroundWithBlock { ( objects, error) -> Void in
if (error == nil) {
let temp: NSArray = objects as NSArray!
dispatch_async(dispatch_get_main_queue(), {
self.noteList = temp.mutableCopy() as! NSMutableArray
self.tableView.reloadData()
})
}
else {
print(error!.userInfo)
}
}
use a if statement after the synchronisation method
if noteObjects.count < 1 {
let Alert = UIAlertView(title: "No Data to Display", message: "ther's nothing bro you should pull to refresh", delegate: self, cancelButtonTitle: "OKAY Got IT")
Alert.show()
}
just moved my code from viewDidLoad to viewDidAppear and it worked For me
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.fetchAllObjectsFromLocalDataStore()
print("\(noteObjects.count) view appears")
if noteObjects.count < 1 {
let Alert = UIAlertView(title: "No Data to Display", message: "ther's nothing bro you should pull to refresh", delegate: self, cancelButtonTitle: "OKAY Got IT")
Alert.show()
}
}
now noteObject.count is getting check after updating its Value

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!

Resources