Update tableView row from AppDelegate Swift 4 - ios

[![enter image description here][1]][1]
Hello. I have a tableview like in the picture above and I'm receiving some silent push notifications. Depending on them I need to reload a specific cell from the tableView. Since I'm getting the notification in the AppDelegate and there at the moment I'm reloading the whole tableView...but personally I don't find this the best solution since I only need to update a specific row.
Any hints please how can I update just a specific cell from appDelegate?
if userInfo["notification_type"] as? String == "update_conversation" {
if let rootVC = (self.window?.rootViewController as? UINavigationController)?.visibleViewController {
if rootVC is VoiceViewController {
let chatRoom = rootVC as! VoiceViewController
chatRoom.getConversations()
// the get Conversations method makes a call to api to get some data then I reload the whole tableView
}
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data{
self.conversations = data
self.onlineRecent = self.conversations
GlobalMainQueue.async {
self.mainTableView.reloadData()
}
}
}
}
}
This is my getConversation method which is used in VoiceViewController to populate my tableview

Have the app delegate broadcast an app-specific notification center notification (on the main thread). Have the view controller that contains your table view listen for that notification and update the cell in question as needed. That way you don't contaminate your app delegate. The app delegate should only deal with system level app stuff, not business logic.

You could get your row’s cell using self.mainTableView.cellForRow(at:IndexPath(…), and update it directly.
Or, I’ve found you save a load of time and your view controllers end up more reliable using ALTableViewHelper [commercial - available on Framework Central here]. It’s free to try.
The helper does the most of the work - you describe how the data connects to the UITableView. I’ve put together an example (on GitHub here), which I hope is something like what you’re trying to do.
import ALTableViewHelper
class VoiceViewController {
// #objc for the helper to see the var’s, and dynamic so it sees any changes to them
#obj dynamic var conversations: Any?
#obj dynamic var onlineRequest: Any?
func viewDidLoad() {
super.viewDidLoad()
tableView.setHelperString(“””
section
headertext "Conversation Status"
body
Conversation
$.viewWithTag(1).text <~ conversations[conversations.count-1]["title"]
$.viewWithTag(2).text <~ "At \\(dateFormat.stringFromDate(conversations[conversations.count-1]["update"]))"
UpdateButton
$.viewWithTag(1).isAnimating <~ FakeConversationGetter.singleton.busy
“””, context:self)
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data {
// change the data on the main thread as this causes the UI changes
GlobalMainQueue.async {
self.conversations = data
self.onlineRequest = self.conversations
}
}
}
}
}

Related

image and label in interface builder overlap my data in the TableView cell

