How to use ASHorizontalScrollView

In this article, you can find the source of the example about how to use ASHorizontalScrollView. You can find the source used in here:

And the source of this control:

What is ASHorizontalScrollView?

It is a horizontal scroll view that behaves as the apps scroll view in App Store.

It has several key features:

  1. It scrolls automatically to the nearest subview with certain (left and right) margin
  2. It would show certain portion of the last subview in the right (or left if the last one is the right-most subview)
  3. The subview can be a button, an image or any UIView

The idea of this control

To look at the app store scroll view, we can find out some keys:

To simplify the use of this control, it should automatically calculate the number of items to fit in the current width of scrollview. Therefore, we would need some constants to do that:

Tricks behind it

How to get the margin between subviews?

Here is the layout:

|-leftMargin-subview1-marginInBetween- ... -subviewN-marginInBetween-a part of last subviewN+1-|


so the constitution of the scrollview is

scrollview.width = leftMargin + (subview.width+marginInBetween) * N + width of a part of last subviewN+1

In order to get the uncertain number ‘N’, we would need to know ‘leftMargin’, ‘marginInBetween’, ‘visible width of N+1 subview’ ‘subview width’, ‘scrollview width’, so that

N = floorf((scrollview.width - leftMargin - visible width of N+1 subview)/(subview.width + marginInBetween))

This ‘N’ is the maximum number of subviews that can be showed in the current width of scrollview. However, the value of ‘N’ is not necessary an integer, which means we should adjust the marginInBetween to fix it. Here is how to calculate the actual margin by using the number ‘N’:

actualMarginInBetween = (scrollview.width - leftMargin - visible width of N+1 subview)/N - subview.width

Finally, we get all the values confirmed, and they will be used to construct the subviews.

How to automatically scroll to subview?

Apple provide a very convenient way to get it done. In the delegate methods of UIScrollView, there is a scrollViewWillEndDragging method which will be called whenever the scrolling action is going to finish. In another word, it is triggered when your finger leave the screen after scrolling. In the method parameters, it gives us a very useful value targetContentOffset. It shows us the final destination of the scrolling end point.

After getting the destination, the next step is to get the closest subview by the destination. First, we need to find out the index of the left item by:

index = (offset.x - leftMargin)/(marginInBetween+subview.width) as integer

Because the margin is set properly before this calculation, so this index should be the correct integer that equal to the index. Then we need to see how much portion does the left-most subview will show. For example, the end point might be in the middle of the left most subview, and in such case, we have to determine if we want to scroll to the next subview or scroll back to the beginning of current subview.

leftItem = items[index]
if offset.x - leftItem.x > leftItem.width * 1/3 then
scroll to next subview
else
scroll back to beginning of leftItem

Please note, you can set the number 1/3 in the above formular in property widthToScrollNextItem. And We don’t need to check whether the scroll view scroll out of its content size. because scrollview will automatically scroll back to fit its content size.

Updates in v1.4

In v1.4, some new properties are introduced to let you setup different margins for different screen sizes from original 4:3 screen to 16:9 screen are all included. Such as marginSettings_320 is for iPhone 5s and lower versions in portrait and marginSettings_480 is for iPhone 4s and lower versions in landscape etc. All screen sizes can be setup with different margin to fit your layout accordingly. Please check following usage for more details.

Basic Usage

This control hides most of complexity inside the control source, all you need is to setup several variable and then add all your subviews. And you are free!!!

Constants

They are not the actual Constant but can be set anytime you want, and do remember to refresh the view to make it happens. Except the uniformItemSize, the others are stored in a structure called MarginSettings, there are different margin settings for different screen size, for example, 320 X 480 is the size for iPhone 4s and below, 414 X 736 is the size for 6(s) and 7 Plus. When your app is launched on these different devices that has different size, the correct margin setting is applied if it is set. Otherswise, the defaultMarginSettings is used to calculate margin.

settings:

public struct MarginSettings {
    /// the margin between left border and first item
    public var leftMargin:CGFloat = 5.0
    /// the minimum margin between items, it is the seed to calculate the actual margin
    /// which is not less than
    public var miniMarginBetweenItems:CGFloat  = 10.0
    /// the minimum width appear for last item of current screen,
    /// set it 0 if you don't want any part of the last item appear on the right
    public var miniAppearWidthOfLastItem:CGFloat = 20.0
    /// number of items per screen, it can be integer like 3, that means total 3 items 
    /// occupy whole screen, 4.5 means total 4 items and one half item show on the right end. 
    /// Please note that if numberOfItemsPerScreen is more than screen width, 
    /// the maximum allowed number of items per screen would be calculated by left margin, 
    /// and last appeared item percentage which is determined by the fractional number of this value
    /// it is default 0, if you want to change it, change the property in `defaultMarginSettings`
    public var numberOfItemsPerScreen:Float = 0 (new for v1.5)
}
/// the uniform size of all added items, please set it before adding any items,
/// otherwise, default size will be applied
uniformItemSize default square with 80% of the scrollview height
MarginSettings (Updates for v1.4 and v1.5)

In v1.5, a new way to setup this control is introduced. Previously, you can only set leftMargin, miniMarginBetweenItems and miniAppearWidthOfLastItem to adjust how does the scroll view layout items. And in v1.5 the top three remain same usage, the new numberOfItemsPerScreen is used to layout items by fixed number of items you would like to show in given screen width.

