On iOS devices the section headers in UITableView's have a nice behavior where they stick or 'float' to the top of the screen as you scroll through a section. The section headers in my particular case are loaded from other XIB files.
Is it possible to change the section headers depending on whether or not they are currently floating? Specifically I'd like to add a small shadow to appear under the header only while it's stuck to the top of the view.
Thanks!
Here's the function I created to update whether each header has a shadow or not. All the section headers in this case are a UIView subclass ListHeader. They're retained and returned by the viewForHeaderInSection function.
- (void) updateHeaderShadows {
int i=0;
int sectionHeight = 0;
int totalHeight = 0;
UIView * sectionHeader;
while (i<[self numberOfSectionsInTableView:self.tableView]) {
sectionHeight = [self.tableView rectForSection:i].size.height;
sectionHeader = [self tableView:self.tableView viewForHeaderInSection:i];
if ([sectionHeader respondsToSelector:#selector(shadow)]) {
if (sectionHeader.frame.origin.y == totalHeight || sectionHeader.frame.origin.y == totalHeight + sectionHeight - sectionHeader.frame.size.height) {
[((ListHeader *) sectionHeader).shadow setHidden:YES];
} else {
[((ListHeader *) sectionHeader).shadow setHidden:NO];
}
}
totalHeight += sectionHeight;
i++;
}
}
I haven't tested it yet, but I don't see any reason why it wouldn't be possible.
Just make sure you set the right bounds (because your shadow needs to be on top of your view, not above it).
You can use the following approach:
Use scrollView:didScroll: to get notified about scroll-events.
In this method, check whether you need to add your shadow-view to your (floating) header-view.
If so, add it. (just [view addSubview:shadowView].) Something like CGRectMake(0.f, yourDefaultHeaderHeight, 320.f, yourShadowHeight) should be the frame of your shadowView.
Now, update the bounds of view, so it can show your shadowView: CGRectMake(0.f, 0.f - yourShadowHeight, 320.f, yourDefaultHeaderHeight + 2 * yourShadowHeight).
When you find out that your header isn't floating anymore (by using scrollView:didScroll:), remove the shadow-view.
Your headerViews bounds should be 0.f - yourShadowHeight because if you use just 0.f, it'll blur (I don't know why ...).
You would have to have your own UIView in the header. Then you would need a reference to it. Then hook into scrollViewWillBeginDragging: with your UIScrollViewDelegate. In that function, add the shadow to the custom view.
Hook into scrollViewDidEndDragging:willDecelerate: and remove the shadow in this function.
#Anthony Mattox answer for Swift
protocol SectionHeaderWithShadowProtocol where Self: UIView {
var shadow: Bool { get set }
}
class SectionHeaderView: UITableViewHeaderFooterView, SectionHeaderWithShadowProtocol {
#IBOutlet weak var shadowView: UIView!
var shadow: Bool = false {
didSet {
shadowView.isHidden = shadow
}
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateHeaderShadows()
}
func updateHeaderShadows() {
var i = 0
var sectionHeight: CGFloat = 0
var totalHeight: CGFloat = 0
while i < numberOfSections() {
sectionHeight = tableView.rect(forSection: i).size.height
if let sectionHeader = tableView.headerView(forSection: i) as? SectionHeaderWithShadowProtocol {
if sectionHeader.frame.origin.y == totalHeight || sectionHeader.frame.origin.y == totalHeight + sectionHeight - sectionHeader.frame.size.height {
sectionHeader.shadow = false
} else {
sectionHeader.shadow = true
}
}
totalHeight += sectionHeight
i += 1
}
}
Related
I'm using Xcode 9 and Swift 4. I'm having trouble trying to use the scroll in y to hide SearchBar through alpha. How to access this background to try to hide it?
code:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let transp:Float = Float(1 - (self.mTableView.contentOffset.y / (self.mTableView.contentSize.height - self.mTableView.frame.size.height) * 60))
self.navigationController!.navigationBar.alpha = CGFloat(transp)
self.mSearchController.searchBar.alpha = CGFloat(transp)
self.mSearchController.searchBar.isTranslucent = true
titleMsgView.alpha = CGFloat(transp)
}
Here's a sample gif link:
https://www.dropbox.com/s/6is5mukpk1yew8e/bgSearch.gif?dl=0
The SearchView is pretty tough when it comes to changing how it looks. You could try looping over the subviews of the SearchView and setting all their alpha values to 0, like this:
for subView in mSearchController.searchBar.subviews {
subView.alpha = 0
for deepSubView in subView.subviews {
deepSubView.alpha = 0
}
}
Also, a quick note: you can remove a lot of the self keywords from your code.
EDIT: That just removed the searchbar, but with some editing we can fix that:
for subview in mSearchController.searchBar.subviews.last!.subviews {
if type(of: subview) == NSClassFromString("UISearchBarBackground") {
subview.alpha = 0
}
}
That should do the trick!
Does anyone know how can I reproduce a similar effect from the native iOS7 weather app?
Basically, the status bar inherits the view's background underneath, but the content doesn't show up.
Also, a 1 pixel line is drawn after the 20 pixels height of the status bar, only if some content is underlayed.
The best thing is to make it through the clipSubview of the view. You put your content into the view and make constraints to left/right/bottom and height. Height on scroll view you check is the cell has minus position and at that time you start to change the height of content (clip) view to get desired effect.
This is a real app you can download and take a look from www.fancyinteractive.com. This functionality will be available soon as next update.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSArray *visibleCells = [convertorsTableView visibleCells];
if (visibleCells.count) {
for (CVConverterTableViewCell *cell in visibleCells) {
CGFloat positionYInView = [convertorsTableView convertRect:cell.frame toView:self.view].origin.y;
[self clipLayoutConstraint:cell.clipHeightLayoutConstraint withPosition:positionYInView defaultHeight:cell.frameHeight];
[cell.converterLabel layoutIfNeeded];
[cell.iconImageView layoutIfNeeded];
}
}
[self checkStatusBarSeperator:scrollView.contentOffset.y];
}
- (void)clipLayoutConstraint:(NSLayoutConstraint *)constraint withPosition:(CGFloat)position defaultHeight:(CGFloat)defaultHeight {
if (position < 0) {
constraint.constant = (defaultHeight - -position - 20 > 10) ? defaultHeight - -position - 20 : 10;
} else
constraint.constant = defaultHeight;
}
You can accomplish this by setting a mask to the table view's layer. You will not be able however to render the animations inside the cells, but you can do those yourself behind the table view, and track their movement with the table view's scrollview delegate methods.
Here is some informations on CALayer masks:
http://evandavis.me/blog/2013/2/13/getting-creative-with-calayer-masks
Swift 5:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let visibleCells = tableView.visibleCells as? [TableViewCell] else { return }
let defaultClipHeight: CGFloat = 24
let statusBarHeight: CGFloat = UIApplication.statusBarHeight
if !visibleCells.isEmpty {
for cell in visibleCells {
let topSpace = cell.frame.size.height - defaultClipHeight - cell.clipBottomConstraint.constant
let cellOffsetY = tableView.contentOffset.y - cell.frame.origin.y + statusBarHeight
if cellOffsetY > topSpace {
let clipOffsetY = cellOffsetY - topSpace
let clipHeight = defaultClipHeight - clipOffsetY
cell.clipHeightConstraint.constant = max(clipHeight, 0)
} else {
cell.clipHeightConstraint.constant = defaultClipHeight
}
}
}
}
Starting Page:
Scrolling First Item:
Scrolling Second Item:
I've seen posts around here that suggest that UIScrollViews should automatically scroll if a subview UITextField becomes the first responder; however, I can't figure out how to get this to work.
What I have is a UIViewController that has a UIScrollView and within the UIScrollView there are multiple textfields.
I know how to do this manually if necessary; however, from what I've been reading, it seems possible to have it autoscroll. Help please.
I hope this example will help you
You can scroll to any point by this code.
scrollView.contentOffset = CGPointMake(0,0);
So if you have textfield, it must have some x,y position on view, so you can use
CGPoint point = textfield.frame.origin ;
scrollView.contentOffset = point
This should do the trick,
But if you don't know when to call this code, so you should learn UITextFieldDelegate methods
Implement this method in your code
- (void)textFieldDidBeginEditing:(UITextField *)textField {
// Place Scroll Code here
}
I hope you know how to use delegate methods.
I know this question has already been answered, but I thought I would share the code combination that I used from #Adeel and #Basil answer, as it seems to work perfectly for me on iOS 9.
-(void)textFieldDidBeginEditing:(UITextField *)textField {
// Scroll to the text field so that it is
// not hidden by the keyboard during editing.
[scroll setContentOffset:CGPointMake(0, (textField.superview.frame.origin.y + (textField.frame.origin.y))) animated:YES];
}
-(void)textFieldDidEndEditing:(UITextField *)textField {
// Remove any content offset from the scroll
// view otherwise the scroll view will look odd.
[scroll setContentOffset:CGPointMake(0, 0) animated:YES];
}
I also used the animated method, it makes for a much smoother transition.
Here is the Swift 4 update to #Supertecnoboff's answer. It worked great for me.
func textFieldDidBeginEditing(_ textField: UITextField) {
scroll.setContentOffset(CGPoint(x: 0, y: (textField.superview?.frame.origin.y)!), animated: true)
}
func textFieldDidEndEditing(_ textField: UITextField) {
scroll.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
Make sure to extend UITextFieldDelegate and set the textfields' delegate to self.
There is nothing you have to do manually. It is the default behavior. There are two possibilities as to why you are not seeing the behavior
The most likely reason is that the keyboard is covering your UITextField. See below for solution
The other possibility is that you have another UIScrollView somewhere in the view hierarchy between the UITextField and the UIScrollView that you want to auto scroll. This is less likely but can still cause problems.
For #1, you want to implement something similar to Apple's recommendations for Moving Content That Is Located Under the Keyboard. Note that the code provided by Apple does not account for rotation. For improvements on their code, check out this blog post's implementation of the keyboardDidShow method that properly translates the keyboard's frame using the window.
- (void)textFieldDidBeginEditing:(UITextField *)textField {
CGRect rect = [textField bounds];
rect = [textField convertRect:rect toView:self.scrollView];
rect.origin.x = 0 ;
rect.origin.y -= 60 ;
rect.size.height = 400;
[self.scrollView scrollRectToVisible:rect animated:YES];
}
You can use this function for autoScroll of UITextField
on UITextFieldDelegate
- (void)textFieldDidBeginEditing:(UITextField *)textField {
[self autoScrolTextField:textField onScrollView:self.scrollView];
}
- (void) autoScrolTextField: (UITextField *) textField onScrollView: (UIScrollView *) scrollView {
float slidePoint = 0.0f;
float keyBoard_Y_Origin = self.view.bounds.size.height - 216.0f;
float textFieldButtomPoint = textField.superview.frame.origin.y + (textField.frame.origin.y + textField.frame.size.height);
if (keyBoard_Y_Origin < textFieldButtomPoint - scrollView.contentOffset.y) {
slidePoint = textFieldButtomPoint - keyBoard_Y_Origin + 10.0f;
CGPoint point = CGPointMake(0.0f, slidePoint);
scrollView.contentOffset = point;
}
EDIT:
Im now using IQKeyboardManager
Kudos to the developer of this, you need to try this.
Solution
extension UIScrollView {
func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) {
guard let firstResponderSubview = findFirstResponderSubview() else { return }
scrollVertically(toFirstResponder: firstResponderSubview,
keyboardFrameHight: keyboardFrameHight, animated: true)
}
private func scrollVertically(toFirstResponder view: UIView,
keyboardFrameHight: CGFloat, animated: Bool) {
let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight
let maxY = contentSize.height - scrollViewVisibleRectHeight
if contentOffset.y >= maxY { return }
var point = view.convert(view.bounds.origin, to: self)
point.x = 0
point.y -= scrollViewVisibleRectHeight/2
if point.y > maxY {
point.y = maxY
} else if point.y < 0 {
point.y = 0
}
setContentOffset(point, animated: true)
}
}
extension UIView {
func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } }
func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] }
class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
parenView.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(from: subView) as [T]
if let view = subView as? T { result.append(view) }
return result
}
}
}
Full Sample
Do not forget to paste the Solution code here
import UIKit
class ViewController: UIViewController {
private weak var scrollView: UIScrollView!
private lazy var keyboard = KeyboardNotifications(notifications: [.willHide, .willShow], delegate: self)
override func viewDidLoad() {
super.viewDidLoad()
let scrollView = UIScrollView()
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
scrollView.contentSize = CGSize(width: view.frame.width, height: 1000)
scrollView.isScrollEnabled = true
scrollView.indicatorStyle = .default
scrollView.backgroundColor = .yellow
scrollView.keyboardDismissMode = .interactive
self.scrollView = scrollView
addTextField(y: 20)
addTextField(y: 300)
addTextField(y: 600)
addTextField(y: 950)
}
private func addTextField(y: CGFloat) {
let textField = UITextField()
textField.borderStyle = .line
scrollView.addSubview(textField)
textField.translatesAutoresizingMaskIntoConstraints = false
textField.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: y).isActive = true
textField.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 44).isActive = true
textField.widthAnchor.constraint(equalToConstant: 120).isActive = true
textField.heightAnchor.constraint(equalToConstant: 44).isActive = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
keyboard.isEnabled = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
keyboard.isEnabled = false
}
}
extension ViewController: KeyboardNotificationsDelegate {
func keyboardWillShow(notification: NSNotification) {
guard let userInfo = notification.userInfo as? [String: Any],
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
scrollView.contentInset.bottom = keyboardFrame.height
scrollView.scrollVerticallyToFirstResponderSubview(keyboardFrameHight: keyboardFrame.height)
}
func keyboardWillHide(notification: NSNotification) {
scrollView.contentInset.bottom = 0
}
}
/// Solution
extension UIScrollView {
func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) {
guard let firstResponderSubview = findFirstResponderSubview() else { return }
scrollVertically(toFirstResponder: firstResponderSubview,
keyboardFrameHight: keyboardFrameHight, animated: true)
}
private func scrollVertically(toFirstResponder view: UIView,
keyboardFrameHight: CGFloat, animated: Bool) {
let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight
let maxY = contentSize.height - scrollViewVisibleRectHeight
if contentOffset.y >= maxY { return }
var point = view.convert(view.bounds.origin, to: self)
point.x = 0
point.y -= scrollViewVisibleRectHeight/2
if point.y > maxY {
point.y = maxY
} else if point.y < 0 {
point.y = 0
}
setContentOffset(point, animated: true)
}
}
extension UIView {
func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } }
func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] }
class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
parenView.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(from: subView) as [T]
if let view = subView as? T { result.append(view) }
return result
}
}
}
// https://stackoverflow.com/a/42600092/4488252
import Foundation
protocol KeyboardNotificationsDelegate: class {
func keyboardWillShow(notification: NSNotification)
func keyboardWillHide(notification: NSNotification)
func keyboardDidShow(notification: NSNotification)
func keyboardDidHide(notification: NSNotification)
}
extension KeyboardNotificationsDelegate {
func keyboardWillShow(notification: NSNotification) {}
func keyboardWillHide(notification: NSNotification) {}
func keyboardDidShow(notification: NSNotification) {}
func keyboardDidHide(notification: NSNotification) {}
}
class KeyboardNotifications {
fileprivate var _isEnabled: Bool
fileprivate var notifications: [NotificationType]
fileprivate weak var delegate: KeyboardNotificationsDelegate?
fileprivate(set) lazy var isKeyboardShown: Bool = false
init(notifications: [NotificationType], delegate: KeyboardNotificationsDelegate) {
_isEnabled = false
self.notifications = notifications
self.delegate = delegate
}
deinit { if isEnabled { isEnabled = false } }
}
// MARK: - enums
extension KeyboardNotifications {
enum NotificationType {
case willShow, willHide, didShow, didHide
var selector: Selector {
switch self {
case .willShow: return #selector(keyboardWillShow(notification:))
case .willHide: return #selector(keyboardWillHide(notification:))
case .didShow: return #selector(keyboardDidShow(notification:))
case .didHide: return #selector(keyboardDidHide(notification:))
}
}
var notificationName: NSNotification.Name {
switch self {
case .willShow: return UIResponder.keyboardWillShowNotification
case .willHide: return UIResponder.keyboardWillHideNotification
case .didShow: return UIResponder.keyboardDidShowNotification
case .didHide: return UIResponder.keyboardDidHideNotification
}
}
}
}
// MARK: - isEnabled
extension KeyboardNotifications {
private func addObserver(type: NotificationType) {
NotificationCenter.default.addObserver(self, selector: type.selector, name: type.notificationName, object: nil)
}
var isEnabled: Bool {
set {
if newValue {
for notificaton in notifications { addObserver(type: notificaton) }
} else {
NotificationCenter.default.removeObserver(self)
}
_isEnabled = newValue
}
get { _isEnabled }
}
}
// MARK: - Notification functions
extension KeyboardNotifications {
#objc func keyboardWillShow(notification: NSNotification) {
delegate?.keyboardWillShow(notification: notification)
isKeyboardShown = true
}
#objc func keyboardWillHide(notification: NSNotification) {
delegate?.keyboardWillHide(notification: notification)
isKeyboardShown = false
}
#objc func keyboardDidShow(notification: NSNotification) {
isKeyboardShown = true
delegate?.keyboardDidShow(notification: notification)
}
#objc func keyboardDidHide(notification: NSNotification) {
isKeyboardShown = false
delegate?.keyboardDidHide(notification: notification)
}
}
If you have multiple textfields say Textfield1, Textfield2, Textfield3 and you want to scroll the scrollview along the y-axis when textfield2 becomes first responder:
if([Textfield2 isFirstResponder])
{
scrollView.contentOffset = CGPointMake(0,yourY);
}
As Michael McGuire mentioned in his point #2 above, the system's default behavior misbehaves when the scroll view contains another scroll view between the text field and the scroll view. I've found that the misbehavior also occurs when there's a scroll view merely next to the text field (both embedded in the scroll view that needs to be adjusted to bring the text field into view when the text field wants to start editing. This is on iOS 12.1.
But my solution is different from the above. In my top-level scroll view, which is sub-classed so I can add properties and override methods, I override scrollRectToVisible:animated:. It simply calls its [super scrollRectToVisible:animated:] unless there's a property set that tells it to adjust the rect passed in, which is the frame of the text field. When the property is non-nil, it is a reference to the UITextField in question, and the rect is adjusted so that the scroll view goes further than the system thought it would. So I put this in the UIScrollView's sub-classed header file:
#property (nullable) UITextField *textFieldToBringIntoView;
(with appropriate #synthesize textFieldToBringIntoView; in the implementation. Then I added this override method to the implementation:
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)how
{
if (textFieldToBringIntoView) {
// Do whatever mucking with `rect`'s origin needed to make it visible
// based on context or its spatial relationship with the other
// view that the system is getting confused by.
textFieldToBringIntoView = nil; // Go back to normal
}
[super scrollRectToVisible:rect animated:how];
}
In the delegate method for the UITextField for when it's about to begin editing, just set textFieldToBringIntoView to the textField in question:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
// Ensure it scrolls into view so that keyboard doesn't obscure it
// The system is about to call |scrollRectIntoView:| for the scrolling
// superview, but the system doesn't get things right in certain cases.
UIScrollView *parent = (UIScrollView *)textField.superview;
// (or figure out the parent UIScrollView some other way)
// Tell the override to do something special just once
// based on this text field's position in its parent's scroll view.
parent.textFieldToBringIntoView = textField;
// The override function will set this back to nil
return(YES);
}
It seems to work. And if Apple fixes their bug, it seems like it might still work (fingers crossed).
Building off of Vasily Bodnarchuk's answer I created a gist with a simple protocol that you can implement and it'll do it all for you.
All you need to do is call registerAsTextDisplacer()
I created a BaseViewController in my project and made that implement it
https://gist.github.com/CameronPorter95/cb68767f5f8052fdc70293c167e9430e
Other solutions I saw, let you set the offset to the origin of the textField but this makes the scroller view go beyond it bounds.
I did this adjustment to the offset instead to not go beyond the bottom nor the top offsets.
Set the keyboardHeightConstraint to the bottom of the page.
When the keyboard shows, update its constraint's constant to negative the keyboard height.
Then scroll to the responderField as we will show below.
#IBOutlet var keyboardHeightConstraint: NSLayoutConstraint?
var responderField: String?
#objc func keyboardNotification(notification: NSNotification) {
guard let keyboardValue = notification.userInfo [UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardHeight = keyboardValue.cgRectValue.height
keyboardHeightConstraint?.constant = -keyboardHeight
scroll(field: responderField!)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
responderField = textField
}
Now we want to make sure we do not scroll greater than the bottom offset nor less than the top offset.
At the same time, we want to calculate the offset of the field's maxY value.
To do that, we subtract the scrollView.bounds.size.height from the maxY value.
let targetOffset = field.frame.maxY - scrollView.bounds.size.height
I found it nicer to scroll an extra distance of the keyboard height, but you could neglect that if you want to scroll right below the field.
let targetOffset = keyboardHeight + field.frame.maxY - scrollView.bounds.size.height
Remember to add the scrollView.contentInset.bottom if you have the tab bar visible.
func scroll(field: UITextField) {
guard let keyboardConstraintsConstant = keyboardHeightConstraint?.constant else { return }
let keyboardHeight = -keyboardConstraintsConstant
view.layoutIfNeeded()
let bottomOffset = scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom
let topOffset = -scrollView.safeAreaInsets.top
let targetOffset = keyboardHeight + field.frame.maxY + scrollView.contentInset.bottom - scrollView.bounds.size.height
let adjustedOffset = targetOffset > bottomOffset ? bottomOffset : (targetOffset < topOffset ? topOffset : targetOffset)
scrollView.setContentOffset(CGPoint(x: 0, y: adjustedOffset), animated: true)
}
If you have scrollView and tableView with invalidating intrinsicContentSize as the subview, you can disable tableView scrolling in storyboard or set tableView.isScrollEnabled to false in code.
paging in UIScrollView is a great feature, what I need here is to set the paging to a smaller distance, for example I want my UIScrollView to page less size that the UIScrollView frame width.
Thanks
There is a UIScrollView delegate method you can use. Set your class as the scroll view's delegate, and then implement the following:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
CGFloat kMaxIndex = 23;
CGFloat targetX = scrollView.contentOffset.x + velocity.x * 60.0;
CGFloat targetIndex = 0.0;
if (velocity.x > 0) {
targetIndex = ceil(targetX / (kCellWidth + kCellSpacing));
} else if (velocity.x == 0) {
targetIndex = round(targetX / (kCellWidth + kCellSpacing));
} else if (velocity.x < 0) {
targetIndex = floor(targetX / (kCellWidth + kCellSpacing));
}
if (targetIndex < 0)
targetIndex = 0;
if (targetIndex > kMaxIndex)
targetIndex = kMaxIndex;
targetContentOffset->x = targetIndex * (kCellWidth + kCellSpacing);
//scrollView.decelerationRate = UIScrollViewDecelerationRateFast;//uncomment this for faster paging
}
The velocity parameter is necessary to make sure the scrolling feels natural and doesn't end abruptly when a touch ends with your finger still moving. The cell width and cell spacing are the page width and spacing between pages in your view. In this case, I'm using a UICollectionView.
Change your scrollView size to the page size you want
Set your scroll.clipsToBounds = NO
Create a UIView subclass (e.g HackClipView) and override the hitTest:withEvent: method
-(UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event
{
UIView* child = [super hitTest:point withEvent:event];
if (child == self && self.subviews.count > 0)
{
return self.subviews[0];
}
return child;
}
Set the HackClipView.clipsToBounds = YES
Put your scrollView in this HackClipView (with the total scrolling size you want)
See this answer for more details
Update:
As stated in lucius answer you can now implement the UIScollViewDelegate protocol and use the - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset method. As the targetContentOffset is a pointer. Using this method will not guarantee you the same result with scroll view pages as the user can scroll through many pages at once. But setting the descelerationRate to fast will almost give you the same result
You should disable paging and add a UIPanGestureRecognizer to your scroll view and handle the paging yourself.
- (void)viewDidLoad {
[super viewDidLoad];
CGRect viewRect = self.view.bounds; // View controller's view bounds
theScrollView = [[UIScrollView alloc] initWithFrame:viewRect];
theScrollView.scrollsToTop = NO;
theScrollView.pagingEnabled = NO;
theScrollView.delaysContentTouches = NO;
theScrollView.delegate = self;
[self.view addSubview:theScrollView];
UIPanGestureRecognizer * peter = [[[UIPanGestureRecognizer alloc] initWithTarget:self
action:#selector(handlePan:)]
autorelease];
[theScrollView addGestureRecognizer:peter];
}
-(void)handlePan:(UIPanGestureRecognizer*)recognizer{
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:{
// panStart and startPoint are instance vars for the viewContainer
panStart = theScrollView.contentOffset;
startPoint = [recognizer locationInView:theScrollView];
break;
}
case UIGestureRecognizerStateChanged:{
CGPoint newPoint = [recognizer locationInView:theScrollView];
CGFloat delta = startPoint.x - newPoint.x;
if ( abs(delta) > 2)
theScrollView.contentOffset = CGPointMake( theScrollView.contentOffset.x + delta, 0);
CGFloat moveDelta = panStart.x - theScrollView.contentOffset.x;
// current witdh should hold the currently displayed page/view in theScrollView
if ( abs(moveDelta) > (currentWidth * 0.40)){
panStart = theScrollView.contentOffset;
startPoint = newPoint;
//NSLog(#"delta is bigger");
if ( moveDelta < 0 )
[self incrementPageNumber]; // you should implement this method and present the next view
else
[self decrementPageNumber]; // you should implement this method and present the previous view
recognizer.enabled = NO; // disable further event until view change finish
}
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
recognizer.enabled = YES;
[self showDocumentPage:currentPage];
break;
default:
break;
}
}
Swift 4.1 solution that simplifies reusing:
/// Protocol that simplifies custom page size configuration for UIScrollView.
/// Sadly, can not be done better due to protocol extensions limitations - https://stackoverflow.com/questions/39487168/non-objc-method-does-not-satisfy-optional-requirement-of-objc-protocol
/// - note: Set `.decelerationRate` to `UIScrollViewDecelerationRateFast` for a fancy scrolling animation.
protocol ScrollViewCustomHorizontalPageSize: UIScrollViewDelegate {
/// Custom page size
var pageSize: CGFloat { get }
/// Helper method to get current page fraction
func getCurrentPage(scrollView: UIScrollView) -> CGFloat
/// Helper method to get targetContentOffset. Usage:
///
/// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
/// targetContentOffset.pointee.x = getTargetContentOffset(scrollView: scrollView, velocity: velocity)
/// }
func getTargetContentOffset(scrollView: UIScrollView, velocity: CGPoint) -> CGFloat
/// Must be implemented. See `getTargetContentOffset` for more info.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
}
extension ScrollViewCustomHorizontalPageSize {
func getCurrentPage(scrollView: UIScrollView) -> CGFloat {
return (scrollView.contentOffset.x + scrollView.contentInset.left) / pageSize
}
func getTargetContentOffset(scrollView: UIScrollView, velocity: CGPoint) -> CGFloat {
let targetX: CGFloat = scrollView.contentOffset.x + velocity.x * 60.0
var targetIndex = (targetX + scrollView.contentInset.left) / pageSize
let maxOffsetX = scrollView.contentSize.width - scrollView.bounds.width + scrollView.contentInset.right
let maxIndex = (maxOffsetX + scrollView.contentInset.left) / pageSize
if velocity.x > 0 {
targetIndex = ceil(targetIndex)
} else if velocity.x < 0 {
targetIndex = floor(targetIndex)
} else {
let (maxFloorIndex, lastInterval) = modf(maxIndex)
if targetIndex > maxFloorIndex {
if targetIndex >= lastInterval / 2 + maxFloorIndex {
targetIndex = maxIndex
} else {
targetIndex = maxFloorIndex
}
} else {
targetIndex = round(targetIndex)
}
}
if targetIndex < 0 {
targetIndex = 0
}
var offsetX = targetIndex * pageSize - scrollView.contentInset.left
offsetX = min(offsetX, maxOffsetX)
return offsetX
}
}
Just conform to ScrollViewCustomPageSize protocol in your UIScrollView/UITableView/UICollectionView delegate and you are done, e.g.:
extension MyCollectionViewController: ScrollViewCustomPageSize {
var pageSize: CGFloat {
return 200
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee.x = getTargetContentOffset(scrollView: scrollView, velocity: velocity)
}
}
For a fancy scrolling I also recommend to set collectionView.decelerationRate = UIScrollViewDecelerationRateFast
Set the contentOffset in
-(void)scrollViewDidScroll:(UIScrollView *)scrollView method.
Also refer to UIScrollViewDelegate refernces
I had the same problem so I have made a custom UIScrollView.
It's available on Github now because when I searched I didn't find any solutions like this. Enjoy!
https://github.com/MartinMetselaar/MMCPSScrollView
MMCPSScrollView* scrollView = [[MMCPSScrollView alloc] initWithFrame:self.view.bounds];
[scrollView setType:MMCPSScrollVertical];
[scrollView setPageHeight:250];
[scrollView setPageSize:2];
[self.view addSubview:scrollView];
If you have any further questions about this component, just ask.
Adding gesture recognizers or other subviews and so on is silly. Just set the delegate for the scroll view an imlement on of the below :
// This is for a vertical scrolling scroll view.
// Let's say you want it to snap to every 160 pixels :
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
int y = scrollView.contentOffset.y;
int yOff = y % 160;
if(yOff < 80)
y -= yOff;
else
y += 160 - yOff;
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, y) animated:YES];
}
// This is for a horizontal scrolling scroll view.
// Let's say you want the same, to snap to every 160 pixels :
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
int x = scrollView.contentOffset.x;
int xOff = x % 160;
if(xOff < 80)
x -= xOff;
else
x += 160 - xOff;
[scrollView setContentOffset:CGPointMake(x, scrollView.contentOffset.y) animated:YES];
}
Swift 4.1, iOS11+:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = CGPoint(
x: round(targetContentOffset.pointee.x / pageWidth) * pageWidth,
y: targetContentOffset.pointee.y
)
}
I had the same problem short ago. My aproach was to add a second UIScrollView to the scrollview. So you can switch to the page. On that page it seems than if the page is bigger than the screen. I hope it works also in your situation. ;-)
Sandro Meier
This seemed to work a lot better for me:
UIScrollView Custom Paging
Here they are adding the scrollview (keeping it's paging niceness) as a subview to an ExtendedTouchView or subclass of UIVIew and overwriting the hit test method
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
if ([[self subviews] count] > 0) {
//force return of first child, if exists
return [[self subviews] objectAtIndex:0];
} else {
return self;
}
}
return nil;
}
This did exactly whatI wanted with minimal code and headache.
The easiest way is to add this code
scrollView.clipsToBounds = false
scrollView.removeGestureRecognizer(scrollView.panGestureRecognizer)
view.addGestureRecognizer(scrollView.panGestureRecognizer)
Possibly a simple one!
Does anyone know how to get the scroll bar of a UIScrollView to constantly show?
It displays when the user is scrolling, so they can see what position of the scroll view they are in.
BUT I would like it to constantly show because it is not immediately obvious to the user that scrolling is available
Any advice would be highly appreciated.
No, you can't make them always show, but you can make them temporarily flash.
[myScrollView flashScrollIndicators];
They are scroll indicators, not scroll bars. You can't use them to scroll.
my solution for show scroll indicators all the time
#define noDisableVerticalScrollTag 836913
#define noDisableHorizontalScrollTag 836914
#implementation UIImageView (ForScrollView)
- (void) setAlpha:(float)alpha {
if (self.superview.tag == noDisableVerticalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (self.frame.size.width < 10 && self.frame.size.height > self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.height < sc.contentSize.height) {
return;
}
}
}
}
if (self.superview.tag == noDisableHorizontalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
if (self.frame.size.height < 10 && self.frame.size.height < self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.width < sc.contentSize.width) {
return;
}
}
}
}
[super setAlpha:alpha];
}
#end
UPDATE: This solution cause some issues on 64-bit. For more detail look here
As far as I know, this isn't possible. The only API call which controls displaying the scroll indicator is showsVerticalScrollIndicator and that can only disable displaying the indicator altogether.
You could flashScrollIndicators when the view appears so that the user knows where in the scroll view they are.
This one worked for me:
#define noDisableVerticalScrollTag 836913
#define noDisableHorizontalScrollTag 836914
#implementation UIImageView (ForScrollView)
- (void) setAlpha:(float)alpha {
if (self.superview.tag == noDisableVerticalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (self.frame.size.width < 10 && self.frame.size.height > self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.height < sc.contentSize.height) {
return;
}
}
}
}
if (self.superview.tag == noDisableHorizontalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
if (self.frame.size.height < 10 && self.frame.size.height < self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.width < sc.contentSize.width) {
return;
}
}
}
}
[super setAlpha:alpha];
}
#end
I got this snippet from here: http://www.developers-life.com/scrollview-with-scrolls-indicators-which-are-shown-all-the-time.html
Swift 3+
1) Timer
var timerForShowScrollIndicator: Timer?
2) Methods
/// Show always scroll indicator in table view
func showScrollIndicatorsInContacts() {
UIView.animate(withDuration: 0.001) {
self.tableView.flashScrollIndicators()
}
}
/// Start timer for always show scroll indicator in table view
func startTimerForShowScrollIndicator() {
self.timerForShowScrollIndicator = Timer.scheduledTimer(timeInterval: 0.3, target: self, selector: #selector(self.showScrollIndicatorsInContacts), userInfo: nil, repeats: true)
}
/// Stop timer for always show scroll indicator in table view
func stopTimerForShowScrollIndicator() {
self.timerForShowScrollIndicator?.invalidate()
self.timerForShowScrollIndicator = nil
}
3) Use
startTimerForShowScrollIndicator in viewDidAppear
stopTimerForShowScrollIndicator in viewDidDisappear
I want to offer my solution. I don't like the most popular variant with category (overriding methods in category can be the reason of some indetermination what method should be called in runtime, since there is two methods with the same selector).
I use swizzling instead. And also I don't need to use tags.
Add this method to your view controller, where you have scroll view (self.categoriesTableView property is a table view where I want to show scroll bars)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Do swizzling to turn scroll indicator always on
// Search correct subview with vertical scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
}
}
// Search correct subview with horizontal scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
if (view.frame.size.height < 10 && view.frame.size.height < view.frame.size.width) {
if (self.categoriesTableView.frame.size.width < self.categoriesTableView.contentSize.width) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
}
}
// Ask to flash indicator to turn it on
[self.categoriesTableView flashScrollIndicators];
}
Add new class
#interface AlwaysOpaqueImageView : UIImageView
#end
#implementation AlwaysOpaqueImageView
- (void)setAlpha:(CGFloat)alpha {
[super setAlpha:1.0];
}
#end
The scroll indicator (vertical scroll indicator in first for cycle and horizontal in second for cycle) will be always at the screen. If you need only one indicator, left only this for cycle in code and remove another one.
For webviews, where the first subview is a scrollview, in the latest SDK, if an HTML page is longer than the frame, no scroll bar is shown, and if the html content happens to line up with the frame, or you have a whitespace at the bottom of the frame, it 'looks' like there is no scroll needed and nothing below the line. In this case, I think you should definately flash the scroll bars in the delegate's
- (void)webViewDidFinishLoad:(UIWebView *)webView;
method to alert the user that there is more stuff 'outside the box'.
NSArray *subViews = [[NSArray alloc] initWithArray:[webView subviews]] ;
UIScrollView *webScroller = (UIScrollView *)[subViews objectAtIndex:0] ;
With HTML, the horizontal content is wrapped automatically, so check the webscroller height.
if (webScroller.contentSize.height > webView.frame.size.height) {
[webScroller flashScrollIndicators];
}
The flash is so short, and happens while over views are loading, that it can be overlooked. To work around that, you could also jiggle or bounce or scroll or scale the content a little via the generic UIView commitAnimations
iOS does not offer the API. But if you really want this, you can add your custom indicator to scroll view and layout it yourself, just as the demo does:
- (void)layoutSubviews
{
[super layoutSubviews];
if (self.showsVerticalScrollIndicatorAlways) {
scroll_indicator_position(self, k_scroll_indicator_vertical);
}
if (self.showsHorizontalScrollIndicatorAlways) {
scroll_indicator_position(self, k_scroll_indicator_horizontal);
}
}
The link is https://github.com/flexih/MazeScrollView
ScrollBar that functions just like the iOS built in one, but you can mess with the color and width.
-(void)persistantScrollBar
{
[persistantScrollBar removeFromSuperview];
[self.collectionView setNeedsLayout];
[self.collectionView layoutIfNeeded];
if (self.collectionView.contentSize.height > self.collectionView.frame.size.height + 10)
{
persistantScrollBar = [[UIView alloc] initWithFrame:(CGRectMake(self.view.frame.size.width - 10, self.collectionView.frame.origin.y, 5, (self.collectionView.frame.size.height /self.collectionView.contentSize.height) * self.collectionView.frame.size.height))];
persistantScrollBar.backgroundColor = [UIColor colorWithRed:207/255.f green:207/255.f blue:207/255.f alpha:0.5f];
persistantScrollBar.layer.cornerRadius = persistantScrollBar.frame.size.width/2;
persistantScrollBar.layer.zPosition = 0;
[self.view addSubview:persistantScrollBar];
}
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect rect = persistantScrollBar.frame;
rect.origin.y = scrollView.frame.origin.y + (scrollView.contentOffset.y *(self.collectionView.frame.size.height/self.collectionView.contentSize.height));
rect.size.height = (self.collectionView.frame.size.height /self.collectionView.contentSize.height) * self.collectionView.frame.size.height;
if ( scrollView.contentOffset.y <= 0 )
{
rect.origin.y = scrollView.frame.origin.y;
rect.size.height = rect.size.height + (scrollView.contentOffset.y);
}
else if (scrollView.contentOffset.y + scrollView.frame.size.height >= scrollView.contentSize.height)
{
rect.size.height = rect.size.height - ((scrollView.contentOffset.y + scrollView.frame.size.height) - scrollView.contentSize.height);
rect.origin.y = (self.collectionView.frame.origin.y + self.collectionView.frame.size.height - 5) - rect.size.height;
}
persistantScrollBar.frame = rect;
}
Swift 3
You can access the scrollbar using scrollView.subviews and modify the alpha as shown here. It works for me.
extension UIScrollView {
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for x in self.subviews {
x.alpha = 1.0
}
}
}
extension MyScrollViewDelegate : UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
for x in scrollView.subviews {
x.alpha = 1.0
}
}
}
Looking through these answers, most of them are downright scary. Got this working in Swift 5 with the following. It still depends on the scroll view using subviews with class "_UIScrollViewScrollIndicator" - but at least there's no swizzling or app wide categories.
class IndicatorScrollView: UIScrollView {
weak var indicatorTimer: Timer?
override func didMoveToSuperview() {
super.didMoveToSuperview()
setupIndicatorTimer()
}
override func removeFromSuperview() {
super.removeFromSuperview()
indicatorTimer?.invalidate()
}
deinit {
indicatorTimer?.invalidate()
}
func setupIndicatorTimer() {
indicatorTimer?.invalidate()
indicatorTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(showIndicators), userInfo: nil, repeats: true)
}
#objc func showIndicators() {
subviews.forEach {
if String(describing: type(of: $0)).contains("ScrollIndicator") {
$0.alpha = 1
}
}
}
}