Controlling "animations" with a UIScrollView

This is something that I've done a number of times myself but also something that I see fairly regularly popping up on Stack Overflow.

The idea is to change something based on the content offset of a scroll view.

Example of changing the background colour based on scroll position. I wouldn't actually do this in a real app just showing the idea.

The required effect is almost always based on having a begin state when the scroll view is not scrolled at all and an end state when the scroll view is scrolled to its maximum scroll distance.

Normally I write this from scratch but I've decided to blog this so people can use it as a resource.

The way I approach this is to work everything out in percentages. I don't want to get into actual points because this can (and will) change depending on device size etc...

In the scroll view delegate method you can have something like this...

// this just calculates the percentages and passes the calculated values off to another method.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    // horizontal
    CGFloat maximumHorizontalOffset = scrollView.contentSize.width - CGRectGetWidth(scrollView.frame);
    CGFloat currentHorizontalOffset = scrollView.contentOffset.x;

    // vertical
    CGFloat maximumVerticalOffset = scrollView.contentSize.height - CGRectGetHeight(scrollView.frame);
    CGFloat currentVerticalOffset = scrollView.contentOffset.y;    
    
    // percentages
    CGFloat percentageHorizontalOffset = currentHorizontalOffset / maximumHorizontalOffset;
    CGFloat percentageVerticalOffset = currentVerticalOffset / maximumVerticalOffset;
    
    [self scrollView:scrollView didScrollToPercentageOffset:CGPointMake(percentageHorizontalOffset, percentageVerticalOffset)];
}

This takes the content offset of the scroll view and turns it into a "percentageOffset".

Percentage Offset (CGPoint)
  • (0, 0) means that the scrollView is not scrolled. This is when contentOffset is (0, 0).
  • (1, 1) means that the scrollView is scrolled to its maximum contentOffset both horizontally and vertically.
  • (0, 0.5) (for example) means that the scroll view is not scrolled horizontally and is at 50% of it's maximum content offset vertically.

This CGPoint is then passed into the next function...

- (void)scrollView:(UIScrollView *)scrollView didScrollToPercentageOffset:(CGPoint)percentageOffset
{
    // here I set the backgroundColor of the text view to a color calculated in the next method
    self.textView.backgroundColor = [self HSBColorForOffsetPercentage:percentageOffset.y];
}

Now I can start doing work. I no longer have to worry about contentOffsets and points. I can just work with the percentageOffset argument that gets passed into me.

The next method does the actual work in my example.

- (UIColor *)HSBColorForOffsetPercentage:(CGFloat)percentage
{
    // set the begin and end "states"
    CGFloat minColorHue = 0.0;
    CGFloat maxColorHue = 0.18;
    
    // calculate the current state from the percentage
    CGFloat actualHue = (maxColorHue - minColorHue) * percentage + minColorHue;
    
    // make the colour from that state
    return [UIColor colorWithHue:actualHue saturation:1.0 brightness:1.0 alpha:1.0];
}

In this method I set my minimum and maximum state and use the percentage value to calculate the required value between that maximum and minimum.

This is everything I need. There is no need to "animate" anything as we are not doing anything over time. All we are doing is responding to changing values from the scroll view. The scroll view handles all aspects of "animation" for us.

Using this method it's possible to control almost anything using a scroll view.

You can control the size, rotation, opacity, colour, position, of anything. You can even change multiple aspects or change different values based of horizontal and vertical scrolling (just use the x and y percentages different).

I've used this exact technique before for creating a fancy scroll on boarding screen thingy.

If you create key frames in the animation you can then just interpolate these key frames based on percentages along the scroll view. Really easy to do from scratch too.

In fact, I have used this so much that it makes me wonder whether there should be a percentageOffset type delegate method on scroll view? LOL!

Understanding a rope around the world

This is a slight break from the norm as it is something that has been flying around my brain for the past few days/decades.

The problem

