Where in view lifecycle to update controller after modal UIViewController dismissed - swift

I have a UIViewController with a UILabel that needs to display either "lbs" or "kg". My app has a settings screen (another UIViewController) that is presented modally over the first view controller and the user can select either of the two units and save their preference. If the units are changed and the modal settings screen is dismissed, I of course want the label on the first view controller to be updated with the new units value (but without refreshing the whole view). I thought I knew how to make it work, but evidently I don't.
On my modal settings screen, I have a UISegmentedControl to allow the user to select units. Anytime it's changed, this function updates userDefaults:
func saveUnitsSelection() {
if unitsControl.selectedSegmentIndex == 0 {
UserDefaultsManager.sharedInstance.preferredUnits = Units.pounds.rawValue
} else {
UserDefaultsManager.sharedInstance.preferredUnits = Units.kilograms.rawValue
}
}
Then they would likely dismiss the settings screen. So, I added this to viewDidLoad in my first view controller:
override func viewDidLoad() {
super.viewDidLoad()
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
units.text = preferredUnits
}
That didn't work, so I moved it to viewWillAppear() and that didn't work either. I did some research and some caveman debugging and found out that neither of those functions is called after the view has been loaded/presented the first time. It seems that viewWillAppear will be called a second time if I'm working within a hierarchy of UITableViewControllers managed by a UINavigationController, but isn't called when I dismiss my modal UIViewController to reveal the UIViewController underneath it.
Edit 1:
Here's the view hierarchy I'm working with:
I'm kinda stuck at this point and not sure what to try next.
Edit 2:
The user can tap a 'Done' button in the navigation bar and when they do, the dismissSettings() function dismisses the Settings view:
class SettingsViewController: UITableViewController {
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
// some other variables set here
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.topItem?.title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .Plain, target: self, action: #selector(self.dismissSettings(_:)))
if preferredUnits == Units.pounds.rawValue {
unitsControl.selectedSegmentIndex = 0
} else {
unitsControl.selectedSegmentIndex = 1
}
}
func dismissSettings(sender: AnyObject?) {
navigationController?.dismissViewControllerAnimated(true, completion: nil)
}
}

THE REAL PROBLEM
You misspelled viewWillAppear. You called it:
func viewWillAppear()
As far as Cocoa Touch is concerned, this is a random irrelevant function that hooks into nothing. You meant:
override func viewWillAppear(animated: Bool)
The full name of the first function is: "viewWillAppear"
The full name of the second function is: "viewWillAppear:animated"
Once you get used to this, the extreme method "overloading" that Cocoa Touch uses gets easier.
This is very different in other languages where you might at least get a warning.
The other lesson that everyone needs to learn when posting a question is: Include All Related Code!
Useful logging function I use instead of print or NSLog, to help find these things:
class Util {
static func log(message: String, sourceAbsolutePath: String = #file, line: Int = #line, function: String = #function, category: String = "General") {
let threadType = NSThread.currentThread().isMainThread ? "main" : "other"
let baseName = (NSURL(fileURLWithPath: sourceAbsolutePath).lastPathComponent! as NSString).stringByDeletingPathExtension ?? "UNKNOWN_FILE"
print("\(NSDate()) \(threadType) \(baseName) \(function)[\(line)]: \(message)")
}
}
[Remaining previous discussion removed as it was incorrect guesses]

Related

swift xcode iOS: can I re-use a loaded modal fullscreen view controller?

