summaryrefslogtreecommitdiff
path: root/Sora/Data/ImageCacheManager.swift
blob: 5561f241ddbd6890f05124e1072fb7a73de8cea2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
@preconcurrency import SwiftUI

@MainActor
final class ImageCacheManager {
  // MARK: - Singleton
  static let shared = ImageCacheManager()

  // MARK: - Private Properties
  private let cache = URLCache(
    memoryCapacity: 100 * 1_024 * 1_024,  // 100 MB
    diskCapacity: 500 * 1_024 * 1_024,  // 500 MB
    directory: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
      .appendingPathComponent("SoraImageCache", isDirectory: true)
  )
  private let downloadQueue = OperationQueue()
  private var preloadingURLs = Set<URL>()
  private var memoryWarningObserver: NSObjectProtocol?

  // MARK: - Initialisation
  private init() {
    downloadQueue.maxConcurrentOperationCount = 3
    downloadQueue.qualityOfService = .utility

    #if os(iOS)
      memoryWarningObserver = NotificationCenter.default.addObserver(
        forName: UIApplication.didReceiveMemoryWarningNotification,
        object: nil,
        queue: .main
      ) { [weak self] _ in
        Task { @MainActor in
          self?.handleMemoryPressure()
        }
      }
    #endif
  }

  // MARK: - Public Methods
  func preloadImages(_ urls: [URL]) {
    let newURLs = urls.filter { url in
      !preloadingURLs.contains(url) && cache.cachedResponse(for: URLRequest(url: url)) == nil
    }

    for url in newURLs {
      preloadingURLs.insert(url)

      downloadQueue.addOperation { [weak self] in
        guard let self else { return }

        let completionSignal = DispatchSemaphore(value: 0)

        URLSession.shared.dataTask(with: url) { data, response, _ in
          Task { @MainActor [weak self] in
            guard let self else {
              completionSignal.signal()
              return
            }

            defer {
              preloadingURLs.remove(url)
              completionSignal.signal()
            }

            guard let data, let response else { return }

            storeCachedResponse(
              data: data,
              response: response,
              for: url
            )
          }
        }
        .resume()

        completionSignal.wait()
      }
    }
  }

  func clearCache() {
    cache.removeAllCachedResponses()
    preloadingURLs.removeAll()
  }

  func storeCachedResponse(data: Data, response: URLResponse, for url: URL) {
    cache.storeCachedResponse(
      CachedURLResponse(response: response, data: data),
      for: URLRequest(url: url)
    )
  }

  func getCachedResponse(for url: URL) -> CachedURLResponse? {
    cache.cachedResponse(for: URLRequest(url: url))
  }

  func loadImageData(for url: URL) async -> Data? {
    if let cachedResponse = getCachedResponse(for: url) {
      return cachedResponse.data
    }

    do {
      let (data, response) = try await URLSession.shared.data(from: url)

      storeCachedResponse(
        data: data,
        response: response,
        for: url
      )

      return data
    } catch {
      return nil
    }
  }

  private func handleMemoryPressure() {
    cache.removeAllCachedResponses()
    downloadQueue.cancelAllOperations()
    preloadingURLs.removeAll()
  }

  deinit {
    if let observer = memoryWarningObserver {
      NotificationCenter.default.removeObserver(observer)
    }
  }
}