Years ago (I must have been about 12 or 13) I was at a dinner party with the family and someone mentioned this mathematical problem.

For anyone not familiar it goes something along the lines of...

Take a length of rope and form it into a circle around a basketball.
Now add around 60cm to the rope and form it into a new circle.
This new circle will have a radius about 10cm bigger than the radius of the basketball.

Now...

Take a length of rope and wrap it around the equator of the earth. (assuming earth is a perfect sphere).
Now add the same 60cm to the rope and form it into a new circle.
Amazingly, mind-blowingly and converse to anything your brain might want to believe this new circle will now float 10cm above the earth all the way around.

All you've added is 60cm of length to a piece of rope that's about 40,000km long and it adds 10cm to the radius.

Year after year my brain has wandered back to this problem and always refuses to believe it. Even when I did the maths behind it and proved it to myself I still struggled to comprehend.

The maths...

Let r be the radius of initial circle.
\displaystyle r=\frac{C}{2\pi}

Let r' be the radius of the new circle.
Let x be the length added to the circumference.
\displaystyle r'=\frac{C+x}{2\pi} \\ r'=\frac{C}{2\pi}+\frac{x}{2\pi} \\ \therefore r' = r+\frac{x}{2\pi}

There you have it. The new radius is equal to the old radius plus the length added over 2 Pi. Thus, add 60cm of length and the radius increases by about 10cm.

In fact, plugging in the number you get an increase in radius of around 9.5493cm.

I did this years ago and still I couldn't wrap my brain around it.

Even if the original circle wraps around the edge of the universe it will still be 9.5493cm bigger in all directions!

Making my brain understand it

So, I started thinking about how to make it understandable enough for my brain to accept this obvious fact.

I wanted to see where each part of that 60cm of additional circumference length went. If I could picture this it would make the problem easier to see.

So I decided to explore what happens to a quarter of the circle when I add 15cm (one quarter of 60cm) in various different ways.

Quarter of a circle.
Quarter of a circle.

If I concentrate on the change in h in the above diagram I might be able to understand what's going on.

So, initially I added all 15cm of (straight line) length at an angle of 0^{\circ} to the horizontal radius of the quarter circle.

Quarter of circle with one added line.
Quarter of circle with one added line.

Obviously in this we can see that the new height is r (the old height) plus the 15 cm we just added.

Now, let's do this again but instead I'll do it twice.

Two sections added.
Two sections added.

Now, how much height did we add? Well obviously at the bottom it's 7.5 cm. But at the half way mark? Let's enlarge this on its own. I have highlighted the added section in orange.

Right angled triangle added.
Right angled triangle added.

In this instance we let \Theta = \frac{\pi}{4}rads.

So, what is \delta h?

Well, we have a right angle triangle. We know the length of the hypotenuse, we know an angle and we want to get the length of the adjacent edge. So...

\displaystyle \cos\Theta = \frac{\delta h}{7.5}\\ 7.5\cos\Theta = \delta h

Plugging in the values we get...

\delta h \approx 5.3cm

So the total increase in height is...

\displaystyle 7.5\cos0 + 7.5\cos\left(\frac{\pi}{4}\right)\\ \to 7.5\left(\cos0 + \cos\left(\frac{\pi}{4}\right)\right)
The first line can be through of as a triangle with angle 0.

So we get an increase of around 12.8033cm.

We can do this again...

Three sections added.
Three sections added.

We can see from this that, using the same geometry as we did previously, the increase in height is...

\displaystyle 5\left(\cos0 + \cos\left(\frac{\pi}{6}\right) + \cos\left(\frac{\pi}{3}\right)\right)

To simplify this we could write it as a sum...

\displaystyle 5 \times \sum_{n=1}^{p=3}\cos\left((n-1)\frac{\pi}{2p}\right)

To explain the above...

p is the number of sections added (3)
5 is the length of each section added to the line. In the is case 15 / 3 (the number of measurements).
n-1 because we want to start at zero radians.

