Skip to content

Instantly share code, notes, and snippets.

@wassafr
Created December 7, 2017 09:50
Show Gist options
  • Save wassafr/9ec5fdbc6fa353c4fc780d828d6e6ba1 to your computer and use it in GitHub Desktop.
Save wassafr/9ec5fdbc6fa353c4fc780d828d6e6ba1 to your computer and use it in GitHub Desktop.

SwiftSpringBoard: implementing the iOS home screen layout

SwiftSpringBoard is a project meant to reproduce iOS's home screen icons disposition and features.

Genesis

The need for SwiftSpringBoard came from one of our client who uses a collection view for the home screen of its iOS application. This client was willing to include the Apple-style home buttons re-ordering functionality and the ability to hide cells as an evolution.

We needed a component able to accomplish these features:

Presenting items in the same way applications are displayed on the iOS home screen Re-ordering (with drag & drop) elements on one or more pages of the screen Deleting elements from the screen

The first thing we did of course was to look up the available frameworks that could fit our needs. And if you do so yourself, you'll probably come to the same conclusion we did: first of all, a very small number of frameworks are relevant for our goal, and secondly as of November 2017 most the tools available did not received an update for at least two years.

Setup

Our library relies on 4 types of objects:

SpringBoardView: the view displaying the home screen-style items (subclass of UICollectionView) SpringBoardManager: the object implementing the core logic of the view behaviour SpringBoardDataSource: the protocol the application uses to setup the component SpringBoardLayout: the custom layout used by the view (subclass of UICollectionViewFlowLayout)

To minimize headaches and unnecessary re-implementations (and to maximize the understanding by the rest of the world), the SwiftSpringBoard component uses mostly standard behaviours from UICollectionViews: since iOS 9.0, UICollectionViews are natively able to offer a re-ordering mechanism that suits basic needs. Furthermore, UIScrollViews are able to use paging since iOS 2.0. Combined together, these features offer us a solid starting point for our needs.

Loading the Content

We designed the SpringBoardDataSource protocol so that users of the classic UICollectionView mechanism would not get lost. First, the user must set the number of columns and rows his spring board will use on a single page. To do so, the user must implement the following methods:

func numberOfColumns(in springBoard: SpringBoardView) -> Int func numberOfRows(in springBoard: SpringBoardView) -> Int

The number of pages in the spring board is passed in a similar way:

func numberOfPages(in springBoard: SpringBoardView) -> Int

Finally, to load the items themselves, the user needs to implement the following methods:

