From 9cfe1135970ac3bc2be5a7152846d1157b511f15 Mon Sep 17 00:00:00 2001 From: peak Date: Tue, 25 Jan 2022 18:54:16 +0800 Subject: [PATCH] Save image to file; use combine framework to download image --- iAppStore.xcodeproj/project.pbxproj | 16 +++++ iAppStore/Shared/UI/ImageLoader.swift | 52 +++++++++----- .../Shared/Utilities/LocalFileManager.swift | 69 +++++++++++++++++++ .../Shared/Utilities/NetworkManager.swift | 49 +++++++++++++ iAppStore/Shared/extensions/Strings.swift | 9 +++ 5 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 iAppStore/Shared/Utilities/LocalFileManager.swift create mode 100644 iAppStore/Shared/Utilities/NetworkManager.swift diff --git a/iAppStore.xcodeproj/project.pbxproj b/iAppStore.xcodeproj/project.pbxproj index 8c70baf..fa33226 100644 --- a/iAppStore.xcodeproj/project.pbxproj +++ b/iAppStore.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 6C51176C27A00D9C00D5E21D /* LocalFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C51176B27A00D9C00D5E21D /* LocalFileManager.swift */; }; + 6C51176E27A00DAC00D5E21D /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C51176D27A00DAC00D5E21D /* NetworkManager.swift */; }; 6D26AAC2278420CB003F82BF /* AboutAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D26AAC1278420CB003F82BF /* AboutAppView.swift */; }; 6D490E542782F35F00B5DD80 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D490E532782F35F00B5DD80 /* Strings.swift */; }; 6D490E56278328E400B5DD80 /* SubscriptionAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D490E55278328E400B5DD80 /* SubscriptionAddView.swift */; }; @@ -59,6 +61,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 6C51176B27A00D9C00D5E21D /* LocalFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFileManager.swift; sourceTree = ""; }; + 6C51176D27A00DAC00D5E21D /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; 6D26AAC1278420CB003F82BF /* AboutAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutAppView.swift; sourceTree = ""; }; 6D490E532782F35F00B5DD80 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 6D490E55278328E400B5DD80 /* SubscriptionAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAddView.swift; sourceTree = ""; }; @@ -122,6 +126,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 6C51176A27A00D8A00D5E21D /* Utilities */ = { + isa = PBXGroup; + children = ( + 6C51176B27A00D9C00D5E21D /* LocalFileManager.swift */, + 6C51176D27A00DAC00D5E21D /* NetworkManager.swift */, + ); + path = Utilities; + sourceTree = ""; + }; 6D738E22277972BD00A4A76E /* components */ = { isa = PBXGroup; children = ( @@ -177,6 +190,7 @@ 6D738E2F277973DA00A4A76E /* Shared */ = { isa = PBXGroup; children = ( + 6C51176A27A00D8A00D5E21D /* Utilities */, 6DB3B783277D6CEB00E8626F /* Common */, 6D738E392779839C00A4A76E /* UI */, 6D738E30277973F700A4A76E /* extensions */, @@ -453,9 +467,11 @@ 6DB3B794277D770F00E8626F /* APIService.swift in Sources */, 6DB3B6FE27799AF200E8626F /* RankFilterForm.swift in Sources */, 6D738E322779743D00A4A76E /* Color.swift in Sources */, + 6C51176C27A00D9C00D5E21D /* LocalFileManager.swift in Sources */, 6D605A15278B15850001C69F /* AppContextMenu.swift in Sources */, 6DB3B782277D6CE700E8626F /* Constants.swift in Sources */, 6D490E56278328E400B5DD80 /* SubscriptionAddView.swift in Sources */, + 6C51176E27A00DAC00D5E21D /* NetworkManager.swift in Sources */, 6D95DC6E27804AAD00EE8B54 /* AppDetail.swift in Sources */, 6D490E542782F35F00B5DD80 /* Strings.swift in Sources */, 6D26AAC2278420CB003F82BF /* AboutAppView.swift in Sources */, diff --git a/iAppStore/Shared/UI/ImageLoader.swift b/iAppStore/Shared/UI/ImageLoader.swift index f8a48de..856b141 100644 --- a/iAppStore/Shared/UI/ImageLoader.swift +++ b/iAppStore/Shared/UI/ImageLoader.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI +import Combine // refer: https://stackoverflow.com/questions/60677622/how-to-display-image-from-a-url-in-swiftui @@ -53,26 +54,39 @@ struct ImageLoaderView: View { class ImageLoaderService: ObservableObject { @Published var image = UIImage() - - convenience init(url: URL) { - self.init() - loadImage(for: url) + + private var imageSubscription: AnyCancellable? + private let fileManager = LocalFileManager.instance + private let folderName: String = "iAppStore_images" + private let url: URL + + init(url: URL) { + self.url = url + loadImage() } - - func loadImage(for url: URL) { - let task = URLSession.shared.dataTask(with: url) { data, res, error in - guard error == nil else { - return - } - - guard let data = data, let image = UIImage(data: data) else { - return - } - - DispatchQueue.main.async { - self.image = image - } + + private func loadImage() { + if let savedImage = fileManager.getImage(imageName: url.path.md5, folderName: folderName) { +// print("get saved image: \(url)") + image = savedImage + } else { +// print("download image: \(url)") + downloadImage() } - task.resume() + } + + private func downloadImage() { + imageSubscription = NetworkingManager.download(url: url) + .tryMap({ (data) -> UIImage? in + return UIImage(data: data) + }) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] (returnedImage) in + guard let self = self, let downloadedImage = returnedImage else { return } + + self.image = downloadedImage + self.imageSubscription?.cancel() + self.fileManager.saveImage(image: downloadedImage, imageName: self.url.path.md5, folderName: self.folderName) + }) } } diff --git a/iAppStore/Shared/Utilities/LocalFileManager.swift b/iAppStore/Shared/Utilities/LocalFileManager.swift new file mode 100644 index 0000000..38e337b --- /dev/null +++ b/iAppStore/Shared/Utilities/LocalFileManager.swift @@ -0,0 +1,69 @@ +// +// LocalFileManager.swift +// iAppStore +// +// Created by peak on 2022/1/25. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Foundation +import SwiftUI + +class LocalFileManager { + static let instance = LocalFileManager() + private init() {} + + func saveImage(image: UIImage, imageName: String, folderName: String) { + createFolderIfNeeded(folderName: folderName) + + guard let data = image.pngData(), + let url = getURLForImage(imageName: imageName, folderName: folderName) + else { + return + } + + do { + try data.write(to: url) + } catch let error { + print("Error saving image. ImageName: \(imageName) \(error)") + } + } + + func getImage(imageName: String, folderName: String) -> UIImage? { + guard let url = getURLForImage(imageName: imageName, folderName: folderName), + FileManager.default.fileExists(atPath: url.path) else { + return nil + } + return UIImage(contentsOfFile: url.path) + } + + // MARK: Private + + private func createFolderIfNeeded(folderName: String) { + guard let url = getURLForFolder(folderName: folderName) else { + return + } + + if !FileManager.default.fileExists(atPath: url.path) { + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) + } catch let error { + print("Error creating directory. FolderName: \(folderName). \(error)") + } + } + } + + private func getURLForFolder(folderName: String) -> URL? { + guard let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first, !folderName.isEmpty else { + return nil + } + return url.appendingPathComponent(folderName) + } + + private func getURLForImage(imageName: String, folderName: String) -> URL? { + guard let folderURL = getURLForFolder(folderName: folderName), !imageName.isEmpty else { + return nil + } + return folderURL.appendingPathComponent(imageName + ".png") + } +} diff --git a/iAppStore/Shared/Utilities/NetworkManager.swift b/iAppStore/Shared/Utilities/NetworkManager.swift new file mode 100644 index 0000000..645f9bc --- /dev/null +++ b/iAppStore/Shared/Utilities/NetworkManager.swift @@ -0,0 +1,49 @@ +// +// NetworkManager.swift +// iAppStore +// +// Created by peak on 2022/1/25. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Foundation +import Combine + +class NetworkingManager { + + enum NetworkingError: LocalizedError { + case badURLResponse(url: URL) + case unknown + + var errorDescription: String? { + switch self { + case .badURLResponse(url: let url): return "[🔥] Bad response from URL: \(url)" + case .unknown: return "[⚠️] Unknown error occured" + } + } + } + + static func download(url: URL) -> AnyPublisher { + return URLSession.shared.dataTaskPublisher(for: url) + .tryMap({ try handleURLResponse(output: $0, url: url) }) + .retry(2) + .eraseToAnyPublisher() + } + + static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data { + guard let response = output.response as? HTTPURLResponse, + response.statusCode >= 200 && response.statusCode < 300 else { + throw NetworkingError.badURLResponse(url: url) + } + return output.data + } + + static func handleCompletion(completion: Subscribers.Completion) { + switch completion { + case .finished: + break + case .failure(let error): + print(error.localizedDescription) + } + } +} diff --git a/iAppStore/Shared/extensions/Strings.swift b/iAppStore/Shared/extensions/Strings.swift index 843e42a..91eab62 100644 --- a/iAppStore/Shared/extensions/Strings.swift +++ b/iAppStore/Shared/extensions/Strings.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI +import CryptoKit extension String { @@ -54,3 +55,11 @@ extension String { } } + +extension String { + var md5: String { + let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!) + return computed.map { String(format: "%02hhx", $0) } + .joined() + } +}