How to dismiss keyboard in UISearchController when changing focus in tvos? - swift

I'm completely new to tvos and I'm trying to implement a UISearchController view where, in my SearchResultsViewController, I have two UICollectionViews displayed one above the other:
The problem is that when the user swipes down to select one of the items in the UICollectionView, the keyboard doesn't dismiss. Even swiping back up to select the keyboard doesn't fully scroll up and it's impossible to see what you're typing. The resulting view is this:
Ideally, I'd like to dismiss the keyboard when the user swipes down to focus on anything else in the interface. I looked at Apple's tvos UIKit Catalog and their example shows a UISearchController which dismisses the keyboard when changing focus, but I don't see that they're doing anything differently.
Here is the code I'm using to setup my UISearchController when the user clicks on a button:
#IBAction func onSearchButton(sender: AnyObject) {
guard let resultsController = storyboard?.instantiateViewControllerWithIdentifier(SearchResultsViewController.storyboardID) as? SearchResultsViewController else { fatalError("Unable to instantiate a SearchResultsViewController.") }
// Create and configure a `UISearchController`.
let searchController = UISearchController(searchResultsController: resultsController)
searchController.searchResultsUpdater = resultsController
searchController.hidesNavigationBarDuringPresentation = false
let searchPlaceholderText = NSLocalizedString("Search for a Show or Movie", comment: "")
searchController.searchBar.placeholder = searchPlaceholderText
// Present the search controller from the root view controller.
guard let rootViewController = view.window?.rootViewController else { fatalError("Unable to get root view controller.") }
rootViewController.presentViewController(searchController, animated: true, completion: nil)
}

After quite a bit of trial and error, I was able to figure out the solution.
The keyboard will automatically dismiss itself as long as:
1) The item the user focuses on is inside of a scrollview
2) The scrollview content size is larger than the screen height by at least 1px (1081px).

After quite a lot of trial and error, finally I figured out.
The reason is that you have nested ScrollViews in searchResultsController.
"ScrollViews" of-course includes UICollectionView, UITableView, and UIScrollView.
According to my investigation, UISearchController behaves as follows.
If the first view which gets focused in searchResultsController is subview of the inner scrollView (which is the horizontal UICollectionView, in your case), then you won't get keyboard hidden as expected.
Interestingly, if the first view which gets focused in searchResultsController is subview of outer scrollView, then you will get keyboard hidden completely, animated, just as expected (!).
I think this is sort of UIKit's bug.

I had exactly same layout and wasn't able to achieve this so far. I believe you return false in tableView(tableView: UITableView, canFocusRowAtIndexPath indexPath: NSIndexPath) -> Bool so that each cells in collection view can scroll horizontally with proper focus behavior. I think it's actually causing the issues. If you make the first cell in the tableview focusable the problem goes away but of course focus behavior is not desired. I just found that out today and will try more tomorrow to find out what I can do about this. I sense that I will need a new design that allows me to use a single collectionview or tableview that has its cells focusable in resultsController. Hope this is easily achievable in tvOS 10.

Work Around Solution: Add one dummy cell at indexPath.row == 0 with height as 1 pixel and enable the focus on it.

Related

How could I keep keyboard up when tableview cells are tapped?

I am a beginner learning Swift and trying to build a search page with Swift. In my search page of the app, I have added two Views in my storyboard with one View above the other.
The upper View contains a Collection View where I have two prototypes of collection view cells. The first type of the cells has Label. The second type of the cells has TextField.
The other View on the bottom half of the screen contains a dynamic Table View where I have a list of items that can be selected. Each row of the table view cells has a selection item.
So, when I tap on a table view cell, the selection item will appear in the collection view. If I type a keyword in the TextField in the collection view, table view reloads and shows all the selection items that has the keyword, so I can tap and add an item to the collection view.
I would like to keep adding by typing a keyword after I tap on a searched item in the table view. So, I made the first cell showing selected items with labels and the second cell that has the TextField separated into two sections of the collection view. So, I only reload the first section (without TextField) for each selection. But somehow the keyboard automatically resign whenever I tap on the table view cell to add an item to the collection view.
Is there any way I can keep the keyboard up even when I tap on the tableview cells?
The keyboard also resigns when I tap the collection view cells.
I would appreciate your advice. Thanks.
I hope you are having a good day.
You can try calling this method on the UITextField you would like to show the keyboard for (maybe call it after the user taps on the UITableViewCell):
textField.becomeFirstResponder()
where "textField" is the variable name of your UITextField.
Please let me know if this fixed your issue.
Edit #1
Hello! Since my previous solution did not achieve your intended behavior. There is another solution in my mind, however I have not tried it before.
As an introduction to the concept of delegation, there is a method created by Apple called "textFieldShouldEndEditing" which is called by Apple whenever any keyboard will disappear on any text field.
This method is created by Apple, but you can override it (i.e. customize it) to suit your needs and tailor its behavior.
To override this method you have to assign your class as the delegate of UITextField by adding UITextFieldDelegate to your class definition as follows:
class YourClassName: UIViewController, UITextFieldDelegate { }
Now you have to set your class as the delegate by saying textField.delegate = self For every UITextField you create in your collection views
You then can re-create the method we discussed earlier in your class:
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
//let's implement it the next steps, but for now, let's return true.
return true
}
Now instead of Apple calling their version of the method, they will call yours.
You then can create a variable in the top level of your class (I will let you know where this will be helpful later), and "maybe" name it as:
var isCellBeingClicked = false
Now upon clicking on a cell, make this variable true, I believe you are using the method didSelectRowAt (but you could be using any other method which is fine):
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
[...]
isCellBeingClicked = true
[...]
}
Now back to our customized method textFieldShouldEndEditing mentioned in step 3. You can add this implementation:
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
//If a cell is being clicked right now, please do not dismiss the keyboard.
if isCellBeingClicked {
isCellBeingClicked = false //reset the value otherwise the keyboard will always be there
return false
}
else { return true }
}
Please let me know if this fixes your issue.
Best regards

