Here we are just simply adding the scrollView as a subview to our main view controller's view, and then setting its data source. But right now it won't compile--because our ViewControler does not conform to UIScrollViewDelegate. Just add the following extension to fix this:

extension ViewController: UIScrollViewDelegate { }

Then, call the layout function in your viewDidLoad:

override func viewDidLoad() {
super.viewDidLoad()
layout()
}

Build and run. You should see just a typical scrollView where you can view the different views and their background colors. Great! But, where's the cube animation?

Setting Up the Cube Animation

We will be doing all of our work on the animating part in the scrollViewDidScroll delegate method. But first, let's go over the strategy used to approach this kind of problem:

If we analyze this video, we can tell a few things:

At any given time, there are 2 views visible on the screen

As we swipe our finger right to left (increasing the content offset x value), the view the furthest to the left of the screen (the cat in the video) is rotating towards an angle of 90°.

As we swipe our finger from left to right (decreasing the content offset x value), the view the furthest to the right is rotating towards an angle of -90°.

When the view is centered and visible on our screen, it is at an angle of 0°.

From these observations, we can deduce:

The view furthest to the left on the screen should oscillate between 0 and 90 degrees.

The view furthest to the right should oscillate between 0 and -90°.

So with that in mind, let's start by adding the following implementation of scrollViewDidScroll under the UIScrollViewDelegate extension:

1: As mentioned before, we'll be dealing with each of the views that are visible on the screen at any given time. To do this, we'll create a method on our custom scrollview class, visibleViews(), and then sort them to put the view with the larger xOrigin first (so the first item in the array will be the view on the right side of the screen).

In 2, we create a variable to keep track of the current xOffset value. This will be used in angle caluclations.

In 3, we create our 2 anchor points. In order to rotate our views, we need to set their anchor point. When you rotate an object, it must be rotated around a certain point--for a view, this is its anchor point. The view on the right of the screen will have an X anchor point of 0. This is because we are starting its rotation in its upper left corner. For the view on the left side however, it's X anchor point will be 1. When we first start rotating, we rotate from the upper right corner of the screen. From the left view's perspective, this is its furthest x point. But from the right view's perspective, this is it's origin. That's why the right view's x anchor point is 0, and the left view's is 1.

At 4, we create our transform. The .m34 value dictates the user's perspective of the scene. Try setting this to a higher value, such as 1 / 200, and you'll notice that there is a lot more white space around the view when you scroll, because the view is set as if it's in the distance, and not closer to the user. For our purposes, any value between 1/500 - 1/1000 is fine.

5: These variables will be used to as a reference for the original states of a view on the left and right side of the screen, respectively

Next, let's add the visiableViews() method to the StoriesScrollView class:

At 1, we get the view furthest to the right, and the view furthest to the left.

At 2, we create a constant, hasCompletedPaging, which will tell us if the scrollview is currently stopped scrolling and is resting on a page. We know that the only time the value of (xOffset / scrollView.frame.width) will have a decimal value of 0 is when the view has paged, ie 1.0, 2.0, etc.

At 3, we calculate how far complete the animation for the view on the right side is. Keep in mind, as we swipe our finger right to left, the view on the right side of the screen is starting at -90, and it going towards 0°. If we have completed paging, then we simply set the angle to 0°. If we haven't, then we need to measure how close we are to completing our paging animation. This can be done using (xOffset / scrollView.frame.width).truncatingRemainder(dividingBy: 1). However, then, when the animation is nearly complete that value would be 0.9999. Idealy, we want the inverse--because we want to take this value and multiply it (as we do in 5) to cleanly get our new angle (which would be 0 when the paging is done). That's why we use the 1 - (xOffset / scrollView.frame.width).truncatingRemainder(dividingBy: 1) instead--this will give us a value of nearly 0, so its decrement mirrors that of the decrementing angle.

At 4--If the xOffset is less than 0, the (xOffset / scrollView.frame.width) will result in a negative number. Then when we do 1 - (xOffset / scrollView.frame.width).truncatingRemainder(dividingBy: 1), we'll get a number 1 higher than we need because we just subtracted a negative. To fix this, if the xOffset is less than 0, we just subtract 1 from it and everything works.

5--We mutliply the original angle by the measurement of how much the animation is complete. This extension on UIView hasn't been written yet, we will add that shortly.

6 Here we set the anchor point for the view. This is the point around which the view will rotate. We'll write this extension shortly as well.

At 7, we only perform this action when the content offset is greater than 0.

Below 7, we perform the same calculations, just for the view on the left side of the screen.

Add the extensions

Add the following extensions on UIView, so our project will compile :D

There are several ways to change the angles and transforms of UIViews, but for 3D transformations, such as what we're doing, we must use Core Animation (not Core Graphics' CGAffineTransform--that is only for 2D transformations). We also know that layers are animatable, not the view itself. With that knowledge, we know we have to call .transform on our view's underlying layer object.

When we change our layer's anchor point, we must also account for the transform we just applied, and change the layer's postion as well.

Build and run...it works!

Hope this tutorial was helpful, and leave any questions/comments below. You can find the completed code for this tutorial here. Have a good one!