animated circle
ImageView
loading animation
mobile UI
user experience

Using the animated circle in an ImageView while loading stuff

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

An animated loading circle gives users immediate feedback while an image is being fetched, decoded, and displayed. Without that feedback, blank image areas look broken and users may retry actions unnecessarily. A production-ready implementation must handle cancellation, cell reuse, and main-thread UI updates correctly.

Add a Spinner Overlay to an Image View

In UIKit, the standard indicator is UIActivityIndicatorView. Place it inside the image view container and keep it centered. Start animation before network work, stop it for every completion path.

swift
1import UIKit
2
3final class PhotoViewController: UIViewController {
4    private let imageView = UIImageView()
5    private let spinner = UIActivityIndicatorView(style: .large)
6    private var task: URLSessionDataTask?
7
8    override func viewDidLoad() {
9        super.viewDidLoad()
10
11        view.backgroundColor = .systemBackground
12        imageView.contentMode = .scaleAspectFit
13        imageView.frame = view.bounds
14        imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
15        view.addSubview(imageView)
16
17        spinner.translatesAutoresizingMaskIntoConstraints = false
18        spinner.hidesWhenStopped = true
19        view.addSubview(spinner)
20
21        NSLayoutConstraint.activate([
22            spinner.centerXAnchor.constraint(equalTo: view.centerXAnchor),
23            spinner.centerYAnchor.constraint(equalTo: view.centerYAnchor)
24        ])
25    }
26
27    func loadImage(from url: URL) {
28        task?.cancel()
29        imageView.image = nil
30        spinner.startAnimating()
31
32        task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
33            DispatchQueue.main.async {
34                guard let self = self else { return }
35                defer { self.spinner.stopAnimating() }
36
37                if let nsError = error as NSError?, nsError.code == NSURLErrorCancelled {
38                    return
39                }
40
41                guard
42                    let http = response as? HTTPURLResponse,
43                    200...299 ~= http.statusCode,
44                    let data,
45                    let image = UIImage(data: data)
46                else {
47                    return
48                }
49
50                self.imageView.image = image
51            }
52        }
53
54        task?.resume()
55    }
56}

This snippet is runnable in a UIKit app and covers the main lifecycle requirements.

Prevent Wrong Images in Reusable Cells

For table and collection views, the biggest issue is stale responses. A cell can start one request, get reused, and then receive the old response. Use a representation token such as URL value to confirm that the response still belongs to the current item.

swift
1import UIKit
2
3final class FeedImageCell: UICollectionViewCell {
4    private let imageView = UIImageView()
5    private let spinner = UIActivityIndicatorView(style: .medium)
6
7    private var representedURL: URL?
8    private var task: URLSessionDataTask?
9
10    override init(frame: CGRect) {
11        super.init(frame: frame)
12
13        imageView.frame = contentView.bounds
14        imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
15        imageView.contentMode = .scaleAspectFill
16        imageView.clipsToBounds = true
17        contentView.addSubview(imageView)
18
19        spinner.center = contentView.center
20        spinner.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
21        spinner.hidesWhenStopped = true
22        contentView.addSubview(spinner)
23    }
24
25    required init?(coder: NSCoder) {
26        fatalError("init(coder:) has not been implemented")
27    }
28
29    override func prepareForReuse() {
30        super.prepareForReuse()
31        task?.cancel()
32        task = nil
33        representedURL = nil
34        imageView.image = nil
35        spinner.stopAnimating()
36    }
37
38    func configure(with url: URL) {
39        representedURL = url
40        spinner.startAnimating()
41
42        task = URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
43            DispatchQueue.main.async {
44                guard let self = self else { return }
45                defer { self.spinner.stopAnimating() }
46                guard self.representedURL == url else { return }
47                guard let data, let image = UIImage(data: data) else { return }
48                self.imageView.image = image
49            }
50        }
51
52        task?.resume()
53    }
54}

This check avoids visual flicker and incorrect image placement while fast scrolling.

Improve UX Beyond a Spinner

The loading circle should not be the only strategy. Add placeholder imagery, crossfade transitions, and optional caching for smoother results.

swift
1extension UIImageView {
2    func setImageWithCrossfade(_ image: UIImage?) {
3        UIView.transition(
4            with: self,
5            duration: 0.2,
6            options: .transitionCrossDissolve,
7            animations: { self.image = image }
8        )
9    }
10}

A short crossfade makes image appearance feel intentional instead of abrupt. Combined with cached responses, it can eliminate spinners for frequently viewed items.

Consider a Library for Advanced Cases

If your app heavily depends on remote images, a library such as SDWebImage or Kingfisher can reduce boilerplate. You still need to understand lifecycle rules so you can configure caching and cancellation correctly.

Even with a library, keep UI ownership local. Cells should still reset state in prepareForReuse and not assume requests always succeed.

Common Pitfalls

  • Updating UIImageView from a background callback thread, which causes random UI glitches or crashes. Always dispatch UI work to main thread.
  • Forgetting to stop the spinner on failure and cancel paths. Use defer to guarantee cleanup.
  • Not canceling previous requests before starting a new one for the same view, leading to race conditions.
  • Ignoring cell reuse, so outdated requests write images into recycled cells.
  • Using spinner-only feedback with no placeholder or transition, which makes the interface feel jumpy during slow networks.

Summary

  • Start spinner before request and stop it in every completion path.
  • Keep all UI updates on the main thread.
  • Cancel in-flight tasks when views are reused or replaced.
  • Validate response ownership in reusable cells with a URL token.
  • Pair spinners with placeholders, caching, and subtle transitions for better perceived performance.

Course illustration
Course illustration

All Rights Reserved.