Published on

FileManager va NSCache yordamida rasmlarni yuklash va saqlash

Authors

Loyiha arxitekturasi

Bu video — oldingi bir necha videoda o'rgangan barcha narsalar: Combine, FileManager, NSCacheni haqiqiy ilovada birgalikda qo'llash.

Papka tuzilmasi (MVVM + Utilities)

DownloadingImages/
├── Models/
│   └── PhotoModel.swift
├── Views/
│   ├── DownloadingImagesBootcamp.swift   ← asosiy ekran
│   ├── DownloadingImagesRow.swift        ← har bir qator
│   └── DownloadingImageView.swift        ← bitta rasm
├── ViewModels/
│   ├── DownloadingImagesViewModel.swift  ← asosiy ekran uchun
│   └── ImageLoadingViewModel.swift       ← bitta rasm uchun
└── Utilities/
    ├── PhotoModelDataService.swiftJSON yuklash
    ├── PhotoModelCacheManager.swiftNSCache
    └── PhotoModelFileManager.swiftFileManager

Utilities papkasi — Model, View, ViewModel-ga tegishli bo'lmagan, lekin ilovaning istalgan joyidan foydalaniladigan yordamchi klasslar.


PhotoModel — ma'lumot modeli

API: https://jsonplaceholder.typicode.com/photos

struct PhotoModel: Identifiable, Codable {
    let albumId: Int
    let id: Int
    let title: String
    let url: String
    let thumbnailUrl: String
}
  • IdentifiableForEach-da id-siz ishlatish uchun
  • Codable — JSON-dan avtomatik decode qilish uchun

PhotoModelDataService — JSON yuklash (singleton)

import Combine
import Foundation

class PhotoModelDataService {
    static let instance = PhotoModelDataService()
    private init() {
        downloadData()
    }

    @Published var photoModels: [PhotoModel] = []
    var cancellables = Set<AnyCancellable>()

    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(handleOutput)
            .decode(type: [PhotoModel].self, decoder: JSONDecoder())
            .sink(
                receiveCompletion: { _ in },
                receiveValue: { [weak self] returnedModels in
                    self?.photoModels = returnedModels
                }
            )
            .store(in: &cancellables)
    }

    private func handleOutput(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
    }
}

Nima uchun alohida servis? — Agar bu yuklash mantig'ini DownloadingImagesViewModel-ga to'g'ridan-to'g'ri yozganda, faqat shu ViewModel-dan foydalanishimiz mumkin bo'lardi. Alohida singlton servis yaratilganda, ilovaning istalgan joyidan PhotoModelDataService.instancega murojaat qilib, bir xil yuklab olingan ma'lumotlardan foydalanish mumkin.


DownloadingImagesViewModel — asosiy ekran

import Combine
import Foundation

class DownloadingImagesViewModel: ObservableObject {
    @Published var dataArray: [PhotoModel] = []
    var cancellables = Set<AnyCancellable>()
    let dataService = PhotoModelDataService.instance

    init() {
        addSubscriber()
    }

    func addSubscriber() {
        dataService.$photoModels
            .sink { [weak self] returnedPhotoModels in
                self?.dataArray = returnedPhotoModels
            }
            .store(in: &cancellables)
    }
}

ViewModel dataService.$photoModels-ga subscriber qo'shadi — ma'lumot yuklanishi bilanoq dataArray yangilanadi.


DownloadingImagesBootcamp — asosiy ekran view

struct DownloadingImagesBootcamp: View {
    @StateObject var vm = DownloadingImagesViewModel()

    var body: some View {
        NavigationView {
            List {
                ForEach(vm.dataArray) { model in
                    DownloadingImagesRow(model: model)
                }
            }
            .navigationTitle("Downloading Images")
        }
    }
}

DownloadingImagesRow — har bir qator

struct DownloadingImagesRow: View {
    let model: PhotoModel

