Locking a UISearchBar to the top of a UITableView like Game Center - ios

There's this cool feature in the UITableViews in Game Center and the search bars they have at their tops. Unlike apps where the search bar is placed in the table header view (so it counts as a standard table cell), instead, it seems to be bolted to the parent navigation bar above it.
So when scrolling the table, the search bar does indeed move, but if you scroll above the boundaries of the table, the search bar never stops touching the navigation bar.
Does anyone know how this might have been done? I was wondering if Apple maybe placed both the search bar and the table in a parent scroll view, but I'm wondering if it may be simpler than that.

Bob's answer is reversed: it ought to be MIN(0, scrollView.contentOffset.y).
Also, in order to properly support resizing (which would occur when rotated), the other frame values should be reused.
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
UISearchBar *searchBar = searchDisplayController.searchBar;
CGRect rect = searchBar.frame;
rect.origin.y = MIN(0, scrollView.contentOffset.y);
searchBar.frame = rect;
}

You could put the searchBar in the table header and implement the - (void)scrollViewDidScroll:(UIScrollView *)scrollView delegate method for the tableView. Doing something like this should work:
-(void) scrollViewDidScroll:(UIScrollView *)scrollView {
searchBar.frame = CGRectMake(0,MAX(0,scrollView.contentOffset.y),320,44);
}
If you used the searchDisplayController, you would access the searchbar using self.searchDisplayController.searchbar.

In Swift 2.1 and iOS 9.2.1
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
/* Search controller parameters */
searchController.searchResultsUpdater = self // This protocol allows your class to be informed as text changes within the UISearchBar.
searchController.dimsBackgroundDuringPresentation = false // In this instance,using current view to show the results, so do not want to dim current view.
definesPresentationContext = true // ensure that the search bar does not remain on the screen if the user navigates to another view controller while the UISearchController is active.
let tableHeaderView: UIView = UIView.init(frame: searchController.searchBar.frame)
tableHeaderView.addSubview(searchController.searchBar)
self.tableView.tableHeaderView = tableHeaderView
}
override func scrollViewDidScroll(scrollView: UIScrollView) {
let searchBar:UISearchBar = searchController.searchBar
var searchBarFrame:CGRect = searchBar.frame
if searchController.active {
searchBarFrame.origin.y = 10
}
else {
searchBarFrame.origin.y = max(0, scrollView.contentOffset.y + scrollView.contentInset.top)
}
searchController.searchBar.frame = searchBarFrame
}

While other answers seem helpful and partially do the job, it doesn't solve the issue of search bar not receiving the user's touches because it moves outside the bounds of its parent view as you change its frame.
What's worse is that, when you click on the search bar to make it the first responder, it is very likely that the tableView delegate will call tableView:didSelectRowAtIndexPath: on cell that is laid out under the search bar.
In order to address those issues described above, you need to wrap the search bar in a plain UIView, a view which is capable of processing touches occurred outside of its boundaries. By this way, you can relay those touches to the search bar.
So let's do that first:
class SearchBarView: UIView {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
for subview in subviews {
if !subview.userInteractionEnabled { continue }
let newPoint = subview.convertPoint(point, fromView: self)
if CGRectContainsPoint(subview.bounds, newPoint) {
return subview.hitTest(newPoint, withEvent: event)
}
}
return super.hitTest(point, withEvent: event)
}
}
Right now, we have a UIView subclass named SearchBarView which is capable of receiving touches occurred outside of its boundaries.
Secondly, we should put the search bar into that new view while the view controller is loading its view:
class TableViewController: UITableViewController {
private let searchBar = UISearchBar(frame: CGRectZero)
...
override func viewDidLoad() {
super.viewDidLoad()
...
searchBar.sizeToFit()
let searchBarView = SearchBarView(frame: searchBar.bounds)
searchBarView.addSubview(searchBar)
tableView.tableHeaderView = searchBarView
}
}
At last, we should update the frame of the search bar as user scrolls down the table view so that it will stay fixed at the top:
override func scrollViewDidScroll(scrollView: UIScrollView) {
searchBar.frame.origin.y = max(0, scrollView.contentOffset.y)
}
Here is the result:
--
Important note: If your table view has sections, they will probably shadow your search bar so you need to bring the search bar on top of them every time the table view's bounds gets updated.
viewDidLayoutSubviews is a good place to do that:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
...
if let tableHeaderView = tableView.tableHeaderView {
tableView.bringSubviewToFront(tableHeaderView)
}
}
--
Hope this helps. You can download the example project from here.

