🙂 이미지 선택 구현
아래와 같은 PhotosPicker를 통해 이미지를 선택하는 화면을 구현했다.
코드는 아래와 같이 간단하게 구현했다. PhotosPickerItem에서 이미지는 여러 가지 방법으로 가져올 수 있었지만 우선 아래와 같이 최대한 가로로 짧은... 방식으로 구현했다.
struct ContentView: View {
@State private var selection: PhotosPickerItem?
@State private var image: UIImage?
var size: CGFloat = 180
var body: some View {
PhotosPicker(selection: $selection) {
Group {
if let uiImage = image {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
} else {
Rectangle()
.fill(.fill)
}
}
.frame(width: size, height: size)
.cornerRadius(size * 0.15)
}
.onChange(of: selection) { fetchImage(from: $1) }
}
private func fetchImage(from selection: PhotosPickerItem?) {
selection?.loadTransferable(type: Data.self) { result in
guard let data = try? result.get() else { return }
image = UIImage(data: data)
}
}
}
🗄️ 이미지 용량이 너무 큼
간단하게 프리뷰를 통해 진행했는데 프리뷰에 있는 이미지만 하더라도 용량이 약 2.45MB가 되었다. 만약에 이런 이미지들을 선택하고 저장할 수 있는 앱을 만든다고 한다면 이미지를 저장할 때 용량이 2MB가 늘어나게 되며, 이것을 원격 저장소에 저장하기에도 너무 큰 용량이라고 생각한다. 어떻게 용량을 줄일 수 있을까?
🥚 데이터 압축하기
jpegData
jpegData(compressionQuality:)를 통해 UIImage를 JPEG data로 압축시킬 수 있다.
코드는 아래와 같이 이미지에 해당 메서드를 붙여서 quality 값을 넣어주게 된다면 간단하게 압축이 가능하다. 0과 1 사이의 값을 넣어야 하고, 1에 가까울수록 원본에 가깝다.
let compressedData = UIImage(data: originalData)?.jpegData(compressionQuality: 0.01)
품질 0.1로 압축을 진행해보니 용량이 딱 0.1배로 줄어들지는 않는 것 같다. 아래 그림의 경우 0.12배로 줄어들었는데 보통 0.1 ~ 0.15배 수로 줄어들게 되는 것을 확인했다. 이 정도만 해도 2.45MB → 0.31MB 로 줄어드는 효과를 볼 수 있었다. 그리고 실제 화면을 보게 되어도 큰 차이가 확인되지 않는다. 실기기에서도 차이점을 찾아보기 힘들었다.
압축률을 0.01로 해도 위와 똑같은 결과가 나온 것을 보니 0.1이 최소인 듯싶다.
UIGraphicsImageRenderer
더 압축하고 싶은데...!라는 생각이 들 수도 있다. 만약 썸네일 이미지나 유저 이미지처럼 작게 보여도 상관없거나 조금 화질이 저하되어도 상관이 없다면 좀 더 압축을 할 수도 있을 것이다. 그때는 UIGraphicsImageRenderer를 활용하여 좀더 디테일하게 압축이 가능하다.
방법은 아래 코드와 같다. 이미지의 픽셀을 강제로 줄여버리는 것이다. 그렇게 된다면 당연히 용량이 줄게 된다. 여기서 나온 renderer에서 jpeg로 압축을 다시 해준다면 용량을 더욱 줄일 수 있을 것이다.
func compress(_ originalImage: UIImage) -> Data {
/// 원본 이미지의 가로세로 비율을 구함
let originalSize = originalImage.size
let ratio = originalSize.width / originalSize.height
/// 압축할 이미지의 사이즈는 화면에 표시될 세로크기의 4배로 결정
let compressedSize = CGSize(width: size * 4 * ratio, height: size * 4)
let renderer = UIGraphicsImageRenderer(size: compressedSize)
return renderer.jpegData(withCompressionQuality: 0.1) { _ in
originalImage.draw(in: CGRect(origin: .zero, size: compressedSize))
}
}
결과는 아래와 같이 2.45MB → 0.04MB 로 줄게 되었다. 살짝 애매하게 화질이 낮아지는 것을 체감할 수 있는데 거의 60배 압축을 한 셈이니 압축을 한 것 치고는 괜찮다. 만약에 조금 화질을 높이고 싶다면 압축하려는 이미지의 크기나 jpegData 품질을 높이면 자유롭게 용량을 선택할 수 있다.
아래와 같이 format을 활용하여 비율 계산 필요 없이 0.2배 크기로 줄여버릴 수도 있다.
func compress(_ originalImage: UIImage) -> Data {
let format = UIGraphicsImageRendererFormat()
format.scale = 0.2
let renderer = UIGraphicsImageRenderer(size: originalImage.size, format: format)
return renderer.jpegData(withCompressionQuality: 0.1) { _ in
originalImage.draw(in: CGRect(origin: .zero, size: originalImage.size))
}
}
결과는 2.45MB → 0.03MB로 0.2배로 줄인 것이 조금 더 용량이 줄어든 것을 확인할 수 있다. 이것은 사진의 원본 크기에 따라 결정되기 때문에 0.2로 줄였는데 너무나 작아질 수도 있어서 첫 번째 방법처럼 크기를 직접 조절하는 것이 조금 더 나은 선택으로 보인다.
차이점
UIGraphicsImageRenderer의 경우 크기를 직접 줄여버리기 때문에 압축효과가 높다. jpegData(compressionQuality:)의 경우 UIKit에서 제공하는 기본 메서드이기 때문에 빠르고 간편하다. 중요한 건 속도 아니겠는가? 속도 테스트를 해보고 싶었다.
물론 정확하지는 않겠지만 아래와 같은 간단한 속도 테스트를 시도해 보았다.
func benchmark() {
guard let originImage = originImage else { return }
/// jpegData
let start1 = Date()
let data = originImage.jpegData(compressionQuality: 0.1)
let end1 = Date()
let _ = UIImage(data: data ?? Data())
print("jpegData 실행 시간: \(end1.timeIntervalSince(start1)) 초")
print("jpegData 용량: \(data?.description ?? "")\n")
/// UIGraphicsImageRenderer
let start2 = Date()
let format = UIGraphicsImageRendererFormat()
format.scale = 0.15
let renderer = UIGraphicsImageRenderer(size: originImage.size, format: format)
let image = renderer.image { _ in
originImage.draw(in: .init(origin: .zero, size: originImage.size))
}
let end2 = Date()
print("UIGraphicsImageRenderer 실행 시간: \(end2.timeIntervalSince(start2)) 초")
print("UIGraphicsImageRenderer 용량: \(image.jpegData(compressionQuality: 1)?.description ?? "")\n\n")
}
실기기로 테스트해 본 결과 두 방법도 속도에서 큰 차이가 없었다. 오히려 UIGraphicsImageRenderer의 경우 이미지를 다시 그리니까 시간이 더 걸릴 줄 알았는데 속도차이가 거의 없이 보인다. jpegData 쪽에서 let _ = UIImage(data: data ?? Data()) 부분을 지우고 다시 테스트해 봐도 결과는 비슷했다.
Original Image 용량: 4277243 bytes
jpegData 실행 시간: 0.041352033615112305 초
jpegData 용량: 391572 bytes
UIGraphicsImageRenderer 실행 시간: 0.06738197803497314 초
UIGraphicsImageRenderer 용량: 284060 bytes
Original Image 용량: 9702868 bytes
jpegData 실행 시간: 0.0902789831161499 초
jpegData 용량: 949476 bytes
UIGraphicsImageRenderer 실행 시간: 0.12993693351745605 초
UIGraphicsImageRenderer 용량: 598651 bytes
다음 포스팅에서는 jpegData에서는 어떠한 일이 일어날까에 대한 학습을 해보고자 한다.
🧾 전체 코드
전체 코드는 아래와 같다.
import SwiftUI
import PhotosUI
struct ContentView: View {
@State private var selection: PhotosPickerItem?
@State private var originData = Data()
@State private var compressedData = Data()
@State private var originImage: UIImage?
@State private var compressedImage: UIImage?
var size: CGFloat = 180
var body: some View {
VStack {
if let uiImage = originImage {
Text("원본")
.font(.title.bold())
image(uiImage)
Text(originData.description)
.fontDesign(.monospaced)
.padding(.bottom)
}
if let uiImage = compressedImage {
Text("압축")
.font(.title.bold())
image(uiImage)
Text(compressedData.description)
.fontDesign(.monospaced)
.padding(.bottom)
}
PhotosPicker("이미지 선택", selection: $selection)
.onChange(of: selection) {
fetchImage(from: $1)
}
}
}
private func fetchImage(from selection: PhotosPickerItem?) {
selection?.loadTransferable(type: Data.self) { result in
guard let originalData = try? result.get(),
let originalImage = UIImage(data: originalData) else { return }
let compressedData = compress(originalImage)
self.originData = originalData
self.compressedData = compressedData
self.originImage = originalImage
self.compressedImage = UIImage(data: compressedData)
}
}
private func compress(_ originalImage: UIImage) -> Data {
let format = UIGraphicsImageRendererFormat()
format.scale = 0.5
let renderer = UIGraphicsImageRenderer(size: originalImage.size, format: format)
return renderer.jpegData(withCompressionQuality: 0.1) { _ in
originalImage.draw(in: CGRect(origin: .zero, size: originalImage.size))
}
}
@ViewBuilder private func image(_ uiImage: UIImage) -> some View {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: size, height: size)
.cornerRadius(size * 0.15)
}
}
#Preview {
ContentView()
}
'→ SwiftUI' 카테고리의 다른 글
[SwiftUI] 네비게이션 뒤로가기 제스쳐 구현하기 (0) | 2025.03.02 |
---|---|
[SwiftUI] UIKit 활용하여 달력 넣고 사용하기 (UICalendarView) (0) | 2024.01.19 |