Implement image caching for the MapSnapshottingService.

This commit is contained in:
CypherPoet 2020-01-31 01:34:24 -06:00
parent d26ac2b866
commit e2f97420a3
6 changed files with 172 additions and 21 deletions

View File

@ -7,6 +7,7 @@
//
import Foundation
import UIKit
import CoreLocation
@ -15,5 +16,12 @@ extension Pad {
var coordinate: CLLocationCoordinate2D {
.init(latitude: latitude, longitude: longitude)
}
func snapshotCacheKey(from size: CGSize) -> String {
"Snapshot Key (\(size.width) X \(size.height))) :: " +
"Pad \(id) :: " +
"Latitude: \(latitude), Longitude: \(longitude)"
}
}

View File

@ -0,0 +1,23 @@
//
// Cache+Entry.swift
// PadFinder
//
// Created by CypherPoet on 1/30/20.
//
//
import Foundation
internal extension Cache {
/// A class (because NSCache entries need to be class instances)
/// to wrap the underlying value (class or not) that we want to cache.
final class Entry {
let value: Value
init(value: Value) {
self.value = value
}
}
}

View File

@ -0,0 +1,34 @@
//
// Cache+WrappedKey.swift
// PadFinder
//
// Created by CypherPoet on 1/30/20.
//
//
import Foundation
internal extension Cache {
/// A class that inherits from `NSObject` (because NSCache keys need to
/// do so) and wraps the underlying key (anything `Hashable`, class or not),
/// that we want to use with a cache entry.
final class WrappedKey: NSObject {
let key: Key
init(_ key: Key) {
self.key = key
}
override var hash: Int { key.hashValue }
override func isEqual(_ object: Any?) -> Bool {
guard let otherValue = object as? WrappedKey else { return false }
return otherValue.key == key
}
}
}

View File

@ -0,0 +1,59 @@
//
// Cache.swift
// PadFinder
//
// Created by CypherPoet on 1/30/20.
//
//
import Foundation
final class Cache<Key: Hashable, Value> {
private let wrappedCache = NSCache<WrappedKey, Entry>()
}
// MARK: - Public API Methods
extension Cache {
func insert(_ value: Value, forKey key: Key) {
let entry = Entry(value: value)
wrappedCache.setObject(entry, forKey: WrappedKey(key))
}
func value(forKey key: Key) -> Value? {
if let entry = wrappedCache.object(forKey: WrappedKey(key)) {
return entry.value
} else {
return nil
}
}
func removeValue(forKey key: Key) {
wrappedCache.removeObject(forKey: WrappedKey(key))
}
}
// MARK: - Subscript
extension Cache {
subscript(key: Key) -> Value? {
get { value(forKey: key) }
set {
guard let valueToCache = newValue else {
// If nil was assigned using our subscript,
// then we remove any value for that key:
removeValue(forKey: key)
return
}
insert(valueToCache, forKey: key)
}
}
}

View File

@ -13,12 +13,17 @@ import Combine
protocol MapSnapshotServicing: class {
typealias SnapshotCache = Cache<String, MKMapSnapshotter.Snapshot>
var snapshotOptions: MKMapSnapshotter.Options { get }
var queue: DispatchQueue { get }
var snapshotCache: SnapshotCache { get }
func takeSnapshot(
with size: CGSize,
at coordinate: CLLocationCoordinate2D,
cacheKey: String?,
size: CGSize,
coordinate: CLLocationCoordinate2D,
latitudeSpan: CLLocationDegrees,
longitudeSpan: CLLocationDegrees
) -> Future<MKMapSnapshotter.Snapshot, Error>
@ -28,37 +33,53 @@ protocol MapSnapshotServicing: class {
extension MapSnapshotServicing {
func takeSnapshot(
with size: CGSize,
at coordinate: CLLocationCoordinate2D,
cacheKey: String? = nil,
size: CGSize,
coordinate: CLLocationCoordinate2D,
latitudeSpan: CLLocationDegrees = 1.75,
longitudeSpan: CLLocationDegrees = 1.75
) -> Future<MKMapSnapshotter.Snapshot, Error> {
let span = MKCoordinateSpan(latitudeDelta: latitudeSpan, longitudeDelta: longitudeSpan)
snapshotOptions.region = MKCoordinateRegion(
center: coordinate,
span: span
)
snapshotOptions.size = size
let snapshotter = MKMapSnapshotter(options: snapshotOptions)
// TOOD: Ideally, we'd implement some kind of cahcing here, or save the images
// as part of each pad model -- which could be persisted in Core Data.
return Future { promise in
Future { promise in
// if
// let cacheKey = cacheKey,
// let cachedSnapshot = self.snapshotFromCache(key: cacheKey)
// {
// return promise(.success(cachedSnapshot))
// }
let span = MKCoordinateSpan(
latitudeDelta: latitudeSpan,
longitudeDelta: longitudeSpan
)
self.snapshotOptions.region = MKCoordinateRegion(
center: coordinate,
span: span
)
self.snapshotOptions.size = size
let snapshotter = MKMapSnapshotter(options: self.snapshotOptions)
snapshotter.start(with: self.queue) { (snapshot, error) in
guard error == nil else {
guard let snapshot = snapshot else {
return promise(.failure(error!))
}
guard let snapshot = snapshot else {
preconditionFailure("No snapshot returned despite snapshotter completing without error.")
if let cacheKey = cacheKey {
self.snapshotCache.insert(snapshot, forKey: cacheKey)
}
return promise(.success(snapshot))
}
}
}
func snapshotFromCache(key: String) -> MKMapSnapshotter.Snapshot? {
snapshotCache.value(forKey: key)
}
}

View File

@ -12,14 +12,20 @@ import MapKit
final class MapSnapshottingService: MapSnapshotServicing {
var snapshotOptions: MKMapSnapshotter.Options
var snapshotCache: MapSnapshotServicing.SnapshotCache
var queue: DispatchQueue
static var sharedSnapshotCache: MapSnapshotServicing.SnapshotCache = .init()
init(
snapshotOptions: MKMapSnapshotter.Options = .init(),
snapshotCache: MapSnapshotServicing.SnapshotCache = sharedSnapshotCache,
queue: DispatchQueue = DispatchQueue(label: "Map Snapshotting Service", qos: .default)
) {
self.snapshotOptions = snapshotOptions
self.snapshotCache = snapshotCache
self.queue = queue
}
}