I am a beginner in iOS development, and I want to make an instagram clone app, and I have a problem when making the news feed of the instagram clone app.
So I am using Firebase to store the image and the database. after posting the image (uploading the data to Firebase), I want to populate the table view using the uploaded data from my firebase.
But when I run the app, the dummy image and label from my storyboard overlaps the downloaded data that I put in the table view. the data that I download will eventually show after I scroll down.
Here is the gif when I run the app:
http://g.recordit.co/iGIybD9Pur.gif
There are 3 users that show in the .gif
username (the dummy from the storyboard)
JokowiRI
MegawatiRI
After asynchronously downloading the image from Firebase (after the loading indicator is dismissed), I expect MegawatiRI will show on the top of the table, but the dummy will show up first, but after I scroll down and back to the top, MegawatiRI will eventually shows up.
I believe that MegawatiRI is successfully downloaded, but I don't know why the dummy image seems overlaping the actual data. I don't want the dummy to show when my app running.
Here is the screenshot of the prototype cell:
And here is the simplified codes of the table view controller:
class NewsFeedTableViewController: UITableViewController {
var currentUser : User!
var media = [Media]()
override func viewDidLoad() {
super.viewDidLoad()
tabBarController?.delegate = self
// to set the dynamic height of table view
tableView.estimatedRowHeight = StoryBoard.mediaCellDefaultHeight
tableView.rowHeight = UITableViewAutomaticDimension
// to erase the separator in the table view
tableView.separatorColor = UIColor.clear
}
override func viewWillAppear(_ animated: Bool) {
// check wheter the user has already logged in or not
Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
RealTimeDatabaseReference.users(uid: user.uid).reference().observeSingleEvent(of: .value, with: { (snapshot) in
if let userDict = snapshot.value as? [String:Any] {
self.currentUser = User(dictionary: userDict)
}
})
} else {
// user not logged in
self.performSegue(withIdentifier: StoryBoard.showWelcomeScreen, sender: nil)
}
}
tableView.reloadData()
fetchMedia()
}
func fetchMedia() {
SVProgressHUD.show()
Media.observeNewMedia { (mediaData) in
if !self.media.contains(mediaData) {
self.media.insert(mediaData, at: 0)
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: StoryBoard.mediaCell, for: indexPath) as! MediaTableViewCell
cell.currentUser = currentUser
cell.media = media[indexPath.section]
// to remove table view highlight style
cell.selectionStyle = .none
return cell
}
}
And here is the simplified code of the table view cell:
class MediaTableViewCell: UITableViewCell {
var currentUser: User!
var media: Media! {
didSet {
if currentUser != nil {
updateUI()
}
}
}
var cache = SAMCache.shared()
func updateUI () {
// check, if the image has already been downloaded and cached then just used the image, otherwise download from firebase storage
self.mediaImageView.image = nil
let cacheKey = "\(self.media.mediaUID))-postImage"
if let image = cache?.object(forKey: cacheKey) as? UIImage {
mediaImageView.image = image
} else {
media.downloadMediaImage { [weak self] (image, error) in
if error != nil {
print(error!)
}
if let image = image {
self?.mediaImageView.image = image
self?.cache?.setObject(image, forKey: cacheKey)
}
}
}
So what makes the dummy image overlaps my downloaded data?
Answer
The dummy images appear because your table view controller starts rendering cells before your current user is properly set on the tableViewController.
Thus, on the first call to cellForRowAtIndexPath, you probably have a nil currentUser in your controller, which gets passed to the cell. Hence the didSet property observer in your cell class does not call updateUI():
didSet {
if currentUser != nil {
updateUI()
}
}
Later, you reload the data and the current user has now been set, so things start to work as expected.
This line from your updateUI() should hide your dummy image. However, updateUI is not always being called as explained above:
self.mediaImageView.image = nil
I don't really see a reason why updateUI needs the current user to be not nil. So you could just eliminate the nil test in your didSet observer, and always call updateUI:
var media: Media! {
didSet {
updateUI()
}
Alternatively, you could rearrange your table view controller to actually wait for the current user to be set before loading the data source. The login-related code in your viewWillAppear has nested completion handers to set the current user. Those are likely executed asynchronously .. so you either have to wait for them to finish or deal with current user being nil.
Auth.auth etc {
// completes asynchronously, setting currentUser
}
// Unless you do something to wait, the rest starts IMMEDIATELY
// currentUser is not set yet
tableView.reloadData()
fetchMedia()
Other Notes
(1) I think it would be good form to reload the cell (using reloadRows) when the image downloads and has been inserted into your shared cache. You can refer to the answers in this question to see how an asynch task initiated from a cell can contact the tableViewController using NotificationCenter or delegation.
(2) I suspect that your image download tasks currently are running in the main thread, which is probably not what you intended. When you fix that, you will need to switch back to the main thread to either update the image (as you are doing now) or reload the row (as I recommend above).
Update your UI in main thread.
if let image = image {
DispatchQueue.main.async {
self?.mediaImageView.image = image
}
self?.cache?.setObject(image, forKey: cacheKey)
}

Displaying download progress in reusable cells

I am trying to display download progress in my collectionview cells. Im currently using the parse progressblock which has an instance of the cell and updates the progress bar.
}, progressBlock: { (percent) in
self.mainQueue.addOperation {
// set the downloadProgess var to update from cellForItemAt
// downloadProgress = (Float(percent) / Float(100))
if let downloadingCell = self.collectionView.cellForItem(at: self.indexPath) as? InnerCollectionCell {
downloadingCell.progressBar.isHidden = false
downloadingCell.contentView.bringSubview(toFront: downloadingCell.progressBar)
downloadingCell.progressBar.setProgress(Float(percent) / Float(100), animated: true)
downloadingCell.setNeedsDisplay()
downloadingCell.setNeedsLayout()
downloadingCell.isUserInteractionEnabled = false
downloadingCell.spinner.isHidden = true
}
}
})
So this works fine, the problem i now have is if i leave this view controller then come back to see how the downloads are going the instance of the cell has been reused and none of the desired UI elements are visible but the progress is still ticking away in the background.
The only place i can think to re-display the UI elements is in cellForItemAt. The problem then is that the progress doesn't update, it just shows the value at the time the cell was reloaded.
How can i go about reusing the instance of the cell that the progress block is using or cleanly displaying ui elements that continue to update?
Presuming that you're dismissing the old view controller with the collection view and presenting a new one, there are two problems here:
You're then trying to update cells in the collection view in the previous view controller; and
You're keeping a strong reference to the old view controller that was dismissed.
If this is the case, the goal is to decouple the progress updates from any particular view controller, collection view, or cell. You also probably want to decouple the item/row number, too, in case you insert/remove any cells at any time. The best way to handle this is notifications:
Define a few constants used when defining the notifications:
private let notificationName = Notification.Name(rawValue: "com.domain.app.downloadProgress")
private let notificationIdentifierKey = "com.domain.app.download.identifier"
private let notificationPercentKey = "com.domain.app.download.percent"
Have your progressBlock post a notification rather than trying to update the UI directly:
let percent: Float = ...
let userInfo: [AnyHashable: Any] = [
notificationIdentifierKey: identifier,
notificationPercentKey: percent
]
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: userInfo)
Please note that there are no reference to self here, which keeps the progress block from hanging on to your view controller.
Define some function that you can use to identify which IndexPath corresponds to the identifier for your download. In my simple example, I'm just going to have an array of download identifiers and use that:
var downloadIdentifiers = [String]()
private func indexPath(for identifier: String) -> IndexPath? {
if let item = downloadIdentifiers.index(of: identifier) {
return IndexPath(item: item, section: 0)
} else {
return nil
}
}
You'd probably have a download identifier as a property of some Download model object, and use that instead, but hopefully it illustrates the idea: Just have some way to identify the appropriate IndexPath for a given download. (By the way, this decoupling the IndexPath from what it was when you first created the download is important, in case you insert/remove any items from your collection view at any point.)
Now, you may ask what should you use for the identifier. You might use the URL's absoluteString. You might use some other unique identifier. But I'd discourage you from relying solely on item/row numbers, because those can change (maybe not now, but perhaps later as you make the app more sophisticated, you might be inserting removing items).
Have your collection view's view controller add itself as an observer of this notification, updating the appropriate progress view:
private var observer: NSObjectProtocol!
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(forName: notificationName, object: nil, queue: .main) { [weak self] notification in
if let identifier = notification.userInfo?[notificationIdentifierKey] as? String,
let percent = notification.userInfo?[notificationPercentKey] as? Float,
let indexPath = self?.indexPath(for: identifier),
let cell = self?.collectionView?.cellForItem(at: indexPath) as? InnerCollectionCell {
cell.progressView.setProgress(percent, animated: true)
}
}
...
}
deinit {
NotificationCenter.default.removeObserver(observer)
}
Please note the [weak self] capture list, to make sure the notification observer doesn't cause a strong reference cycle with the view controller.

