Parallax effect for the pageViewController - ios

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

Related

UIScrollView in UIPopoverController

I am struggling to enable zooming in a UIPopoverController using a UIScrollView. The scrollView is 600x600 and it should display a view controller that displays a UIImageView. The image appears except it's not centered.
This is the code from the viewDidLoad method in the view controller that is displayed in the popover.
- (void)viewDidLoad {
self.imageView = [[UIImageView alloc] init];
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
self.imageView.image = self.image;
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.backgroundColor = [UIColor greenColor];
[self.scrollView setContentSize:self.imageView.image.size];
[self.scrollView setDelegate:self];
[self.imageView setFrame:CGRectMake(0, 0, 600, 600)];
self.view = self.scrollView;
[self.scrollView addSubview:self.imageView];
// Do any additional setup after loading the view.
}
The UIScrollView and UIImageView are declared as properties inside the UIViewController that's displayed in the popover. image is another property that is set to point to an image when the UIViewController is created.
This is what is looks like.
I would like to center the image in the popover and make it fit. How can I do that? Thanks.
what you need to do is:
first:
in your uiviewcontroller which is presented as a popover
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
preferredContentSize = CGSize(width: collectionPreference.bounds.width, height: collectionPreference.bounds.height)
println("I am appearing?")
}
then from the viewcontroller that is presenting the popover:
make it conform to UIPresentationStyleForPresentingController protocol,
and implement this two things:
number one:
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
number two: in your prepareforsegue method:
if let identifier = segue.identifier {
if identifier == "segueToYourPopoverVC" {
if let pVC = segue.destinationViewController as? YourPopoverVC {
let ppc = pVC.popoverPresentationController
ppc?.delegate = self
}
}
}
best of luck

UICollectionview scroll horizontal and vertical

