Trying to reloadData() in viewWillAppear - ios

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

Related

How do refresh my UITableView after reading data from FirebaseFirestore with a SnapShotListener?

UPDATE at the bottom.
I have followed the UIKit section of this Apple iOS Dev Tutorial, up to and including the Saving New Reminders section. The tutorials provide full code for download at the beginning of each section.
But, I want to get FirebaseFirestore involved. I have some other Firestore projects that work, but I always thought that I was doing something not quite right, so I'm always looking for better examples to learn from.
This is how I found Peter Friese's 3-part YT series, "Build a To-Do list with Swift UI and Firebase". While I'm not using SwiftUI, I figured that the Firestore code should probably work with just a few changes, as he creates a Repository whose sole function is to interface between app and Firestore. No UI involved. So, following his example, I added a ReminderRepository.
It doesn't work, but I'm so close. The UITableView looks empty but I know that the records are being loaded.
Stepping through in the debugger, I see that the first time the numberOfRowsInSection is called, the data hasn't been loaded from the Firestore, so it returns 0. But, eventually the code does load the data. I can see each Reminder as it's being mapped and at the end, all documents are loaded into the reminderRepository.reminders property.
But I can't figure out how to get the loadData() to make the table reload later.
ReminderRepository.swift
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
init() {
loadData()
}
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
}
The only difference from the Apple code is that in the ListDataSource.swift file, I added:
var remindersRepository: ReminderRepository
override init() {
remindersRepository = ReminderRepository()
}
and all reminders references in that file have been changed to
remindersRepository.reminders.
Do I need to provide a callback for the init()? How? I'm still a little iffy on the matter.
UPDATE: Not a full credit solution, but getting closer.
I added two lines to ReminderListViewController.viewDidLoad() as well as the referenced function:
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshTournaments(_:)), for: .valueChanged)
#objc
private func refreshTournaments(_ sender: Any) {
tableView.reloadData()
refreshControl?.endRefreshing()
}
Now, when staring at the initial blank table, I pull down from the top and it refreshes. Now, how can I make it do that automatically?
Firstly create some ReminderRepositoryDelegate protocol, that will handle communication between you Controller part (in your case ReminderListDataSource ) and your model part (in your case ReminderRepository ). Then load data by delegating controller after reminder is set. here are some steps:
creating delegate protocol.
protocol ReminderRepositoryDelegate: AnyObject {
func reloadYourData()
}
Conform ReminderListDataSource to delegate protocol:
class ReminderListDataSource: UITableViewDataSource, ReminderRepositoryDelegate {
func reloadYourData() {
self.tableView.reloadData()
}
}
Add delegate weak variable to ReminderRepository that will weakly hold your controller.
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
weak var delegate: ReminderRepositoryDelegate?
init() {
loadData()
}
}
set ReminderListDataSource as a delegate when creating ReminderRepository
override init() {
remindersRepository = ReminderRepository()
remindersRepository.delegate = self
}
load data after reminder is set
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
delegate?.reloadYourData()
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
Please try changing var reminders = [Reminder]() to
var reminders : [Reminder] = []{
didSet {
self.tableview.reloadData()
}
}

Create new event in CalendarKit

In my app, I'm trying to add ability to create a new event with CalendarKit when I press an empty space, but I can't find how to do that.
I know it's possible because you can do it in the demo app:
I've tried adding this to my app's code:
override func create(event: EventDescriptor, animated: Bool = false) {
self.events.append(event) // self.events is my events data source
}
But it didn't worked, in fact, it doesn't even get called when I long press an empty space.
I also tried to look in the source code, but I found nothing. How can I do that? thanks in advance
override func dayView(dayView: DayView, didLongPressTimelineAt date: Date) {
let newEvent = Event()
newEvent.startDate = date
newEvent.endDate = date.addingTimeInterval(3600)
// Customize your event...
newEvent.text = randomName() // A function that generates a new random name that haven't been used before.
self.create(event: newEvent)
}
override func create(event: EventDescriptor, animated: Bool = false) {
super.create(event: event, animated: animated)
self.events.append(event)
}
override func dayView(dayView: DayView, didUpdate event: EventDescriptor) {
for (index, eventFromList) in events.enumerated() {
if eventFromList.text == event.text {
events[index] = event
}
}
self.endEventEditing()
self.reloadData()
}
Please make sure that every Event have it's own unique name, otherwise it won't work

Listing scheduled notifications in a table view controller