We can now use this to work out the total change in height of around 11.83 cm.

Wolfram Alpha result

So we continue using this same method with more and more sections (triangles) and calculate the change in height each time.

p=1 \to \delta h=15\\ p=2 \to \delta h\approx 12.8033\\ p=3 \to \delta h\approx 11.8301\\ p=4 \to \delta h\approx 11.3013\\ p=5 \to \delta h\approx 10.9706\\ p=6 \to \delta h\approx 10.7447\\ p=7 \to \delta h\approx 10.5806

In fact, we can plot this as a graph...

Graph of increase in height against number of sections added.
Graph of increase in height against number of sections added.

From here we can clearly see that the graph asymptotes towards a value of around 9.5cm... which if we look above in the post is exactly what we were expecting. The actual value of \frac{60}{2\pi} which is around 9.5493cm.

Something things to consider

Yes, I was only looking at the change in height of quarter of the circle. But the problem is symmetrical. Not only is the change in height is translated to each of the other 3 quarters. But, dues to the symmetry of the cos and sin functions the increase in height is matched at every measurement by the "opposite" increase in width. i.e. for every triangle that adds a height of x there is an opposite triangle that adds a width of x and vice versa.

I have not made any assumptions about the size of the circle I have been adding sections to. The only thing I have used is the angle at which line segment is added. Therefore this works for a circle of any size.

Working through it this way I got to a point when I realised the obviousness of this whole problem.

Well duh!
Well duh!

Take this picture. I have done the first step of adding 15cm to each quarter circle. It is fairly obvious from this that by adding 4 sections of length 15 cm like this the point that is 90^{\circ} around from it will be 15cm further away. This can be comprehended even if the circle is a million light years in radius.

Lastly, I am pretty sure I'm just a couple of steps away from equating my summation using the circumference to the equation C=2\pi r but I'll leave that for another day.

Conclusion

Anyway, I was quite pleased with how my calculations turned out and the little formula I came up with. It was nice to get my head into some proper analytical maths and for it to work so well after so many years off.

Who knows, I might make this a regular thing.

Thanks for reading :)

Paint Code Slider

I saw the Humble Bundle today for $19.99 and in it was PaintCode! Definite must buy if it’s still available when you’re reading this.

I tried PaintCode a few months ago but never investigated what it could do. When I last looked the tutorials were unhelpful. All they showed is how to produce a static image onto the iPhone screen with no interaction.

I’ve been looking in to making more dynamic and intractable UI elements using Paint Code. At the same time I thought I’d start a series of tutorials creating different simple UI elements each time.

So, my first on is a slider implementation. I’ve tried to make it as like the iOS UISlider as possible for no particular reason other than the iOS one seems to work pretty well.

Anyway, here's a quick video of what I created...

You can get the repository here... Paint Code Slider

I'll eventually be making a screencast of how I made it but I haven't yet. Come back in a few days and I may have it made.

Anyway, thanks for reading.

Animate with springs!

Today I was having a look at some of the different UIView animation block methods and I noticed one that I'd never seen before. I read the docs and it is an amazing little function added for iOS 7.

The function (which is a bit of a mouthful as usual) is...

[UIView animateWithDuration:(CGFloat)duration
                      delay:(CGFloat)delay
     usingSpringWithDamping:(CGFloat)damping
      initialSpringVelocity:(CGFloat)velocity
                    options:(UIviewAnimationOptions)options
                 animations:^() (animations)
                 completion:^(BOOL finished) (completion)]

Like I said, it's a bit if a mouthful.

I haven't fully explored what is possible with this yet but from my first couple if attempts I was able to get some very effective UI animations that would have taken a lot of messing around using the standard block animations.

The two new parameters are fairly self explanatory but just to be certain...

usingSpringWithDamping
This is a float between 0 and 1. It affects how "strong" the spring is. A value of 1 will not oscillate at all. It will slow down until reaching the target value. A value of 0 is the opposite, the spring will oscillate infinitely. Now, I'm not sure how this works given the time constraint but I'll have a mess around in a bit.

