[SwiftUI Continued Learning Review] download & save Images

Woozoo·2023년 2월 21일
0

[SwiftUI Review]

목록 보기
6/41
post-custom-banner


미니앱을 만들어볼겨


필요한 사전 지식은 요정도가 되겠다


간단한 뷰 구성해주고

jsonPlaceholder사이트를 사용해서
photos 데이터들을 받아줄 거다!!

json 데이터들을 다운받고 이 데이터엔 이미지 url이 있는데
이미지들은 따로 다운받아줄거임!



JSON 데이터를 참고해서 포토모델을 구성해보자

struct PhotoModel: Identifiable, Codable {
    let albumId: Int
    let id: Int
    let title: String
    let url: String
    let thumbnailUrl: String
}

/*
 {
     "albumId": 1,
     "id": 1,
     "title": "accusamus beatae ad facilis cum similique qui sunt",
     "url": "https://via.placeholder.com/600/92c952",
     "thumbnailUrl": "https://via.placeholder.com/150/92c952"
   }
 */

요런형태가 되야겠죠


그리고 뷰모델을 만들어줍시다!
뷰모델에는 PhotoModel 타입의 array가 있을거고 이 데이터를 바탕으로 list를 그려줄거

이미지를 다운로드 하는 건 제네릭하게 쓸 수 있으니까 Utilites 폴더 안에
이 로직을 처리할 클래스를 만들어주면 됨

import Foundation
import Combine

class PhotoModelDataService {
    static let instance = PhotoModelDataService()
    @Published var photoModels: [PhotoModel] = []
    var cancellables = Set<AnyCancellable>()
    
    private init() {
        downloadData()
    }
    
    func downloadData() {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/photos") else { return }
        
        URLSession.shared.dataTaskPublisher(for: url)
            .subscribe(on: DispatchQueue.global(qos: .background))
            .receive(on: DispatchQueue.main)
            .tryMap(handleOutput)
            .decode(type: [PhotoModel].self, decoder: JSONDecoder())
            .sink { completion in
                switch completion {
                case .finished:
                    break
                case .failure(let error):
                    print("Error downloading data. \(error)")
                }
            } receiveValue: { [weak self] returnedPhotoModels in
                self?.photoModels = returnedPhotoModels
            }
            .store(in: &cancellables)

    }
    
    private func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data {
        guard
            let response = output.response as? HTTPURLResponse,
            response.statusCode >= 200 && response.statusCode < 300 else {
            throw URLError(.badServerResponse)
            }
        return output.data
    }
}

다운로드 데이터는 콤바인으로 처리해주고 tryMap같은 경우엔 메소드로 따로 빼줬으
여기서 이해 안되는거? 없음 ㅋㅋㅋ🥳
와 진짜 꾸준히 하면 되는구나


한 가지 문제가 있다면 지금 DataService에서 데이터를 처리하고 있기 때문에

뷰모델에선 photoModels 어레이에 접근하기가 어렵죠

photoModels가 @Published로 값이 방출되고 있으니까 이걸 바인딩해서 .sink해서 뷰모델의 dataArray에 넣고, 마찬가지로 .store해주면 됩니다!!

다시 뷰로 돌아오면 데이터가 다운되서 리스트가 그려지고 있겠죠


HStack을 따로 빼서 뷰로 만들어주면 보기 편할 거 같다

지금은 Circle인데 Image를 다운로드 받는 뷰를 만들고 넣어주면 될 것 같지 않음?


로딩 중이라면 프로그레스뷰를 보여주게 하자

다운로드 받는 로직은 뷰모델로 따로 빼줘야할 듯


cancellables에 store도 해줘야함!! (까먹은듯)

뷰모델이 init될 때 url을 받을 수 있게 해주고

다운로드 이미지 뷰에서 기존에 선언되어 있던 뷰모델의 형태를 조금 바꿔줘야함 init될 때 url을 받아서 StateObject로 선언한 loader에게 전달해주려면 지금과 같은 방법으로 작성해줘야한다
_loader = StateObject(wrappedValue: ImageLoadingViewModel(url: url))

이제 선언해준 것들 제대로 맞춰서 들어가게 조정해주면끝!

잘 나옵니다잉