I have a storyboard with two view controllers. First one, VC_1, has one button that opens 2nd one - VC_2.
VC_2 also has a button that opens VC_1.
Both controllers have almost identical code:
class VC_1: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
print(“VC_1 loaded")
}
override func viewDidAppear(_ animated: Bool){ print(“VC_1 appeared") }
override func viewDidDisappear(_ animated: Bool){ print(“VC_1 disappeared") }
#IBAction func btnShowVC_2(_ sender: UIButton)
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
secondVC = storyboard.instantiateViewController(identifier: “VC_2”)
secondVC.modalPresentationStyle = .fullScreen
show(secondVC, sender: self)
}
}
The difference is only in "VC_2" instead of "VC_1" in the 2nd controller code.
I have seen this View Controller creation code in Apple documentation and many other examples around the Internet.
When I press the button on the VC_1, I see in the debug window, that VC_2 is loaded and appeared, and VC_1 is disappeared. And same, of course, happens when I press the button on VC_2 - it disappears, and VC_1 is loaded again.
My questions are:
what happens with View Controller object after "viewDidDisappear" has been called? Does it really disappear from memory, or "disappear" only means "you cannot see it on the screen?". I do not see "viewDidUnload" in the documentation...
I suppose that "viewDidLoad" means that new View Controller object was created in memory. Is there any way to load the View Controller object only once, and then hide and show it without causing "viewDidLoad" to be called? I tried to do it with global variable "secondVC" but got "Application tried to present modally an active controller" error.
viewDidDisappear: called after the view is removed from the windows’
view hierarchy. No, View controller object just left the view property. By the way the amount of memory used by view controllers is negligible. So dont think about too much. If you want to catch when Your View controller object release from the memory put
deinit { print("vc deallocated") }
viewDidUnload, it has been deprecated since the iOS
6, which used to do some final cleaning.
Partly true. Keep in mind ViewDidload called one time for the life cycle of view controller. There is a method called before viewdidload but this is not related with your question.
In addition to "There is a method before viewdidload" -> loadView( ) is a method managed by the viewController. The viewController calls it when its current view is nil. loadView( ) basically takes a view (that you create) and sets it to the viewController’s view (superview).

How to check if UIViewController is already being displayed?

I'm working on an app that displays a today extension with some information. When I tap on the today extension, it opens the app and navigates to a subview from the root to display the information. Normally the user would then click the back arrow to go back to the main view, but there is no way to tell if this is actually done. It is possible for the user to go back to the today extension and tap again. When this is done, the subview is opened once again with new information. If this is done a bunch of times, I end up with a bunch of instances of the subview and I have to click the back button on each of them to get back to the main view.
My question: Is it possible to check if the subview is already visible? I'd like to be able to just send updated information to it, instead of having to display an entirely new view.
I am currently handling this by keeping the instance of the UIViewController at the top of my root. If it is not nil, then I just pass the information to it and redraw. If it is nil, then I call performSegue and create a new one.
I just think that there must be a better way of handling this.
Edit: Thanks to the commenter below, I came up with this code that seems to do what I need.
if let quoteView = self.navigationController?.topViewController as? ShowQuoteVC {
quoteView.updateQuoteInformation(usingQuote: QuoteService.instance.getQuote(byQuoteNumber: quote))
}
else {
performSegue(withIdentifier: "showQuote", sender: quote)
}
This is different from the suggested post where the answer is:
if (self.navigationController.topViewController == self) {
//the view is currently displayed
}
In this case, it didn't work because I when I come in to the app from the Today Extension, it goes to the root view controller. I needed to check whether a subview is being displayed, and self.navigationController.topViewcontroller == self will never work because I am not checking to see if the top view controller is the root view controller. The suggestions in this post are more applicable to what I am trying to accomplish.
u can use this extension to check for currently displayed through the UIApplication UIViewController:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
and usage example:
if let topController = UIApplication.topViewController() {
if !topController.isKind(of: MainViewController.self) { //MainViewController- the controller u wish to equal its type
// do action...
}
}

NSTabViewController add NSToolbarItems

I'd like to use the NSTabViewController for switching through 6 different Tabs with the toolbar style.
All tabs have in common that they show different aspects of a Customer entity.
Now I want to add aditional NSToolbarItems to the toolbar of the NSTabViewController? But I haven't found a way to access the toolbar.
I also would like to add Space between the ToolbarItems.
Is there a way to do so?
Or how can I add my ViewController from the Storyboard to a NSTabView without using NSTabViewController?
Regards
Oliver
In the meantime I've tried another approach that I thought was more promising but lead to another strange behaviour:
I've created a new NSViewController and put a NSTabView inside. In order to load my already existing ViewControllers I used this
override func viewDidLoad() {
super.viewDidLoad()
let customerController = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("CustomerVCID")) as! CustomerViewController
let servicesController = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("ServicesVCID")) as! ServicesController
customerController.customer = self.customer
servicesController.customer = self.customer
self.tabView.tabViewItems[0].view = customerController.view
self.tabView.tabViewItems[1].view = servicesController.view
}
That indeed worked, but now all my NSButtons that have actions will cause my application to crash.
There is only one toolbar per window. So your NSTabViewController shares it.
Select toolbar mode of NSTabViewController
Override NSWindowController and add your items
Example:
override func windowDidLoad() {
super.windowDidLoad()
window?.toolbar?.insertItem(withItemIdentifier: .print, at: 0)
}
You can always access your toolbar via following path view->window->toolbar
Your only issue is that there is one delegate per NSToolbar. Which means you have to create your custom NSToolbarItem inside NSTabViewController delegate.
override func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if itemIdentifier == .export {
return ExportToolbarItem.new()
} else {
return super.toolbar(toolbar, itemForItemIdentifier: itemIdentifier, willBeInsertedIntoToolbar: flag)
}
}
Remember your are required to call super. This is because underlying method wants to create bindings to view controller.
In case you need actionable buttons in toolbar just add them without calling super.

