본문 바로가기

iOS

SwiftUI 이미지 캐시처리

728x90
반응형
SMALL

SwiftUI 이미지 캐시처리

SwiftUI에서 이미지 캐시 처리는 이미지를 다운로드하고 캐시에 저장하여 빠르게 로드할 수 있도록 하는 기술입니다. 이미지를 캐싱하면 앱이 이미지를 다시 다운로드하지 않고 캐시된 이미지를 사용할 수 있으므로, 불필요한 데이터 사용을 줄이고 로딩 시간을 단축시킬 수 있습니다.

SwiftUI에서 이미지 캐시 처리를 구현하는 방법에는 여러 가지가 있습니다.

예를 들어,

1. 이미지 캐시 라이브러리를 사용

2. View Modifier를 이용하여 CacheKey를 지정

3. URLSession에서 URLCache를 사용

 

이미지 캐시 처리는 앱의 성능을 향상시키는 데 도움이 되는 기술이므로, 적절한 방법으로 구현하는 것이 중요합니다. 적절한 캐싱 전략을 선택하고, 이미지 크기를 최적화하고, 메모리와 디스크 사용을 최소화하는 등의 방법으로 이미지 캐시 처리를 효율적으로 구현할 수 있습니다.

원하는 것

Asset 내의 이미지 경우 이미지가 10MB가 넘더라도 이미지 전환이 부드럽습니다.

캐시이미지로 이런 사용을 원합니다요 :0...

첫 시도

AsyncImage

import SwiftUI

struct ContentView: View {
    private var urls: [String] = [
        "https://upload.wikimedia.org/wikipedia/commons/5/53/%22_Cooperativa_de_fluido_electrico%2C_%22_Mapa_del_Cadi._%22_Estudios_y_construcciones_Locher%2C_S._A.%2C_%22_Barcelona_-_Aixecament_fet_per_L%C3%A9o_Acgerter..._-_btv1b53065199x.jpg",
        "https://cdn.pixabay.com/photo/2016/03/08/20/03/flag-1244649_1280.jpg",
    ]
    
    private var imageNames: [String] = [
        "bigImage",
        "smallImage"
        
    ]
    @State private var imageNum: Int = 0
    var body: some View {
        Button(action: {
            imageNum += 1
        }){
            Rectangle()
        }
        Image(imageNames[imageNum % imageNames.count])
            .resizable()
            .aspectRatio(contentMode: .fit)
        CachedImageView(imageUrl: urls[imageNum % imageNames.count])
    }
}

struct CachedImageView: View {
    let imageUrl: String
    
    var body: some View {
        AsyncImage(url: URL(string: imageUrl)) { phase in
            switch phase {
            case .empty:
                Circle()
                    .foregroundColor(.yellow)
                // 이미지 다운로드 전 표시될 뷰
                ProgressView()
            case .success(let image):
                // 이미지 다운로드 및 캐싱 성공 시 처리할 작업
                Circle()
                    .foregroundColor(.green)
                image
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            case .failure(let error):
                // 이미지 다운로드 및 캐싱 실패 시 처리할 작업
                Circle()
                    .foregroundColor(.red)
                Text("Error loading image: \(error.localizedDescription)")
            @unknown default:
                // 이미지 다운로드 전 표시될 뷰
                ProgressView()
            }
        }
        .shadow(radius: 7)
    }
}

(200kb)저용량의 경우 페이지가 바로 넘어 가지만 (10mb)대용량의 경우

그냥 한번 로드되었던 Image() 는 바로 넘어가지지만 AsyncImage()의 경우 한참의 시간이 걸렸습니다.

알잘딱 안되서 너무 아쉽읍니다. ㅠㅠㅠ

 

 

 

두번째 시도

URLCache 

URLCache 클래스의 public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) 메서드의 파라미터에 대한 설명은 다음과 같습니다.

  1. memoryCapacity: Int
  2. 메모리 캐시에 사용될 바이트 수입니다. 이 값은 URLCache 객체가 사용하는 메모리 용량 상한을 결정합니다. 메모리 캐시는 앱이 실행 중일 때만 유지되며, 앱이 종료되면 자동으로 삭제됩니다.
  3. diskCapacity: Int
  4. 디스크 캐시에 사용될 바이트 수입니다. 이 값은 URLCache 객체가 사용하는 디스크 용량 상한을 결정합니다. 디스크 캐시는 앱이 종료되어도 보존됩니다.
  5. diskPath: String?
  6. 캐시 파일이 저장될 디렉토리 경로입니다. 이 값이 nil인 경우, URLCache는 기본 디렉토리에 파일을 저장합니다. URLCache 객체가 사용하는 디렉토리는 앱 실행 중에 유지되며, 앱이 삭제될 때까지 유지됩니다.

이러한 파라미터를 사용하여 앱에서 적절한 메모리 및 디스크 용량을 할당하고, 캐시 파일을 저장할 경로를 설정하여 URLCache 객체를 초기화할 수 있습니다.

import SwiftUI

struct ContentView: View {

