Add variable blur to images on iOS and MacCatalyst. UIKit & Metal

VariableBlurImageView

Framework for adding variable blur, or progressive blur, to images on iOS and MacCatalyst. Works with UIKit using Metal.

Left image has a horizontal variable blur from the leading edge to the middle. Right image has a vertical variable blur from the top edge to the middle.

Table of contents

Installation

To use this package in a SwiftPM project, you need to set it up as a package dependency:

// swift-tools-version:5.9
import PackageDescription

let package = Package(
  name: "MyPackage",
  dependencies: [
    .package(
      url: "https://github.com/Eskils/VariableBlurImageView", 
      .upToNextMinor(from: "1.0.0") // or `.upToNextMajor
    )
  ],
  targets: [
    .target(
      name: "MyTarget",
      dependencies: [
        .product(name: "VariableBlurImageView", package: "VariableBlurImageView")
      ]
    )
  ]
)

Usage

This frameworks provides an UIImageView subclass and a class to apply variable blur to CGImages. The following types are supported:

Vertical

Horizontal

Between two points

VariableBlurImageView

VariableBlurImageView is a subclass of UIImageView which asynchronously applies the wanted progressive blur.

You provide an image, start point, end point, and their respective blur radiuses.

Example

let imageView = VariableBlurImageView()
imageView.contentMode = .scaleAspectFill
let backgroundImage = UIImage(resource: .onboardingBackground)
imageView.verticalVariableBlur(
    image: backgroundImage, 
    startPoint: 0, 
    endPoint: backgroundImage.size.height / 4, 
    startRadius: 15, 
    endRadius: 0
)

Vertical

public func verticalVariableBlur(
    image: UIImage, 
    startPoint: CGFloat, 
    endPoint: CGFloat, 
    startRadius: CGFloat, 
    endRadius: CGFloat
)

Horizontal

public func horizontalVariableBlur(
    image: UIImage, 
    startPoint: CGFloat, 
    endPoint: CGFloat, 
    startRadius: CGFloat, 
    endRadius: CGFloat
)

Between two points

public func variableBlur(
    image: UIImage, 
    startPoint: CGPoint, 
    endPoint: CGPoint, 
    startRadius: CGFloat, 
    endRadius: CGFloat
)

VariableBlurEngine

VariableBlurEngine is an object used to apply progressive blur to CGImages.

You provide a CGImage, start point, end point, and their respective blur radiuses. A new CGImage is returned with the variable blur effect.

Example

let variableBlurEngine = VariableBlurEngine()
let leavesImage = UIImage(resource: .leaves)
let blurredImage = variableBlurEngine.applyVerticalVariableBlur(
    toImage: leavesImage, 
    startPoint: 0, 
    endPoint: leavesImage.size.height / 4, 
    startRadius: 15, 
    endRadius: 0
)

Vertical

public func applyVerticalVariableBlur(
    toImage image: CGImage, 
    startPoint: CGFloat, 
    endPoint: CGFloat, 
    startRadius: CGFloat, 
    endRadius: CGFloat
) throws -> CGImage

Horizontal

public func applyHorizontalVariableBlur(
    toImage image: CGImage, 
    startPoint: CGFloat, 
    endPoint: CGFloat, 
    startRadius: CGFloat, 
    endRadius: CGFloat
) throws -> CGImage

Between two points

public func applyVariableBlur(
    toImage image: CGImage, 
    startPoint: CGPoint, 
    endPoint: CGPoint, 
    startRadius: CGFloat, 
    endRadius: CGFloat
) throws -> CGImage

Roadmap

  • macOS Support
  • SwiftUI support
  • Variable blur for array of ranges
  • Providing grayscale image to use for controlling blur
  • Separable Gaussian Blur (Performance optimization)
  • Looking into applying variable blur to other UIViews

Project Organization

This framework is written in Swift and Metal.

VariableBlurImageView is the primary framework. GenerateTestImages is a small executable used to produce images to test against.

The tests for VariableBlurImageView check if the current state of the code produce the same set of images as has previously been generated by GenerateTestImages.

When implementing altering the look of an existing blur type, expect the tests to fail. Running GenerateTestImages from Xcode will produce new images and make the tests succeed.

When working on performance improvements, the tests should ideally not fail.

Implementing new blur types

When implementing a new blur type, new tests and generating methods need to be provided.

Supplying tests

Generally, at least two tests are written for each blur type — one to check if the images produced are as expected, and one to measure performance.

Checking similarity can be done with the ìsEqual(inputImageName:expectedImageName:afterPerformingImageOperations:) method, like so:

func testVerticalVariableBlur() throws {
    XCTAssertTrue(
        try isEqual(
            inputImageName: inputImageName,
            expectedImageName: "\(inputImageName)-VerticalBlur...",
            afterPerformingImageOperations: { input in
                try variableBlurEngine.applyVerticalVariableBlur(
                    toImage: input,
                    startPoint: 0,
                    endPoint: CGFloat(input.height / 2),
                    startRadius: 20,
                    endRadius: 0
                )
            }
        )
    )
}

Measuring performance can be done with the provideInputImage(inputImageName:) and measure methods, like so:

func testPerformanceOfVerticalVariableBlur() throws {
    let inputImage = try provideInputImage(inputImageName: inputImageName)
    measure {
        _ = try! variableBlurEngine.applyVerticalVariableBlur(
            toImage: inputImage,
            startPoint: 0,
            endPoint: CGFloat(inputImage.height / 2),
            startRadius: 20,
            endRadius: 0
        )
    }
}

Generate images to use in the test

The GenerateImages.swift file in GenerateTestImages provides the implementation to generate images.

Use the from(image:named:performingOperations:) method on OutputImage, and add the result to the outputImages array. The entries in this array are written to the ExpectedOutputs directory under Tests.

// Vertical blur
OutputImage
    .from(image: inputImage, named: "\(name)-Vertical...") { input in
        try variableBlurEngine.applyVerticalVariableBlur(
            toImage: input,
            startPoint: 0,
            endPoint: CGFloat(input.height / 2),
            startRadius: 20,
            endRadius: 0
        )
    }?
    .adding(to: &outputImages)

Contributing to VariableBlurImageView

Contributions are welcome and encouraged. Feel free to check out the project, submit issues and code patches.

GitHub

View Github