I have to build a UICollectionView scrollable horizontal and vertical, I know that the grid layout scrolls along one axis only, either horizontally or vertically, so I have read some posts and I have tried different solutions but the most simple is to put the UICollectionview inside a UIScrollView. In this way the CollectionView scroll vertically and the UIScrollView horizontally.
The problem is that the vertical scroll is difficult, not fluid and often is stop until you tap again and drag again.
Can you suggest a solution? Thanks
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
UIScrollView *backgroundScroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
backgroundScroll.scrollEnabled = YES;
[self.view addSubview:backgroundScroll];
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(10, 15, 1020, [UIScreen mainScreen].bounds.size.height - 35) collectionViewLayout:layout];
[backgroundScroll addSubview:_collectionView];
_collectionView.contentInset = UIEdgeInsetsMake(0, 0, 50, 0);
_collectionView.scrollEnabled = YES;
And I have implemented the method:
- (void)viewDidLayoutSubviews {
backgroundScroll.contentSize = self.collectionView.frame.size;
}
The way to do this is to create a custom UICollectionViewLayout subclass.
I had to do this recently.
Let me go get the files... One sec...
First of all, you can't use a subclass of UICollectionViewFlowLayout easily for this. Flow layout is designed to fit the content in one direction and scroll in the other direction. This isn't what you want.
It isn't very difficult though to create a custom layout to do this for you.
Header File
#interface GridCollectionViewLayout : UICollectionViewLayout
// properties to configure the size and spacing of the grid
#property (nonatomic) CGSize itemSize;
#property (nonatomic) CGFloat itemSpacing;
// this method was used because I was switching between layouts
- (void)configureCollectionViewForLayout:(UICollectionView *)collectionView;
#end
Implementation
#import "GridCollectionViewLayout.h"
#interface GridCollectionViewLayout ()
#property (nonatomic, strong) NSDictionary *layoutInfo;
#end
#implementation GridCollectionViewLayout
Create inits for code and interface builder...
- (id)init
{
self = [super init];
if (self) {
[self setup];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
[self setup];
}
return self;
}
Setup defaults property values...
- (void)setup
{
self.itemSize = CGSizeMake(50.0, 50.0);
self.itemSpacing = 10.0;
}
This was used because I was changing between different layouts but it shows what is needed to set the layout..
- (void)configureCollectionViewForLayout:(UICollectionView *)collectionView
{
collectionView.alwaysBounceHorizontal = YES;
[collectionView setCollectionViewLayout:self animated:NO];
}
Required method. This iterates the items and creates frames CGRect for each one. Saving them into a dictionary.
- (void)prepareLayout
{
NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
NSInteger sectionCount = [self.collectionView numberOfSections];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
for (NSInteger section = 0; section < sectionCount; section++) {
NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < itemCount; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes =
[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
itemAttributes.frame = [self frameForAssessmentAtIndexPath:indexPath];
cellLayoutInfo[indexPath] = itemAttributes;
}
}
self.layoutInfo = cellLayoutInfo;
}
This is a convenience method for quickly getting a frame at a given index.
- (CGRect)frameForIndexPath:(NSIndexPath *)indexPath
{
NSInteger column = indexPath.section;
NSInteger row = indexPath.item;
CGFloat originX = column * (self.itemSize.width + self.itemSpacing);
CGFloat originY = row * (self.itemSize.height + self.itemSpacing);
return CGRectMake(originX, originY, self.itemSize.width, self.itemSize.height);
}
Required method to calculate the content size. This just multiplies the number of sections or items by the size and spacing properties. This is what allows scrolling in both directions because the content size can be bigger than the collection view's width AND height.
- (CGSize)collectionViewContentSize
{
NSInteger sectionCount = [self.collectionView numberOfSections];
if (sectionCount == 0) {
return CGSizeZero;
}
NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
CGFloat width = (self.itemSize.width + self.itemSpacing) * sectionCount - self.itemSpacing;
CGFloat height = (self.itemSize.height + self.itemSpacing) * itemCount - self.itemSpacing;
return CGSizeMake(width, height);
}
Required methods. These tell the collection view where each item needs to be placed.
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return self.layoutInfo[indexPath];
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *allAttributes = [NSMutableArray array];
[self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *stop) {
if (CGRectIntersectsRect(attributes.frame, rect)) {
[allAttributes addObject:attributes];
}
}];
return allAttributes;
}
#end
Of course, the layout in this case is specific to my individual problem.
The layout worked by having each section be a column and the items in each section were the rows. So something like this...
xy = item y in section x
00 10 20 30 ...
01 11 21 31 ...
02 12 22 32 ...
. . . .
. . . .
. . . .
Obviously there can be an unlimited number of sections or items in sections so I had to have scrolling in both directions.
Once you have created your layout class you just need to set it as the layout for your collection view. You can do this in code collectionView.collectionViewLayout = myLayout or you can do it in Interface Builder with the "layout" property on the collection view.
I'd like to introduce a different approach to creating a UICollectionView that scrolls in one direction while displaying cells containing a CollectionView that scrolls in the opposite direction. By implementing this collection view, setting the scrollDirection on the UICollectionViewFlowLayout instance used for the collection view in question this solution provides a seamless response to the user's interaction.
The solution subclasses the UICollectionView, and adds a delay gesture recognizer that intercepts the user's touches, delays them for a split seconds to figure out which direction the user is intending to scroll, then cancelling panningRecognizer on the collection view that don't scroll in that specific direction.
import Foundation
import UIKit
class UIDirectionAbidingCollectionView : UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
setupDelayRecognizer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupDelayRecognizer()
}
func setupDelayRecognizer() {
addGestureRecognizer(delayPanGestureRecognizer)
// Delay the touches on the default recognizer on the collection view
panGestureRecognizer.delaysTouchesBegan = true
}
// This gesture recognizer controls the response to the user's touches
// by cancelling by failing panGesture recognizer on the collection view
// that scrolls in the opposite direction.
lazy var delayPanGestureRecognizer: UIPanGestureRecognizer = {
var recognizer = UIPanGestureRecognizer()
recognizer.delegate = self
return recognizer
}()
}
extension UIDirectionAbidingCollectionView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// Ensure that the delay recognizer needs to fails for the
// default panning recognizers to receives the touches
if (gestureRecognizer == delayPanGestureRecognizer &&
otherGestureRecognizer == panGestureRecognizer)
{
return true
}
return false
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// If the recognizer in question is our delay recognizer
// lets check to see if it should begin receiving touches
if gestureRecognizer == delayPanGestureRecognizer {
// First retrieve the direction our flowlayout intends to scroll
if let flowLayout = self.collectionViewLayout as? UICollectionViewFlowLayout {
let scrollDirection = flowLayout.scrollDirection
// Retrieve the translation of the delayPanningRecognizer
let translation = delayPanGestureRecognizer.translation(in: self)
// Calculate the magnitude of change for the y and x axis
let xTransaltionValue = (translation.x * translation.x)
let yTransaltionValue = (translation.y * translation.y)
if scrollDirection == .vertical && xTransaltionValue > yTransaltionValue {
// If the scroll direction of the flowlayout is vertical,
// and the magnitude in the horizontal direction
// is greater than the horizontal, begin receiving touches.
// Since the delay recognizer doesn't fail, the vertical
// panning recognizer will fail to start on the collection view
return true
}
else if scrollDirection == .horizontal && xTransaltionValue < yTransaltionValue {
// If the scroll direction of the flowlayout is horizontal,
// and the magnitude in the vertical direction
// is greater than the horizontal, begin receiving touches.
// Since the delay recognizer doesn't fail, the horizontal
// panning recognizer will fail to start on the collection view
return true
}
else {
// Fail the delay recognizer, and allows the collection
// view to continue as usual
return false
}
}
}
return true
}
}

UIPageViewController detecting pan gestures

Is there a way to determine the panning location of a UIPageViewController while sliding left/right? I have been trying to accomplish this but its not working. I have a UIPageViewController added as a subview and i can slide it horizontally left/right to switch between pages however i need to determine the x,y coordinates of where I am panning on the screen.
I figured out how to do this. Basically a UIPageViewController uses UIScrollViews as its subviews. I created a loop and set all the subviews that are UIScrollViews and assigned their delegates to my ViewController.
/**
* Set the UIScrollViews that are part of the UIPageViewController to delegate to this class,
* that way we can know when the user is panning left/right
*/
-(void)initializeScrollViewDelegates
{
UIScrollView *pageScrollView;
for (UIView* view in self.pageViewController.view.subviews){
if([view isKindOfClass:[UIScrollView class]])
{
pageScrollView = (UIScrollView *)view;
pageScrollView.delegate = self;
}
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(#"Im scrolling, yay!");
}
My personal preference is not to rely too much on the internal structure of the PageViewController because it can be changed later which will break your code, unbeknownst to you.
My solution is to use a pan gesture recogniser. Inside viewDidLoad, add the following:
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handler))
gestureRecognizer.delegate = yourDelegate
view.addGestureRecognizer(gestureRecognizer)
Inside your yourDelegate's definition, you should implement the following method to allow your gesture recogniser to process the touches
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Now, you should be able to access the X/Y location of the user's touches:
func handler(_ sender: UIPanGestureRecognizer) {
let totalTranslation = sender.translation(in: view)
//...
}

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
}
}
}

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

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.

Resources