    private var urls: [String] = [
        "https://upload.wikimedia.org/wikipedia/commons/5/53/%22_Cooperativa_de_fluido_electrico%2C_%22_Mapa_del_Cadi._%22_Estudios_y_construcciones_Locher%2C_S._A.%2C_%22_Barcelona_-_Aixecament_fet_per_L%C3%A9o_Acgerter..._-_btv1b53065199x.jpg",
        "https://cdn.pixabay.com/photo/2016/03/08/20/03/flag-1244649_1280.jpg",
    ]
    
    private var imageNames: [String] = [
        "bigImage",
        "smallImage"
    ]
    
    @State private var imageNum: Int = 0
    @State private var image: UIImage?
    
    var body: some View {
        VStack{
            Button(action: {
                imageNum += 1
                getImage()
            }){
                Rectangle()
            }
            Image(imageNames[imageNum % imageNames.count])
                .resizable()
                .aspectRatio(contentMode: .fit)
            CachedImageView(image: self.$image)
        }.onAppear {
            checkDirectory()
            getImage()
        }
    }
    
    func checkDirectory(){
        // 용량을 늘리고자 하는 값 설정
        let newDiskCapacity: Int = 50 * 1024 * 1024 // 50MB
        let newMemoryCapacity: Int = 100 * 1024 * 1024 // 100MB

        // 용량 설정 적용
        URLCache.shared.diskCapacity = newDiskCapacity
        URLCache.shared.memoryCapacity = newMemoryCapacity

        // 설정한 용량이 적용되었는지 확인
    }
    
    func getImage() {
        let urlStr: String = urls[imageNum % imageNames.count]
        if let url = URL(string: urlStr) {
            if let cachedResponse = URLCache.shared.cachedResponse(for: URLRequest(url: url)) {
                // 이미지를 캐시에서 불러오는 경우
                self.image = UIImage(data: cachedResponse.data)
                print("reuseImage")
                // 이미지를 사용합니다.
            } else {
                // 이미지를 다운로드하는 경우
                URLSession.shared.dataTask(with: url) { data, response, error in
                    guard let data = data else {
                        return
                    }
                    // 이미지를 캐시합니다.
                    let cachedResponse = CachedURLResponse(response: response!, data: data)
                    URLCache.shared.storeCachedResponse(cachedResponse, for: URLRequest(url: url))
                    print("saveCache")
                    // 이미지를 사용합니다.
                    if let image = UIImage(data: data) {
                           DispatchQueue.main.async {
                               self.image = image
                           }
                       }
                }.resume()
            }
        }
    }
}

struct CachedImageView: View {
    @Binding var image: UIImage?
    
    var body: some View {
        Group {
            if let image = image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
            } else {
                ProgressView()
            }
        }
    }
}

결과적으로 캐시에서 이미지 재사용을 하긴 했습니다. 그런데 짧아졌음에도 여전히 뷰 로드 시간이 너무 길어요.

물론 한번 다운로드한 이미지는 재사용이 가능해서 비행기모드나 인터넷이 되지않아도 사용할 수 있는 장점이 있습니다!

 

 

세번째 시도

FileManager 로 Cache Directory 를 만들어서 url을 SHA256 해시알고리즘으로 변환 후 저장, 로드

 

*** FileManager ***

FileManager는 iOS 및 macOS 앱에서 파일 시스템에 액세스하는 데 사용되는 클래스입니다. FileManager 클래스는 파일 및 디렉토리를 생성, 삭제, 이동, 복사, 검색 등 다양한 작업을 수행할 수 있습니다. 또한 파일 속성 및 메타데이터를 읽고 쓰는 데 사용할 수도 있습니다. 이 클래스는 Foundation 프레임워크에 포함되어 있으며, Swift 또는 Objective-C로 작성된 모든 앱에서 사용할 수 있습니다.

import Foundation
import SwiftUI

struct ContentView: View {

    private var urls: [String] = [
        "https://upload.wikimedia.org/wikipedia/commons/5/53/%22_Cooperativa_de_fluido_electrico%2C_%22_Mapa_del_Cadi._%22_Estudios_y_construcciones_Locher%2C_S._A.%2C_%22_Barcelona_-_Aixecament_fet_per_L%C3%A9o_Acgerter..._-_btv1b53065199x.jpg",
        "https://cdn.pixabay.com/photo/2016/03/08/20/03/flag-1244649_1280.jpg",
    ]
    
    private var imageNames: [String] = [
        "bigImage",
        "smallImage"
    ]
    
    @State private var imageNum: Int = 0
    @State private var image: UIImage?
    
    var body: some View {
        VStack{
            Button(action: {
                imageNum += 1
                getImage()
            }){
                Rectangle()
            }
            Image(imageNames[imageNum % imageNames.count])
                .resizable()
                .aspectRatio(contentMode: .fit)
            CachedImageView(image: self.$image)
        }.onAppear {
            checkDirectory()
            getImage()
        }
    }
    
