Implementing Dynamic Background Color from Images in Swift — SwiftUI

Oğuzhan Aslan
5 min readDec 24, 2023
Photo by Igor Kasalovic on Unsplash

In recent years, dynamic color has become more and more popular in the field of computer graphics. If you inspect Spotify, Youtube, or Youtube Music, you will find that the background color of the page changes dynamically according to the cover of the song. This is a very interesting feature, and it is also a very good way to improve the user experience. In this article, I will introduce how to implement this feature in Swift.

First and foremost, we need to have a way to get color from an image. Fortunately, there is a very good sample provided by Hacking with Swift. By calling the following extension, we can get the average color of an image.

 extension UIImage {
var averageColor: UIColor? {
guard let inputImage = CIImage(image: self) else { return nil }
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)

guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
guard let outputImage = filter.outputImage else { return nil }

var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: kCFNull!])
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)

return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
}
}

After getting the average color as UIColor, we can get the HSL values to convert it to a background color. It is not necessary to use HSL. You can use RGB or another color space. (I just prefer HSL because it is more intuitive to me.)

var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
image.averageColor?.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
let newColor = UIColor(hue: hue, saturation: 0.8, brightness: 0.1, alpha: alpha)

Now we have the color by changing the saturation and brightness. Do not forget that you should keep the contrast between background and content at least 3:1 to meet the accessibility requirements.

Now, I believe that things are a bit too abstract. Let’s see this work on a real example.

Firstly, create a SwiftUI application and add a new image to the assets folder. You can name it whatever you want; I named mine “ice”.

Then add the following code to the ContentView.swift file.

@State private var background: Color = .white

var body: some View {
let image = UIImage(imageLiteralResourceName : "ice")
VStack {
Image(uiImage: image)
.resizable()
.frame(
width: 96,
height: 96
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(background)
}

Now, the screen should look like this.

Then, we need to add the following code to the end of the body of the Image

.onAppear {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
image.averageColor?.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
let newColor = UIColor(hue: hue, saturation: 0.9, brightness: 0.06, alpha: alpha)
background = Color(newColor)
}

Now, by applying the average color of the image to the background, we can get the following result.

As you can see, the background color is now the average color of the image. However, the contrast between the background and the content is not enough. So we need to make some adjustments to the brightness and saturation of the color.

 let newColor  = UIColor(hue: hue, saturation: 0.9, brightness: 0.06, alpha: alpha)

By changing the saturation to 0.9 and brightness to 0.06, we can get the following result.

Go ahead and try it yourself. You can change the saturation and brightness to whatever you want. try different values and see what you like the most. If you need here is all we cover so far.

  struct ContentView: View {

@State private var background: Color = .white

var body: some View {
let image = UIImage(imageLiteralResourceName : "YOUR_IMAGE_NAME")
VStack {
Image(uiImage: image)
.resizable()
.frame(
width: 96,
height: 96
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(background)
}


}

extension UIImage {
var averageColor: UIColor? {
guard let inputImage = CIImage(image: self) else { return nil }
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)

guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
guard let outputImage = filter.outputImage else { return nil }

var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: kCFNull!])
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)

return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
}
}

In this article, we learned how to get the average color of an image and how to use it to change the background color of a view. I hope you enjoyed it. If you have any questions or suggestions, please feel free to leave a comment below.

--

--