How to reload UIViewController data from App Delegate - ios

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 .

Related

Saving data in UITextView

I'm writing notes app for iOS and I want all data which user enter in notes will be automatically saved when user typing automatically. I'm using Core Data and now I save data on viewWillDisappear, but I want the data also be saved if user terminate the app or the app will be automatically terminated in the background.
I use this code:
import UIKit
import CoreData
class AddEditNotes: UIViewController, UITextViewDelegate {
#IBOutlet weak var textView: UITextView!
var note: Note!
var notebook: Notebook?
var userIsEditing = true
var context: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
context = appDelegate.persistentContainer.viewContext
if (userIsEditing == true) {
textView.text = note.text!
title = "Edit Note"
}
else {
textView.text = ""
}
}
override func viewWillDisappear(_ animated: Bool) {
if (userIsEditing == true) {
note.text = textView.text!
}
else {
self.note = Note(context: context)
note.setValue(Date(), forKey: "dateAdded")
note.text = textView.text!
note.notebook = self.notebook
}
do {
try context.save()
print("Note Saved!")
}
catch {
print("Error saving note in Edit Note screen")
}
}
}
I understand what I can use applicationWillTerminate for this, but how I can pass there the data user entered? This functionality is in default notes app from Apple. But how it can be released?
There are two subtasks to saving the data: updating the Core Data entity with the contents of the text view and saving the Core Data context.
To update the contents of the Core Data entity, add a function to the AddEditNotes class that saves the text view contents.
func saveTextViewContents() {
note.text = textView.text
// Add any other code you need to store the note.
}
Call this function either when the text view ends editing or the text changes. If you call this function when the text changes, the Core Data entity will always be up to date. You won't have to pass the data to the app delegate because the app delegate has the Core Data managed object context.
To save the Core Data context, add a second function to the AddEditNotes class that saves the context.
func save() {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.saveContext()
}
}
This function assumes you selected the Use Core Data checkbox when you created the project. If you did, the app delegate has a saveContext function that performs the Core Data save.
You can now replace the code you wrote in viewWillDisappear with the calls to the two functions to save the text view contents and save the context.
The last code to write is to go to your app delegate file and add the following line of code to the applicationDidEnterBackground and applicationWillTerminate functions:
self.saveContext()
By adding this code your data will save when someone quits your app.

Update tableView row from AppDelegate Swift 4

[![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
}
}
}
}
}

View through IBOutlet or manually created is nil after views hierarchy is loaded