    func checkDirectory(){
        let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
        let urlCacheDirectory = cacheDirectory.appendingPathComponent("MyCacheDirectory")

        if !FileManager.default.fileExists(atPath: urlCacheDirectory.path) {
            do {
                print("fileExists")
                try FileManager.default.createDirectory(at: urlCacheDirectory, withIntermediateDirectories: true, attributes: nil)
            } catch {
                print(error.localizedDescription)
            }
        }
        
    }
    
    func getCacheFileName(for url: URL) -> String? {
        guard let hash = url.absoluteString.data(using: .utf8)?.base64EncodedString() else { return nil }
        return hash + ".cached"
    }

    func saveCachedResponseToDisk(response: CachedURLResponse, fileName: String) {
        let data = response.data
        
        let fileManager = FileManager.default
        do {
            let url = try fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
            let fileURL = url.appendingPathComponent(fileName.sha256())
            try data.write(to: fileURL)
        } catch let error {
            print("Error saving cached response to disk: \(error.localizedDescription)")
        }
    }

    func loadCachedResponseFromDisk(fileName: String) -> Data? {
        let fileManager = FileManager.default
        do {
            let url = try fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
            let fileURL = url.appendingPathComponent(fileName.sha256())
            print(fileURL, "fileURL")
            return try Data(contentsOf: fileURL)
        } catch let error {
            print("Error loading cached response from disk: \(error.localizedDescription)")
            return nil
        }
    }

    func getImage() {
        let urlStr: String = urls[imageNum % imageNames.count]
        if let url = URL(string: urlStr) {
            if let fileName = getCacheFileName(for: url),
                      let data = loadCachedResponseFromDisk(fileName: fileName) {
                // 캐시된 응답(response)를 파일에서 불러오는 경우
                self.image = UIImage(data: data)
                print("reuseCachedResponse")
                // 이미지를 사용합니다.
            } else {
                // 이미지를 다운로드하는 경우
                URLSession.shared.dataTask(with: url) { data, response, error in
                    guard let data = data else {
                        return
                    }
                    // 이미지를 캐시합니다.
                    let cachedResponse = CachedURLResponse(response: response!, data: data)
                    if let fileName = getCacheFileName(for: url) {
                        saveCachedResponseToDisk(response: cachedResponse, fileName: fileName)
                    }
                    print("saveCache")
                    // 이미지를 사용합니다.
                    if let image = UIImage(data: data) {
                        DispatchQueue.main.async {
                            self.image = image
                        }
                    }
                }.resume()
            }
        }
    }
}

struct CachedImageView: View {
    @Binding var image: UIImage?
    
    var body: some View {
        Group {
            if let image = image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
            } else {
                ProgressView()
            }
        }
    }
}

import CommonCrypto

extension String {
    func sha256() -> String {
        if let stringData = self.data(using: String.Encoding.utf8) {
            var digest = [UInt8](repeating: 0, count:Int(CC_SHA256_DIGEST_LENGTH))
            stringData.withUnsafeBytes {
                _ = CC_SHA256($0.baseAddress, CC_LONG(stringData.count), &digest)
            }
            let hexBytes = digest.map { String(format: "%02hhx", $0) }
            return hexBytes.joined()
        } else {
            return ""
        }
    }
}

 

당연히 됩니다. 그리고 당연히 버벅거립니다.

아 모르겠습니다. 어디서부터 어떻게 공부해야할지...... UIImage(data: )와 Image() 둘의 차이가 뭔지 알아야할 것 같읍니다. 선생님들..도와주새오....

 

URLCache

장점:

  • URL 요청 및 응답 데이터를 메모리에 캐시하므로 빠른 속도로 접근 가능
  • 네트워크 대역폭을 줄여 데이터 사용량을 줄일 수 있음
  • URL 요청 시간에 따라 캐시 데이터의 만료시간을 설정하여 관리 가능

단점:

  • 캐시된 데이터는 앱의 메모리 용량에 영향을 미치므로, 큰 데이터를 캐시할 경우 메모리 부족으로 앱이 종료될 수 있음
  • URL 요청 및 응답 데이터를 메모리에 캐시하므로, 앱이 종료되면 캐시된 데이터가 모두 사라짐

 FileManager

장점:

  • 파일을 디스크에 저장하므로, 앱이 종료되어도 데이터를 보존 가능
  • 메모리 용량에 영향을 미치지 않으므로, 큰 데이터도 저장 가능

단점:

  • 파일을 저장하고 불러오는 과정에서 시간이 소요될 수 있음
  • 디스크에 파일을 저장하므로, 파일 저장 및 불러오기 과정에서 디스크 사용량이 증가할 수 있음
  •  

따라서 URLCache는 메모리 용량이 적은 데이터의 캐싱에 적합하고, FileManager는 큰 용량의 데이터나 보존이 필요한 데이터의 캐싱에 적합합니다.

728x90
반응형
LIST

'iOS' 카테고리의 다른 글

UIKit 화면 상태  (0) 2023.05.18
SwiftUI active, inavtive, background 상태  (0) 2023.05.18
UIKit 화면 전환 방법과 예제  (0) 2023.05.09
WWDC22 Challenge: Learn Switch Control through gaming  (0) 2023.05.07
iOS 연속 탭 위치 인식 방법  (0) 2023.05.06