Skip to content

Commit

Permalink
Merge pull request #3 from chenxi92/peak
Browse files Browse the repository at this point in the history
Save image to file; use combine framework to download image
  • Loading branch information
iHTCboy authored Jan 25, 2022
2 parents 72ef197 + 9cfe113 commit 60cfda3
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 19 deletions.
16 changes: 16 additions & 0 deletions iAppStore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -59,6 +61,8 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
6C51176B27A00D9C00D5E21D /* LocalFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFileManager.swift; sourceTree = "<group>"; };
6C51176D27A00DAC00D5E21D /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
6D26AAC1278420CB003F82BF /* AboutAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutAppView.swift; sourceTree = "<group>"; };
6D490E532782F35F00B5DD80 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
6D490E55278328E400B5DD80 /* SubscriptionAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAddView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -122,6 +126,15 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
6C51176A27A00D8A00D5E21D /* Utilities */ = {
isa = PBXGroup;
children = (
6C51176B27A00D9C00D5E21D /* LocalFileManager.swift */,
6C51176D27A00DAC00D5E21D /* NetworkManager.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
6D738E22277972BD00A4A76E /* components */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -177,6 +190,7 @@
6D738E2F277973DA00A4A76E /* Shared */ = {
isa = PBXGroup;
children = (
6C51176A27A00D8A00D5E21D /* Utilities */,
6DB3B783277D6CEB00E8626F /* Common */,
6D738E392779839C00A4A76E /* UI */,
6D738E30277973F700A4A76E /* extensions */,
Expand Down Expand Up @@ -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 */,
Expand Down
52 changes: 33 additions & 19 deletions iAppStore/Shared/UI/ImageLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -53,26 +54,39 @@ struct ImageLoaderView<Placeholder: View, ConfiguredImage: View>: 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)
})
}
}
69 changes: 69 additions & 0 deletions iAppStore/Shared/Utilities/LocalFileManager.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
49 changes: 49 additions & 0 deletions iAppStore/Shared/Utilities/NetworkManager.swift
Original file line number Diff line number Diff line change
@@ -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<Data, Error> {
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<Error>) {
switch completion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}
}
9 changes: 9 additions & 0 deletions iAppStore/Shared/extensions/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import SwiftUI
import CryptoKit

extension String {

Expand Down Expand Up @@ -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()
}
}

0 comments on commit 60cfda3

Please sign in to comment.