FileManager의 Combine을 통한 비동기 처리
func save(image: UIImage, id: UUID) throws {
guard let data = image.pngData(),
let filePath = filePath(of: id) else { return }
try data.write(to: filePath)
}
func load(withID id: UUID) -> UIImage? {
guard let filePath = filePath(of: id) else { return nil }
do {
let data = try Data(contentsOf: filePath)
return UIImage(data: data)
}
...
qos: .utility
로 지시하고, read 작업은 기본 qos로 지시함func save(image: UIImage, id: UUID, completion: ((FileManagerError?) -> Void)? = nil) {
guard let data = image.pngData(),
let filePath = filePath(of: id) else { return }
DispatchQueue.global(qos: .utility).async {
do {
try data.write(to: filePath)
completion?(nil)
} catch {
completion?(.failToWrite(error: error))
}
}
}
func load(withID id: UUID, completion: @escaping (UIImage?) -> Void) {
guard let filePath = filePath(of: id) else {
completion(nil)
return
}
DispatchQueue.global().async {
do {
let data = try Data(contentsOf: filePath)
let image = UIImage(data: data)
completion(image)
} catch {
completion(nil)
}
}
}
...
Repository 에서 Clothes 목록을 가져오는 메서드 (파일매니저 메서드를 호출)
// Repository 에서 Clothes 목록을 가져오는 메서드
func fetchClothesList(completion: @escaping (ClothesList?) -> Void) {
guard let realm = realm else {
completion(nil)
return
}
// Realm 에서 먼저 데이터를 가져오고,
let clothesEntities = realm.objects(ClothesEntity.self)
var clothesList = ClothesList(clothesByCategory: [:])
let dispatchGroup = DispatchGroup()
let serialQueue = DispatchQueue(label: "serialQueue")
clothesEntities.forEach { entity in
var model = entity.toModelWithoutImage()
dispatchGroup.enter()
// 각각의 옷 모델에 image를 매핑해준다.
ImageFileStorage.shared.load(withID: model.id) { image in
if let image = image {
model.image = image
}
// 딕셔너리에 동시 접근 때문에 발생하는 문제를 serialQueue로 방지
serialQueue.async {
clothesList.clothesByCategory[model.category, default: []].append(model)
}
dispatchGroup.leave()
}
}
// 모든 이미지의 로딩이 완료된 시점을 dispatchGroup으로 notify 함.
dispatchGroup.notify(queue: .main) {
completion(clothesList)
}
}
subscribe(on:)
을 해줬을 떄는 백그라운드 스레드로 변경되지 않았음 -> Future의 특성 때문// ImageFileManager 의 이미지를 로딩하는 부분을 Combine으로 리팩터링
func load(withID id: UUID) -> AnyPublisher<UIImage, FileManagerError> {
return Deferred { // Deferred를 사용함으로서 Future가 즉시 바로 실행되지 않게 함
Future { promise in
guard let filePath = self.filePath(of: id) else {
promise(.failure(.invalidFilePath))
return
}
do {
let data = try Data(contentsOf: filePath)
guard let image = UIImage(data: data) else {
promise(.failure(.invalidData))
return
}
promise(.success(image))
} catch {
promise(.failure(.failToWrite(error: error)))
}
}
}
.subscribe(on: DispatchQueue.global()) // 백그라운드 스레드로로 upstream을 변경
.eraseToAnyPublisher()
}
realm
에서 먼저 entity 를 로딩하여 model 로 매핑 한 후, ImageFileManager
에서 이미지를 로딩하여 넣어주고 반환하는ClothesStorage
의 로직을 Combine으로 리팩터링 (아직 Repository 객체 분리 전이라 Storage끼리 서로 참조하고 있는 형태)func fetchClothesList() -> AnyPublisher<ClothesList, StorageError> {
return Future { [weak self] promise in
guard let self = self else { return }
guard let realm = realm else {
promise(.failure(.realmNotInitialized))
return
}
// 반영한 모델들을 다 합한 결과를 future로 내뱉음.
let clothesEntities = Array(realm.objects(ClothesEntity.self))
let clothesModelsWithoutImage = clothesEntities.map { $0.toModelWithoutImage() }
addingImagePublishers(to: clothesModelsWithoutImage)
.sink { clothesModels in
// 이미지가 모두 반영 된 ClothesList
let clothesList = clothesModels.toClothesList()
promise(.success(clothesList))
}
.store(in: &cancellables)
}
.eraseToAnyPublisher()
}
private func addingImagePublishers(to clothesModels: [Clothes]) -> AnyPublisher<[Clothes], Never> {
// ImageFileStorage를 호출해 이미지를 로딩해서 clothes에 넣는 것을 처리하는 Publisher들
let clothesWithImagePublishers: [AnyPublisher<Clothes, Never>] = clothesModels.map { model in
ImageFileStorage.shared.load(withID: model.id)
.replaceError(with: UIImage())
.map { image in
var clothes = model
clothes.image = image
return clothes
}
.eraseToAnyPublisher()
}
// 위에서 만든 단일의 Clothes를 방출하는 여러 Publisher들을 모아서 [Clothes] 를 방출하는 하나의 Publisher로 만듬
return Publishers.MergeMany(clothesWithImagePublishers)
.collect()
.eraseToAnyPublisher()
}