FileManager and NSCache in SwiftUI
Updated:
FileManager in SwiftUI
We can dave data on iOS devices is file manager. File manager works exactly like the file manager on your computer. When you go to save a document you usually open up a folder where you want to save it and then you click save.
File manger on iPhone works the exact same way, we first find a folder where we want to save our documents and then we can save it and of course we can get it when we need it
But, difference is that we are not actively double clicking and opening folders on our mac. In the code, the code exactly where to save a file and to fetch and find a file.
For example, we can’t save an image directly to core data but we can save it directly to the file manager. File manger is greate to save images, videos, audio files we can streo Json data. We can store any document we want in this file mananger
General case save photo from assets to file manager
import SwiftUI
// MARK: - LOCALFILEMANGER
class LocalFileManger {
// singleton instance
static let instance = LocalFileManger()
func saveImage(image: UIImage, name: String) {
guard let data = image.jpegData(compressionQuality: 0.5) else {
print("Error getting data")
return } // compress 50 percent of quality from original size
// image.pngData() // if image is png format this code use
// let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
// let directory2 = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
// let directory3 = FileManager.default.temporaryDirectory
// let path = directory2?.appendingPathComponent("\(name).jpg")
guard
let path = FileManager
.default
.urls(for: .cachesDirectory, in: .userDomainMask)
.first?
.appendingPathComponent("\(name).jpg") else {
print("Eoor getting path.")
return
}
do {
try data.write(to: path)
print("Success saving")
} catch let error {
print("Error saving. \(error)")
}
}
}
// MARK: - VIEWMODEL
class FileMagerViewModel: ObservableObject {
// MARK: - PROPERTY
@Published var image: UIImage? = nil
let imageName: String = "pic"
let manager = LocalFileManger.instance
// MARK: - INIT
init() {
getImageFromAssetsFolder()
}
// MARK: - FUNCTION
func getImageFromAssetsFolder() {
image = UIImage(named: imageName)
}
func saveImage() {
guard let image = image else { return }
manager.saveImage(image: image, name: imageName)
}
}
// MARK: - VIEW
struct FileManagerBootCamp: View {
// MARK: - PROPERTY
@StateObject var vm = FileMagerViewModel()
// MARK: - BODY
var body: some View {
NavigationView {
VStack (spacing: 20) {
if let image = vm.image {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipped()
.cornerRadius(10)
}
Button {
vm.saveImage()
} label: {
Text("Save to File Manager")
.foregroundColor(.white)
.font(.headline)
.padding()
.padding(.horizontal)
.background(Color.blue.cornerRadius(10))
}
Spacer()
} //: VSTACK
.navigationTitle("File Manager ")
} //: NAVIGATION
}
}
Getting it back from the file manger
This is great for persisting images in your app if you download some really important content you can save it to the file manger and it will save and persist
import SwiftUI
// MARK: - LOCALFILEMANGER
class LocalFileManger {
// singleton instance
static let instance = LocalFileManger()
func saveImage(image: UIImage, name: String) {
guard
let data = image.jpegData(compressionQuality: 0.5),
let path = getPathForImage(name: name) else {
print("Error getting data")
return } // compress 50 percent of quality from original size
// image.pngData() // if image is png format this code use
// let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
// let directory2 = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
// let directory3 = FileManager.default.temporaryDirectory
// let path = directory2?.appendingPathComponent("\(name).jpg")
do {
try data.write(to: path)
print("Success saving")
} catch let error {
print("Error saving. \(error)")
}
}
func getIamge(name: String)-> UIImage? {
guard
let path = getPathForImage(name: name)?.path,
FileManager.default.fileExists(atPath: path) else {
print("Error getting path.")
return nil
}
return UIImage(contentsOfFile: path)
}
func getPathForImage(name: String)-> URL? {
guard
let path = FileManager
.default
.urls(for: .cachesDirectory, in: .userDomainMask)
.first?
.appendingPathComponent("\(name).jpg") else {
print("Error getting path.")
return nil
}
return path
}
}
// MARK: - VIEWMODEL
class FileMagerViewModel: ObservableObject {
// MARK: - PROPERTY
@Published var image: UIImage? = nil
let imageName: String = "pic"
let manager = LocalFileManger.instance
// MARK: - INIT
init() {
// getImageFromAssetsFolder()
getImageFromFileManager()
}
// MARK: - FUNCTION
func getImageFromAssetsFolder() {
image = UIImage(named: imageName)
}
func getImageFromFileManager() {
image = manager.getIamge(name: imageName)
}
func saveImage() {
guard let image = image else { return }
manager.saveImage(image: image, name: imageName)
}
}
// MARK: - VIEW
struct FileManagerBootCamp: View {
// MARK: - PROPERTY
@StateObject var vm = FileMagerViewModel()
// MARK: - BODY
var body: some View {
NavigationView {
VStack (spacing: 20) {
if let image = vm.image {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipped()
.cornerRadius(10)
}
Button {
vm.saveImage()
} label: {
Text("Save to File Manager")
.foregroundColor(.white)
.font(.headline)
.padding()
.padding(.horizontal)
.background(Color.blue.cornerRadius(10))
}
Spacer()
} //: VSTACK
.navigationTitle("File Manager ")
} //: NAVIGATION
}
}
Delete the file from File manager
You should assume that if you save it to the manager it’s going to be there until you explicitly delete it so if you are saving hundreds of images they’re all getting saved and you don’t want to take up too much space on that user’s device
So, it’s very important to monitor and delete odl items from the file manager
If it’s actually saving or deleting because in your app you probably want to give some user feedbacklike if it actually saved or if it actually deleted
import SwiftUI
// MARK: - LOCALFILEMANGER
class LocalFileManger {
// singleton instance
static let instance = LocalFileManger()
func saveImage(image: UIImage, name: String) -> String {
guard
let data = image.jpegData(compressionQuality: 0.5),
let path = getPathForImage(name: name) else {
print("Error getting data")
return "Error getting data" } // compress 50 percent of quality from original size
// image.pngData() // if image is png format this code use
// let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
// let directory2 = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
// let directory3 = FileManager.default.temporaryDirectory
// let path = directory2?.appendingPathComponent("\(name).jpg")
do {
try data.write(to: path)
print("Success saving")
return "Success saving"
} catch let error {
print("Error saving. \(error)")
return "Error saving.\(error)"
}
}
func getIamge(name: String)-> UIImage? {
guard
let path = getPathForImage(name: name)?.path,
FileManager.default.fileExists(atPath: path) else {
print("Error getting path.")
return nil
}
return UIImage(contentsOfFile: path)
}
func deleteImage(name: String)-> String {
guard
let path = getPathForImage(name: name),
FileManager.default.fileExists(atPath: path.path) else {
print("Error getting path.")
return "Error getting path."
}
do {
try FileManager.default.removeItem(at: path)
print("Sucessfully deleted.")
return "Sucessfully deleted."
} catch let error {
print("Error deleting image. \(error)")
return "Error deleting image. \(error)"
}
}
func getPathForImage(name: String)-> URL? {
guard
let path = FileManager
.default
.urls(for: .cachesDirectory, in: .userDomainMask)
.first?
.appendingPathComponent("\(name).jpg") else {
print("Error getting path.")
return nil
}
return path
}
}
// MARK: - VIEWMODEL
class FileMagerViewModel: ObservableObject {
// MARK: - PROPERTY
@Published var image: UIImage? = nil
let imageName: String = "pic"
let manager = LocalFileManger.instance
@Published var inforMessage: String = ""
// MARK: - INIT
init() {
getImageFromAssetsFolder()
// getImageFromFileManager()
}
// MARK: - FUNCTION
func getImageFromAssetsFolder() {
image = UIImage(named: imageName)
}
func getImageFromFileManager() {
image = manager.getIamge(name: imageName)
}
func saveImage() {
guard let image = image else { return }
inforMessage = manager.saveImage(image: image, name: imageName)
}
func deleteImage() {
inforMessage = manager.deleteImage(name: imageName)
}
}
// MARK: - VIEW
struct FileManagerBootCamp: View {
// MARK: - PROPERTY
@StateObject var vm = FileMagerViewModel()
// MARK: - BODY
var body: some View {
NavigationView {
VStack (spacing: 20) {
if let image = vm.image {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipped()
.cornerRadius(10)
}
HStack {
Button {
vm.saveImage()
} label: {
Text("Save to File Manager")
.foregroundColor(.white)
.font(.headline)
.padding()
.padding(.horizontal)
.background(Color.blue.cornerRadius(10))
}
Button {
vm.deleteImage()
} label: {
Text("Delete from File Manager")
.foregroundColor(.white)
.font(.headline)
.padding()
.padding(.horizontal)
.background(Color.red.cornerRadius(10))
}
} //: HSTACK
Text(vm.inforMessage)
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.purple)
Spacer()
} //: VSTACK
.navigationTitle("File Manager ")
} //: NAVIGATION
}
}
Save stuff in custom folder (crete, delete folder)
import SwiftUI
// MARK: - LOCALFILEMANGER
class LocalFileManger {
// singleton instance
static let instance = LocalFileManger()
let folderName = "MyApp_Images"
init() {
createFolderIfNeeded()
}
// MARK: - FUNCTION
func createFolderIfNeeded() {
guard
let path = FileManager
.default
.urls(for: .cachesDirectory, in: .userDomainMask)
.first?
.appendingPathComponent(folderName) // custom create own folder in FM
.path else {
return
}
if !FileManager.default.fileExists(atPath: path) {
do {
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
print("Success creating folder")
} catch let error {
print("Error creating folder .\(error)")
}
}
}
func deleteFolder() {
guard
let path = FileManager
.default
.urls(for: .cachesDirectory, in: .userDomainMask)
.first?
.appendingPathComponent(folderName) // custom create own folder in FM
.path else {
return
}
do {
try FileManager.default.removeItem(atPath: path)
print("Success deleting folder")
} catch let error {
print("Error deleting folder. \(error)")
}
}
func saveImage(image: UIImage, name: String) -> String {
guard
let data = image.jpegData(compressionQuality: 0.5),
let path = getPathForImage(name: name) else {
print("Error getting data")
return "Error getting data" } // compress 50 percent of quality from original size
// image.pngData() // if image is png format this code use
// let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
// let directory2 = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
// let directory3 = FileManager.default.temporaryDirectory
// let path = directory2?.appendingPathComponent("\(name).jpg")
do {
try data.write(to: path)
print(path)
return "Success saving"
} catch let error {
print("Error saving. \(error)")
return "Error saving.\(error)"
}
}
func getIamge(name: String)-> UIImage? {
guard
let path = getPathForImage(name: name)?.path,
FileManager.default.fileExists(atPath: path) else {
print("Error getting path.")
return nil
}
return UIImage(contentsOfFile: path)
}
func deleteImage(name: String)-> String {
guard
let path = getPathForImage(name: name)?.path,
FileManager.default.fileExists(atPath: path) else {
print("Error getting path.")
return "Error getting path."
}
do {
try FileManager.default.removeItem(atPath: path)
print("Sucessfully deleted.")
return "Sucessfully deleted."
} catch let error {
print("Error deleting image. \(error)")
return "Error deleting image. \(error)"
}
}
func getPathForImage(name: String)-> URL? {
guard
let path = FileManager
.default
.urls(for: .cachesDirectory, in: .userDomainMask)
.first?
.appendingPathComponent(folderName)
.appendingPathComponent("\(name).jpg") else {
print("Error getting path.")
return nil
}
return path
}
}
// MARK: - VIEWMODEL
class FileMagerViewModel: ObservableObject {
// MARK: - PROPERTY
@Published var image: UIImage? = nil
let imageName: String = "pic"
let manager = LocalFileManger.instance
@Published var inforMessage: String = ""
// MARK: - INIT
init() {
getImageFromAssetsFolder()
// getImageFromFileManager()
}
// MARK: - FUNCTION
func getImageFromAssetsFolder() {
image = UIImage(named: imageName)
}
func getImageFromFileManager() {
image = manager.getIamge(name: imageName)
}
func saveImage() {
guard let image = image else { return }
inforMessage = manager.saveImage(image: image, name: imageName)
}
func deleteImage() {
inforMessage = manager.deleteImage(name: imageName)
manager.deleteFolder()
}
}
// MARK: - VIEW
struct FileManagerBootCamp: View {
// MARK: - PROPERTY
@StateObject var vm = FileMagerViewModel()
// MARK: - BODY
var body: some View {
NavigationView {
VStack (spacing: 20) {
if let image = vm.image {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipped()
.cornerRadius(10)
}
HStack {
Button {
vm.saveImage()
} label: {
Text("Save to File Manager")
.foregroundColor(.white)
.font(.headline)
.padding()
.padding(.horizontal)
.background(Color.blue.cornerRadius(10))
}
Button {
vm.deleteImage()
} label: {
Text("Delete from File Manager")
.foregroundColor(.white)
.font(.headline)
.padding()
.padding(.horizontal)
.background(Color.red.cornerRadius(10))
}
} //: HSTACK
Text(vm.inforMessage)
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.purple)
Spacer()
} //: VSTACK
.navigationTitle("File Manager ")
} //: NAVIGATION
}
}
NSCache in SwiftUI
We are going to create a local cache in our code so that we can savwe some of the data that we doenload from the Internet in a temporary location
Chching is something this’s used in all software development and there’s a ton of different ways to implement. When you downloaded data from the Internet that is important that the user might reuse while they’re in your app right now
It is actually important enough to save it to the file manager or somewhere that it’s going to save forever
// MARK: - CACHEMANAGER
class CacheManger {
// make singleton : means this is going to be the only instance of catch manager in our entire app
static let instance = CacheManger()
private init() { }
var imageCache: NSCache<NSString, UIImage> = {
let cache = NSCache<NSString, UIImage>()
// when we store any kind of data in this local cache it's going to store it in the memoryof the device. So, put a count limit on your cache
cache.countLimit = 100 // the maximum number of objects the cache should hold
// Themaximum total cost that the cache can hold before it starts evicting object
cache.totalCostLimit = 1024 * 1024 * 100 // 100mb
return cache
}()
func add(image: UIImage, name: String) -> String {
imageCache.setObject(image, forKey: name as NSString)
return "Added to cache!"
}
func remove(name: String) -> String {
imageCache.removeObject(forKey: name as NSString)
return "Removed from cache!"
}
func get(name: String) -> UIImage? {
return imageCache.object(forKey: name as NSString)
}
}
// MARK: - VIEWMODEL
class CacheViewModel: ObservableObject {
// MARK: - PROPERTY
@Published var startingImage: UIImage? = nil
@Published var cachedImage: UIImage? = nil
@Published var infoMessage: String = ""
let imageName: String = "pic"
let manager = CacheManger.instance
// MARK: - INIT
init() {
getImageFromAssetsFolder()
}
// MARK: - FUNCTION
func getImageFromAssetsFolder() {
startingImage = UIImage(named: imageName)
}
func saveToCache() {
guard let image = startingImage else { return }
infoMessage = manager.add(image: image, name: imageName)
}
func removeFromCache() {
infoMessage = manager.remove(name: imageName)
}
func getFromCache() {
if let returnedImage = manager.get(name: imageName) {
cachedImage = returnedImage
infoMessage = "Get image from Cache"
} else {
infoMessage = "Image not found in Cache"
}
cachedImage = manager.get(name: imageName)
}
}
// MARK: - VIEW
struct ChcheBootCamp: View {
// MARK: - PROPERTY
@StateObject var vm = CacheViewModel()
// MARK: - BODY
var body: some View {
NavigationView {
VStack (spacing: 20) {
// Image
if let image = vm.startingImage {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipped()
.cornerRadius(10)
}
// InfoMessage
Text(vm.infoMessage)
.font(.headline)
.foregroundColor(.purple)
// Buttons
HStack {
Button {
vm.saveToCache()
} label: {
Text("Save to Cache")
.font(.headline)
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
Button {
vm.removeFromCache()
} label: {
Text("Delete from Cache")
.font(.headline)
.foregroundColor(.white)
.padding()
.background(Color.red)
.cornerRadius(10)
}
} //: HSTACK
Button {
vm.getFromCache()
} label: {
Text("Getfrom Cache")
.font(.headline)
.foregroundColor(.white)
.padding()
.background(Color.green)
.cornerRadius(10)
}
// Load Cache Image
if let image = vm.cachedImage {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipped()
.cornerRadius(10)
}
Spacer()
} //: VSTACK
.navigationTitle("Cache Pratice")
} //: NAVIGATION
}
}
Practice example Download and Save images
This is mini app that download and and images in the local storage using FileMnager and NSCache and Also it is practice to build up background thread, weak self, Combine, Publishers / Subscribers
{JSON} Placeholder - Free fake API for testing and prototyping.
https://jsonplaceholder.typicode.com/photos
Step 1. Fetch Data from API
// in PhotoModel
import Foundation
struct PhotoModel: Identifiable, Codable {
let albumId: Int
let id: Int
let title: String
let url: String
let thumbnailUrl: String
}
// in PhotoModelDataService
import Foundation
import Combine
class PhotoModelDataService {
// MARK: - PROPERTY
static let instance = PhotoModelDataService() // Singleton
@Published var photoModel: [PhotoModel] = []
var cancellables = Set<AnyCancellable>()
// MARK: - INIT
private init() {
downloadData()
}
// MARK: - FUNCTION
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(handelOutPut)
.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?.photoModel = returnedPhotoModels
}
.store(in: &cancellables)
}
private func handelOutPut(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
}
}
// in DownloadingImagesViewModel
import Foundation
import Combine
class DownloadingImagesViewModel: ObservableObject {
// MARK: - PROPERTY
@Published var dataArray: [PhotoModel] = []
var cancellables = Set<AnyCancellable>()
let dataService = PhotoModelDataService.instance
// MARK: - INIT
init() {
addSbuscribers()
}
// MARK: - FUNCTION
func addSbuscribers() {
dataService.$photoModel
.sink { [weak self] returnedPhotoModels in
self?.dataArray = returnedPhotoModels
}
.store(in: &cancellables)
}
}
// in DownloadingImagesBootCamp
import SwiftUI
struct DownloadingImagesBootCamp: View {
// MARK: - PROPERTY
@StateObject var vm = DownloadingImagesViewModel()
// MARK: - BODY
var body: some View {
NavigationView {
List {
ForEach(vm.dataArray) {
Text($0.title)
}
} //: LIST
.listStyle(.plain)
.navigationTitle("Downloading Images!")
} //: NAVIGATION
}
}
Step 2. Download Image
// ImageLoadingViewModel
import Foundation
import SwiftUI
import Combine
class ImageLoadingViewModel: ObservableObject {
// MARK: - PROPERTY
@Published var image: UIImage? = nil
@Published var isLoading: Bool = false
var cancellables = Set<AnyCancellable>()
let urlString: String
// MARK: - INIT
init(url: String) {
urlString = url
downloadImage()
}
// MARK: - FUNCTION
func downloadImage() {
isLoading = true
guard let url = URL(string: urlString) else {
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
// .map { (data, response) -> UIImage? in
// return UIImage(data: data)
// }
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
}
.store(in: &cancellables)
}
}
// DownloadingImageView
import SwiftUI
struct DownloadingImageView: View {
// MARK: - PROPERTY
@StateObject var loader: ImageLoadingViewModel
// MARK: - INIT
init(url: String) { // init 이 먼저 실행 되게 하는 방법임
_loader = StateObject(wrappedValue: ImageLoadingViewModel(url: url))
}
// MARK: - BODY
var body: some View {
ZStack {
if loader.isLoading {
ProgressView()
} else if let image = loader.image {
Image(uiImage: image)
.resizable()
.clipShape(Circle())
}
} //: ZSTACK
}
}
// MARK: - PREVIEW
struct DownloadingImageView_Previews: PreviewProvider {
static var previews: some View {
DownloadingImageView(url: "https://via.placeholder.com/600/92c952")
.frame(width: 75, height: 75)
.previewLayout(.sizeThatFits)
}
}
// DownloadingImagesRow
import SwiftUI
struct DownloadingImagesRow: View {
// MARK: - PROPERTY
let model: PhotoModel
// MARK: - BODY
var body: some View {
HStack {
DownloadingImageView(url: model.url)
.frame(width: 75, height: 75)
VStack (alignment: .leading){
Text(model.title)
.font(.headline)
Text(model.url)
.foregroundColor(.gray)
.italic()
} //: VSTACK
.frame(maxWidth: .infinity, alignment: .leading)
} //: HSTACK
}
}
// MARK: - PREVIEW
struct DownloadingImagesRow_Previews: PreviewProvider {
static var previews: some View {
DownloadingImagesRow(model: PhotoModel(albumId: 1, id: 1, title: "Title", url: "https://via.placeholder.com/600/92c952", thumbnailUrl: "https://via.placeholder.com/600/92c952"))
.padding()
.previewLayout(.sizeThatFits)
}
}
// DownloadingImagesBootCamp
import SwiftUI
struct DownloadingImagesBootCamp: View {
// MARK: - PROPERTY
@StateObject var vm = DownloadingImagesViewModel()
// MARK: - BODY
var body: some View {
NavigationView {
List {
ForEach(vm.dataArray) { model in
DownloadingImagesRow(model: model)
} //: LOOP
} //: LIST
.listStyle(.plain)
.navigationTitle("Downloading Images!")
} //: NAVIGATION
}
}
Step 3. Store downloaded images using FileManger and NSCache
// in PhotoModelCacheManager
import Foundation
import SwiftUI
class PhotoModelCacheManager {
// MARK: - PROPERTY
static let instance = PhotoModelCacheManager() // Singleton
var phtoCache: NSCache<NSString, UIImage> = {
var cache = NSCache<NSString, UIImage>()
cache.countLimit = 200
cache.totalCostLimit = 1024 * 1024 * 200 // 200mb limit
return cache
}()
// MARK: - INIT
private init() {
}
// MARK: - FUNCTION
func add(key: String, value: UIImage) {
phtoCache.setObject(value, forKey: key as NSString)
}
func get(key: String) -> UIImage? {
return phtoCache.object(forKey: key as NSString)
}
}
// in PhotoModelFileManager
import Foundation
import SwiftUI
class PhotoModelFileManager {
// MARK: - PROPERTY
static let instance = PhotoModelFileManager()
let folderName = "downloaded_photos"
// MARK: - INIT
private init() {
createFolderIfNeeded()
}
// MARK: - FUNCTION
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, attributes: nil)
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/
// ... /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 manger: \(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)
}
}
// ImageLoadingViewModel
import Foundation
import SwiftUI
import Combine
class ImageLoadingViewModel: ObservableObject {
// MARK: - PROPERTY
@Published var image: UIImage? = nil
@Published var isLoading: Bool = false
var cancellables = Set<AnyCancellable>()
// let manager = PhotoModelCacheManager.instance
let manager = PhotoModelFileManager.instance
let urlString: String
let imageKey: String
// MARK: - INIT
init(url: String, key: String) {
urlString = url
imageKey = key
getImage()
}
// MARK: - FUNCTION
func getImage() {
if let savedImage = manager.get(key: imageKey) {
image = savedImage
print("Getting saved image!")
} else {
downloadImage()
print("Downloading image now!")
}
}
func downloadImage() {
print("Downloading image now!")
isLoading = true
guard let url = URL(string: urlString) else {
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
// .map { (data, response) -> UIImage? in
// return UIImage(data: data)
// }
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] returnedImage in
guard let self = self,
let image = returnedImage else { return }
self.image = image
self.manager.add(key: self.imageKey, value: image)
}
.store(in: &cancellables)
}
}
🗃 Reference
SwiftUI Thinking - https://www.youtube.com/watch?v=ymXRX6ZB-J0&list=PLwvDm4VfkdpiagxAXCT33Rkwnc5IVhTar&index=27
Leave a comment