I am creating a macOS app on Xcode 9.2 and i can't figure out why after the views hierarchy is created in viewDidLoad(), i can't use anymore the IBOutlet which is referencing the view in the StoryBoard.
I have found similar question, where they suggest to save the view that i want to update, as a variable, when i am in viewDidLoad() so i can use it later; if so what is the advantage of using an IBOutlet?
Can someone pls explain how to update in this case the content of the PDFView outside viewDidLoad() without getting nil?
The AppDelegate
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Properties
var filePath: URL?
var result: Int?
var fileObject: Data?
// MARK: - Outlets
#IBOutlet weak var openFileMenuItem: NSMenuItem!
// MARK: - App Life Cycle
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
// MARK: - My Functions
#IBAction func openFile(_ sender: NSMenuItem) {
//Get the window of the app
var window = NSApplication.shared.mainWindow!
// Create NSOpenPanel and setting properties
let panel = NSOpenPanel()
panel.title = "Select file"
panel.allowsMultipleSelection = false;
panel.canChooseDirectories = false;
panel.canCreateDirectories = false;
panel.canChooseFiles = true;
// Filtering file extension
let fileTypes: [String] = ["pdf"]
panel.allowedFileTypes = fileTypes
// Showing OpenPanel to the user for selecting the file
panel.beginSheetModal(for: window) {
(result) in
if result.rawValue == NSFileHandlingPanelOKButton {
// Getting url of the file
self.filePath = panel.urls[0]
print(self.filePath)
// Creating Data Object of the file for creating later the PDFDocument in the controller
do {
if let fileData = self.filePath {
self.fileObject = try Data.init(contentsOf: self.filePath!)
print("\(self.fileObject) ")
}
} catch let error as NSError {
print("Error with file Data Object: \(error)")
}
// Getting the mainViewController
let storyboard = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)
let mainController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "mainViewControllerID")) as! MainViewController
// Appdelegate call function of mainViewController to update the content of PDFView
mainController.showPdfDocument(fileData: self.fileObject!)
}
}
}
}
The MainViewController:
import Foundation
import Quartz
class MainViewController: NSViewController {
// MARK: - Property
var pdfdocument: PDFDocument?
// MARK: - Outlets
#IBOutlet var mainView: NSView!
#IBOutlet var pdfView: PDFView!
// MARK: - App Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
// MARK: - My Functions
func showPdfDocument(fileData: Data) {
print("appdelegate calling showPdfDocument on mainViewController " )
pdfdocument = PDFDocument(data: fileData)
pdfView.document = pdfdocument
}
}
The Error I Am Getting:
fatal error: unexpectedly found nil while unwrapping an Optional value
I solved the problem with the current code in the AppDelegate. Basically i am using the contentViewController to update the PDFView but i don't know if this is a good practice because i should update the PDFView with its own controller and not with the one of the window.
in the openFile function of the AppleDelegate:
let storyboard = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)
let mainViewController = NSApplication.shared.mainWindow?.contentViewController as! MainViewController
mainViewController.showPdfDocument(fileData: self.fileObject!)
Reading from the Apple documentation at this link (https://developer.apple.com/documentation/appkit/nswindow/1419615-contentviewcontroller) it says 'Directly assigning a contentView value clears out the root view controller.'. Should I programmatically assign my mainView to the contentView to automatically set my mainViewController as the initial controller?
I have also found that i can set my controller as the initial controller from IB as you can see form this image:
The ideal thing i want to do is to leave the contentViewController as the initial viewController (the default setting in IB) and then instantiate my mainViewController to update the pdfView. The problem is that with instantiation, the new viewController has everything to nil(IBOutlet, vars etc..). Is it the downcasting from contentViewController to mainViewController a good approach to achieve this task?

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

change ViewController with SegmentedControl without destroy it

I have a few controllers. for some reason, I'm using UISegmentedControl instead of tab bar.
each few controllers download data from the servers. my problem is, if I move to next view controller and go back to the previous view controllers, I need to redownload again.
how to change view controller with UISegmentedControl without destroy the previous controller, so I don't need to redownload again. each time I move to different viewcontroller
here's my code
class ContentViewController: UIViewController {
private let homeViewController: HomeViewController!
private let aboutViewController: AboutViewController!
private let liveTVViewController: LiveTVViewController!
private let programsViewController: ProgramsViewController!
private var currentViewController: UIViewController!
var userDeviceType: Int!
override func viewDidLoad() {
super.viewDidLoad()
let viewController = viewControllerForSegmentIndex(0)
self.addChildViewController(viewController)
viewController.view.frame = self.view.bounds
self.view.addSubview(viewController.view)
currentViewController = viewController
NSNotificationCenter.defaultCenter().addObserver(self, selector: "segmentChanged:", name: "SegmentChangedNotification", object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func segmentChanged(notification: NSNotification) {
let userInfo = notification.userInfo as [String: AnyObject]
let selectedIndex = userInfo["selectedIndex"] as Int
let viewController = viewControllerForSegmentIndex(selectedIndex)
self.addChildViewController(viewController)
self.transitionFromViewController(currentViewController,
toViewController: viewController,
duration: 0.0,
options: UIViewAnimationOptions.CurveEaseIn,
animations: { () -> Void in
self.currentViewController.view.removeFromSuperview()
viewController.view.frame = self.view.bounds
self.view.addSubview(viewController.view)
}) { (finished: Bool) -> Void in
viewController.didMoveToParentViewController(self)
self.currentViewController.removeFromParentViewController()
self.currentViewController = viewController
}
}
func viewControllerForSegmentIndex(index: Int) -> UIViewController {
var viewController: UIViewController
if index == 0 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
(viewController as HomeViewController).userDeviceType = userDeviceType
} else if index == 1 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("ProgramsPage") as ProgramsViewController
} else if index == 2 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("LiveTVPage") as LiveTVViewController
} else if index == 3 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("AboutPage") as AboutViewController
} else {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
}
return viewController
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
thank you very much and sorry for my bad English
You should adopt the MVC design pattern. This will allow you to store the data you need downloaded in the Model. Then, when a certain view controller is loaded, you simply ask the model if that data exists. If it does, you'll get it back. Otherwise, you can download it as normal.
To further explain:
The Model-View-Controller (MVC) design pattern assigns objects in an application one of three roles: model, view, or controller. The pattern defines not only the roles objects play in the application, it defines the way objects communicate with each other.
Model objects encapsulate the data specific to an application and define the logic and computation that manipulate and process that data. For example, a model object might represent a character in a game or a contact in an address book. A model object can have to-one and to-many relationships with other model objects, and so sometimes the model layer of an application effectively is one or more object graphs. Much of the data that is part of the persistent state of the application (whether that persistent state is stored in files or databases) should reside in the model objects after the data is loaded into the application. Because model objects represent knowledge and expertise related to a specific problem domain, they can be reused in similar problem domains. Ideally, a model object should have no explicit connection to the view objects that present its data and allow users to edit that data—it should not be concerned with user-interface and presentation issues.
User actions in the view layer that create or modify data are communicated through a controller object and result in the creation or updating of a model object. When a model object changes (for example, new data is received over a network connection), it notifies a controller object, which updates the appropriate view objects
The above quotes are from the link I mentioned in the first paragraph.
You are instantiate new view controllers in viewControllerForSegmentIndex(index: Int) by mistake.
What you should do to avoid instantiate each view controller every time you switch back to it is to modify the properties as:
private let homeViewController: HomeViewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
private let aboutViewController: AboutViewController = self.storyboard!.instantiateViewControllerWithIdentifier("AboutPage") as AboutViewController
private let liveTVViewController: LiveTVViewController = self.storyboard!.instantiateViewControllerWithIdentifier("LiveTVPage") as LiveTVViewController
private let programsViewController: ProgramsViewController = self.storyboard!.instantiateViewControllerWithIdentifier("ProgramsPage") as ProgramsViewController
And change viewControllerForSegmentIndex(index: Int) to:
func viewControllerForSegmentIndex(index: Int) -> UIViewController {
var viewController: UIViewController
switch index {
case 0:
return homeViewController
case 1:
return programsViewController
case 2:
return liveTVViewController
case 3:
return aboutViewController
default:
return homeViewController
}
}

Resources