I am trying to list all the notifications a user has created and scheduled in my app, similar to that of the list of alarms in the 'Clock' app from apple. However, each time I get the array of notifications and attempt to display them, they are not being correctly displayed all the time.
Each notification is repeated each day at the same time, so I am using UNUserNotificationCenter.current().getPendingNotificationRequests to get an array of the notifications. With this array of notifications, I iterate over each notification, create a new custom 'Reminder' object and add it to my array of 'Reminders' which I use when I display the notifications in the table view controller.
I do this every time using the viewWillAppear function.
Here is the code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
generateReminders()
tableView.reloadData()
}
func generateReminders()
{
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests { (notifications) in
for item in notifications {
if let trigger = item.trigger as? UNCalendarNotificationTrigger,
let triggerDate = trigger.nextTriggerDate() {
var withSound = true
if(item.content.sound != UNNotificationSound.default)
{
withSound = false
}
self.reminders.append(Reminder(identifier: item.identifier, time: triggerDate, message: item.content.body, withSound: withSound, isAPendingNotification: true))
}
}
self.remindersCount = notifications.count
}
}
When the cells are about to be displayed in the table view controller, I use the 'Reminder' array to customise each cell to display the information of the notification. This is all done in the 'cellForRowAt' function, code below.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Reminder", for: indexPath)
var text = ""
var detailText = ""
if(indexPath.row < remindersCount) {
let reminder = reminders[indexPath.row]
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
text = formatter.string(from: reminder.Time)
detailText = reminder.Message
}
cell.textLabel?.text = text
cell.detailTextLabel?.text = detailText
return cell
}
When a user selects another tab to view, I reset the 'reminders' object to be empty so that when they return to this tab, an updated array of notifications is displayed, code below.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
reminders = [Reminder]()
remindersCount = 0
tableView.setNeedsDisplay()
}
The issue I am facing is this is extremely inconsistent, sometimes all the notifications are displayed, sometimes only some are displayed and other times none of them are displayed. However, each time I print out the count of the number of notifications in the UNUserNotificationCenter.current().getPendingNotificationRequests method it is always the correct number. Furthermore, whenever I click on a cell that should contain information about a notification, the information is there, it is just not being displayed.
Here is a short video of these issues.
https://imgur.com/1RVerZD
I am unsure how to fix this, I have attempted to run the code on the main queue and on the global queue with the quality of service set to '.userInteractive' as shown below, but, still no dice.
let center = UNUserNotificationCenter.current()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
center.getPendingNotificationRequests { (notifications) in
for item in notifications {
if let trigger = item.trigger as? UNCalendarNotificationTrigger,
let triggerDate = trigger.nextTriggerDate() {
var withSound = true
if(item.content.sound != UNNotificationSound.default)
{
withSound = false
}
self.reminders.append(Reminder(identifier: item.identifier, time: triggerDate, message: item.content.body, withSound: withSound, isAPendingNotification: true))
}
}
self.remindersCount = notifications.count
}
}
A small application of this issue occurring can be downloaded from this Github repository.
https://github.com/AlexMarchant98/LitstingNotificationsIssue
There are some issues in your code of tableview as listed below.
You used static cells in your tableview which is not the proper way if you have dynamic rows.
Suggestion : Use Dynamic Prototype Cell for your tableview.
remindersCount not required at all as its already there in your array count.
Suggestion : Use self.reminders.count for array count.
unwindToRemindersTableViewController() method have generateReminders() call which is not required as viewWillAppear() will call when view dismiss.
Suggestion : Check ViewController life cycle you will get proper idea how to reload data.
I have updated some code in your sample project.
Please find updated code here.
Github updated demo
Hope this will helps!
The problem with your code is the timing when tableView.reloadData() gets executed.
UNUserNotificationCenter.current().getPendingNotificationRequests() is a asynchronous call and therefore the reminders array gets filled after tableView.reloadData() was called.
Moving tableView.reloadData() to the end of the callback-block of getPendingNotificationRequests() should fix your issue. (And don't forget to trigger the reloadData() from the main thread)
func generateReminders()
{
let center = UNUserNotificationCenter.current()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
center.getPendingNotificationRequests { (notifications) in
for item in notifications {
if let trigger = item.trigger as? UNCalendarNotificationTrigger,
let triggerDate = trigger.nextTriggerDate() {
var withSound = true
if(item.content.sound != UNNotificationSound.default)
{
withSound = false
}
self.reminders.append(Reminder(identifier: item.identifier, time: triggerDate, message: item.content.body, withSound: withSound, isAPendingNotification: true))
}
}
self.remindersCount = notifications.count
DispatchQueue.main.async {
self.tableView.reloadData() // <---------
}
}
}
}

Refreshing table view with a UIRefreshControl in swift

I am working on an iOS app in which I have a UITableView which needs to be refreshed on command. I have added a UIRefreshControl object and hooked up a function to it so that whenever I drag down on the table view, my refresh code is supposed to take place. The code fragment is below:
#IBAction func refresh(sender: UIRefreshControl?) {
self.tableView.setContentOffset(CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl!.frame.size.height), animated: true)
sender?.beginRefreshing()
if (profileActivated) {
self.matches = []
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.defaultDynamoDBObjectMapper()
let queryExpression = AWSDynamoDBScanExpression()
queryExpression.limit = 100;
// retrieving everything
dynamoDBObjectMapper.scan(DDBMatch.self, expression: queryExpression).continueWithBlock({ (task:AWSTask!) -> AnyObject! in
if task.result != nil {
let paginatedOutput = task.result as! AWSDynamoDBPaginatedOutput
//adding all to the matches array
for item in paginatedOutput.items as! [DDBMatch] {
self.matches.append(item)
}
//code to filter and sort the matches array...
self.tableView.reloadData()
}
return nil
})
self.tableView.reloadData()
}
sender?.endRefreshing()
self.tableView.setContentOffset(CGPointMake(0, self.tableView.contentOffset.y - (0.5*self.refreshControl!.frame.size.height)), animated: true)
}
Unfortunately, this code does not quite work right. Whenever I drag down on the refresh control, my table populates but then immediately goes away, and then refreshes about 10-15 seconds later. I inserted some print statements, and the data is all there, it just does not appear for a long time, and I am having trouble making it appear as soon as it is retrieved. I am new to iOS programming so any help would be appreciated.
Thanks in advance

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

Resources