In order to display a text field right above the user's keyboard, I overrode inputAccessoryView in my custom view controller.
I also made sure that the view controller may become the first responder by overriding canBecomeFirstResponder (and returning true) and by calling self.becomeFirstResponder() in viewWillAppear().
Now, as I am displaying some messages as UICollectionViewCells in my view controller, I want to scroll down whenever the keyboard shows up. So I added a notification in viewDidLoad():
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: Notification.Name.UIKeyboardDidShow, object: nil)
keyboardDidShow() then calls the scrolling function:
#objc private final func scrollToLastMessage() {
// ('messages' holds all messages, one cell represents a message.)
guard messages.count > 0 else { return }
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView?.scrollToItem(at: indexPath, at: .bottom, animated: true)
}
Indeed, by setting breakpoints in Xcode, I found out that the function gets triggered after the keyboard has appeared. But additionally, it also triggers after I resigned the first responder (f.ex. by hitting the return key [I resign the first responder and return true in textFieldShouldReturn ]) and the keyboard has disappeared. Although I think that it shouldn't: as the Apple docs say:
Posted immediately after the display of the keyboard.
The notification also triggers when accessing the view controller, so after the main view has appeared and when clicking on a (customized) UICollectionViewCell (the cell does not have any editable content, only static labels or image views, so the keyboard shouldn't even appear).
To give some more information: I pretty much followed this tutorial on Youtube: https://www.youtube.com/watch?v=ky7YRh01by8
The UIKeyboardDidShow notification may be posted more often than you might expect, not just when it initially appears. For example, when the frame changes after it was already visible, UIKeyboardDidShow is posted.
However you can know if the keyboard is truly visible by inspecting the keyboard's end frame from within the userInfo dictionary. This will tell you its size and position on screen, which you can then use to determine how best to react in your user interface.
Related
I have added a UI Button inside of a stack view which is inside of a table view in my storyboard. When I click on my button the correct output is printed in my debugger console but there is no indication in the app that the button has been clicked (no default animation). I have tried looking at my view hierarchy and changing all of the parent views to clip to bounds. Any idea why the button is functioning but not being animated to the user?
The quick fix to your problem is to set delaysContentTouches = false for your table view.
According to the Apple Docs,
If the value of this property is true, the scroll view delays handling the touch-down gesture until it can determine if scrolling is the intent. If the value is false, the scroll view immediately calls touchesShouldBegin(_:with:in:). The default value is true.
See the class description for a fuller discussion.
Alternatively, if you have subclassed the UIScrollView, you can get the same thing done by overriding the following function,
class MyScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return type(of: view) == UIButton.self
}
}
I switched my apps one screen from UISearchBar to UISearchController. It's a tableview controller. As per design I should not keep the search bar on UI initially unless it is activated, (Normally it's a common practice to keep search bar as the 'tableHeaderView'). The problem was, I have a search button, when tapped 'search bar' should be activated and become first responder.
When tapped on cancel button, it should be removed from UI. However when I'm tapping on the 'Search Bar Button' on navigation bar, the UISearchController gets activated, providing a dim background but the keyboard doesn't appear. I need to tap one more time on search bar to bring the keyboard upon UI.
Here's my search bar button action:
#IBAction func onTapSearch(_ sender: AnyObject) {
self.view.addSubview(searchController.searchBar)
searchController.isActive = true
searchController.becomeFirstResponder()
isSearchActive = true
self.navigationController?.setToolbarHidden(true, animated: false)
}
I'm configuring the UISearchController in my viewDidLoad method. Let me know if that part code is any of you want to see, however it's usual code. And I verified I'm not calling anywhere resignFirstResponder() method anywhere.
try this,
Just replace this line,
searchController.becomeFirstResponder()
With this below,
searchController.searchBar.becomeFirstResponder()
Edit,
func didPresentSearchController(_ searchController1: UISearchController) {
searchController1.searchBar.becomeFirstResponder()
}
Implement this delegate method and try.
I have a subclass of UICollectionViewController that is nested inside a UINavigationController. The collection contains several cells (currently, 3) and each cell is as big as the full screen.
When the whole thing is shown, the collection view initally scrolls to a specific cell (which works flawlessly for each cell):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // this is set in the beginning
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
}
}
However, the collection view refuses to scroll horizontally, hereafter, as if the user interaction was disabled. I am not sure what is happening, but this is what I have checked so far:
user interaction is enabled for the collection view
the next cell (right or left, depending on the scroll direction) is requested correctly which I found out by inspecting collectionView:cellForItemAtIndexPath:
the requested imagePath is the right one
scrollToItemAtIndexPath... does not work either if I try to trigger a scroll programmatically after everything has been loaded (nothing happens)
scrollRectToVisible... does neither
setting collectionView?.contentInset = UIEdgeInsetsZero before the programmatic scroll attempts take place does not change anything
the content size of the collection view is 3072x768 (= 3 screens, i.e. 3 cells)
Which bullet points are missing, here?
Although the post did not precisely tackle the root of my problem it forced me to ponder the code that I posted. If you look at it you will see that it basically says: Whenever the views need to be layouted, scroll to the cell at position currentlyPresentedPhotoCellIndexPath. However, and this you cannot see without any context, this variable is only set once, when the whole controller is being initialized. Thus, when you try to scroll, the layout changes, the controller then jumps back to the initial cell and it looks like nothing happens, at all.
To change this, you just have to enforce a single scroll, e.g. by doing this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // only once possible
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
currentlyPresentedPhotoCellIndexPath = nil // because of this line
// "initiallyPresentedPhotoCellIndexPath" would probably a better name
}
}
A big thanks to Mr.T!
I'm using SWRevealController to display 3 (left, center, right) panels. Basically, the right panel is a tableview of numbers and the middle panel shows possible even divisions when a user clicks on a number. I connected a segue to the IBAction of a reusable tablecell in the right view controller which loads the MainViewController. This all works fine. The problem is that if the number can't be divided evenly it triggers a notification, which the main view controller observes on ViewDidLoad. This notification sets the alpha of a pseudo-"alert" (UIView at the bottom of screen) to 1.0 for 4 seconds, at which point it returns to 0. Unfortunately this is where the problem starts: the notification box appears for a brief second while the animation runs but when the main viewcontroller finishes animating, the alert box disappears. I have a hunch it's because the ViewDidLoad fires at this point and resets the NSNotificationOberser – if I remove the segue on Touch Up Inside and manually switch view controllers the alert-box remains present.
Can you help me think of what I'm getting wrong? Like I said, I think it's because the observer is initialized in the ViewDidLoad. Assuming this is the case, where should I initialize the observer so that this doesn't happen anymore?
Basically the main VC displays a calculator, the code for which runs in a Calculator.swift file. If, when the number is passed through Calculate(), there is an error, it triggers an "alert" which the main VC picks up on, then reveals the box so that the user knows. Each time Calculate() is called, it logs the user's calculation to a tableview in the RVC – idea being they can reload previous calculations. Is this an improper usage of Notification Center?
The way I want the timeline of events to be:
User clicks on cell in the right panel
Main view controller (the calculator) is pushed via segue
Calculate() is called on the selected number, if there is a remainder a notification is triggered
The main view displays the results from Calculate(), if an alert fired then it would unhide a popup box on main view.
What is currently happening:
User clicks on the cell in right panel
Calculate() is called on the number, if there is a remainder the notification fires
Before the main view is pushed via segue, I can see briefly in the animation the result of the trigger firing and the calculation
As soon as the segue animation completes the view hides
My main VC Code (PopUpView is the alert box)
class CalculatorVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "triggerAlert:", name: "alert", object: nil)
self.PopUpView.alpha = 0.0
}
the triggerAlert function:
func triggerAlert(notification: NSNotification) {
PopUpView.alpha = 1.0
let returnedRemainder = (notification.userInfo)
let sample: Double = (returnedRemainder!["userTotal"]as! Double)
self.label.text = "Warning! Remainder: \(Double(round(100 * sample)/100))"
}
the tableView didSelect of the right VC:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
newUser.calculate()
//code here assigns the value in the selected table cell to newUser
}
Then in the Calculate() function:
dispatch_async(dispatch_get_main_queue(), {
NSNotificationCenter.defaultCenter().postNotificationName("alert", object: nil, userInfo: userTotalDictionary)
}
Notification observation/de-observation should generally be balanced between viewDidAppear/viewWillDisappear or viewWillAppear/viewDidDisappear. Either pair is usually fine, but it's wise to keep things that happen "when just offscreen" separate from things that happen "when just onscreen".
viewDidLoad is a poor place to set up observation, because you don't have a good place to balance removing the observation (viewDidUnload no longer exists). You should only remove observations in deinit that you set up in init, and view controllers really should never be observing things when they are not on screen.
I have a UITableViewController in my app with a UIRefreshControl added to it. Sometimes however (I'm not sure how to reproduce this, it happens every now and then), I get some extra whitespace at the top of the table view with the refresh control being offset even below that.
This is what it looks like (idle on the left, being pulled down on the right):
I don't have any clue what could be causing this. In my viewdidload I'm only instantiating the refresh control and calling an update function that sets the attributed title. I've moved adding the refresh control to the table view into the viewDidAppear as I've read elsewhere. This is what that code looks like:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl = UIRefreshControl()
updateData()
}
override func viewDidAppear(animated: Bool) {
refreshControl!.addTarget(self, action: "updateData", forControlEvents: UIControlEvents.ValueChanged)
tableView.insertSubview(self.refreshControl!, atIndex: 0)
}
func updateData() {
//...
ServerController.sendParkinglotDataRequest() {
(sections, plotList, updateError) in
//...
// Reload the tableView on the main thread, otherwise it will only update once the user interacts with it
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
// Update the displayed "Last update: " time in the UIRefreshControl
let formatter = NSDateFormatter()
formatter.dateFormat = "dd.MM. HH:mm"
let updateString = NSLocalizedString("LAST_UPDATE", comment: "Last update:")
let title = "\(updateString) \(formatter.stringFromDate(NSDate()))"
let attributedTitle = NSAttributedString(string: title, attributes: nil)
self.refreshControl!.attributedTitle = attributedTitle
})
}
}
Do you need to add the refresh control as a subview of the tableView? I think all you need to do is assign self.refreshControl. According to the documentation:
The default value of this property is nil.
Assigning a refresh control to this property adds the control to the
view controller’s associated interface. You do not need to set the
frame of the refresh control before associating it with the view
controller. The view controller updates the control’s height and width
and sets its position appropriately.
Adding a subview in viewDidAppear could get executed more than once. If you push a controller from a cell and pop back this will get called again. It could be that insertSubview checks if the refresh already has a parent and removes it first, so might not be your issue. You should only do the insert when the controller appears for the first time.
updateData could also be getting added multiple times.
So I think you only need to assign self.refreshControl and then add a handler for the refresh action as you do now using addTarget but this time do it on self.refreshControl.
You can also do all this from storyboard. In storyboard you select the UITableViewController and on the attribute inspector simply set the Refreshing attribute to enabled. This adds a UIRefreshControl into the table and you can see it on the view hierarchy. You can then simply CTRL drag as normal from the refresh control into the .h file and add an action for valueChange which will be fired when you pull down on the refresh control in the table.
Well, I believe that your described behavior might not necessarily be caused by the refresh control.
According to the fact that you don't have any other subviews below your table view I would recommend you to try to place a "fake"-view below your table view. I usually prefer an empty label with 0 side length.
I had similar issues like yours where my table view insets were broken in some cases. And as soon as I used this "fake" subview the problems disappeared. I've read about this issue in some other threads, too. And the solution was this. Seems to be an odd behavior/bug.
Give it a try :)