Recreating native iOS scroll and momentum

MrManafon
Homullus
Published in
9 min readFeb 17, 2017

We recreated iOS native scroll with momentum and spring in Unity. I won’t show a lot of code, but i will share with you the principles and magic numbers that Apple themselves use.

I am certain that i am not the only person ever to feel amused by the fact that in 2017, in the era of UX, not every scroll is the same. On second thought, it may be a poor decision to standardize everything, and while you may argue that one type of scroll physics is better than other, this is in fact a question of opinion. And my boss’ opinion was that we need to implement a clone of iOS scroll physics into our Unity mobile app.

Why would you use Unity for a mobile app?

— This was the question i asked myself every day.

Regardless of why, we solved the issue. But what we found out will surprise you! Yes, we did this in C# — but who cares, the underlying algorithm is important, not the language.

Don’t want to listen to all the drama?

Just scroll to the bottom.

Free image from Erik Hersman

NGUI and Unity UI

From start we knew that it will be impossible to find a finished product/plugin on the Asset store, and just implement it, but i also knew that our boss is an old-school senior programmer, and does not take no for an answer. So we examined the physics of both NGUI and native Unity UI scrollviews. And it was horrible.

While we cannot fully inspect the inner workings of Unity UI, we could learn a lot by observation. To put it simply, it seems that the developers were in a hurry, and did not take their time to do some proper research on how other platforms handle scrolling of UI elements. Maybe they were not told to care? After all, Unity UI is not meant to be used for full-on mobile applications (social network in our case). No platform is the same, but so far we know that both Android and iOS agree on a few things:

  1. Disable or reuse elements outside viewport
  2. Set up a momentum and stay true to it
  3. Have some implementation of spring mechanism or similar
  4. Horizontal and vertical scroll should not coexist

Lets use font-weight: 1700 for that first one. There is a fairly good reason why Apple and Google went trough all the trouble of making stuff disappear or deactivate — performance issues. When you are using Unity UI to scroll an in-game menu of 10 items, it behaves good in terms of pure fps performance. But once you start representing all 1244 contacts rendered from Facebook API with their respective profile pictures, all hell breaks loose. Memory and CPU go up and fps goes down. As i have discovered iOS uses a cool technique — it keeps track of when an element leaves the viewport (for the sake of everyone, lets just call it that) and when it does, the element is emptied, filled with new data and appended to the bottom of the list. So, the list never has more than 12–13 elements that are being reused all the time. We decided this functionality is a must, but more on that later.

Of course, Unity UI does not do this, and as i have been told by my colleague Miloš, NGUI seems to have a semi-developed system, which is never acctually used anywhere (huh, right?).

As for their momentum and spring options, we spent the whole day customizing them, and they never worked as expected.

Moving the research to the Web

In a moment of desperation, i posted a question on StackOverflow (spoiler inside):

For those of you that opt not to read the S/O thread because of spoilers, here is the rub. I asked if anyone has any ideas about the physics of Apple’s momentum, and all i received back are people annoyed that we “don’t use Unity.UI” or that we “still use NGUI like we are 95”. Very mature.

I decided that enough is enough and so i started looking for help elsewhere, older colleagues seldom had any comments on the matter, and web forums were sprinkled with people asking the same thing (for a vide array of languages), but not receiving an answer or getting a similar response like i did. I remembered that some JS libraries try to mimic this behavior. At one point Apple did not allow native momentum to be applied to overflow:scroll-y elements on the web, and the only way to get a position:fixed header on mobile was to wrap the whole body into two sibling divs with overflow scroll. (At the moment, this seems to have been solved) So developers hurled a bunch of poorly written JS and created what is known as iScroll. If you don’t care about the underlying code, this is an awesome 55kb library that automates the process of creating fixed headers, scrolling overflow windows and much more on mobile devices. It is awesome. If you need a black box type of thing. Which is what we did not need.

But no one likes magic numbers as they always point to developer’s desperation.

Enter iScroll & alternatives

So we inspected the library and found a bunch of magic numbers and weird solutions. To cut the story short, most of the code is used for touch recognition and switching between a touch, tap, swipe etc. Don’t take my word for it, as it was a few months ago and everything seems scrambled from this perspective, but here is some proto code of what i remember it being like:

final_distance = swipe_px + (swipe_px * 35 * 0.015)
time_y = swipe_time

So they would mash up a bunch of numbers, and scroll the screen by that much, during time-y seconds. Surprisingly the library worked really good. The general feel was almost as good as the real deal. But no one likes magic numbers as they always point to developer’s desperation. And this was to be discovered shortly after.

When we implemented this solution we noticed that it is behaving differently than expected. Ok i said, the easing is different, let’s solve that now. So we dug in and discovered that iScroll uses some custom written easings that span 46 lines of JS. Wow. Even if we wanted to, we couldn’t reproduce that with a simple bezier curve in Unity, and we don’t want to. I mean, look at this pile of code:

