Design a Beautiful Loading Screen for Your iOS App | by Margels | May, 2022

Create aesthetic Dribbble-like UIs for your iOS app

We’ve all been there. Creating our app, trying to make it work, and realizing we left the design behind. But who likes a perfect app with average looks? Not me! And probably you neither, considering as you’re still reading.

If you’re looking for a simple but beautiful loading screen to lighten up your iOS App’s UI, you’ve come to the right place!

Read on to implement this easy animation in your project!

Since we will be using system’s circle.hexagongrid and circle.hexagongrid.fill images to make the circles, the only objects we need to set up are the blurry screen in the background and the label with text that will appear under the circles.

For the blurry view, we will use UIVisualEffectView with UIBlurEffect(style: .extraLight)although you can choose any other different style available (just delete .extraLight and type a dot “.” to see all your options and play around with them until you find what’s best for you!)

// blur view behind loading scren
lazy var blurry: UIVisualEffectView = {
let blurEffect = UIBlurEffect(style: .extraLight)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.alpha = 0
blurEffectView.frame = self.view.bounds
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
return blurEffectView

For our label, create a variable loadingLabel and set its default text, font, size, position, and shadow if you see it fit:

// loading screen
var loadingLabel: UILabel = {
let l = UILabel()
// text
l.font = UIFont.systemFont(ofSize: 18, weight: .bold)
l.textColor = .label
l.numberOfLines = 0
l.textAlignment = .center
l.text = "loading..."
// position
l.frame = CGRect(x: 20, y: 250, width: UIScreen.main.bounds.width-40, height: 100)
// shadow
l.layer.shadowRadius = 5
l.layer.shadowColor =
l.layer.shadowOffset =
l.layer.shadowOpacity = 0.5
return l

Finally, create an array of strings for your loadingLabel:

// strings to show on loading
var randomStrings = [
"Picking flowers from Eden Garden...",
"Making flower crowns for nymphs...",
"Spreading petals along the lake...",
"Smelling cherry blossoms off a tree...",
"Blowing poplar fluff in the wind...",
"Painting orchids baby blue...",
"Watching the lotus floating in the pond...",
"Clipping daisies in my hair...",
"Caressing young doves...",
"Singing spring songs with the nightingale...",
"Catching the treasure at the end of the rainbow...",
"Napping on a cloud...",
"Sipping tea with a wood fairy..."

Nice! And now, it’s time to set up some of this stuff!

First off, you will need to blur the view. To do that, you will need to create a function that:

  • checks whether or not user’s settings allow blurry views with
  • adds the blurry view and brings it in front of every other view that’s in the view controller so far.

An example:

You can consider providing an alternative to the blurry view with another regular view, just in case isReduceTransparencyEnabled returns true.

Next, we will create a function that:

  • picks a random sentence from the randomStrings array by using Int.random(in:) as our index, and
  • sets a timer of 6 seconds before it repeats itself and changes sentence.

Here’s the code:

Cool! Now all that’s left is our loading screen animation function.

This is where all the magic will happen. It will be quite a long function, but we will go through this bit by bit.

First off, create your function:

func loadingScreen(animating: Bool) {}

As you can see, we are defining a Boolean called animating which will allow us to determine whether the animation should start or if it’s already ongoing and should stop.

The first statement we will add to this function is an if...else statement where we can define whether or not animating equals true:

func loadingScreen(animating: Bool) {   if animating == true {   } else {   }}

…the label

Let’s start with the animating == true condition. Let’s add our loadingLabel to the view and call our function labelText() to set its text.

You may be tempted to use a simple view.addSubview() type of code to add the label to your view; however, when we will remove it with loadingLabel.removeFromSuperview()it will become harder to get the label back in the view again.

The easiest workaround is to use loadingLabel.isHidden = true instead. But that would mean that every time we call loadingScreen(animating: true) with view.addSubview(loadingLabel), a new label is added to the view. Ew! Imagine how many of those can be added in just one session… not the best way to work it, no.

What we will do instead, is check if the view contains our loadingLabel. If it does, we will simply unhide it with loadingLabel.isHidden = false; otherwise, we will add it to the view.

You can do that in just one line of code, plus the function labelText():

// add the label
self.view.contains(self.loadingLabel) ? self.loadingLabel.isHidden = false : self.view.addSubview(self.loadingLabel)

Sweet! At this point, if you test your app and run loadingScreen(animating: true) You should see the label display on the screen with a different text every six seconds.

It will look nicer by the end of this tutorial I promise.

Not that cute yet. But we’re getting there!

…the circles

As mentioned above, we will be using system images circle.hexagongrid and circle.hexagongrid.fill to create the circles. Create two objects named img1 and img2 for our images:

// use system circle hexagon grid images as circles
let img1 = UIImage(systemName: "circle.hexagongrid")
let img2 = UIImage(systemName: "circle.hexagongrid.fill")

Add a UIImageView for each image and assign them with img1 and img2:

// add the circles
let iv1 = UIImageView(image: img1)
let iv2 = UIImageView(image: img2)

Since the view will be displaying one circle at a time, it makes sense to set one of the UIImageView‘s alpha to zero:

iv1.alpha = 0

Create a variable called i and set it to be a Int value of 100. This will be the view tag and we will use it to access the view and remove it when the loading animation needs to stop.

var i = 100

Create an array of image views so you can set up both with the same size and position in one go.

let images = [iv1, iv2]

…the formatting

Let’s use that array and create a statement to set both image views properties all at once:

for image in images {}

Inside that statement, we will set each image view’s tag. As above mentioned, that will be useful for later when removing the view, since it was declared nested inside the function and not inside the class.

// give a tag to the image view so you can find it and remove it later
i += 1
image.tag = i

Now set the circle’s position and size to be the same, so they look like they merge together:

// set the circles position
let hw: CGFloat = 100
image.frame = CGRect(x: UIScreen.main.bounds.width/2 - hw/2, y: 100, width: hw, height: hw)
image.contentMode = .scaleAspectFill

Set the image’s color. I used the new iOS 15 colors, but I set fallback colors for devices using earlier versions, too.

// set the circles colors
if #available(iOS 15.0, *) {
image.tintColor = (image.image == img1 ? .systemMint : .systemPink)
} else {
image.tintColor = (image.image == img1 ? .systemGreen : .systemRed)

Then, add the image view to the view:

// add to the view

And finally, add the animation, using linear and repeat to make the view rotate always at the same speed on a loop:

// rotation animatio
UIView.animate(withDuration: 2, delay: 0, options: [.repeat, .curveLinear]) {
image.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi))

…the blur view

Blur the view using the function we defined above:

// add blur view
self.blurView { success in

Once the blurry view is added, add all the other views:

// bring the circles in front of the blurred view

Start a fading animation separately for each circle, with the second circle’s delay set to the same duration as the first circle’s animation, like so:

// switch views
UIView.animate(withDuration: 1, delay: 0, options: [.repeat, .autoreverse]) {
iv2.alpha = 0
iv1.alpha = 1
UIView.animate(withDuration: 1, delay: 1, options: [.repeat, .autoreverse]) {
iv1.alpha = 0
iv2.alpha = 1

As you can see, both circles have the same duration and the second circle’s delay is set to one second. That means that the circle will start appearing once the first circle starts fading, and because we set the animation to repeat and autoreverse, the circles will keep fading and reappearing intermittently.

…end the loading screen

Okay, fun’s over, time to go back to reality. How do we end this loading screen with all these views and animations?

Easy! Create an array with all the views and remove them.

// find image views by tag
let views = [self.view.viewWithTag(101), self.view.viewWithTag(102), self.blurry]
for view in views {
view?.alpha = 0

Except for the label of course; As we said above, the label will be taken off the screen with isHidden.

// hide loading label
self.loadingLabel.isHidden = true

Leave a Comment