How do you animate a UICollectionView using auto layout anchors?

I'm trying to animate a collectionview by using layout constraints however I cannot get it to work. I have a collectionview that takes up the entire screen and on a button tap I want to essentially move the collectionview up to make room for another view to come in from the bottom - see image below
The incoming UIView animates just fine (the view coming up from the bottom) - The reason I want to move the collectionview is that the incoming UIView obscures the collection view so am just trying to move the collectionview up at the same time as the new view so that all of the content in the collectionview can be displayed without being hidden by the new view - I use a reference view to get the right layout constraints for the final position for the collectionview Image to show what I am trying to achieve Am I going about it the right way?
Nothing happens with the code example below and I am not sure where to go from here - the same approach is used for animating the incoming view and works just fine but doesn't seem to work for the collectionview...
Any help would be kindly appreciated
var colViewBottomToReferenceTop: NSLayoutConstraint?
var colViewBottomToViewBottom: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
colViewBottomToReferenceTop = musicCollectionView.bottomAnchor.constraint(equalTo: referenceView.topAnchor)
colViewBottomToViewBottom = musicCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.active([colViewBottomToViewBottom!])
NSLayoutConstraint.deactivate([colViewBottomToReferenceTop!])
}
func playerShow() {
NSLayoutConstraint.activate([colViewBottomToReferenceTop!])
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})
}
#IBAction func btnTapped(_ sender: UIButton) {
playerShow()
}
You want to deactivate the other before animation
NSLayoutConstraint.deactivate([colViewBottomToViewBottom!])
NSLayoutConstraint.activate([colViewBottomToReferenceTop!])
Look to this Demo

Prefer Large Titles and RefreshControl not working well

I am using this tutorial to implement a pull-to-refresh behavior with the RefreshControl. I am using a Navigation Bar. When using normal titles everything works good. But, when using "Prefer big titles" it doesn't work correctly as you can see in the following videos. Anyone knows why? The only change between videos is the storyboard check on "Prefer Large Titles".
I'm having the same problem, and none of the other answers worked for me.
I realised that changing the table view top constraint from the safe area to the superview fixed that strange spinning bug.
Also, make sure the constant value for this constraint is 0 🤯.
At the end what worked for me was:
In order to fix the RefreshControl progress bar disappearing bug with large titles:
self.extendedLayoutIncludesOpaqueBars = true
In order to fix the list offset after refreshcontrol.endRefreshing():
let top = self.tableView.adjustedContentInset.top
let y = self.refreshControl!.frame.maxY + top
self.tableView.setContentOffset(CGPoint(x: 0, y: -y), animated:true)
If you were using tableView.tableHeaderView = refreshControl or tableView.addSubView(refreshControl) you should try using tableView.refreshControl = refreshControl
It seems there are a lot of different causes that could make this happen, for me I had a TableView embedded within a ViewController. I set the top layout guide of the tableview to the superview with 0. After all of that still nothing until I wrapped my RefreshControl end editing in a delayed block:
DispatchQueue.main.async {
if self.refreshControl.isRefreshing {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
self.refreshControl.endRefreshing()
})
}
}
The only working solution for me is combining Bruno's suggestion with this line of code:
tableView.contentInsetAdjustmentBehavior = .always
I've faced the same problem. Call refreshControl endRefreshing before calling further API.
refreshControl.addTarget(controller, action: #selector(refreshData(_:)), for: .valueChanged)
#objc func refreshData(_ refreshControl: UIRefreshControl) {
refreshControl.endRefreshing()
self.model.loadAPICall {
self.tableView.reloadData()
}
}
The only solution that worked for me using XIBs was Bruno's one:
https://stackoverflow.com/a/54629641/2178888
However I did not want to use a XIB. I struggled a lot trying to make this work by code using AutoLayout.
I finally found a solution that works:
override func loadView() {
super.loadView()
let tableView = UITableView()
//configure tableView
self.view = tableView
}
I had this issue too, and i fixed it by embedded my scrollView (or tableView \ collectionView) inside stackView, and it's important that this stackView's top constraint will not be attached to the safeArea view (all the other constraints can). the top constraint should be connect to it's superview or to other view.
I was facing the same issue for very long, the only working solution for me was adding refresh control to the background view of tableview.
tableView.backgroundView = refreshControl
Short Answer
I fixed this by delaying calling to API until my collection view ends decelerating
Long Answer
I notice that the issue happens when refresh control ends refreshing while the collection view is still moving up to its original position. Therefore, I delay making API call until my collection view stops moving a.k.a ends decelerating. Here's a step by step:
Follow Bruno's suggestion
If you set your navigation bar's translucent value to false (navigationBar.isTranslucent = false), then you will have to set extendedLayoutIncludesOpaqueBars = true on your view controller. Otherwise, skip this.
Delay api call. Since I'm using RxSwift, here's how I do it.
collectionView.rx.didEndDecelerating
.map { [unowned self] _ in self.refreshControl.isRefreshing }
.filter { $0 == true }
.subscribe(onNext: { _ in
// make api call
})
.disposed(by: disposeBag)
After API completes, call to
refreshControl.endRefreshing()
Caveat
Do note that since we delay API call, it means that this whole pull-to-refresh process is not as quick as it could have been done without the delay.
Unfortunately, no advice helped. But I found a solution that helped me. Setting the transparency of the navigation bar helped.enter image description here
Problem can be solved if add tableview or scroll view as root view in UIViewController hierarchy (like in UITableViewController)
override func loadView() {
view = customView
}
where customView is UITableView or UICollectionView