initialSpringVelocity
This is also a float and is a relative value. The actual velocity unit is "total distance of the animation per second". I.e. If your animation has a total change of 100 points then a value of 1 will set the initial velocity at 100 points per second in the direction of the animation.
This last bit is quite important. The polarity of the velocity is not up or down but it is relative to the direction of the animation change. If you give it a negative number then your animation will start by "popping" in the opposite direction before springing back.

A quick note
I talk about "direction" in this blog but again that's not to say that this can only be used to animate changes in position. The spring essentially maps to a custom animation curve. This can be used for rotation, scale, transformation, anything that a standard animation can be used for.

Have fun messing around with it.

A quick video of something I made using the animate with springs API.

Why I wish I'd never used Magical Record

The first time I used Core Data I thought it was amazing. However, I got it all very wrong. I had "helper" functions in the App Delegate, I didn't use any relationships in the data model, etc... All very wrong.

Then I learned more about it and did it all myself again, this time it was excellent! I understood how the relationships worked, I was using NSFetchedResultsController. Merging changes from background contexts, syncing changes up to a server, everything.

Then I found Magical Record... this is awesome! (I thought).
It makes everything to do with Core Data so easy! (I thought).

It puts hundreds of different functions in to one line of code. Creating fetches becomes so easy, saving is easy, NSFetchedResultsController creation is now one line of code.

I went to town. Completely integrated it into my app.
...
Then I migrated my app from Xcode 4 to Xcode 5.

Ran the app... everything is fine! Woop!
...
Wait... the UI isn't responding, I can't press any buttons, no exceptions thrown either, what's going on?!

I paused the app and the first line in the stack trace looked a bit odd.

magicalrecord

Looks like something to do with Core Data is blocking the main thread. Time to put my debug hat on...

Right, CoreData, where could the error be?

Wait... I have no idea. Something in Magical Record is doing something that looks like it's causing a deadlock between threads.

Do I know where it is? Nope, I didn't write it.
Do I know where to start looking? Nope, this code is alien to me.
Do I wish I could pull Magical record out of the project? Absolutely.

Is this blog really just a rant?

OK, let me get to my point.

I doubt that Magical Record is fully to blame for this. I'm guessing that somewhere I used it in the wrong way that caused this error. I guess...

But that's just it. I can only guess at what is wrong.

Because I've relied so much on someone else's code there isn't really anything I can do to work out how to fix it. I don't know the inner workings of my app when the inner workings is what I'm building.

I've since started a couple of other projects using CoreData and networking and neither of them use AFNetworking or Magical Record. Not because I think they are bad or don't work, but because I know that I can do everything I need just as easily and just as well. In fact, in both cases, my own code worked better for what it needed to do than either of these frameworks could do for me.

Of course, I'm not suggesting that everyone should stop using Magical Record and AFNetworking. Just make sure you know how and why they work before diving in head first. There will come a time when you need to know why something is acting in a particular way and you'll need to know the fundamentals.

I'm stuck with two options at the moment.

1. Try and work out what is happening that is causing this deadlock. I have no idea if it's my code or Magical Record code.
2. Try and remove Magical Record completely from my app. This is no mean feat. There are uses of it all over the place.

::Sigh:: back to work :(

NSCalendar isDate:equalToDate:toUnitGranularity:

During the beta releases of iOS 7 there were several mentions of a couple of new functions on NSCalendar.

- (BOOL)isDate:(NSDate *)date1 equalToDate:(NSDate *)date2 toUnitGranularity:(NSCalendarUnit)granularity;
- (NSComparisonResult)compareDate:(NSDate *)date1 toDate:(NSDate *)date2 toUnitGranularity:(NSCalendarUnit)granularity;

When I first saw these it was awesome. It makes comparing dates a hell of a lot easier and puts a lot of maths in to one line of code.

The usage is along the lines of...

NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];

