I recently came across a StackOverflow question (https://stackoverflow.com/questions/51544588/drawing-an-infinite-grid-in-ios) in which the challenge was to create an infinite grid on iOS, using a standard UIScrollView and/or UIKit. Here’s how I solved this.

Defining the requirements

iPhone will show an infinite grid

Must use only UIKit native classes

Scrolling should have the expected feel of a scrollview

User must have the impression they can scroll forever

Must be memory efficient

Content must be generated in tiles so as to construct a grid

Initial coordinate should be specifiable

Understanding the constraints

Before we get started, we have to understand what will be the limiting elements.

UIScrollView require to have a finite set of child views at any one point in time.

When you reach the end of a scrollview, the scrollview stops or bounces, we need it to continuously scroll infinitely.

Attempting to scroll past the edge of a scrollview with bouncing enabled has some “elastic” feel to it, and at some point the scrollview will bounce back to within its content limit; we cannot rely on scrolling past the scrollview edges for our purpose.

Memory allocation is limited on mobile devices, if an application attempts to allocate too much memory (think too many grid tiles) the application will be terminated by iOS.

UIViews have a maximum size, we cannot create a single container view to put in the scrollview and make it infinitely large.

Getting started, setting up the scrollview

We begin by adding a UIScrollView to our view controller, and layout out the constraints so the scrollview covers the entire safe area. We then define a small UIView (size doesn’t matter!) within our scrollview and set the leading, trailing, bottom and top constraints to 0.

Base scrollview with small reference view

Running the code, you have a scrollview that doesn’t scroll at all, but before we do more work, let’s look at how much memory our very simple app is using so far:

Memory requirements, base scrollview + reference view only

Let me see some scrolling!

There are various ways to approach configuring our scrollview; in our case, we are going change our reference view to a “GridView” custom class, and do all of our grid control from that view. Let’s add a new file to our project, called GridView.swift:

GridView.swift, base code

Then, in Interface Builder lets change the reference view class to GridView:

Make sure you connect the IBOutlets we have defined for the top, bottom, left and right layout constraints, as well as to our project scrollview. Let’s see how this runs:

Scrollview with large scrollable area

Perfect! We now have one very large scrolling area, not very useful yet let’s make sure we understand the code we introduced.

First, we added an “arbitraryLargeOffset” with a value of 10000000.0; as the name implies this value is arbitrary and could be set to almost anything. This value controls how far the user can scroll on either side of our reference view before reaching the end of the scrollview.

Since UIScrollView does not allocate a pixel buffer for the scrollable area, we can make this value gigantic without affecting memory usage. The larger this value is, the more the user is able to scroll continuously before we have to do “magic”. In theory, if this value is big enough, a user would tire of continuously scrolling before this happens. Changing temporarily this value to something in the 1000.0 range should allow you to get an idea of why this number needs to be big.

In order to properly setup this large scrollable area, we hooked into the awakeFromNib() function to execute our custom defineScrollableArea() function and update all the layout constraints to our arbitrary value. We also execute the centreOurReferenceView() function so we start with our reference view visible on the screen.

Adding our grid

In our example, let’s assume that when the grid is first setup, the initial coordinates at the centre of the screen are (0,0).

Grid initial coordinates

As we add grid tiles to our view, we will want to keep track of which coordinates are visible on screen, which “tile” is associated to which coordinates, as well as the visibile coordinates at the centre of our scrollview.

Let us thus create a custom UIView class named GridTile:

GridTile.swift, a very simple tile

Our simple tile will simply display a UILabel with the coordinates assigned to the tile. Let’s also update our GridView.swift file with a few more lines of code:

GridView.swift, tile allocation code

Then update our awakeFromNib() to call allocateInitialTiles() function we just added:

GridView.swift, updated awakeFromNib()

Running our sample code, we now obtain:

Testing allocateTile(at:) function

The GridTile.swift code is relatively simple in this example, its a custom view with a UILabel as subview showing the coordinates. In a real world example, these could be map tiles, or whatever needs to be displayed in your infinite grid.

As part of the code we added in GridView.swift, we have allocateTile(at:) which as the name implies will allocate a new grid tile at the specified coordinates. This function uses the frameForTile(at:) function to compute where in the grid this tile has to be placed.

The computations in frameForTile(at:) are purposefully a bit more complex than they could be. The motivation is to allow for the grid tile size to be decoupled from the reference view size we defined in Interface Builder. Go ahead and change the ‘tileSize’ variable and see how this affect placement of the tiles.

Partially automating grid construction

So far, we have manually called the allocateTile(at:) function to generate a 3x3 grid, let’s automate this so we can quickly re-adjust our grid size. Let’s update allocateInitialTiles() function and add populateGridInBounds(…) function.

GridView.swfit, populateGridInBounds(…)

Launching our sample code, we now have:

Testing populateGridInBounds(…)

Let us now attempt to increase our grid size to 50 tiles in every direction. Using our new function, this is really easy:

With result, as expected, a 101 x 101 scrollable grid!

Testing our new scrollable grid

Are we done?

Far from it unfortunately. Back at the beginning of this tutorial I mentioned that this should be memory efficient. Let’s peek at the monster we just created:

Memory consumption exceeding 350MB for a 101 x 101 grid

Yikes! We are now using in excess of 350MB of memory for our simple grid!?

Indeed, remember those UILabel() that we added as subview to our GridTile? Each one of those UILabel generates a memory buffer to render the label text that will be sent to the mobile display. We just allocated 10,201 labels, each using 10,000 pixels of memory buffer. This solution will not be scalable to our “infinite” grid.

Observing scrollview contentOffset

In order to get started at solving the memory requirements issue, we will first need to identify where in the grid we are, so that we can make sure only those grid tiles we need are allocated, and we deallocate the grid tiles that are no longer needed.

If you are not familiar with Key-Value Observing and want to learn, head over to the Apple Developer archives at: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html

Let’s update our GridView.swift with some additional code:

GridView.swift, observing scrollview contentOffset

And updating awakeFromNib() once more:

GridView.swift, final awakeFromNib()

Essentially, when the GridView is instantiated, we add the GridView as a Key-Value observer to the scrollview, asking to be notified whenever the contentOffset variable is updated. Since the contentOffset is updated every time the scrollview is dragged around, the observeValue(forKeyPath:…) function will be called so we can update the grid.

The current adjustGrid(for:) function is not yet updating the grid; it currently keeps track of the coordinates visible at the center of the scrollview as the scrollview is dragged around. The coordinates should now be printed in the console:

Console logs showing centre coordinates updates

Dynamic grid expansion

Now that we can detect when the centre coordinates change, let’s implement dynamically allocating new grid tiles. Let’s begin by replacing the allocateTile(at:) function with the following code:

GridView.swift, new allocateTile(at:) function with check for existing tile

When a request is made to allocate a tile, the code will first check if the tile already exists; if it does it has nothing else to do. Time to update the adjustGrid(for:) function:

GridView.swift, updated adjustGrid(for:) for dynamic expansion

This updated adjustGrid(for:) function now computes how many tiles are required to fill the screen width and height, and automatically request to populate the grid within the calculated coordinates. And since we can now do this dynamically, let’s also update our allocateInitialTiles() function:

GridView.swift, final allocateInitialTiles() using adjustGrid(for:) function

Now, let’s see how this looks:

Grid starting blank, then populating properly

At first glance, it looks like the grid is empty when initially created, and then as the centre coordinates change, the grid properly populates. A quick survey in the code reveals that we manually set the centreCoordinates to (0, 0) so when the allocateInitialTiles() function calls the adjustGrid(for:) function, the code assumes there is nothing to do since the centre coordinates are the same. We simply need to set the centreCoordinates to some non-0 value:

Updated the initial values of centreCoordinates to non-0 values

Running the code again…

Grid properly populating dynamically

Success! Our next step is to remove the tiles that are not needed otherwise as the user scroll the infinite grid, our memory usage would continuously increase until iOS terminates our app.

Dynamic grid tile deallocation

We will create a clearGridOutsideBounds(…) function which will act in pair with the populateGridInBounds(…) function. Add the following code to GridView.swift:

GridView.swift, clearGridOutsideBounds(…) function

Then, let’s finalize our adjustGrid(for:) function:

GridView.swift, final adjustGrid(for:) function

Testing our code change, we can see the grid being properly displayed, and our console log showing dynamic allocation and deallocation happening as required:

Console logs with dynamic allocation/deallocation details

Let’s confirm our memory requirements are met by scrolling for a while and checking how much memory is used:

Memory requirements with dynamic allocation in place

Making our grid infinite

While our abritraryLargeNumber is huge, it may be tempting to think we are done, but a dedicated user would eventually be able to reach the end of our scrollview. This can easily be tested by setting the arbitraryLargeNumber to say 10000.0 then scrolling to the top of the scrollview:

Scrollview reaching the top and bouncing

As the user reaches the top of the scrollview, the scrollview bounces and our infinite scrollview is only a very large finite scrollview.

We can fix this by adding a few scrollview delegate methods, still within the GridView.swift but outside of our GridView class definition, let’s add:

GridView.Swift, scrollview delegate implementation

And update our observeScrollview()

GridView.swift, final observeScrollview() function

The end result, is our scrollview being reset to centre every time it is no longer active. Visually, the only changes are the scrollview indicators being reset to middle and the grey reference view being relocated on the tile near the center.

Final infinite grid implementation

To complete the infinite grid implementation, the scrollview indicators should be hidden and the reference view background color should be set to UIColor.clear.

Source code

Source code for this project is hosted on GitHub: https://github.com/freshcode/Infinite-Grid-Swift

About the author

Dave Poirier is a senior software developer, currently working on creating some really interesting iOS applications at ID Fusion Software Inc.

Need help with your mobile app software development? Visit our website at http://idfusion.com