Add custom recognizer delay

I've disabled delaysContentTouches in my tableview subclass using:
delaysContentTouches = false
subviews.forEach { ($0 as? UIScrollView)?.delaysContentTouches = false }
But in one of my sections, I still want to keep the delay. Is there a way to cancel the delay for certain sections or perhaps I can add a custom recognizer delay to a section?
Sections are not actual objects within a tableView, so my answer to your first question is no. The .delaysContentTouches applies to the entire tableView.
For your second inquiry, I believe that one way it could be possible is through setting a delay for desired cells' scrollView subview. In your tableView(cellForRowAt: indexPath) func, you could have something like this:
if indexPath.section == 3 { //or whatever your desired section is
for view in cell.subviews {
if view is UIScrollView {
let currentView = view as! UIScrollView
currentView.delaysContentTouches = true
}
}
}
This will find the UIScrollView in your cell's subviews in your desired section. It will then set the .delaysContentTouches property accordingly.
I have not personally executed this code, just researched it, so let me know if it works.
Edit
Apparently the UIScrollView in UITableViewCell has been deprecated, so the above method will not work anymore.
My next best suggestion to you is to use a UILongPressGuestureRecognizer. This will not be quite the same thing as delaying the touch, but could have a similar effect in real execution.
You could use it in the same tableView(cellForRowAt: indexPath) func as so:
let press = UILongPressGestureRecognizer(target: self, action: #selector(handlePress))
press.minimumPressDuration = 2.0 //however long you want
cell.addGestureRecognizer(press)
Whatever you are trying to achieve by selecting certain rows of your tableView could be placed in the handlePress func above which would be trigged upon the long press.

Swift tableView bottom loading indicator

I am currently struggling with implementing a bottom loading indicator for my app, exactly like Instagram and Facebook has. Simply I want to show a loading indicator at the bottom (on reverse drag) just like a normal table view loading.
Here is the code that I have for the regular table view update:
var refreshControl: UIRefreshControl!
//In viewDidLoad
refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: "Refresh:", forControlEvents: UIControlEvents.ValueChanged)
tableView.addSubview(refreshControl)
In my func Refresh() I simply just fetch the data, and controls the activity indicator from there. However, how would I approach this, if I wanted to enable this in the bottom of my tableView?
Help is much appreciated.
Add it to the table footer view
tableView.tableFooterView = footerView
Adding a refreshControl would be difficult.
Add a UIIndicatorView to the footerview.
Implement scrollViewDidScroll:
func scrollViewDidScroll(scrollView: UIScrollView) {
if (scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height {
tableView.tableFooterView!.hidden = true
// call method to add data to tableView
}
}
Refresh Controller is for pulltorefresh which usually use to reload data.
Spinner at the bottom of the screen is used for pagination.
What you need to do in numberOfRows method return arraysize+1.
in cellforRowAtIndexPath method check if indexpath.row > arraySize than return a cell having uiactivitycenter in the center.
Hope this will help you.
If you are getting the data from API call with pagination, which has to show in table view Then Kuntal's answer is right, in addition you can make 0 hight for cell containing indicator view when there is no more data, and after completion of reload. It is easy manage than check and return arraysize+1.