BOOL datesAreEqual = [calendar isDate:firstDate equalToDate:secondDate toUnitGranularity:NSDayCalendarUnit];

datesAreEqual would then be YES for dates on the same day. Not necessarily the same hour, minute or second, etc...

HOWEVER

When the GM of iOS 7 went live, Apple seemingly removed these functions. ::sad face::

Anyway, I've implemented my own version which works in pretty much exactly the same way.

It's a category on NSCalendar.

The header just has the functions.

#import <Foundation/Foundation.h>

@interface NSCalendar (equalWithGranularity)

- (BOOL)ojf_isDate:(NSDate *)date1 equalToDate:(NSDate *)date2 withGranularity:(NSCalendarUnit)granularity;
- (NSComparisonResult)ojf_compareDate:(NSDate *)date1 toDate:(NSDate *)date2 withGranularity:(NSCalendarUnit)granularity;

@end

You can get it on GitHub.

It will also be available shortly through CocoaPods.

Feel free to add it in to your projects.

The Hamburger Menu - is it a good thing?

I've always been doubtful about the "Hamburger menu" in iOS apps.

For those who don't know what I'm talking about it is this style of side menu used more and more in apps recently...

Facebook Hamburger Menu
Facebook hamburger menu.

It gets its name from the icon on the button used to open and close it that looks like a hamburger.

The more I see it the more I think it's just a dumping ground for miscellaneous items in apps.

It's a place that designers go to when they don't know where to put stuff in the app.

Take a look at any Apple app. None of them uses a Hamburger Menu.

For a few other examples of great apps with not a single Hamburger Menu in sight take a look at these...

If something doesn't deserve to have pride of place in your app then it shouldn't be there at all. Don't just stuff it into a hamburger menu.

There was a quote from WWDC 2013 about "Designing great apps". The guy said something like...

Find out what the primary function of your app is. Then list the secondary functions and the tertiary functions.

Make the primary function visible as soon as the app starts up. Make it look nice and make it engaging.

Put the secondary functions only a few taps away but don't make them detract from the primary function.

For the tertiary functions try to find a way to write them out of the app. Are they absolutely necessary?

I agree with this 100%. In my view the hamburger menu is a placeholder for these tertiary functions. It shouldn't exist. Apple didn't create it. Apple doesn't use it. Why should we?

A couple of "firsts"

I've recently done a few things in my app development that I've never used before. I've done things in a similar way but I've never been satisfied with the resulting code. The apps worked but the code either wasn't elegant or tidy or I felt like I'd bodged it.

Anyway, I thought I'd share them just in case you feel the same about your code.

Auto Layout constraints in Interface Builder

After reading Ray Wenderlich's book iOS 6 by tutorials especially the Auto Layout chapters I decided to upgrade my current app to use Interface Builder auto layout constraints.

My usual practise is to use IB to throw the UI elements on the screen and then code the constraints myself. I always felt like it was a bit messy. I had an interface that didn't look like the app and code that seemingly did half a job.

Anyway, I followed all the rules from the excellent tutorials in the book and everything worked like a dream! I was animating, constraining and truncating like a demon! The end result was a massively reduced code base and a better looking interface file and everything worked perfectly.

UIPageViewController

Again, I've used this before but not for a very long time. The last time I used it was in the early days of my iOS dev and it was a complete mess.

I added a UIPageViewController subclass to my storyboard and with very little code got it to work. Taking a previously single view controller and converting it to a navigator with swipe gestures and spaces between each page.

Another really pleasing outcome from this with not too much effort.

This lead on to the next first...

NSObject IBOutlets

When configuring the UIPageViewController above I had to give it a datasource property. Much like a UITableView or UICollectionView it relies on this datasource object to tell it what to display on the screen.

Usually I would just make it the datasource of its self. However I've now started trying to practise clean coding and wanted to create a specific class to do the job.