There's one more step if you want to fully emulate the search bars in Game Center.
If you start with friedenberg's excellent answer, as well as followben's modification for iOS 6+ mentioned in the comments, you still need to adjust the functionality when the search bar itself is active.
In Game Center, the search bars will scroll with the table as you scroll down, but will remain fixed below the navigation bar when you attempt to scroll up past the boundaries of the table. However, when the search bar is active and search results are being displayed, the search bar no longer scrolls with the table; it remains fixed in place below the navigation bar.
Here's the complete code (for iOS 6+) for implementing this. (If you're targeting iOS 5 or below, you don't need to wrap the UISearchBar in a UIView)
CustomTableViewController.m
- (void)viewDidLoad
{
UISearchBar *searchBar = [[UISearchBar alloc] init];
...
UIView *tableHeaderView = [[UIView alloc] initWithFrame:searchBar.frame];
[tableHeaderView addSubview:searchBar];
[self.tableView setTableHeaderView:tableHeaderView];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
UISearchBar *searchBar = self.tableView.tableHeaderView.subviews.lastObject;
CGRect searchBarFrame = searchBar.frame;
/*
* In your UISearchBarDelegate implementation, set a boolean flag when
* searchBarTextDidBeginEditing (true) and searchBarTextDidEndEditing (false)
* are called.
*/
if (self.inSearchMode)
{
searchBarFrame.origin.y = scrollView.contentOffset.y;
}
else
{
searchBarFrame.origin.y = MIN(0, scrollView.contentOffset.y);
}
searchBar.frame = searchBarFrame;
}
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
self.inSearchMode = YES;
}
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
self.inSearchMode = NO;
}
Voilà! Now, when the search bar is inactive it will move with the table, and remain fixed when attempting to move beyond the table boundaries. When active, it will remain fixed in place, just like in Game Center.

All of the other answers here provided me with helpful information, but none of them worked using iOS 7.1. Here's a simplified version of what worked for me:
MyViewController.h:
#interface MyViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UISearchDisplayDelegate> {
}
#end
MyViewController.m:
#implementation MyViewController {
UITableView *tableView;
UISearchDisplayController *searchDisplayController;
BOOL isSearching;
}
-(void)viewDidLoad {
[super viewDidLoad];
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
searchBar.delegate = self;
searchDisplayController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
searchDisplayController.delegate = self;
searchDisplayController.searchResultsDataSource = self;
searchDisplayController.searchResultsDelegate = self;
UIView *tableHeaderView = [[UIView alloc] initWithFrame:searchDisplayController.searchBar.frame];
[tableHeaderView addSubview:searchDisplayController.searchBar];
[tableView setTableHeaderView:tableHeaderView];
isSearching = NO;
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
UISearchBar *searchBar = searchDisplayController.searchBar;
CGRect searchBarFrame = searchBar.frame;
if (isSearching) {
searchBarFrame.origin.y = 0;
} else {
searchBarFrame.origin.y = MAX(0, scrollView.contentOffset.y + scrollView.contentInset.top);
}
searchDisplayController.searchBar.frame = searchBarFrame;
}
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
isSearching = YES;
}
-(void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller {
isSearching = NO;
}
#end
Note: If you're using "pull down to refresh" on your list, you'll need to replace scrollView.contentInset.top in scrollViewDidScroll: with a constant to allow the search bar to scroll over the refresh animation.

If your deployment target is iOS 9 and higher then you can use anchors and set UISearchBar and UITableView programmatically:
private let tableView = UITableView(frame: .zero, style: .plain)
private let searchBar = UISearchBar(frame: CGRect .zero)
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.contentInset = UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
searchBar.delegate = self
view.addSubview(tableView)
view.addSubview(searchBar)
NSLayoutConstraint.activate([
searchBar.heightAnchor.constraint(equalToConstant: 44.0),
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
searchBar.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor)
])
}
I assume that you create UISearchBar and UITableView from code, not in storyboard.

Related

UISearchBar increases navigation bar height in iOS 11