Who came first? IBAction or ViewDidLoad

I have a Button on First VC which is directed to two active states.
1) SecondVC
override func viewDidLoad() {
super.viewDidLoad()
subjectPickerView.dataSource = self
subjectPickerView.delegate = self
SwiftyRequest()
// Used the text from the First View Controller to set the label
}
func SwiftyRequest(){
print("SecondViewController METHOD BEGINS")
let jsonobj = UserDefaults.standard.object(forKey: "PostData")
let json = JSON(jsonobj as Any)
for i in 0 ..< json.count{
let arrayValue = json[i]["name"].stringValue
print(arrayValue)
self.subjects.append(arrayValue)
self.subjectPickerView.reloadAllComponents()
}
print(self.subjects)
}
2) IBAction of FirstVC
#IBAction func buttonPressed(_ sender: Any) {
Alamofire.request("http://localhost/AIT/attempt3.php",method: .post, parameters: ["something": semValue, "branch" : streamValue])
.responseJSON { response in
print(response.result)
if let JSON1 = response.result.value {
print("Did receive JSON data: \(JSON1)")
// JSONData.someData = JSON1 as AnyObject?
UserDefaults.standard.set(JSON1, forKey: "PostData")
UserDefaults.standard.synchronize()
}
else {
print("JSON data is nil.")
}
}
}
NOW, Whenever i pressed the button it calls the viewDidLoad of SecondVC before IBAction of FirstVC which is a bit problematic for my app! How can i decide the priority between these two function.
You have to think about what you want to happen. Clearly the Alamofire call is going to take some time. What do you want to do with the 2nd VC while that time elapses? What do you want to do if the call does not return at all?
This is a common problem when dependent on external resources. How do you manage the UI? Do you present the UI in a partial state? Do you put a popover saying something like "loading". Or do you wait for the resource to complete before presenting the 2nd VC at all?
We cannot make that decision for you, since it depends on your requirement. There are ways to implement each one, though. If the resource usually responds quickly you could show the VC in a partial state and then populate it on some kind of callback. Typically call backs are either (1) blocks (2) delegate methods or (3) notifications. There is also (less commonly) (4) KVO. You should probably research the pros and cons of each.

UITableView not updating when switching between tabs