I created the NSObject subclass and added a property to the UIPageViewController. However, with my new "get things working in IB" frame of mind I decided to use the NSObject outlet available in IB. I added the object, set the subclass, connected it to the property and finally set it as the datasource all from within IB.

Again, it worked perfectly first time. I've been wanting to use this for a while but never had the opportunity or the time (or knowledge) to fully understand how it works.

I will definitely be using this more in the future. I can see myself subscribing to Graham Lee's idea of creating separate datasource and delegate for table views and collection views.

I know from past experiments that OS X dev uses this principle quite a bit too. Who knows, maybe my next app will be a Mac app?

Interface Builder Y U no like auto layout?!

OK, a simple idea in auto layout terms...

I have a view, it has several buttons on it.

All buttons will have equal widths.
All buttons will have default spacing (between adjacent buttons and the superview).

Width of buttons therefore specified by width of super view.

In code it would go something like this...

// make all button widths equal
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:button2
                                                      attribute:NSLayoutAttributeWidth
                                                      relatedBy:NSLayoutRelationEqual
                                                         toItem:button1
                                                      attribute:NSLayoutAttributeWidth
                                                     multiplier:1.0
                                                       constant:0.0]];

[self.view addConstraint:[NSLayoutConstraint constraintWithItem:button3
                                                      attribute:NSLayoutAttributeWidth
                                                      relatedBy:NSLayoutRelationEqual
                                                         toItem:button1
                                                      attribute:NSLayoutAttributeWidth
                                                     multiplier:1.0
                                                       constant:0.0]];

// set spacing between buttons and subview to default
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[button1]-[button2]-[button3]-|"
                                                                  options:NSLayoutFormatAlignAllCenterY
                                                                  metrics:nil
                                                                    views:NSDictionaryOfVariableBindings(button1, button2, button3)]];

This is not ambiguous along the horizontal and has no conflicts at all. i.e. it's a perfectly acceptable auto-layout setup. Obviously it would also need vertical layout but I'm not talking about them at the moment.

So, setting this up in interface builder...

I started out by making all the buttons width 20 (not as a user constraint, just a system constraint).
I then added "widths equally" between button1-button2 and button1-button3.
I then added the spacing constraints to this point... @"|-[button1]-[button2]-[button3]".

This all worked fine. I was almost there. I added the final spacing constraint which was button3 trailing edge to superview. It added the constraint with the current value which happened to be 14.

This was all fine.

However, I found it had added a user constraint to the width of button1.

I deleted this which is fine.

I then selected the superview constraint I just added to button3 and ticked the "standard" check box. Now, what should happen is all the buttons should resize (slightly smaller widths) to accommodate the new constraint. Instead the constraint just deleted itself and I'm left in the same situation... WTF!?

If I can do this in code so easily why is it not even possible at all to do it in Interface Builder?

If anyone wants to try this I'm actually using 6 buttons in my project and they are spaced on a view of width 320.

Let me know if you get any further than I did...

An Auto Layout Experiment

I've only used Auto Layout very briefly in the past and not used it to do anything more than layout a static view (which seems kind of pointless) so I decided to get my hands dirty and do something with Auto Layout that I haven't seen done before.

After a couple of hours I had a brief sample of a Bejewelled style game together all using Auto Layout!

Here's a quick screencast of me playing the game...

It was improved slightly after recording this.

All of the animation was done using auto layout constants. It was hard work getting my head around deleting and adding constants and I think it would be fairly simple to improve the animation for instance when a column of three blocks is deleted or when a brick falls into a row/column that then gets removed as a result of it landing.

Anyway, you can see the code on Github. It isn't documented, it isn't tidy and it wasn't meant for production, I just made it as a learning exercise. But if you can learn anything from the code then all the better for it.

If you have any questions about the code then let me know in the comments and I'll respond ASAP.

Update

I've now pushed the latest changes of the code on to Github so you can get a more complete version of the game.

iOS Developer