I have my UISearchBar being part of the navigation bar like:
let searchBar = UISearchBar()
//some more configuration to the search bar
.....
navigationItem.titleView = searchBar
After updating to iOS 11 something weird happened to the search bar in my app. On iOS 10 and prior I had my navigation bar looking like:
Now with iOS 11 I have:
As you can see there is difference in the rounding of the two search bars which does not bothers me. The problem is that the search bar increases the height of the navigation bar. So when I go to another controller it looks weird too:
In fact that weird black line's height plus the current navigation bar's height is equal to the height of navigation bar shown in the second picture ...
Any ideas how to get rid of the black line and having consistent navigation bar height across all view controllers ?
I got black line under NavigationBar with SearchBar in iOS 11 in two cases:
when i pushed another ViewControllers from ViewController with UISearchBar
when i dismissed ViewController with UISearchBar with "drag right to dismiss"
My solution was: adding this code to my ViewController with UISearchBar:
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[self.navigationController.view setNeedsLayout]; // force update layout
[self.navigationController.view layoutIfNeeded]; // to fix height of the navigation bar
}
Swift 4 Update
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.view.setNeedsLayout() // force update layout
navigationController?.view.layoutIfNeeded() // to fix height of the navigation bar
}
You can add a constraint of height 44 to the search bar for iOS 11.
// Swift
if #available(iOS 11.0, *) {
searchBar.heightAnchor.constraint(equalToConstant: 44).isActive = true
}
// Objective-C
if (#available(iOS 11.0, *)) {
[searchBar.heightAnchor constraintEqualToConstant:44].active = YES;
}
I believe in iOS 11 UISearchBar now has the height equals to 56, and UINavigationBar uses autolayout to fit its subviews hence it increases the height. If you still want to have UISearchBar as titleView as in pre-iOS 11, I found out the best way to do it is to embed UISearchBar in a custom view, and set this view's height to 44, and assign it to navigationItem.titleView
class SearchBarContainerView: UIView {
let searchBar: UISearchBar
init(customSearchBar: UISearchBar) {
searchBar = customSearchBar
super.init(frame: CGRect.zero)
addSubview(searchBar)
}
override convenience init(frame: CGRect) {
self.init(customSearchBar: UISearchBar())
self.frame = frame
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
searchBar.frame = bounds
}
}
class MyViewController: UIViewController {
func setupNavigationBar() {
let searchBar = UISearchBar()
let searchBarContainer = SearchBarContainerView(customSearchBar: searchBar)
searchBarContainer.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 44)
navigationItem.titleView = searchBarContainer
}
}
try this code on "ACKNOWLEDGEMENTS" view controller
in viewDidLoad
self.extendedLayoutIncludesOpaqueBars = true
Thank you all! I finally found a solution.
Adding the following code to ViewController with UISearchBar.
First step: viewDidLoad
-(void)viewDidLoad
{
[super viewDidLoad];
self.extendedLayoutIncludesOpaqueBars = YES;
...
}
override func viewDidLoad() {
super.viewDidLoad()
self.extendedLayoutIncludesOpaqueBars = true
}
Second step:viewWillDisappear
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
// force update layout
[self.navigationController.view setNeedsLayout];
// to fix height of the navigation bar
[self.navigationController.view layoutIfNeeded];
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.view.setNeedsLayout() // force update layout
navigationController?.view.layoutIfNeeded() // to fix height of the navigation bar
}
In Objective-C
if (#available(iOS 11.0, *)) {
[self.searchBar.heightAnchor constraintLessThanOrEqualToConstant: 44].active = YES;
}
This happen to me too, all running well in iOS 12.4 and getting weird in 13 above.
The problem is in iOS 13 navigation bar height increase from 88 to 100 after jump from UIViewController that implement searchBar.
Try this in your UIViewController that implement searchBar.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.view.setNeedsLayout()
navigationController?.view.layoutIfNeeded()
}
Preview after fixing:
Preview before fixing:
EDIT: The #zgjie answer is a better solution for this problem: https://stackoverflow.com/a/46356265/1713123
It seems this happens because in iOS 11 the default height value of SearchBar was changed to 56, instead 44 on previous iOS versions.
For now, I've applied this workaround, setting searchBar height back to 44:
let barFrame = searchController.searchBar.frame
searchController.searchBar.frame = CGRect(x: 0, y: 0, width: barFrame.width, height: 44)
Another solution could be use the new searchController property on navigationItem in iOS 11:
navigationItem.searchController = searchController
But this way da searchBar appears below navigation title.
All solution didn't work for me so before I pushed view controller I did:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationItem.titleView = UIView()
}
And to make search bar present when going back:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.titleView = UISearchBar()
}
I couldn't use the solution of keeping the navBar at 44.
So it took me a day but finally, I found a solution that doesn't change the bar height and position the button in the middle of the bar. The issue is that the buttons are placed in a stack view which is configured as Horizontal stack view and therefore doesn't adjust to the height change.
This is done on init:
UIBarButtonItem *cancelButton;
if (#available(iOS 11.0, *)) {
// For iOS11 creating custom button to accomadate the change of navbar + search bar being 56 points
self.navBarCustomButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.navBarCustomButton setTitle:#"Cancel"];
[self.navBarCustomButton addTarget:self action:#selector(cancelButtonTapped) forControlEvents:UIControlEventTouchUpInside];
cancelButton = [[UIBarButtonItem alloc] initWithCustomView:self.navBarCustomButton];
} else {
cancelButton = [[UIBarButtonItem alloc] initWithTitle:MagicLocalizedString(#"button.cancel", #"Cancel")
style:UIBarButtonItemStylePlain
target:self
action:#selector(cancelButtonTapped)];
}
on viewWillApear (or anytime after the view was added to the navigation stack)
if (#available(iOS 11.0, *)) {
UIView *buttonsStackView = [navigationController.navigationBar subviewOfClass:[UIStackView class]];
if (buttonsStackView ) {
[buttonsStackView.centerYAnchor constraintEqualToAnchor:navigationController.navigationBar.centerYAnchor].active = YES;
[self.navBarCustomButton.heightAnchor constraintEqualToAnchor:buttonsStackView.heightAnchor];
}
}
And subviewOfClass is a category on UIView:
- (__kindof UIView *)subviewOfClass:(Class)targetClass {
// base case
if ([self isKindOfClass:targetClass]) {
return self;
}
// recursive
for (UIView *subview in self.subviews) {
UIView *dfsResult = [subview subviewOfClass:targetClass];
if (dfsResult) {
return dfsResult;
}
}
return nil;
}
All you have to do is to subclass UISearchBar and override "intrinsicContentSize":
#implementation CJSearchBar
-(CGSize)intrinsicContentSize{
CGSize s = [super intrinsicContentSize];
s.height = 44;
return s;
}
#end
Unable to comment, but wanted to share some additional issues I ran into while spending many hours trying to get to the bottom of this issue even after using one of the other solutions.
It appears the best fix for me was Andrew's answer:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.view.setNeedsLayout() // force update layout
navigationController?.view.layoutIfNeeded() // to fix height of the navigation bar
}
However, at the very least in iOS 12.1, if your UINavigationBar:
has isTranslucent set to false, the View Controller with the search bar appears to not get it's view's layout adjusted back when interactively dismissing (normal dismissing via back button appears to work).
has it's background image set using setBackgroundImage(UIImage(), for: .default), the transition animation doesn't work properly and will jump back to its position after finishing.
These particular properties were set to get the Navigation Bar to appear in a certain way however, so I need to do some adjusting to get it back, or put up with the weird behaviour. Will try to remember to update the above if I run into anything else or find other solutions or differences in other OS versions.
In my case, bigger UINavigationBar's height wasn't a problem for me. I just needed to realign left and right bar button items. That's the solution i've come up with:
- (void)iOS11FixNavigationItemsVerticalAlignment
{
[self.navigationController.navigationBar layoutIfNeeded];
NSString * currSysVer = [[UIDevice currentDevice] systemVersion];
if ([currSysVer compare:#"11" options:NSNumericSearch] != NSOrderedAscending)
{
UIView * navigationBarContentView;
for (UIView * subview in [self.navigationController.navigationBar subviews])
{
if ([subview isKindOfClass:NSClassFromString(#"_UINavigationBarContentView")])
{
navigationBarContentView = subview;
break;
}
}
if (navigationBarContentView)
{
for (UIView * subview in [navigationBarContentView subviews])
{
if (![subview isKindOfClass:NSClassFromString(#"_UIButtonBarStackView")]) continue;
NSLayoutConstraint * topSpaceConstraint;
NSLayoutConstraint * bottomSpaceConstraint;
CGFloat topConstraintMultiplier = 1.0f;
CGFloat bottomConstraintMultiplier = 1.0f;
for (NSLayoutConstraint * constraint in navigationBarContentView.constraints)
{
if (constraint.firstItem == subview && constraint.firstAttribute == NSLayoutAttributeTop)
{
topSpaceConstraint = constraint;
break;
}
if (constraint.secondItem == subview && constraint.secondAttribute == NSLayoutAttributeTop)
{
topConstraintMultiplier = -1.0f;
topSpaceConstraint = constraint;
break;
}
}
for (NSLayoutConstraint * constraint in navigationBarContentView.constraints)
{
if (constraint.firstItem == subview && constraint.firstAttribute == NSLayoutAttributeBottom)
{
bottomSpaceConstraint = constraint;
break;
}
if (constraint.secondItem == subview && constraint.secondAttribute == NSLayoutAttributeBottom)
{
bottomConstraintMultiplier = -1.0f;
bottomSpaceConstraint = constraint;
break;
}
}
CGFloat contentViewHeight = navigationBarContentView.frame.size.height;
CGFloat subviewHeight = subview.frame.size.height;
topSpaceConstraint.constant = topConstraintMultiplier * (contentViewHeight - subviewHeight) / 2.0f;
bottomSpaceConstraint.constant = bottomConstraintMultiplier * (contentViewHeight - subviewHeight) / 2.0f;
}
}
}
}
Basically, we search for stack views that contain bar button items and then changing their's top and bottom constraints values. Yeah, it's a dirt hack, and won't recommend to use it if you can fix your issue in any other way.
//
// Created by Sang Nguyen on 10/23/17.
// Copyright © 2017 Sang. All rights reserved.
//
import Foundation
import UIKit
class CustomSearchBarView: UISearchBar {
final let SearchBarHeight: CGFloat = 44
final let SearchBarPaddingTop: CGFloat = 8
override open func awakeFromNib() {
super.awakeFromNib()
self.setupUI()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// fatalError("init(coder:) has not been implemented")
}
func findTextfield()-> UITextField?{
for view in self.subviews {
if view is UITextField {
return view as? UITextField
} else {
for textfield in view.subviews {
if textfield is UITextField {
return textfield as? UITextField
}
}
}
}
return nil;
}
func setupUI(){
if #available(iOS 11.0, *) {
self.translatesAutoresizingMaskIntoConstraints = false
self.heightAnchor.constraint(equalToConstant: SearchBarHeight).isActive = true
}
}
override func layoutSubviews() {
super.layoutSubviews()
if #available(iOS 11.0, *) {
if let textfield = self.findTextfield() {
textfield.frame = CGRect(x: textfield.frame.origin.x, y: SearchBarPaddingTop, width: textfield.frame.width, height: SearchBarHeight - SearchBarPaddingTop * 2)`enter code here`
return
}
}
}
}
I found Mai Mai's solution to be the only one that's really usable.
However it's still not perfect:
When rotating the device, the search bar is not properly resized and remains in the smaller dimension.
I have found a fix for that. Here is my code in Objective C with the relevant parts annotated:
// improvements in the search bar wrapper
#interface SearchBarWrapper : UIView
#property (nonatomic, strong) UISearchBar *searchBar;
- (instancetype)initWithSearchBar:(UISearchBar *)searchBar;
#end
#implementation SearchBarWrapper
- (instancetype)initWithSearchBar:(UISearchBar *)searchBar {
// setting width to a large value fixes stretch-on-rotation
self = [super initWithFrame:CGRectMake(0, 0, 4000, 44)];
if (self) {
self.searchBar = searchBar;
[self addSubview:searchBar];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.searchBar.frame = self.bounds;
}
// fixes width some cases of resizing while search is active
- (CGSize)sizeThatFits:(CGSize)size {
return size;
}
#end
// then use it in your VC
#implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.titleView = [[SearchBarWrapper alloc] initWithSearchBar:self.searchController.searchBar];
}
#end
Now there is still one case left that I haven't figured out yet. To reproduce do the following:
- start in portrait
- activate search field
- rotate to landscape
- error: the bar doesn't resize
I fixed this by added the constraint to viewDidAppear on the map view controller where the search bar is embedded
public override func viewDidAppear(_ animated: Bool) {
if #available(iOS 11.0, *) {
resultSearchController?.searchBar.heightAnchor.constraint(equalToConstant: 44).isActive = true
// searchBar.heightAnchor.constraint(equalToConstant: 44).isActive = true
}
}
Hi to the people who uses UISearchController and then attaching its UISearchBar to the navigationItem.titleView. I've spend a crazy 4-5 hours of my day to solve this. Following the iOS 11+ recommended approach, which is putting the searchController to the navigation.searchController is not just right for my case. The screen that has this searchController/searchBar has a backButton, a custom one.
I have tested this in iOS 10, iOS 11, and 12. In different devices. I just had to. I can't go home without solving this demon. This is the most perfect I could do for today, given my tight deadline.
So I just wanna share this hard work that I did, it's up to you to put everything into where ever you want (ex. variables in your viewModel). Here it goes:
In my first screen (say home screen, that does not have this search controller), I have this in my viewDidLoad().
self.extendedLayoutIncludesOpaqueBars = true
In my second screen, the one that has the searchController, I have this in my viewDidAppear.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let systemMajorVersion = ProcessInfo.processInfo.operatingSystemVersion.majorVersion
if systemMajorVersion < 12 {
// Place the search bar in the navigation item's title view.
self.navigationItem.titleView = self.searchController.searchBar
}
if systemMajorVersion >= 11 {
self.extendedLayoutIncludesOpaqueBars = true
UIView.animate(withDuration: 0.3) {
self.navigationController?.navigationBar.setNeedsLayout()
self.navigationController?.navigationBar.layoutIfNeeded()
}
self.tableView.contentInset = UIEdgeInsets(top: -40, left: 0, bottom: 0, right: 0)
if self.viewHadAppeared {
self.tableView.contentInset = .zero
}
}
self.viewHadAppeared = true // this is set to false by default.
}
and here's my searchController's declaration:
lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.hidesNavigationBarDuringPresentation = false
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.textField?.backgroundColor = .lalaDarkWhiteColor
searchController.searchBar.textField?.tintColor = .lalaDarkGray
searchController.searchBar.backgroundColor = .white
return searchController
}()
So I hope this helps someone someday.
I tried various things to get the size back to the original 44, but then the search bar always looks and behaves weird - like being to far stretched, y-offset and alike.
I found a nice solution here (via some other stackoverflow post):
https://github.com/DreamTravelingLight/searchBarDemo
Just derive your viewcontroller from the SearchViewController and include in your project the SearchViewController and WMSearchbar classes. Worked out of the box for me without any ugly if (iOS11) else... uglyness.
In my case, I have to decrease the textField's height 36pt -> 28pt.
So I tried to change the frame's height, layer's height. But the ways didn't work.
Finally, I found a solution that's the mask.
I think, It's not a good way but it works.
let textField = searchBar.value(forKey: "searchField") as? UITextField
textField?.font = UIFont.systemFont(ofSize: 14.0, weight: .regular)
textField?.textColor = #colorLiteral(red: 0.1960784314, green: 0.1960784314, blue: 0.1960784314, alpha: 1)
textField?.textAlignment = .left
if #available(iOS 11, *) {
let radius: CGFloat = 5.0
let magnifyIconWidth: CGFloat = 16.0
let inset = UIEdgeInsets(top: 4.0, left: 0, bottom: 4.0, right: 0)
let path = CGMutablePath()
path.addArc(center: CGPoint(x: searchBar.bounds.size.width - radius - inset.right - magnifyIconWidth, y: inset.top + radius), radius: radius, startAngle: .pi * 3.0/2.0, endAngle: .pi*2.0, clockwise: false) // Right top
path.addArc(center: CGPoint(x: searchBar.bounds.size.width - radius - inset.right - magnifyIconWidth, y: searchBar.bounds.size.height - radius - inset.bottom), radius: radius, startAngle: 0, endAngle: .pi/2.0, clockwise: false) // Right Bottom
path.addArc(center: CGPoint(x: inset.left + radius, y: searchBar.bounds.size.height - radius - inset.bottom), radius: radius, startAngle: .pi/2.0, endAngle: .pi, clockwise: false) // Left Bottom
path.addArc(center: CGPoint(x: inset.left + radius, y: inset.top + radius), radius: radius, startAngle: .pi, endAngle: .pi * 3.0/2.0, clockwise: false) // Left top
let maskLayer = CAShapeLayer()
maskLayer.path = path
maskLayer.fillRule = kCAFillRuleEvenOdd
textField?.layer.mask = maskLayer
}
You can change the insets, if you want to change the textField's frame.

Hide UINavigationBar without losing Status Bar background

I'm working on an app where we use the white status bar tint and a dark background for the navigationBar. There is one scene where we want the navigationBar hidden but it also takes away the background color for the status bar. Is there a simple solution to keep a dark background up with hiding the navigationBar at the same time?
My code to hide the navigation bar is:
[self.navigationController setNavigationBarHidden:YES];
or in Swift:
self.navigationController?.navigationBarHidden = true
Assuming you are developing for iOS7+: The statusbar doesn' have any background color on its own. In fact, the reason you are seeing a dark background when the navigation bar is visible, is because it extends upwards underneath the statusbar. So if you want to keep the status bar background you can simply add a view with an appropriate background color to the current scene (viewController, window , etc.). Give it a frame of UIApplication.sharedApplication.statusBarFrame.
-- UPDATE 1 --
Sample code (Swift 3) Gives you a solid black status bar background:
class ViewController: UIViewController {
private let statusBarUnderlay = UIView()
override func viewDidLoad() {
super.viewDidLoad()
statusBarUnderlay.backgroundColor = UIColor.black
view.addSubview(statusBarUnderlay)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
statusBarUnderlay.frame = UIApplication.shared.statusBarFrame
}
}
-- UPDATE 2 --
While we're at it. The above code is not how you should lay out your views. Instead, subclass UIView and do your layout there. Then override loadView of your UIViewController subclass and return an instance of your custom view.
class View: UIView {
private let statusBarUnderlay = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
statusBarUnderlay.backgroundColor = UIColor.black
addSubview(statusBarUnderlay)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Storyboards are incompatible with truth and beauty.")
}
override func layoutSubviews() {
super.layoutSubviews()
statusBarUnderlay.frame = UIApplication.shared.statusBarFrame
}
}
All I needed to do was add a secondary view and set it to below the status bar like this:
-(void)viewForStatusBar {
UIView *view = [UIView new];
[self.view addSubview:view];
view.backgroundColor = [UIColor blackColor];
view.frame = CGRectMake(0, -20, [UIScreen mainScreen].bounds.size.width, 20);
}
Or in Swift:
func viewForStatusBar() {
let view = UIView()
self.view.addSubview(view)
view.backgroundColor = .blackColor()
view.frame = CGRectMake(0, -20, UIScreen.mainScreen().bounds.size.width, 20)
}
Here's the implementation of LTNavigationBar
I think it may help you with.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat offsetY = scrollView.contentOffset.y;
if (offsetY > 0) {
if (offsetY >= 44) {
[self setNavigationBarTransformProgress:1];
} else {
[self setNavigationBarTransformProgress:(offsetY / 44)];
}
} else {
[self setNavigationBarTransformProgress:0];
self.navigationController.navigationBar.backIndicatorImage = [UIImage new];
}
}
- (void)setNavigationBarTransformProgress:(CGFloat)progress
{
[self.navigationController.navigationBar.transform = CGAffineTransformMakeTranslation:(0, -44 * progress)];
}
It makes the navigation bar hidden and status bar have the same background color as navigation bar when scrolling the view.If you don't need scrolling, you can just call [self setNavigationBarTransformProgress:1]

Parallax effect for the pageViewController

I have a pageViewController - I would like to add a scrollview with an image view behind it and while I scroll the pages in my pageViewController - the background should scroll in the same direction but with a lower see. I use auto-layout in storyboards:
so I add the pageViewController:
pageController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:nil];
pageController.delegate = self;
pageController.dataSource = self;
[self addChildViewController:pageController];
CGRect pageFrame = self.view.frame;
pageFrame.origin.y += 50.f;
pageFrame.size.height -= 50.f;
pageController.view.frame = pageFrame;
pageController.view.backgroundColor = [UIColor clearColor];
[self.view addSubview:pageController.view];
get it's scrollview:
for (UIView *possibleScrollView in pageController.view.subviews) {
if ([possibleScrollView isKindOfClass:[UIScrollView class]]) {
((UIScrollView *)possibleScrollView).delegate = self;
}
}
and listening for its delegate:
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(#"%f", scrollView.contentOffset.x);
[parlaxScrollView setContentOffset:CGPointMake(scrollView.contentOffset.x * .2f, scrollView.contentOffset.y) animated:NO];
}
And here I have some confusing results when I scrolling to the second page:
378.500000
386.500000
403.500000
419.000000
448.000000
469.000000
...
747.000000
750.000000
375.000000 ///!!!!!THE CONTENT OFFSET RETURNED TO THE INITIAL VALUE!!!!!
Why does my content view offset reset? Wha't wrong with it?
5 years later, sorry for the delay 😅
Maybe other people will still have the same issue.
You are not supposed to change the delegate of the page view controller's scroll view. It can break its normal behaviour.
Instead, you can:
Add a pan gesture to the page view controller's view:
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panRecognized(gesture:)))
view.addGestureRecognizer(panGesture)
panGesture.delegate = self
Add the new function in order to know how the view is being scrolled.
#objc func panRecognized(gesture: UIPanGestureRecognizer) {
// Do whatever you need with the gesture.translation(in: view)
}
Declare your ViewController as UIGestureRecognizerDelegate.
Implement this function:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
I think you need to keep track of which page is currently being displayed and add the width to the content offset.
something like this:
let currentPage:Int

