iOS나 Mac OS의 현행 디자인 코드는 Flat으로 음영과 그림자 등의 그래픽 적인 요소들을 최대한 배제하고,
색과 선, 레이어의 적층으로 깊이와 구분감을 부여하는 것이 특징이다.
개인적으로 이러한 상황에서 현재 시각적으로 가장 미려하다고 생각되는 부분은 바로 Material이다.
일반적인 Blur와는 조금 다르게 굉장히 Matte 한 느낌을 주는 것이 특징이고,
텍스트 등 강조가 필요한 부분은 반대로 투명하게 표현해 경우에 따라 시원한 분위기를 만들 수 있다.
위와 같이 Material도 훌륭하지만 이번에는 화면에 표시된 이미지의 평균적인 색상이 어떤 건지 판단하는 방법에 대해 살펴본다.
이를테면 위와 같이 앨범 재킷이 어떤 색을 주로 가지고 있는지에 따라 더 단순한 형태로 배경을 바꾸는 것이 가능해진다.
CoreImage 사용하기
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)
}
}
Source
//
// ContentView.swift
// BGTest
//
// Created by Martin.Q on 2023/04/06.
//
import SwiftUI
struct ContentView: View {
@State private var choosen = 1
@State private var bgc: UIColor = .clear
var body: some View {
TabView(selection: $choosen) {
Group {
Image("image1")
.resizable()
.tag(1)
Image("image2")
.resizable()
.tag(2)
}
.frame(width: 350, height: 350)
}
.tabViewStyle(.page)
.background(
Color(bgc)
.ignoresSafeArea()
.overlay(Material.thinMaterial)
)
.onChange(of: choosen) { newValue in
withAnimation {
if choosen == 1 {
bgc = UIImage(named: "image1")?.averageColor ?? .clear
} else if choosen == 2 {
bgc = UIImage(named: "image2")?.averageColor ?? .clear
}
}
}
.onAppear {
if choosen == 1 {
bgc = UIImage(named: "image1")?.averageColor ?? .clear
} else if choosen == 2 {
bgc = UIImage(named: "image2")?.averageColor ?? .clear
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
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)
}
}
Pixel 접근 방식 사용하기
extension UIImage {
enum AverageColorAlgorithm {
case simple
case squareRoot
}
func findAverageColor(algorithm: AverageColorAlgorithm = .simple) -> UIColor? {
guard let cgImage = cgImage else { return nil }
let size = CGSize(width: 40, height: 40)
let width = Int(size.width)
let height = Int(size.height)
let totalPixels = width * height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo) else { return nil }
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
guard let pixelBuffer = context.data else { return nil }
let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height)
var totalRed = 0
var totalBlue = 0
var totalGreen = 0
for x in 0 ..< width {
for y in 0 ..< height {
let pixel = pointer[(y * width) + x]
let r = red(for: pixel)
let g = green(for: pixel)
let b = blue(for: pixel)
switch algorithm {
case .simple:
totalRed += Int(r)
totalBlue += Int(b)
totalGreen += Int(g)
case .squareRoot:
totalRed += Int(pow(CGFloat(r), CGFloat(2)))
totalGreen += Int(pow(CGFloat(g), CGFloat(2)))
totalBlue += Int(pow(CGFloat(b), CGFloat(2)))
}
}
}
let averageRed: CGFloat
let averageGreen: CGFloat
let averageBlue: CGFloat
switch algorithm {
case .simple:
averageRed = CGFloat(totalRed) / CGFloat(totalPixels)
averageGreen = CGFloat(totalGreen) / CGFloat(totalPixels)
averageBlue = CGFloat(totalBlue) / CGFloat(totalPixels)
case .squareRoot:
averageRed = sqrt(CGFloat(totalRed) / CGFloat(totalPixels))
averageGreen = sqrt(CGFloat(totalGreen) / CGFloat(totalPixels))
averageBlue = sqrt(CGFloat(totalBlue) / CGFloat(totalPixels))
}
return UIColor(red: averageRed / 255.0, green: averageGreen / 255.0, blue: averageBlue / 255.0, alpha: 1.0)
}
private func red(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 16) & 255)
}
private func green(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 8) & 255)
}
private func blue(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 0) & 255)
}
}
Source
//
// ContentView.swift
// BGTest
//
// Created by Martin.Q on 2023/04/06.
//
import SwiftUI
struct ContentView: View {
@State private var choosen = 1
@State private var bgc: UIColor = .clear
var body: some View {
TabView(selection: $choosen) {
Group {
Image("image1")
.resizable()
.tag(1)
Image("image2")
.resizable()
.tag(2)
}
.frame(width: 350, height: 350)
}
.tabViewStyle(.page)
.background(
Color(bgc)
.ignoresSafeArea()
.overlay(Material.thinMaterial)
)
.onAppear {
if choosen == 1 {
withAnimation {
if choosen == 1 {
bgc = UIImage(named: "image1")?.findAverageColor(algorithm: .squareRoot) ?? .clear
} else if choosen == 2 {
bgc = UIImage(named: "image2")?.findAverageColor(algorithm: .squareRoot) ?? .clear
}
}
}
}
.onChange(of: choosen) { newValue in
withAnimation {
if choosen == 1 {
bgc = UIImage(named: "image1")?.findAverageColor(algorithm: .squareRoot) ?? .clear
} else if choosen == 2 {
bgc = UIImage(named: "image2")?.findAverageColor(algorithm: .squareRoot) ?? .clear
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension UIImage {
enum AverageColorAlgorithm {
case simple
case squareRoot
}
func findAverageColor(algorithm: AverageColorAlgorithm = .simple) -> UIColor? {
guard let cgImage = cgImage else { return nil }
let size = CGSize(width: 40, height: 40)
let width = Int(size.width)
let height = Int(size.height)
let totalPixels = width * height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo) else { return nil }
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
guard let pixelBuffer = context.data else { return nil }
let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height)
var totalRed = 0
var totalBlue = 0
var totalGreen = 0
for x in 0 ..< width {
for y in 0 ..< height {
let pixel = pointer[(y * width) + x]
let r = red(for: pixel)
let g = green(for: pixel)
let b = blue(for: pixel)
switch algorithm {
case .simple:
totalRed += Int(r)
totalBlue += Int(b)
totalGreen += Int(g)
case .squareRoot:
totalRed += Int(pow(CGFloat(r), CGFloat(2)))
totalGreen += Int(pow(CGFloat(g), CGFloat(2)))
totalBlue += Int(pow(CGFloat(b), CGFloat(2)))
}
}
}
let averageRed: CGFloat
let averageGreen: CGFloat
let averageBlue: CGFloat
switch algorithm {
case .simple:
averageRed = CGFloat(totalRed) / CGFloat(totalPixels)
averageGreen = CGFloat(totalGreen) / CGFloat(totalPixels)
averageBlue = CGFloat(totalBlue) / CGFloat(totalPixels)
case .squareRoot:
averageRed = sqrt(CGFloat(totalRed) / CGFloat(totalPixels))
averageGreen = sqrt(CGFloat(totalGreen) / CGFloat(totalPixels))
averageBlue = sqrt(CGFloat(totalBlue) / CGFloat(totalPixels))
}
return UIColor(red: averageRed / 255.0, green: averageGreen / 255.0, blue: averageBlue / 255.0, alpha: 1.0)
}
private func red(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 16) & 255)
}
private func green(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 8) & 255)
}
private func blue(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 0) & 255)
}
}
코드가 조금 길어지긴 했지만 pixel 접근 방식도 못지않은 결과를 보여준다.
두 방식은 비슷한 결과를 가지지만 접근 방식이 다른 만큼 내부적인 차이가 존재한다.
왼쪽이 CoreImage를 사용한 방식, 오른쪽이 Pixel 접근 방식이다.
동작시 메모리 사용량이 4MB 정도 차이 나는 것을 확인할 수 있다.
사진에서 약간 높아 보이는 CPU 사용량은 이벤트가 많아지면 동일한 수준으로 수렴한다.
이외에도 다른 방법이 존재하는데, 이미지를 구성하는 몇 가지 색을 추려내는 방식인데 응용하면 동일한 효과를 볼 수 있다.
코드 그 자체로도 유용해 보이니 나중에 한 번 더 다뤄 보는 것도 좋을 것 같다.
참고
'학습 노트 > Swift UI Trick' 카테고리의 다른 글
SwiftUI에서 Blur를 사용하는 4가지 방법 (0) | 2022.12.28 |
---|---|
SwiftUI에서 키보드 숨기기 (0) | 2022.10.15 |
TextField 입력값 제한하기 (0) | 2022.10.01 |