Christmas time is finished. New Year’s Eve is finished as well. I hope that everyone had a great time and you are ready to start working hard and learning new things!
I want to start this year with a wonderful article-tutorial on how to create similar to Periscope fast rewind control.
To build this component we will have to use video player — AVPlayer. We will add UILongPressGestureRecognizer to begin rewind and calculate finger translation — it will help us to change rewind speed and calculate time. While rewinding is in progress we will use UIVisualEffectView to blur video, and AVAssetImageGenerator to generate preview thumbnails.
OK. Let’s stop talking, just do it.
Create single view application and create new class VideoViewController — subclass of UIViewController. Import AVFoundation and put this code inside class declaration:
Everything we did here is quite easy:
OK. Our video view controller is ready for early stage test. In order to test it we have to do two simple steps:
Now run project on your device or simulator and make sure that video view controller is presented, video is playing, and all orientations are supported.
Our next stage is to recognize touches and blur content.
Declare these two variables in VideoViewController:
Paste this code to the end of loadView method:
Add this code to the end of viewWillLayoutSubviews method:
Implement this function:
The purpose of this function is to pause video and fade in visual effect view when touches began, and resume video and fade out visual effect view when touches cancelled, ended or failed. In the future we will add more logic to this method.
Run project and test long press.
That’s how it works on my device:
Next step is to create timeline view and implement rewind functionality. But let’s have a very quick look at Periscope UI to understand what exactly our timeline should have and how it should work.
As you can see — timeline goes from the left side to the right side of the view controller. It has a white dot which indicates where video was stopped when user started rewinding. If you compare screenshots you will notice, that interval width is different — it is because rewind speed (we will call it zoom in our project) values are not equal. And the last thing to notice is that current time is always in the middle of the timeline.
Create TimelineView as a subclass of UIView and declare these variables:
The purpose of each variable is obvious, but let’s make sure everyone understands:
minZoom, maxZoom, intervalWidth and intervalDuration could be constants, but I decided to make them vars — if you want to reuse this view controller with different videos you might want to adjust these values.
Implement these functions:
Implement draw rect:
Do not be scared. Everything here is simple :) I will explain only those things, which I think might need explanation:
And final bit on timeline view for now — implement new initializer and set opaque to false:
I feel that our timeline is finished. Next stage is to add it to view controller and make it work!
Add these variables:
First variable is a content view, where all rewind related views/controls will be added. We will fade in and fade out this view when rewinding begins and finishes. Second variable is our actual timeline view which we implemented in a previous stage. And the last variable will help us to calculate how far finger has moved and how far we should rewind.
Put this code to the end of loadView method:
It adds all new subviews to their superviews and sets duration of the timeline to the duration of the asset.
Update longPressed function with this code:
Last stage before we try it is to layout our newly created views. Put this code to the end of viewWillLayoutSubviews:
Run project on your device or simulator and check it out!
That’s how it looks on my device:
I really hope that you managed to do everything and it works exactly the same. If not — quickly go threw tutorial again.
As you might have noticed it does not rewind when we release finger. In order to do that add update longPressed function with this code:
We added 2 new lines — line 17 and line 18. With the first line we calculate new time, and with the second line we seek to that time.
Run your project again.. and enjoy!
It works now!
Lazy people can close tutorial at this stage (hope you are not lazy), because core stuff is built and next stage is mainly improvements and some nice-to-have features.
Our goal is to add two new views:
In order to update these views I decided to add closure to timeline view which will be triggered every time when currentTime value changes.
Add this variable to TimelineView:
Update currentTime variable implementation:
Add these variables to the VideoViewController class:
Add these two lines to the end of init(videoURL: NSURL):
Maximum size specifies the maximum dimensions for generated image. We want maximum height to be rewindPreviewMaxHeight. Generated image is never scaled up. But in most of the cases video size is much higher than the expected thumbnail size.
Add these lines to the end of loadView method:
Update viewWillLayoutSubviews function with this code:
Feel free to change verticalSpacing value. It indicates gap height between preview image view, current time label and timeline view.
And the last step in this section is to implement currentTimeDidChange closure. Add this code just before adding timeline view to it’s superview:
Run you project and enjoy! It looks fantastic!
Let’s do the final touch! In Periscope they have a nice shadow behind preview image view. We will add it as well!
Add this variable to VideoViewController:
Add this block of code to the loadView function just before setting up and adding rewindPreviewImageView to subviews (if you add it after rewindPreviewImageView, then it will be higher in a layer hierarchy and will be shown on top of imageView):
For those who does not understand the purpose of line number 6 I would suggest to read about iOS implicit animations.
Update viewWillLayoutSubviews:
Run and check it out.
Everything is great. But.. wait.. our preview image is pixelated!
It is because AVAssetImageGenerator.maximumSize expects us to provide pixels instead of points. Documentation on how points are different from pixels can be found here.
Let’s fix this minor issue.
Update this line all over the class:
with this:
Now run project again.
What’s going on.. Where is our shadow?! Where is our corner radius?!
We forgot one little thing — AVAssetImageGenerator returns us CGImage and we initialize UIImage with CGImage without providing required scale. In our case our needed scale is UIScreen.mainScreen().scale.
Find this line:
And update with this:
Ruuun again!
Wonderful! Everything is back and it works great! Woop woop!!!
Thank you for going threw this tutorial. If you liked it please press heart button and share it with your friends & colleagues.
Source code from this tutorial with some improvements and more features can be downloaded here.
See you in my next articles-tutorials! Bye!