Eureka: How to push a VC from a PushRow without Segue

I am using Eureka to do a settingsVC and I am having trouble pushing a newVC from a pushRow. Note that I do not use storyboard
My setup is relatively simple, and my newVC is a simple UIViewController with some uitextfields and labels. Because I do not use storyboard, I can't seem to use performSegues and $0.presentationMode = .segueName(segueName: "RowsExampleViewControllerSegue", onDismiss: nil)
I have also reviewed this post but this post is relatively far fetched for my application.
My implementation so far as such:
class AccountsViewController: FormViewController {
override func viewDidLoad() {
super.viewDidLoad()
form +++ Section("Profile")
<<< PushRow<String>() { row in
row.title = "Edit phone number"
row.value = "+61 12345678"
}.onCellSelection({ (cell, row) in
self.navigationController?.pushViewController(ChangePhoneNumberViewController(), animated: true)
})
The above approach pushes the VC twice.
My reason for wanting to adopt pushRow is because I want it to adopt the pushRow aesthetic looks, like the image attached. I have also considered exploring ButtonRow but the UI/UX is just not right. Moving ahead I would also need to use protocols to bring the new "phoneNumber" to the AccountsViewController.
EDIT:
Attempted the following code but produces the error message.
I was having the same error, you can try to include the return type and also the argument label builder to the callback:
row.presentationMode = PresentationMode.show(
controllerProvider: ControllerProvider.callback(builder: { () -> UIViewController in
return AcknowListViewController()
}),
onDismiss: nil)
I think you just need to set the presentation mode like this:
presentationMode = PresentationMode.show(
controllerProvider: ControllerProvider.callback(
{return ChangePhoneNumberViewController()}
), onDismiss: nil)
ControllerProvider.callback lets you provide a VC by returning one from a closure, instead of from a nib file or storyboard.

Force refresh on another ViewController component with swift

I'm trying to pass a trigger between 2 view controller but i can't find something that works ...
I have the main controller with a NSTableView, source linked, View Base.
main Controller class :
class ViewController: NSViewController {
I have an array defined in Global variables.
on the viewDidLoad i add 2 elements in my array, then i use setDelegate and setDataSource.
It works fine.
myLib.myCoins = fillCoinsTable()
print ("My lib has \(myLib.myCoins.count) objects ")
mainCoinTable.setDelegate(self)
mainCoinTable.setDataSource(self)
I have a segue from the WINDOWS CONTROLLER to my second view Controller.
(It's a ToolBar button). The segue is "Sheet" kind.
This second controller allows me to add an element in my global variable arrays, with a button "Save" .
Code on the second controller
#IBAction func addCoinButton(sender: AnyObject) {
//We add the coin
let newCoin:coin = coin(n: textName.stringValue)
newCoin.Year = Int.init(textYear.stringValue)
myLib.myCoins.append(newCoin)
self.dismissController(self)
}
If i add a button on the main controller, to reloadData on the TableView, it works fine. The third element is added.
but i would like that to be automatic ....
I tried segue, but mine is not between view controller but with the windows controller.
Can you please help ?
Thanks,
Nicolas
I think, you want to add data from oneViewController and without pressing button, you want after navigation or pressing back button, you want to reload automatically tableview to show updated results, right.
If I'm not wrong then, this solution will work for you.
Follow this below steps:
Step 1 :
Add this code in your view controller, from which you want to add data in array or in database, instead of button click.
override func viewDidLoad()
{
super.viewDidLoad()
let backItem = UIBarButtonItem(title: "Back", style: .Plain, target: self, action: "goBack")
self.navigationItem.leftBarButtonItem = backItem
}
func goBack()
{
NSNotificationCenter.defaultCenter().postNotificationName("load", object: nil)
self.navigationController?.popViewControllerAnimated(true)
}
Step 2:
Now its final step.
Now add this below code, to your result view controller, where you want to automatically update the result.
func loadList(notification: NSNotification){
self.TableView.reloadData()
}
override func viewWillAppear(animated: Bool) {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "loadList:",name:"load", object: nil)
}
Now implement this code-stuff in your coding. Hope it works for you.
If you want the table to reload automatically when the View appears add this to viewWillAppear
self.tableView.performSelectorOnMainThread(#selector(UITableView.reloadData), withObject: nil, waitUntilDone: true)