keep the UISearchBarDisplayController on Top of tableview

i'm trying to keep the UISearchBarDisplayController on top of UItableview in swift
i tried this code (which i convert from objectC to swift) but no luck:
override func scrollViewDidScroll(scrollView: UIScrollView) {
//
var searchBar: UISearchBar = (self.searchDisplayController?.searchBar)!
//
var rect = searchBar.frame
self.searchDisplayController?.searchBar.frame = CGRectMake(0, max(0, scrollView.contentOffset.y), rect.width, rect.height);
}
the search bar still scroll with tableview when i scroll the table. Does anyone can help me to fix it? thanks
i found that i have to make a new view controller , add a tableview and add a searchBar outside of the tableview. and the search bar won't scroll with the tableview
In Swift 2.1, iOS 9.2.1 and Xcode 7.2
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
/* Search controller parameters */
searchController.searchResultsUpdater = self // This protocol allows your class to be informed as text changes within the UISearchBar.
searchController.dimsBackgroundDuringPresentation = false // In this instance,using current view to show the results, so do not want to dim current view.
definesPresentationContext = true // ensure that the search bar does not remain on the screen if the user navigates to another view controller while the UISearchController is active.
let tableHeaderView: UIView = UIView.init(frame: searchController.searchBar.frame)
tableHeaderView.addSubview(searchController.searchBar)
self.tableView.tableHeaderView = tableHeaderView
}
override func scrollViewDidScroll(scrollView: UIScrollView) {
let searchBar:UISearchBar = searchController.searchBar
var searchBarFrame:CGRect = searchBar.frame
if searchController.active {
searchBarFrame.origin.y = 10
}
else {
searchBarFrame.origin.y = max(0, scrollView.contentOffset.y + scrollView.contentInset.top)
}
searchController.searchBar.frame = searchBarFrame
}