    var body: some View {
        HStack {
            DownloadingImageView(
                url: model.url,
                key: "\(model.id)"
            )
            .frame(width: 75, height: 75)

            VStack(alignment: .leading) {
                Text(model.title)
                    .font(.headline)
                Text(model.url)
                    .foregroundColor(.gray)
                    .italic()
            }
            .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
}

PhotoModelCacheManager — NSCache singleton

import SwiftUI

class PhotoModelCacheManager {
    static let instance = PhotoModelCacheManager()
    private init() { }

    var photoCache: NSCache<NSString, UIImage> = {
        let cache = NSCache<NSString, UIImage>()
        cache.countLimit = 200
        cache.totalCostLimit = 1024 * 1024 * 200  // ~200 MB
        return cache
    }()

    func add(key: String, value: UIImage) {
        photoCache.setObject(value, forKey: key as NSString)
    }

    func get(key: String) -> UIImage? {
        return photoCache.object(forKey: key as NSString)
    }
}

PhotoModelFileManager — FileManager singleton

import SwiftUI

class PhotoModelFileManager {
    static let instance = PhotoModelFileManager()
    private init() {
        createFolderIfNeeded()
    }

    let folderName = "downloaded_photos"

    private func getFolderPath() -> URL? {
        return FileManager
            .default
            .urls(for: .cachesDirectory, in: .userDomainMask)
            .first?
            .appendingPathComponent(folderName)
    }

    private func getImagePath(key: String) -> URL? {
        guard let folder = getFolderPath() else { return nil }
        return folder.appendingPathComponent("\(key).png")
    }

    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 {
                print("Error creating folder: \(error)")
            }
        }
    }

    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 {
            print("Error saving to FileManager: \(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 — bitta rasm uchun

Bu — har bir DownloadingImageView uchun alohida ViewModel. U avval cache-ni tekshiradi, yo'q bo'lsa internetdan yuklaydi:

import SwiftUI
import Combine

class ImageLoadingViewModel: ObservableObject {
    @Published var image: UIImage? = nil
    @Published var isLoading: Bool = false

    let urlString: String
    let imageKey: String
    let manager = PhotoModelCacheManager.instance
    // FileManager versiyasi uchun:
    // let manager = PhotoModelFileManager.instance

    var cancellables = Set<AnyCancellable>()

    init(url: String, key: String) {
        self.urlString = url
        self.imageKey = key
        getImage()
    }

    func getImage() {
        // 1. Avval cache-dan tekshir
        if let savedImage = manager.get(key: imageKey) {
            image = savedImage
            print("Getting saved image")
        } else {
            // 2. Cache-da yo'q — internetdan yukla
            downloadImage()
            print("Downloading image now")
        }
    }

    func downloadImage() {
        isLoading = true
        guard let url = URL(string: urlString) else {
            isLoading = false
            return
        }

        URLSession.shared
            .dataTaskPublisher(for: url)
            .map { UIImage(data: $0.data) }
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] _ in
                    self?.isLoading = false
                },
                receiveValue: { [weak self] returnedImage in
                    guard let self, let image = returnedImage else { return }
                    self.image = image
                    // Cache-ga saqla
                    self.manager.add(key: self.imageKey, value: image)
                    self.isLoading = false
                }
            )
            .store(in: &cancellables)
    }
}

DownloadingImageView — bitta rasm view

struct DownloadingImageView: View {
    @StateObject var loader: ImageLoadingViewModel
    let url: String
    let key: String

    init(url: String, key: String) {
        self.url = url
        self.key = key
        _loader = StateObject(
            wrappedValue: ImageLoadingViewModel(url: url, key: key)
        )
    }

    var body: some View {
        ZStack {
            if loader.isLoading {
                ProgressView()
            } else if let image = loader.image {
                Image(uiImage: image)
                    .resizable()
                    .clipShape(Circle())
            }
        }
    }
}

Muhim: @StateObject-ni init ichida _loader = sintaksisi bilan ishga tushirish kerak — bu URL-ni to'g'ri uzatishning yagona usuli.


NSCache vs FileManager — real farq

XususiyatNSCacheFileManager
Saqlash joyiRAM (xotira)Disk
Ilova yopilgandaO'chadiQoladi
Xotira sarfiYuqori (~300MB)Past (~48MB)
TezlikJuda tezBiroz sekinroq
Avtomatik tozalash✅ (limit oshsa)❌ (qo'lda)
Qurilmalar orasida✅ (iCloud bilan)

Qaysi biri qachon

  • NSCache: Joriy sessiya davomida qayta-qayta ko'riladigan rasmlar (masalan, scroll qilayotganda)
  • FileManager: Har safar ilova ochilganda kerak bo'lgan rasmlar (profil rasmi, asosiy ekran rasmlari)

Cache optimallashtirish — xotira kuzatuvi

Simulator-da Xcode-ning Memory ko'rsatkichini kuzating:

  • NSCache ishlatganda: ~300MB gacha ko'tarilishi mumkin → countLimit va totalCostLimit muhim
  • FileManager ishlatganda: ~48MB atrofida → disk-ga saqlanadi, RAM-ga ta'sir minimal

NSCache-ning ulug'vorligi: totalCostLimit oshsa, eski elementlarni avtomatik chiqarib tashlaydi — siz qo'lda hech narsa qilmasangiz ham.


Ma'lumot oqimi (to'liq arxitektura)

PhotoModelDataService (singleton)
    ↓  @Published photoModels
DownloadingImagesViewModel
    ↓  dataArray
DownloadingImagesBootcamp (View)
ForEach
DownloadingImagesRow (View)
    ↓  url + id
DownloadingImageView (View)
ImageLoadingViewModel
    ├── Cache-dan olish (PhotoModelCacheManager)
    └── Yuklab olish (URLSession + Combine)
        Cache-ga saqlash

Bu arxitektura reusable bo'lgani uchun, DownloadingImageView-ni PhotoModel-dan tashqari istalgan URL va ID bilan ishlatish mumkin — masalan, UserModel rasmlari uchun ham.

Buy mea coffee