For example, previously, if you want to show fixed 3 items in iPhone 4s and show 5 items in iPhone 6 +, you would need to calculate the appropriate minimum margin and width in the setup, so that this control would show the items as expected. Now you would only need to set the number you preferred and this control will calculate it for you by just doing the similar setting as previously.

/// whether to arrange items by frame or by number of items, if set by frame, 
/// all margin would be calculated by frame size, otherwise, calculated by number of items per screen
/// - check `numberOfItemsPerScreen` for arranged by number type
open var arrangeType:ArrangeType = .byFrame
/// center subviews when items do not occupy whole screen
public var shouldCenterSubViews:Bool = false

After setting these constants used for calculation, all you need is to add your subviews by using either addItem or addItems.

Setup by frame:

let horizontalScrollView = ASHorizontalScrollView(frame:CGRectMake(0, 0, 320, 60))
//The default values will be used if you don't set it
//for iPhone 5s and lower versions in portrait
horizontalScrollView.marginSettings_320 = MarginSettings(leftMargin: 10, miniMarginBetweenItems: 5, miniAppearWidthOfLastItem: 20)
// for iPhone 6 plus and 6s plus in portrait
horizontalScrollView.marginSettings_414 = MarginSettings(leftMargin: 10, miniMarginBetweenItems: 5, miniAppearWidthOfLastItem: 20)
// for iPhone 6 plus and 6s plus in landscape
horizontalScrollView.marginSettings_736 = MarginSettings(leftMargin: 10, miniMarginBetweenItems: 10, miniAppearWidthOfLastItem: 30)
//for all other screen sizes that doesn't set here, it would use defaultMarginSettings instead
horizontalScrollView.defaultMarginSettings = MarginSettings(leftMargin: 10, miniMarginBetweenItems: 10, miniAppearWidthOfLastItem: 10)
horizontalScrollView.uniformItemSize = CGSizeMake(50, 50)
//This must be called after changing any size or margin property of this class to get acurrate margin
horizontalScrollView.setItemsMarginOnce()
for _ in 1...20{
    let button = UIButton(frame: CGRectZero)
    button.backgroundColor = UIColor.blueColor()
    horizontalScrollView.addItem(button)
}
self.view.addSubview(horizontalScrollView)
//refresh it if needed

Or Setup by number of items per screen:

let horizontalScrollView = ASHorizontalScrollView(frame:CGRectMake(0, 0, 320, 60))
//instead of using frame to determine margin, 
//using number of items per screen to calculate margin maybe eaiser than setting mini margin for multiple screen size
horizontalScrollView.arrangeType = .byNumber
horizontalScrollView.marginSettings_320 = MarginSettings(leftMargin: 10, numberOfItemsPerScreen: 4.25)
horizontalScrollView.marginSettings_480 = MarginSettings(leftMargin: 10, numberOfItemsPerScreen: 5.25)
horizontalScrollView.marginSettings_414 = MarginSettings(leftMargin: 10, numberOfItemsPerScreen: 4.25)
horizontalScrollView.marginSettings_736 = MarginSettings(leftMargin: 10, numberOfItemsPerScreen: 7.375)
//for all the other screen sizes which are not set here, margin would be calculated by frame instead 
//if defaultMarginSettings.numberOfItemsPerScreen is not change to other value than 0
horizontalScrollView.defaultMarginSettings.numberOfItemsPerScreen = 5.3 
//this means by default each screen would show 5 compelete items and 0.3 item (by width) on the right, 
//if smaller screens can not contain 5.3 items, x.3 items would be showed where x is the maximum allowed whole items
horizontalScrollView.uniformItemSize = CGSize(width: 80, height: 50)
//this must be called after changing any size or margin property of this class to get acurrate margin
horizontalScrollView.setItemsMarginOnce()
for _ in 1...20{
    let button = UIButton(frame: CGRect.zero)
    button.backgroundColor = UIColor.gray
    horizontalScrollView.addItem(button)
}

Even though you have set the value for number of items per screen, this control still need the left margin you preferred to calculate the acutal margin while the other two properties miniMarginBetweenItems and miniAppearWidthOfLastItem are not required unless you don’t set the value of numberOfItemsPerScreen to any positive value other than 0

Advanced Usage

Once you get used to it, you can customize it fully by your own. For example, instead of inserting a button, you can insert a view containing an image and a text just like the app store did. Or you can make the button with corner radius through its layer, etc.

For example, you can add gesture on item, and perform action correspondingly.

//in setup view
let view = UIView()
...
//setup gesture
let tapGesture = ...
view.addGestureRecognizer(tapGesture)
...
//gesture function
func tapAction(gesture:UITapGestureRecognizer) {
    if let index = horizontalScrollView.items.index(of: gesture.view) {
        //perform action accordingly
    }
}



In addition, you can change the setting of this control. Such as the deceleration rate of the scrollview by self.decelerationRate = UIScrollViewDecelerationRateNormal or UIScrollViewDecelerationRateFast(default). This control is fully customizable in MIT license, feel free to use it in anyway you like :)

Summary

This is a simple control built with UIScrollView and its delegate methods. There isn’t much complicated parts except a little bit of math. And I think this is all you need to know about using this control. If you have any question, feel free to leave a comment or email it to me.