Dismissing the keyboard in a UIScrollView

Alright, I have a couple of UITextFields and UITextViews inside a UIScrollView, and I'd like to set the keyboard to disappear whenever the scrollview is touched or scrolled (except when you touch down inside the text field/view, of course).
My current attempt at doing this is replacing the UIScrollView with a subclass, and setting it to call a removeKeyboard function (defined inside the main view controller) inside the touchesBegan method. However, this only removes the keyboard for a normal touch, not when the view is simply scrolled. So, what's the best way to remove the keyboard inside a UIScrollView?
Thanks in advance for your help.
Here is the cleanest way to achieve this in iOS 7.0 and above.
scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
Or
scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
In Swift:
scrollView.keyboardDismissMode = .onDrag
Or
scrollView.keyboardDismissMode = .interactive
Bit late but if anyone else is searching an answer to this problem, this is how I have gone about solving it:
1) Create a tap gesture recognizer with a target callback method to dismiss your keyboard using resignFirstResponder on all your fields.
2) Add the tap gesture to the scrollview.
Here's an example:
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(hideKeyboard)];
// prevents the scroll view from swallowing up the touch event of child buttons
tapGesture.cancelsTouchesInView = NO;
[pageScrollView addGestureRecognizer:tapGesture];
[tapGesture release];
...
// method to hide keyboard when user taps on a scrollview
-(void)hideKeyboard
{
[myTextFieldInScrollView resignFirstResponder];
}
Although the essence is the same, I prefer less code.
Setting the keyboard to disappear when the scrollView is scrolled in Attributes inspector:
Then disappear keyboard when scrollView is tapped:
Drag a Tap Gesture Recognizer onto your scrollView
Only one line in the action —— scrollView.endEditing(true). If you are using Objective-C, [self.scrollView endEditing: YES];
In Swift:
Bit late but if anyone else is searching an answer to this problem, this is how I have gone about solving it:
1) Create a tap gesture recognizer with a target callback method to dismiss your keyboard using resignFirstResponder on all your fields.
2) Add the tap gesture to the scrollview.
Here's an example:
import UIKit
class ViewController: UIViewController {
#IBOutlet var t1: UITextField!
#IBOutlet var t2: UITextField!
#IBOutlet var t3: UITextField!
#IBOutlet var t4: UITextField!
#IBOutlet var srcScrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideKeyboard")
// prevents the scroll view from swallowing up the touch event of child buttons
tapGesture.cancelsTouchesInView = false
srcScrollView.addGestureRecognizer(tapGesture)
}
func hideKeyboard() {
t1.resignFirstResponder()
t2.resignFirstResponder()
t3.resignFirstResponder()
t4.resignFirstResponder()
}
}
Look at keyboardDismissMode property of UIScrollView.
// will hide keyboard when your text field is about to go beyond the keyboard.
vwScrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
// will hide keyboard instantly once the scroll view started scrolling by user.
vwScrollView.keyboardDismissMode = UIScrollViewKeyboardDismissOnDrag;
// If you need to hide keyboard on tap of scroll view,consider adding a tap gesture or sub class and override touchesbegan: method.
Swift Version
vwScrollView.keyboardDismissMode = .interactive
vwScrollView.keyboardDismissMode = .onDrag
Create a extension class for hiding keyboard when touches scrollview/view anywhere
extension UIViewController {
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
#objc func dismissKeyboard() {
view.endEditing(true)
}
}
And call this method in viewDidLoad like
override func viewDidLoad() {
super.viewDidLoad()
self.hideKeyboardWhenTappedAround()
}
Try This
[self.selectedViewController.view endEditing:YES];
A bit late but if anyone else is searching an answer to this problem with Swift 3:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
view.endEditing(true)
}
When I added the gesture to a subclass of UIScrollView, I was having problems with the various gestures in my view tree interfering with each other, such as being able to click on subviews, scroll the view, and have the keyboard dismiss in all cases. I came up with this solution, which can be setup from a superclass of UIScrollView or from a UIViewController.
The DismissKeyboardTapGesture class uses ARC, works with any text fields under the view, and doesn't take over any clicks from subviews like buttons. Also takes advantage of iOS7 scrolling effect to dismiss keyboard.
Setting up from UISScrollView superclass:
_dismissKeyboard = [[DismissKeyboardTapGesture alloc] initWithView:self];
or from UIViewController:
_dismissKeyboard = [[DismissKeyboardTapGesture alloc] initWithView:self.view];
Here is the class:
#interface DismissKeyboardTapGesture : NSObject <UIGestureRecognizerDelegate>
#end
#implementation DismissKeyboardTapGesture
- (id)initWithView:(UIView *)view
{
self = [super init];
if (self) {
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTap:)];
singleTap.cancelsTouchesInView = NO;
singleTap.delegate = self;
[view addGestureRecognizer:singleTap];
if ([view respondsToSelector:#selector(setKeyboardDismissMode:)]) {
// Bonus effect to dismiss keyboard by scrolling
((UIScrollView *)view).keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
}
}
return self;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
// Don't stop any existing gestures in our view from working
if (otherGestureRecognizer.view == gestureRecognizer.view) {
return YES;
}
return NO;
}
- (void)singleTap:(UIGestureRecognizer*)gestureRecognizer
{
// Close keyboard for any text edit views that are children of the main view
[gestureRecognizer.view endEditing:YES];
}
#end
Try this scroll view delegate method -
link delegate in IB to scroll view and then cop this code (modify as per your need).
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
//sample code
[challengeABallotComponent.voterNameTextField resignFirstResponder];
[challengeABallotComponent.ballotNumberTextField resignFirstResponder];
[locationInformation.pollingLocation resignFirstResponder];
}
This should work. You can try other delegate methods too like
-(void)scrollViewDidScroll: (UIScrollView *)scrollView
{
//do your stuff
}
scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
extension UIView{
//Set tag via storyboard
func keyboardDissmissInteractiveMode(_ tag:Int){
if let scrollView = self.viewWithTag(tag) as? UIScrollView{
scrollView.keyboardDismissMode = .interactive
}
if let tableview = self.viewWithTag(tag) as? UITableView{
tableview.keyboardDismissMode = .interactive
}
}
func keyboardDissmissOnDragMode(_ tag:Int){
if let scrollView = self.viewWithTag(tag) as? UIScrollView{
scrollView.keyboardDismissMode = .onDrag
}
if let tableview = self.viewWithTag(tag) as? UITableView{
tableview.keyboardDismissMode = .onDrag
}
}
func keyboardDissmissInteractiveMode(_ view:UIView){
if let scrollView = view as? UIScrollView{
scrollView.keyboardDismissMode = .interactive
}
if let tableview = view as? UITableView{
tableview.keyboardDismissMode = .interactive
}
}
func keyboardDissmissOnDragMode(_ view:UIView){
if let scrollView = view as? UIScrollView{
scrollView.keyboardDismissMode = .onDrag
}
if let tableview = view as? UITableView{
tableview.keyboardDismissMode = .onDrag
}
}
}

Resources