지금 다운로드가 몇개만큼 리스트로 먼저 되는지 궁금하니까
donwloadImage 메소드 구현한 뷰모델에 프린트로 찍어봅시다


어느 정도 갯수의 이미지를 먼저 다운로드 하고 스크롤을 움직이게 될 때
새로운 다운로드들이 시작됨!
근데 문제가 뭐냐면!!!
스크롤을 내렸다가 올리게 되면 다시 다운로드가 된다는겨!!!!
-> 파일매니저랑 캐시 쓰면 좋겠죠


캐시 매니저를 유틸리티 폴더에 만들어줍시다

import Foundation
import SwiftUI

class PhotoModelCacheManager {
    static let instance = PhotoModelCacheManager()
    private init() { }
    
    var photoCache: NSCache<NSString, UIImage> = {
        var cache = NSCache<NSString, UIImage>()
        cache.countLimit = 20
        cache.totalCostLimit = 1024 * 1024 * 200  // 200mb Maybe...?
        return cache
    }()
    
    func add(key: String, value: UIImage) {
        photoCache.setObject(value, forKey: key as NSString)
    }
    
    func get(key: String) -> UIImage? {
        return photoCache.object(forKey: key as NSString)
    }
}

photoCache를 관리해줄 수 있는 매니저 파일을 싱글톤으로 만들어줌
add랑 get하는 메소드도 작성해주고!

photoFileManager 클래스도 만들어줍시다!

import Foundation
import SwiftUI

class PhotoModelFileManager {
    static let instance = PhotoModelFileManager()
    let folderName = "downloaded_photos"
    
    private init() {
        createFolderIfNeeded()
    }
    
    private func createFolderIfNeeded() {
        guard let url = getFolderPath() else {return}
        
        if !FileManager.default.fileExists(atPath: url.path) {
            do {
                try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
                print("Created Folder!")
            } catch let error {
                print("Error creating folder. \(error)")
            }
        }
    }
    
    private func getFolderPath() -> URL? {
        return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
            .first?
            .appendingPathComponent(folderName)
    }
    
    // ... /downloaded_photos/image_name.png
    private func getImagePath(key: String) -> URL? {
        guard let folder = getFolderPath() else {
            return nil
        }
        return folder.appendingPathComponent(key + ".png")
    }
    
    func add(key: String, value: UIImage) {
        guard
            let data = value.pngData(),
            let url = getImagePath(key: key) else { return }
        do {
            try data.write(to: url)
        } catch let error {
            print("Error saving to file Manager. \(error)")
        }
    }
    
    func get(key: String) -> UIImage? {
        guard
            let url = getImagePath(key: key),
            FileManager.default.fileExists(atPath: url.path) else {
            return nil
        }
        return UIImage(contentsOfFile: url.path)
    }
    
}

이미지 로딩뷰모델로 돌아와서 방금만들어준 두 매니저를 선언해주자

get하는걸 구현하려고 하는데 이걸 만들 때 key 자체를 urlString으로 써도 될테지만 모델 자체에 id가 있으니까 그걸 사용해서 저장해주면 좋을 거 같음

음 여기서 생각해볼 점은
지금 url이랑 key를 나눠서 넘겨주는 뷰를 구성하게 됐는데
사실 어떻게 보면 model 전체를 넘겨주는 방법도 있을 거임
닉은 지금처럼 구현을 하는게 더 reusable하다고 얘기함

아아아

만약에 모델로 넘겨줬고 내가 또 사용해야한다고 가정해보셈.
photoModel 전체를 넘겨줬었다면 전부다 수정해줬어야할텐데 url이라는 string이랑 key라는 String 타입으로 지정함으로써 내가 string만 넘기면 어떤 모델이든지 스트링 값만 넘겨주면 되니까 이게 맞네!





캐시로 저장할 때 좋은 점은 일정 메모리 (내가 설정해준 limit)을 넘어가게 되면 알아서 삭제하고 다운로드 가능한 만큼만 유지하게 됨
단점은 permanant하게 저장되지 않는다는거!!

manager로 선언한 싱글톤 cacheManager를 FileManager로 이름만 바꿔서 테스트 해보자! (add 랑 get을 똑같이 선언해줘서 바로 테스트 가능)

다운받고나면 기기 자체에 저장되는 걸 볼 수 있다

profile
우주형
post-custom-banner

0개의 댓글