fn: function (k) {
if ( ( k /= 1 ) < ( 1 / 2.75 ) ) {
return 7.5625 * k * k;
} else if ( k < ( 2 / 2.75 ) ) {
return 7.5625 * ( k -= ( 1.5 / 2.75 ) ) * k + 0.75;
} else if ( k < ( 2.5 / 2.75 ) ) {
return 7.5625 * ( k -= ( 2.25 / 2.75 ) ) * k + 0.9375;
} else {
return 7.5625 * ( k -= ( 2.625 / 2.75 ) ) * k + 0.984375;
}
}

Deeper into the rabbit hole, enter PastryKit

No one on the planet seems to know about Apple’s PastryKit, which is exactly what we needed. Apple has erased every trace of it, and developers never really caught on, so it is almost impossible to find, and it is utterly useless nowadays, since App Stores became a thing in 2009. But apart from it being unreadable, this JS framework was a thing of beauty and belongs to some computer history museum as it was ahead of it’s time and mobile-webapps are indeed coming back (Cordova, Electron, Appcelerator, Xamarin, React Native to name a few).

I have never heard of PastryKit, but i accidentally stumbled upon this post by John Gruber at Daring Fireball and at the explanation of it, here.

A Flashback: Since i worked for Apple as an ACMT for a few years, i knew Gruber’s name, and i clearly remember the period when iPhone 2G went out and changed a lot of things. Some of the things it lacked at the time were language, store, apps and developers. It was only year and a half later that App Store was there, and two years later that Google Play existed. So at this point in time, Steve Jobs believed strongly that this new HTML5 standard is gonna obliterate Adobe Flash (remember that?) and that all apps a person would ever need should be written using HTML5 and JS. They even made a Framework that they themselves used in creating an early version of iPhone User Guide, in order to force everyone to use the same UX principles that are available natively in iOS. They nor anyone else ever mentioned it again. Guess the name? PastryKit.

But apart from it being utterly unreadable, this JS framework was a thing of beauty and belongs to some computer history museum as it was ahead of it’s time and mobile-webapps are indeed coming back

I figured if anyone on the planet knows how Apple had set up the momentum, it’s Apple. So, i dissected this thing for two days (i never said i am any good, i even asked for help) and i found out only enough to understand the principles. Everything is being handled by a buttload of event listeners, because of the complexity the naming conventions are weird like removeFromSuperview, other var names are named simply x or ky. To be honest, when i first saw the code, i was certain that i will not be able to find out what the hell is going on, as my breakpoints went from ln 43 to 450 to 1300 to 3000+ to 22… But at one moment i discovered a pattern and was amazed by the simplicity of the scroll mechanism.

const PKScrollViewDecelerationFrictionFactor = 0.95;this.decelerationVelocity.y *= PKScrollViewDecelerationFrictionFactor;

Wow. All that trouble that iScroll authors went trough, and Apple solved it with a magic constant of 0.95. To be truthful, there are additional rules and verifications, but the basic scroll bezier curve is dimply set at 0.95.

Let’s see what i have learned from the code:

  1. Apple is as desperate as the rest of us, but it’s just more complicated.
  2. Judging by the amount of fallbacks for every possible scenario, it must be a coder’s nightmare to work in Apple.
  3. When i say every possible scenario, i mean it.

Now that we got first impressions out of the way, let’s see what i have really learned:

Basically, while the touch lasts, apple lets you move the screen 1:1.On touch end Apple would get momentum by dividing number of pixels that the user had swiped, and time that the user has swiped for. If the number of pixels was less than 10 or time was less than 0.5, momentum would be clamped to zero.Anyways, once the momentum (speed) was known to us, they would multiply it by 0.95 in every frame, and then move the screen by that much.The bounce seems to be fixed to a certain speed that i did not bother to calculate, i just set it up manually as i saw fit.

So idiotically simple and elegant, that it hurts my eyes.

Unity Scroll

It is so simple that i believe that i don’t even have to post our C# code for it. Nevertheless, this is what we did. Miloš cloned NGUI’s ScrollView class, created a custom easing based on this data.

speed *= speedCoeficient;

As for the reuse of containers, we decided to use a soft approach and added the GameObject disabling functionality by simply using the SetActive method on them if they are not visible. Basically, when deciding on the distance for the next frame, we would also check if any of the GameObjects are not visible and if so, we would disable them. Seems like a lot of CPU work, but saves a huge amount of memory and fps.

If anyone needs me, i’ll be in my Batmobile.

Conclusion

There it is. I really believe that someone from iScroll community of developers reads this, scolds me for bad-mouthing their code, and then uses this knowledge to improve their code.

Also it would be really nice if someone with more experience than I takes interest into the PastryKit code, i believe that we can learn far more about this than i have managed with my limited time and knowledge.

--

--