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
}
}

image

image

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
}
}

image

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
}
}

스크린샷

image

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

Categories:

Updated:

Leave a comment