I believe this is a non-trivial problem related to UIKeyCommands, hierarchy of ViewControllers and/or responders.
In my iOS 9.2 app I have a class named NiceViewController that defines UIKeyCommand that results in printing something to the console.
Here's NiceViewController:
class NiceViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let command = UIKeyCommand(input: "1", modifierFlags:UIKeyModifierFlags(),
action: #selector(keyPressed), discoverabilityTitle: "nice")
addKeyCommand(command)
}
func keyPressed() {
print("works")
}
}
When I add that NiceViewController as the only child to my main view controller all works correctly - pressing button "1" on external keyboard (physical keyboard when used in simulator) works like a charm. However when I add a second view controller to my main view controller the UIKeyCommands defined in NiceViewController stop working.
I'd love to understand why does it happen and how to ensure that having multiple child view controllers to my main view controller doesn't stop those child view controllers from handling UIKeyCommands.
Here is my main view controller:
class MainViewController: UIViewController {
let niceViewController = NiceViewController()
let normalViewController = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(niceViewController.view)
self.addChildViewController(niceViewController)
self.view.addSubview(normalViewController.view)
// removing below line makes niceViewController accept key commands - why and how to fix it?
self.addChildViewController(normalViewController)
}
}
I do not believe this is a problem with UIKeyCommands
In iOS, only one View Controller at a time may manage key commands. So with your setup, you have a container view controller with a couple child view controllers. You should tell iOS that you would like NiceViewController to have control of key commands.
Defining First Responders
At a high level, in order to support key commands, you not only must create a UIKeyCommand and add it to the view controller, but you must also enable your view controller to become a first responder so that it is able to respond to the key commands.
First, in any view controller that you would like to use key commands for, you should let iOS know that that controller is able to become a first responder:
override func canBecomeFirstResponder() -> Bool {
// some conditional logic if you wish
return true
}
Next, you need to make sure the VC actually does become the first responder. If any VCs contain some sort of text fields that become responders (or something similar), that VC will probably become the first responder on its own, but you can always call becomeFirstResponder() on NiceViewController to make it become the first responder and, among other things, respond to key commands.
Please see the docs for UIKeyCommand:
The system always has the first opportunity to handle key commands. Key commands that map to known system events (such as cut, copy and paste) are automatically routed to the appropriate responder methods. For other key commands, UIKit looks for an object in the responder chain with a key command object that matches the pressed keys. If it finds such an object, it then walks the responder chain looking for the first object that implements the corresponding action method and calls the first one it finds.
Note: While someone is interacting with the other VC and it is the first responder, NiceViewController cannot be the first responder at the same time, so you might want some key commands on the other VC as well.
Why this isn't always necessary
When only one VC is presented, iOS appears to assume that it will be the first responder, but when you have a container VC, iOS seems to treat the container as the first responder unless there is a child that says it is able to become the first responder.
Following #Matthew explanation solution is adding becomeFirstResponder() request; in viewDidAppear instead of viewDidLoad resolve my similar problem.
Swift4
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
print("becomeFirstResponder?: \(isFirstResponder)")
}
I found while experimenting with this that if you manually call becomesFirstResponder() on the child view controllers, it allows you to have multiple first responders and all key commands show up when hitting command.
I'm not sure why this works exactly as surely you're only supposed to have a single firstResponder at any one time.
Related
I have a project in which I need to log an analytics event whenever any View Controller (log the name of the View Controller) comes on screen.
I was trying to avoid littering all of my existing View Controller classes with call to the analytics SDK.
I tried making an AnalyticsViewController and all my View Controllers would subclass this View Controller, and then I add analytics event in AnalyticsViewController class's viewDidLoad method. But the problem with this approach is that AnalyticsViewController does not which child View Controller is the call coming from.
I am using Swift 3.0. I believe that Swift with its powerful language features should be able provide me with an abstraction of some sorts.
Is there any way through this problem without littering all the View Controllers?
You were on the right track. Making a UIViewController parent class is a good idea.
In viewDidLoad method you can just add this:
let className = NSStringFromClass(self.classForCoder)
It will give you the name of current loaded view controller and then you can use that name in your event to specify which view controller was actually loaded.
Edit: added example.
So your parent's viewDidLoad would look something like this:
override func viewDidLoad() {
super.viewDidLoad()
let className = NSStringFromClass(self.classForCoder)
sendEvent(withViewControllerName: className)
}
The answer given by #JPetric is an amazing starting point. I just had to do a little modification to get it to work.
I've put this in my AnalyticsViewController to retrieve the name of the current subclass.
private func currentClassName() -> String? {
return NSStringFromClass(self.classForCoder).components(separatedBy: ".").last
}
I'm trying to write a simple to-do list in Swift that will store the list as an array of Strings and then call it back from memory when the app loads.
I've got the following code:
var itemList = [String]()
func loadData() -> [String] {
var arr = [String]()
if NSUserDefaults.standardUserDefaults().objectForKey("storedData") != nil {
arr = NSUserDefaults.standardUserDefaults().objectForKey("storedData")! as! [String]
}
else {
arr = ["Nothing to do..."]
}
return arr
}
func saveData(arr: [String]) {
NSUserDefaults.standardUserDefaults().setObject(arr, forKey: "storedData")
}
Where I'm getting stuck is in where to place the call to loadData(). This is an app that has two view controllers (one for the list, one for an add item setup), so if I place the loadData() call in viewDidLoad() for the main view controller, the array is called back in from memory (and over-written) every time I switch back to the main view controller.
Where is the best place to call this so that it will load once only, upon the app starting up?
the array is called back in from memory (and over-written) every time I switch back to the main view controller.
No. viewDidLoad only loads once, when the app starts. Only viewWillApprear and viewDidAppear get called everytime the viewcontroller changes.
Also you could make your code a bit more compact by using if let:
if let storedData = NSUserDefaults.standardUserDefaults().objectForKey("storedData") as! [String]{
arr = storedData
}
But if you want to make sure to load this only once, you can put it in your AppDelegate file in your applicationDidFinishWithOptions method.
But you'd have to make a variable in your AppDelegate file which you can access from your viewController.
viewDidLoad() only happens when the View Controller is first instantiated. If it is your root view controller you can have it in viewDidLoad().
The other goes, viewDidLoad > viewWillAppear > viewDidAppear. After the view is first loaded only the latter 2 methods are called whenever you navigate.
you can also always register for a NSApplicationDidFinishLaunchingNotification notification at the notification center
check it out here:
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSApplication_Class/index.html#//apple_ref/c/data/NSApplicationDidFinishLaunchingNotification
Use or overwrite respectively
application(_:didFinishLaunchingWithOptions:)
of your application delegate.
That is called only once upon application launch.
See
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/#//apple_ref/occ/intfm/UIApplicationDelegate/application:didFinishLaunchingWithOptions:
viewDidLoad is called after the view for a single controller is first loaded. It shouldn't be called more than once for the life-cycle of a single viewController. Maybe it is possible that, if you are not calling "super.viewDidLoad()" in your own viewDidLoad method, then it may be called again? While you can generally assume that the rootViewController for an application is only created once, I think it's theoretically possible that it might be cleared out of memory by the app if required and then recreated again - so I would never assume it's only called once.
One thing you could do is just set a boolean (default false) to true whenever you load the data and then not call it again if the flag is already set to true.
Alternatively, it's a good idea to separate the data management from your viewControllers. A relatively simple solution would be to have a class called "AppData" say, which might be a singleton (so you can only ever have one instance of it) or a member of your AppDelegate. Then, in your app delegate's "applicationDidFinishLaunchingWithOptions method, you could create the one instance of the AppData class and call the loadData method on it. This class would then live independently of whichever view is currently showing, and the current view could call methods on this object to load/save/update data as required.
Thank you for viewing this page.
I have downloaded the following from GH: https://github.com/watsonbox/ios_google_places_autocomplete
It uses a nib file to initiate a autocomplete feature within a ViewController (in the Main Storyboard.
Issues
The following issues are hindering my progress;
I am unable to close the nib view using the X (or Stop button). The
nib loads via ViewDidLoad, therefore every time it dismisses itself,
it will be shown again. I have attempted to do the following but it
does not work.
When any cell is selected, I am unable to go back to the ViewController I originally navigated from. (same as point 1,
however should happen once I select any of the cells).
extension LocoSearch: GooglePlacesAutocompleteDelegate {
func placeSelected(place: Place) {
println(place.description)
println(place.id)
var locoResult = PFUser.currentUser()
locoResult["placeDesc"] = place.description
locoResult["placeId"] = place.id
locoResult.pin()
self.performSegueWithIdentifier("locoDone", sender: self)
}
func placeViewClosed() {
dismissViewControllerAnimated(true, completion: {
self.performSegueWithIdentifier("locoDone", sender: self)
})
To stop the autocomplete controller from loading every time the view loads, either remove it from viewDidLoad (and put it in a button click handler, for example), or if that's really where it belongs then perhaps use a variable stored property to store the currently selected Place and only show the autocomplete controller if none exists.
placeSelected is the correct callback for handling selections. Perhaps you should dismiss the autocomplete view before performing the segue as you do in your close handler? Please link a Github project with this issue if you really can't get it working.
I'm building an app for iOS using the Swift language. I start with a table view controller as my root view controller, and then I have a secondary view controller in which a variable (passData) is defined. This all works fine, and it passes the data correctly (I think) from the secondary view controller back to the primary view controller. However, when the user returns back to the primary view controller, I need a function to execute which will then add the 'addTitle' value to an array. I know how to add it to the array, but...
I don't know how to initiate the function when the view is returned to. What I mean is, after the user is finished on the secondary view controller AND the variable "passData" is defined, they will then push the back button on the navigation bar. I then need the primary view controller to recognise that it is once again being displayed to the user, and then execute the following code:
tableData += [passData]
tableSubtitle += [passDescription]
I have tried the following:
override func viewDidAppear() {
tableData += [passData]
tableSubtitle += [passDescription]
}
But this gives the error as Method does not override any method from its superclass.
Essentially, I just need to know how to start a function when the view displays. How can i achieve this?
you need to call super.viewDidAppear(animated) and the method signature takes a Bool so you should say:
override func viewDidAppear(animated: Bool)
ProTip: If you want to override a method you can just start typing the method name you want to overload and Xcode will auto suggest the method name and fill in the override declarative. So on a new line start typing viewDid and you should see the viewDidAppear method in the autocompletion drop down. Pressing enter will complete the method signature for you.
I'm new to swift and ios programming in general. I'm trying to display a modal view when my app first loads which it does. The problem I'm running into is that my modal keeps appearing over and over and over. Not sure where I'm going wrong.
BONUS QUESTION: Ultimately I'd like this to only happen the first time the user opens the app.
class ViewController: UIViewController {
var introModalDidDisplay = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
showIntroModal()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func showIntroModal() {
if (!introModalDidDisplay) {
println(introModalDidDisplay)
introModalDidDisplay = true
let intro = self.storyboard?.instantiateViewControllerWithIdentifier("introModal") as IntroModalViewController
intro.modalPresentationStyle = UIModalPresentationStyle.FormSheet
self.presentViewController(intro, animated: true, completion: nil)
}
}
}
Found it. My "intro" class was extending ViewController rather than UIViewController...apparently that's bad. Thanks for the help! Sorry for the wild goose chase.
When you close the modal view you show your ViewController view again, firing viewDidAppear once more and entering an infinite loop of showing your modal view, since the first view is always "appearing"
I'd suggest doing this in viewDidLoad, as the view is supposed to load only once. Try and experiment with these events and see when they are fired.
As for firing only once I'd suggest setting a flag in localStorage (plist) indicating whether it's the first time the user opens the app or not. For example set a flag in the first view's viewDidLoad and if that flag is false show your modal view and set the flag to true.
Here's a question about writing in plists in Swift: Save Data to .plist File in Swift
A couple of observations:
Are you saying that you're seeing this appear again and again while you're using the app? That would suggest that you have multiple instances of this view controller instantiated. For example, you might be doing a segue back to this view controller (which will create new instance) rather than unwinding/popping/dismissing back to it (which will return to the previous instance).
I'd suggest you have a breakpoint or logging statement in viewDidLoad and confirm that you see this once and only once. If you're seeing it multiple times, that means that you have some circular reference amongst your storyboard scenes (and, BTW, you are abandoning memory, a type of leak).
To handle this only presenting itself once between uses of the app, you need to save this introModalDidDisplay in some form of persistent storage. Often NSUserDefaults is used for this. For example, define introModalDidDisplay to look up the status in the standard user defaults:
var introModalDidDisplay = NSUserDefaults.standardUserDefaults().boolForKey("introModalDidDisplay")
Then your showIntroModal can update this setting in the user defaults:
func showIntroModal() {
if !introModalDidDisplay {
introModalDidDisplay = true
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "introModalDidDisplay")
NSUserDefaults.standardUserDefaults().synchronize()
let intro = self.storyboard?.instantiateViewControllerWithIdentifier("introModal") as IntroModalViewController
intro.modalPresentationStyle = UIModalPresentationStyle.FormSheet
self.presentViewController(intro, animated: true, completion: nil)
}
}
Clearly, you can use whatever persistent storage technique you want (plist, archive, user defaults, Core Data, SQLite), but the idea is the same: Retrieve the status from persistent storage and once the intro screen has been presented, update that persistent storage accordingly.
By the way, by looking this up in persistent storage, we also fix the symptom of the problem I discussed in point #1. But you really want to fix the root cause of that first point, too, because otherwise you'll be leaking memory (if, of course, you're really instantiating multiple copies of the ViewController class).
By the way, looking ahead to the future, rather than storing just a boolean, I might suggest storing a version number identifier, too. That way, when you release version 2.0 of the app, you'll be able to decide whether the v1.0 users might see the updated intro screen again (or perhaps a custom one that focuses on what's new).