Custom UI Master Class: Infinite Paging Scroll View
The success of any app rides on user retention, which in turn relies heavily on successful user experience and interface design. When designing an app, we need to ensure that our user is able to achieve what they want through minimal and intuitive interaction. Moreover, the interaction needs to be engaging, and even fun. This is the first article in an ongoing series about developing custom UI components that achieve all of these ideals.
Fork the repository for this demo here:
In this demo we will build an infinite paging scroll view. As you can see from the animation above, this UI component is useful when you have a small number of predetermined options that you want the user to choose from. You want to present these options using very little screen real estate, and you want to have a default option already selected. Now it’s a simple matter of tapping the view to make a new selection and pass it through the MVC chain. Let’s get started!
The Illusion of Infinite Scrolling
Before we get into the implementation of our project, lets discuss how the illusion of infinite scrolling works. We want to setup our scroll view to page horizontally representing the data in an array.
Fig 1. Imagine we have four elements each with a colour and number that we want to display as a separate page in our scroll view’s content view. Normally, we would set our content size to be four times the width of our scroll view such that each element gets its own ‘page’. Unfortunately in this case, we won’t get the effect that we want because once we scroll as far as our fourth page, the only way back the first is to assign our content offset back to zero. While this does get us back to the beginning, we don’t get the pretty paging animation that we are looking for.
Fig 2. Instead, we can modify our input data so that the first and last elements are copied to opposite ends. This means that we now have six pages to show four elements. Now, we can get the nice paging animation from element four to element one. (or one to four going the opposite direction)
Fig 3. Once the paging animation has finished, our scroll view is displaying our first element data at the end of our content view. Now we can sneakily set our content offset so that we are showing the same data but from an element earlier in the content view. This switch will be imperceptible to the user.
Special Note: In this demo, the content is being scrolled in only one direction whereas the setup described above shows how to get bi-directional infinite scrolling. While it means that there is some redundant code in our demo, I have chosen to implement it with logic that will allow for two directions so that you can make use of it in your own projects if you wish.
Step 1: Basic Setup
We create our custom class InfiniteScrollView subclassing UIView and set the background colour to gray to visualize our frame (line 20). Then we define two properties: scrollView and tapView. The scroll view (lines 3–9) is setup with the scroll indicator removed and has paging enabled. The tap view has been marked as lazy var so that we can add our tap gesture to it later. For now, it is only configured to be transparent (lines 11–15). We layout these subviews so that the tapView fills the bounds of our view and sits on top of the scroll view, which is only half the size (lines 27–36). Now our user can interact with a larger view while the scrolling happens in a smaller one.
When we initialize the InfiniteScrollView with a frame and add it to our View Controller’s subview hierarchy, we get something that looks like this:
Step 2: Modify Datasource and Layout ScrollView Content
Our next step is to add some data to our infiniteScrollView and display the elements in labels that are added to our content view. To do this, we need to create two properties: datasource is publicly facing and accepts an array of Strings (lines 3–7), and our private _datasource will be a modified version of the input, so that we have our first and last elements added to opposite ends of the array, as was discussed in the previous section (lines 9–13).
Next, we have two methods modifyDatasource and setupContentView. Let’s look at modifyDatasource first (lines 15–25). Notice that this method is called by the didSet property observer in datasource, so we can ensure that our custom view is updated anytime the data changes. We check that the datasource is not nil by binding it to a variable tempInput, and then we check that the count is two or more (line 16). Obviously if these conditions are not met, there is not enough data to scroll through. In such a case, our method will return with no setup of subviews. Assuming our data input and count are valid, we can modify our datasource. We do this by putting the first and last elements into a tuple (line 18). Force unwrapping these values is safe as we have already performed checks in the previous step to ensure that the indices contain values. Then we append the first value to the end of our tempInput, and insert the last value to the beginning (lines 17–18). For demonstration purposes, we can add a print statement (line 22) and then set our private _datasource with our modified data (line 24).
Our setup is continued by a property observer in _datasource, which calls through to setupContentView() (line 10–12). Here, we remove any subviews that may already exist from a previous setup (lines 31–34). Then, we use optional binding to unwrap our _datasource and set our content size. In our case we want horizontal paging, so the height of our content will be the same as our scrollView height, and then the width will be the same as our scrollView width multiplied by the number of elements in our _datasource (lines 36–38). To add our labels in the correct position, we create them in a loop with the new label origin calculated using the i value. We use the same i value to subscript the appropriate string from our datasource and set it as our label text, before adding it as a subview of our scrollView (lines 41–50). Once all of the labels are added with their appropriate text, we can set the content offset to show our first element (lines 51–52).
To see our work in action, we can add a datasource to our infiniteScrollView object in the View controller and then run our app. Notice that the printout to the console shows that the modified _datasource has the first and last elements added correctly (lines 3–5).
Step 3: Add Tap Gesture and Paging Logic
While we can see that our data is being added to the scrollView we are yet to page through it. To achieve this effect, we add a tapGesture with a selector method didReceiveTap(sender:) in our tapView property (lines 6–7). This method (lines 19–28) creates the next rect that we want to make visible by adding the scrollview width to the current content offset x value. We get the cool paging animation from the method scrollRectToVisible (line 27).
Next we need to add logic to set the content offset when we reach the end of our data source. This logic should be called once the scrolling animation has completed. If we set our custom class as the scrollView delegate, we can then add the override method scrollViewDidScroll which is called each time the scrolling animation is completed. We set the delegate in our init method (line 15). Then in scrollViewDidScroll, we figure out if the position of our viewable content is beyond the bounds of our data source and then reset it appropriately using contentOffset (lines 33–41).
Now when we build and run, we should see the paging effect we have been looking for.
Step 4: Create Delegate To Pass Selected Option
We are almost there! The InfiniteScrollView certainly has the look and feel that we are trying to achieve, but there is still one thing missing. We want to be able to pass our selected option back to our view controller. The best way to do this is using the delegate pattern.
Declare a protocol InfiniteScrollViewDelegate with a single method that passes the option as an input parameter optionChanged(to option:) (lines 1–3). Then create a delegate property as an optional protocol type (line 13) and a selectedOption string property that calls the delegate method through a didSet property observer (lines 7–11). Now whenever we set selectedOption our delegate will receive the selection. There are two places where we will set this property. Once we have our content view laid out, we change our content offset to show the first option, and now we also set that first option as our selectedOption (line 39).
Finally, we can set our selected option when the user taps, just before the paging animation begins. We use a guard statement to unwrap our datasource, calculate the index for the next option and then use it to subscript our datasource assigning the string value to our selectedOption (lines 47–51).
Over in our view controller, we can adopt the InfiniteScrollViewDelegate protocol and handle the incoming option however we want. Then we set our self as the delegate. Let’s build and run our app. With any luck, we should see our options getting sent to our view controller!
And that’s it! There was a fair amount of logic that we had to implement in order to make our UI component work the way we want, but we now have something that is very reusable. If we initialize an InfiniteScrollView with a frame, set the delegate, and pass an array of options that we want to scroll through, we have a working component in three simple lines! You can also explore interesting variations to have paging associated with a swipe gesture or have bi-directional scrolling. You could also explore removing the tapView altogether and having the tap logic triggered by cell selection in a tableView. Hopefully you will see lots of interesting options and find suitable places to use it in your own apps.
In the second article in this series, we learn how to build an AutocompleteTextField. You can find it here.
As always, thank you for reading!