UIImage array
export movie
iOS development
Swift programming
video creation

How do I export UIImage array as a movie?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

To turn an array of UIImage frames into a movie on iOS, the usual solution is AVAssetWriter plus one pixel buffer per frame. The workflow is straightforward once the pieces are clear: choose one output size, convert each image into a CVPixelBuffer, assign presentation times, and finish the writer cleanly.

Build the Writer First

AVAssetWriter creates the output file, and AVAssetWriterInput describes the encoded video stream.

swift
1import AVFoundation
2import UIKit
3
4func makeWriter(outputURL: URL, size: CGSize) throws -> (AVAssetWriter, AVAssetWriterInput) {
5    let writer = try AVAssetWriter(outputURL: outputURL, fileType: .mp4)
6
7    let settings: [String: Any] = [
8        AVVideoCodecKey: AVVideoCodecType.h264,
9        AVVideoWidthKey: Int(size.width),
10        AVVideoHeightKey: Int(size.height)
11    ]
12
13    let input = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
14    input.expectsMediaDataInRealTime = false
15
16    guard writer.canAdd(input) else {
17        throw NSError(domain: "MovieExport", code: 1)
18    }
19
20    writer.add(input)
21    return (writer, input)
22}

Every frame in the movie must match the same output dimensions. If your images come from different sources, normalize them before or during conversion.

Convert UIImage to CVPixelBuffer

The writer cannot append UIImage objects directly. It needs a pixel buffer in a consistent format.

swift
1import CoreVideo
2import UIKit
3
4extension UIImage {
5    func pixelBuffer(size: CGSize) -> CVPixelBuffer? {
6        var buffer: CVPixelBuffer?
7        let attrs = [
8            kCVPixelBufferCGImageCompatibilityKey: true,
9            kCVPixelBufferCGBitmapContextCompatibilityKey: true
10        ] as CFDictionary
11
12        let status = CVPixelBufferCreate(
13            kCFAllocatorDefault,
14            Int(size.width),
15            Int(size.height),
16            kCVPixelFormatType_32ARGB,
17            attrs,
18            &buffer
19        )
20
21        guard status == kCVReturnSuccess, let pixelBuffer = buffer else {
22            return nil
23        }
24
25        CVPixelBufferLockBaseAddress(pixelBuffer, [])
26        defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, []) }
27
28        guard let context = CGContext(
29            data: CVPixelBufferGetBaseAddress(pixelBuffer),
30            width: Int(size.width),
31            height: Int(size.height),
32            bitsPerComponent: 8,
33            bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer),
34            space: CGColorSpaceCreateDeviceRGB(),
35            bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
36        ) else {
37            return nil
38        }
39
40        UIGraphicsPushContext(context)
41        draw(in: CGRect(origin: .zero, size: size))
42        UIGraphicsPopContext()
43
44        return pixelBuffer
45    }
46}

This redraws each image into a stable canvas, which is exactly what the encoder expects.

Append Frames With Predictable Timing

Once you have a writer and a pixel-buffer adaptor, append frames in order using one presentation time per frame.

swift
1import AVFoundation
2import UIKit
3
4func exportMovie(
5    images: [UIImage],
6    outputURL: URL,
7    fps: Int = 30,
8    size: CGSize,
9    completion: @escaping (Result<URL, Error>) -> Void
10) {
11    do {
12        if FileManager.default.fileExists(atPath: outputURL.path) {
13            try FileManager.default.removeItem(at: outputURL)
14        }
15
16        let (writer, input) = try makeWriter(outputURL: outputURL, size: size)
17
18        let attrs: [String: Any] = [
19            kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32ARGB),
20            kCVPixelBufferWidthKey as String: Int(size.width),
21            kCVPixelBufferHeightKey as String: Int(size.height)
22        ]
23
24        let adaptor = AVAssetWriterInputPixelBufferAdaptor(
25            assetWriterInput: input,
26            sourcePixelBufferAttributes: attrs
27        )
28
29        writer.startWriting()
30        writer.startSession(atSourceTime: .zero)
31
32        let frameDuration = CMTime(value: 1, timescale: CMTimeScale(fps))
33
34        for (index, image) in images.enumerated() {
35            while !input.isReadyForMoreMediaData {
36                Thread.sleep(forTimeInterval: 0.01)
37            }
38
39            guard let pixelBuffer = image.pixelBuffer(size: size) else {
40                throw NSError(domain: "MovieExport", code: 2)
41            }
42
43            let presentationTime = CMTimeMultiply(frameDuration, multiplier: Int32(index))
44            adaptor.append(pixelBuffer, withPresentationTime: presentationTime)
45        }
46
47        input.markAsFinished()
48        writer.finishWriting {
49            if let error = writer.error {
50                completion(.failure(error))
51            } else {
52                completion(.success(outputURL))
53            }
54        }
55    } catch {
56        completion(.failure(error))
57    }
58}

This takes you from an image array to an .mp4 file with deterministic frame timing.

Decide the Frame Rate Up Front

The chosen fps controls how long each still image stays on screen. At 30 fps, each image lasts one thirtieth of a second. If you want a slower slideshow effect, either lower the frame rate or append the same frame multiple times.

That is a design choice, not an export bug. People often think the movie is wrong when the real issue is that the frame timing does not match the intended playback speed.

Common Pitfalls

The most common mistake is allowing frames to have inconsistent sizes. Video encoders expect one stable width and height for the whole stream.

Another mistake is forgetting to remove an existing file at the output path. AVAssetWriter will not silently overwrite it.

Developers also sometimes skip input.isReadyForMoreMediaData and then wonder why frames are dropped or append calls fail.

Finally, finishWriting completes asynchronously. If you try to use the file before the completion handler runs, you may read an incomplete movie.

Summary

  • Exporting UIImage frames to video usually means AVAssetWriter plus pixel buffers.
  • Convert every image to a CVPixelBuffer using one fixed output size.
  • Append frames with explicit presentation times based on your target frame rate.
  • Remove any existing output file before writing.
  • Treat finishWriting as asynchronous and wait for completion before using the movie.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.