Preface: I've tried adding tableView.reloadData() to viewWillAppear (...and viewDidLoad, viewDidAppear, etc.) of the UITableViewController that's not updating. I threw in setNeedsDisplay for S's & G's, too.
I have a UITabBarController with 3 tabs on it. Each tab is a TableViewController is backed by Core Data and is populated with NSManagedObjects from one NSManagedObjectContext.
In TableViewController1 I make changes to the cells, the tableView reloads properly and reflects the changes. If I click the tab for TableViewController2, the changes made on TVC1 aren't reflected.
The changes made on TVC1 are persisting between launches, as I see them on TVC2 when I close the app and relaunch it.
What am I missing? Any ideas would be greatly appreciated.
Update
Here's the code in question:
func markFavorite(sender: AnyObject) {
// Store the sender in case you need it later. Might not need this.
clickedFavoriteButton = sender as! UIButton
if resultsSearchController.active {
let indexPath = sender.tag
let selectedSound = self.filteredSounds[indexPath]
print("markFavorite's sender tag is \(indexPath)")
if selectedSound.favorite == 1 {
selectedSound.favorite = 0
} else {
selectedSound.favorite = 1
}
saveManagedObjectContext()
} else {
let indexPath = sender.tag
let selectedSound = self.unfilteredSounds[indexPath]
print("markFavorite's sender tag is \(indexPath)")
if selectedSound.favorite == 1 {
selectedSound.favorite = 0
} else {
selectedSound.favorite = 1
}
saveManagedObjectContext()
}
}
func saveManagedObjectContext() {
if managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
} catch {
// catch error here
}
}
self.tableView.reloadData()
}
You should always use a NSFetchedResultsController to display Core Data items in a table view. It comes with delegate methods that update your table as the underlying data changes (even before saving).
To get started, examine the Xcode template (Master-Detail) implementation. Once you get the hang of it you will love it. Everything works pretty much out of the box.
You may have to trigger context.save() manually because core-data isn't saving the data right away.
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
runOnMain() {
do {
try! self.context.save()
} catch let error as NSError {
//Handle any upcoming errors here.
}
}
Its important to run the method on the main thread otherwise you will get an error.
this method should do the job:
func runOnMain(block: dispatch_block_t) {
if NSThread.isMainThread() {
block()
}else{
dispatch_sync(dispatch_get_main_queue(), block)
}
}
Please let me know if this approach worked for you.
You should not try to reload data at any point in view controller lifecycle. Instead create delegates for each tab bar controller, set them properly and call delegate methods only when something really change in your data source. If you are not familiar with delegation you can learn more about it here

How to reload UIViewController data from App Delegate

I have some methods that I am calling from the appDelegate to sync changes with persistent storage and iCloud.
The methods and the appDelegate work fine, my app syncs changes fine; however when the app is mergingChanges and persistentStoreDidChange I am trying to refresh the view controller data and change the view controller title to syncing.
I have tried changing the UIViewController title text and it does not change when merging or persistentStoreWillChange, also when using the reloadData() method for the view controller collection the app crashes with an unexpected nil when unwrapping an optional value.
The thing is the project has many tableview & colectionview all within a UITabController so I really need a refresh the data of the view controller in the window not just one specific view. Does anybody know how to refresh the viewcontroller data from the appDelegate ?
func mergeChanges(notification: NSNotification) {
NSLog("mergeChanges notif:\(notification)")
if let moc = managedObjectContext {
moc.performBlock {
moc.mergeChangesFromContextDidSaveNotification(notification)
self.postRefetchDatabaseNotification()
}
}
let vc = CollectionViewController()
let view = self.window?.rootViewController
vc.title = "Syncing"
view?.title = "Syncing"
}
func persistentStoreDidImportUbiquitousContentChanges(notification: NSNotification) {
self.mergeChanges(notification);
}
func storesWillChange(notification: NSNotification) {
NSLog("storesWillChange notif:\(notification)");
if let moc = self.managedObjectContext {
moc.performBlockAndWait {
var error: NSError? = nil;
if moc.hasChanges && !moc.save(&error) {
NSLog("Save error: \(error)");
} else {
// drop any managed objects
}
moc.reset();
}
let vc = CollectionViewController()
vc.title = "Syncing"
// reset UI to be prepared for a totally different
// don't load any new data yet.
}
}
func storesDidChange(notification: NSNotification) {
// here is when you can refresh your UI and
// load new data from the new store
let vc = CollectionViewController()
// vc.collectionView.reloadData()
NSLog("storesDidChange posting notif");
self.postRefetchDatabaseNotification();
}
For above functionality you can use NSNotification Fire that notification to multiple classes when you want to update .

Resources