func springBoard(_ springBoard: SpringBoardView, numberOfItemsInPage page: Int) -> Int func springBoard(_ springBoard: SpringBoardView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell

Which should be familiar to UICollectionViewDataSource users.

Receiving events

The methods called by the component to notify interactions are pretty straightforward for UICollectionViewDelegate users:

@objc optional func springBoard(_ springBoard: SpringBoardView, didSelectItemAt indexPath: IndexPath) @objc optional func springBoard(_ springBoard: SpringBoardView, didDeselectItemAt indexPath: IndexPath)

// Tells the data source that the specified cell is about to be displayed in the spring board. @objc optional func springBoard(_ springBoard: SpringBoardView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath)

// Called when an item is moved by the user @objc optional func springBoard(_ springBoard: SpringBoardView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)

// When the editing button if an item is touched func springBoard(_ springBoard: SpringBoardView, didTouchEditingButtonFor cell: UICollectionViewCell, at indexPath: IndexPath)

Understanding the Layout

The layout of the spring board is computed using the sectionInsets, minimuInterItemSpacing and minimumLineSpacing properties found on UICollectionViewFlowLayouts.

We decided to let the user choose what sizing mode should be used: the SpringBoardView object takes an optional itemSize parameter. If set to nil, the layout of the items will be computed to fill the available space in the view. Otherwise, the items will have a fixed size and will be spaced evenly in the view. The trickiest part of the project was to handle moving items between screens. We chose to consider each page as a different section of the spring board. As a subclass of UICollectionViewFlowLayout, the layout's default behaviour is to compute the items' positions according to the scrolling direction:

For a vertical scrolling, items will be loaded row after row:

For an horizontal scrolling, items will be loaded column after column:

The problem is, since paging in the home screen occurs horizontally, we have to stick with horizontal scroll. But as you can see, vertical scroll places items in the way one would expect home items to be placed. So we had to compute item positions ourselves the way a vertical layout would. Computing item positions ourselves meant handling item creation and deletion, otherwise we would be keeping references to useless variables which most certainly causes crashes when working with UIKit's default components, and we would be unaware of new items being added to the view, meaning they would never be displayed. The layout would also have to handle pages that would not be entirely filled, which is the behaviour we can see on the home screen.

After much troubleshooting we succeeded at handling the layout and its several updates. But part of the component's logic still has to be handled by the user, since he is the only one with full access to the raw data that is loaded in the spring board.

Example Implementation

Let's say your spring board data is composed of arrays of numbers stored in one main array called dataContent. Your implementation for the SpringBoardDataSource should look something like this:

class ViewController: UIVIewController {

// IMPORTANT: your data source has to be a variable type if you want to handle re-ordering
var dataContent: [[Int]] = [
                             [1,2,3,4,5],
                             [4,5,6,7],
                             [8,9,10,11,12,13]
                           ]

}

extension ViewController: SpringBoardDataSource {

/* The user can customize the appearance of the editing buttons and the animation triggered when the spring board is in editing mode by returning custom values for the editingAnimation & editingButton variables */ var editingAnimation: CAAnimation? { return nil } var editingButton: UIButton? { return nil }

func numberOfPages(in springBoard: SpringBoardView) -> Int { return dataContent.count }

func numberOfRows(in springBoard: SpringBoardView) -> Int { // Choose the number of rows of your spring board return 5 }

func numberOfColumns(in springBoard: SpringBoardView) -> Int { // Choose the number of columns of your spring board return 4 }

func springBoard(_ springBoard: SpringBoardView, numberOfItemsInPage page: Int) -> Int { return dataContent[page].count }

func springBoard(_ springBoard: SpringBoardView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { /* Choose the cell type to load according to your model */ return springBoard.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) }

func springBoard(_ springBoard: SpringBoardView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { /* Handle the appearance of your cells here */ let label = UILabel(frame: cell.bounds) label.text = "(dataContent[indexPath.section][indexPath.item])" cell.addSubview(label) }

func springBoard(_ springBoard: SpringBoardView, didTouchEditingButtonFor cell: UICollectionViewCell, at indexPath: IndexPath) {

// remove the item in the raw data
dataContent[indexPath.section].remove(at: indexPath.row)    
springBoard.deleteItems(at: [indexPath])

}

func springBoard(_ springBoard: SpringBoardView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {

// You should handle the creation of new pages when moving items
if dataContent.count <= destinationIndexPath.section {
  dataContent.append([])
}

// Handle the re-ordering in the raw data
let object = dataContent[sourceIndexPath.section].remove(at: sourceIndexPath.item)
dataContent[destinationIndexPath.section].insert(object, at: destinationIndexPath.item)

// If the new count of items in the section exceeds the page capacity,
// you should move the last item to the next section
if data[destinationIndexPath.section].count > numberOfRows(in: springBoard) * numberOfColumns(in: springBoard) {
  self.springBoard(springBoard,
                   moveItemAt: IndexPath(item: data[destinationIndexPath.section].count-1, section: destinationIndexPath.section),
                   to: IndexPath(item: 0, section: destinationIndexPath.section+1))
  springBoard.reloadData()
}

} }

Future Improvements

As of today, several ways in which we intend to improve SwiftSpringBoard include:

Dynamic columns & rows counts. This will be our next evolution. As of today the user has to set manually its number of rows and columns for the layout to compute dimensions properly. We expect to accomplish an automatic computation of the number of rows and columns based solely on the insets and spacing attibutes of the layout and a fixed size for the items.

Handling folders. Although users can already implement this behaviour, we are looking for a clean and simple way to add this feature to our library. An option would be to create another instance of SpringBoardView for the opened folder.

If you happen to use our component, we are really curious as to what you will do with it. And if you have any question / suggestion, feel free to contact us.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment