Dangerfile Example/MapleBacon Example.xcodeproj/project.pbxproj Example/MapleBacon Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata Example/MapleBacon Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist Example/MapleBacon Example.xcodeproj/xcshareddata/xcschemes/MapleBacon Example.xcscheme Example/MapleBacon Example/AppDelegate.swift Example/MapleBacon Example/Assets.xcassets/AppIcon.appiconset/Contents.json Example/MapleBacon Example/Assets.xcassets/Contents.json Example/MapleBacon Example/Base.lproj/LaunchScreen.storyboard Example/MapleBacon Example/Base.lproj/Main.storyboard Example/MapleBacon Example/Controllers/CollectionViewController.swift Example/MapleBacon Example/Controllers/DownsamplingViewController.swift Example/MapleBacon Example/Controllers/EntryViewController.swift Example/MapleBacon Example/Controllers/ImageTransformerViewController.swift Example/MapleBacon Example/Controllers/PrefetchViewController.swift Example/MapleBacon Example/Helpers/ImageCollectionViewCell.swift Example/MapleBacon Example/Helpers/UICollectionView+MapleBacon.swift Example/MapleBacon Example/Info.plist Example/MapleBacon Example/SceneDelegate.swift Example/MapleBacon Example/images.plist Gemfile Gemfile.lock LICENSE MapleBacon.podspec MapleBacon.xcodeproj/project.pbxproj MapleBacon.xcodeproj/project.xcworkspace/contents.xcworkspacedata MapleBacon.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist MapleBacon.xcodeproj/xcshareddata/xcbaselines/F247A24623FF15410083DB03.xcbaseline/C1DF3A3C-F888-4084-9C7D-6D8EC38B7591.plist MapleBacon.xcodeproj/xcshareddata/xcbaselines/F247A24623FF15410083DB03.xcbaseline/Info.plist MapleBacon.xcodeproj/xcshareddata/xcschemes/MapleBacon.xcscheme MapleBacon/Core/Cache/Cache.swift MapleBacon/Core/Cache/CacheClearOptions.swift MapleBacon/Core/Cache/CacheResult.swift MapleBacon/Core/Cache/DiskCache.swift MapleBacon/Core/Cache/MemoryCache.swift MapleBacon/Core/DisplayOptions.swift MapleBacon/Core/Download.swift MapleBacon/Core/Downloader.swift MapleBacon/Core/ImageTransforming.swift MapleBacon/Core/MapleBacon.swift MapleBacon/Extensions/CGSize+MapleBacon.swift MapleBacon/Extensions/Data+MapleBacon.swift MapleBacon/Extensions/DataConvertible.swift MapleBacon/Extensions/DispatchQueue+MapleBacon.swift MapleBacon/Extensions/DownsamplingImageTransformer.swift MapleBacon/Extensions/FileManager.swift MapleBacon/Extensions/TimePeriod.swift MapleBacon/Extensions/UIImageView+MapleBacon.swift MapleBacon/Info.plist MapleBacon/MapleBacon.h MapleBaconTests/CacheTests.swift MapleBaconTests/DiskCacheTests.swift MapleBaconTests/DownloaderTests.swift MapleBaconTests/ImageTransformingTests.swift MapleBaconTests/Info.plist MapleBaconTests/MapleBaconTests.swift MapleBaconTests/MemoryCacheTests.swift MapleBaconTests/MockURLSessionConfiguration.swift MapleBaconTests/TestHelpers.swift Package.swift fastlane/Fastfile <<<<<< network # path=./MapleBaconTests.xctest.coverage.txt /Users/runner/work/MapleBacon/MapleBacon/MapleBaconTests/CacheTests.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |@testable import MapleBacon 6| |import XCTest 7| | 8| |final class CacheTests: XCTestCase { 9| | 10| | private static let cacheName = "CacheTests" 11| | 12| 6| private let cache = Cache(name: CacheTests.cacheName) 13| | 14| 6| override func tearDownWithError() throws { 15| 6| cache.clear(.all) 16| 6| } 17| | 18| 1| func testStorage() { 19| 1| let expectation = self.expectation(description: #function) 20| 1| 21| 1| let data = dummyData() 22| 1| 23| 1| cache.store(value: data, forKey: #function) { error in 24| 1| XCTAssertNil(error) 25| 1| expectation.fulfill() 26| 1| } 27| 1| 28| 1| waitForExpectations(timeout: 5, handler: nil) 29| 1| } 30| | 31| 1| func testRetrieval() { 32| 1| let expectation = self.expectation(description: #function) 33| 1| 34| 1| let data = dummyData() 35| 1| 36| 1| cache.store(value: data, forKey: #function) { _ in 37| 1| self.cache.value(forKey: #function) { result in 38| 1| switch result { 39| 1| case .success(let cacheResult): 40| 1| XCTAssertEqual(cacheResult.value, data) 41| 1| XCTAssertEqual(cacheResult.type, .memory) 42| 1| case .failure: 43| 0| XCTFail() 44| 1| } 45| 1| expectation.fulfill() 46| 1| } 47| 1| } 48| 1| 49| 1| waitForExpectations(timeout: 5, handler: nil) 50| 1| } 51| | 52| 1| func testClearAll() { 53| 1| let expectation = self.expectation(description: #function) 54| 1| 55| 1| let data = dummyData() 56| 1| 57| 1| cache.store(value: data, forKey: "test") { _ in 58| 1| self.cache.clear(.all) 59| 1| 60| 1| self.cache.value(forKey: "test") { result in 61| 1| switch result { 62| 1| case .success: 63| 0| XCTFail() 64| 1| case .failure(let error): 65| 1| XCTAssertNotNil(error) 66| 1| } 67| 1| expectation.fulfill() 68| 1| } 69| 1| } 70| 1| 71| 1| waitForExpectations(timeout: 5, handler: nil) 72| 1| } 73| | 74| 1| func testClearMemory() { 75| 1| let expectation = self.expectation(description: #function) 76| 1| 77| 1| let data = dummyData() 78| 1| 79| 1| cache.store(value: data, forKey: #function) { _ in 80| 1| self.cache.clear(.memory) 81| 1| 82| 1| self.cache.value(forKey: #function) { result in 83| 1| switch result { 84| 1| case .success(let cacheResult): 85| 1| XCTAssertEqual(cacheResult.value, data) 86| 1| XCTAssertEqual(cacheResult.type, .disk) 87| 1| case .failure: 88| 0| XCTFail() 89| 1| } 90| 1| expectation.fulfill() 91| 1| } 92| 1| } 93| 1| 94| 1| waitForExpectations(timeout: 5, handler: nil) 95| 1| } 96| | 97| 1| func testMemoryPromotion() { 98| 1| let expectation = self.expectation(description: #function) 99| 1| 100| 1| let data = dummyData() 101| 1| 102| 1| cache.store(value: data, forKey: #function) { _ in 103| 1| self.cache.clear(.memory) 104| 1| 105| 1| self.cache.value(forKey: #function) { _ in 106| 1| self.cache.value(forKey: #function) { result in 107| 1| switch result { 108| 1| case .success(let cacheResult): 109| 1| XCTAssertEqual(cacheResult.value, data) 110| 1| // Assert that upon second time access the data has been promoted into memory 111| 1| XCTAssertEqual(cacheResult.type, .memory) 112| 1| case .failure: 113| 0| XCTFail() 114| 1| } 115| 1| } 116| 1| expectation.fulfill() 117| 1| } 118| 1| } 119| 1| 120| 1| waitForExpectations(timeout: 5, handler: nil) 121| 1| } 122| | 123| 1| func testIsCached() { 124| 1| let expectation = self.expectation(description: #function) 125| 1| 126| 1| let data = dummyData() 127| 1| 128| 1| cache.store(value: data, forKey: #function) { _ in 129| 1| XCTAssertTrue(try! self.cache.isCached(forKey: #function)) 130| 1| 131| 1| self.cache.clear(.memory) { _ in 132| 1| XCTAssertTrue(try! self.cache.isCached(forKey: #function)) 133| 1| expectation.fulfill() 134| 1| } 135| 1| } 136| 1| 137| 1| waitForExpectations(timeout: 5, handler: nil) 138| 1| } 139| | 140| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBaconTests/DiskCacheTests.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |@testable import MapleBacon 6| |import XCTest 7| | 8| |final class DiskCacheTests: XCTestCase { 9| | 10| 6| private let cache = DiskCache(name: "DiskCacheTests") 11| | 12| 6| override func tearDownWithError() throws { 13| 6| cache.clear() 14| 6| } 15| | 16| 1| func testWrite() { 17| 1| let expectation = self.expectation(description: #function) 18| 1| 19| 1| cache.insert(dummyData(), forKey: "test") { error in 20| 1| XCTAssertNil(error) 21| 1| expectation.fulfill() 22| 1| } 23| 1| 24| 1| waitForExpectations(timeout: 5, handler: nil) 25| 1| } 26| | 27| 1| func testReadWrite() { 28| 1| let expectation = self.expectation(description: #function) 29| 1| let key = "test" 30| 1| let data = dummyData() 31| 1| 32| 1| cache.insert(data, forKey: key) { _ in 33| 1| self.cache.value(forKey: key) { result in 34| 1| switch result { 35| 1| case .success(let cacheData): 36| 1| XCTAssertEqual(cacheData, data) 37| 1| case .failure: 38| 0| XCTFail() 39| 1| } 40| 1| expectation.fulfill() 41| 1| } 42| 1| } 43| 1| 44| 1| waitForExpectations(timeout: 5, handler: nil) 45| 1| } 46| | 47| 1| func testReadInvalid() { 48| 1| let expectation = self.expectation(description: #function) 49| 1| 50| 1| cache.value(forKey: #function) { result in 51| 1| switch result { 52| 1| case .success: 53| 0| XCTFail() 54| 1| case .failure(let error): 55| 1| XCTAssertNotNil(error) 56| 1| } 57| 1| expectation.fulfill() 58| 1| } 59| 1| 60| 1| waitForExpectations(timeout: 5, handler: nil) 61| 1| } 62| | 63| 1| func testClear() { 64| 1| let expectation = self.expectation(description: #function) 65| 1| 66| 1| cache.clear { error in 67| 1| XCTAssertNil(error) 68| 1| expectation.fulfill() 69| 1| } 70| 1| 71| 1| waitForExpectations(timeout: 5, handler: nil) 72| 1| } 73| | 74| 1| func testClearExpired() { 75| 1| let expectation = self.expectation(description: #function) 76| 1| cache.maxCacheAgeSeconds = 0.seconds 77| 1| 78| 1| cache.insert(dummyData(), forKey: "test") { _ in 79| 1| // Tests that setting maxCacheAgeSeconds does work 80| 1| let expired = try! self.cache.expiredFileURLs() 81| 1| XCTAssertFalse(expired.isEmpty) 82| 1| 83| 1| self.cache.clearExpired { error in 84| 1| XCTAssertNil(error) 85| 1| 86| 1| // After clearing expired files, there should be no further expired URLs 87| 1| let expired = try! self.cache.expiredFileURLs() 88| 1| XCTAssertTrue(expired.isEmpty) 89| 1| 90| 1| expectation.fulfill() 91| 1| } 92| 1| } 93| 1| 94| 1| waitForExpectations(timeout: 5, handler: nil) 95| 1| } 96| | 97| 1| func testIsCached() { 98| 1| let expectation = self.expectation(description: #function) 99| 1| 100| 1| cache.insert(dummyData(), forKey: "test") { _ in 101| 1| XCTAssertTrue(try! self.cache.isCached(forKey: "test")) 102| 1| expectation.fulfill() 103| 1| } 104| 1| 105| 1| waitForExpectations(timeout: 5, handler: nil) 106| 1| } 107| | 108| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBaconTests/DownloaderTests.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |@testable import MapleBacon 6| |import XCTest 7| | 8| |final class DownloaderTests: XCTestCase { 9| | 10| | private static let url = URL(string: "https://example.com/mapleBacon.png")! 11| | 12| 1| func testDownload() { 13| 1| let expectation = self.expectation(description: #function) 14| 1| let downloader = Downloader(sessionConfiguration: .dummyDataProviding) 15| 1| 16| 1| _ = downloader.fetch(Self.url) { response in 17| 1| switch response { 18| 1| case .success(let data): 19| 1| XCTAssertNotNil(data) 20| 1| case .failure: 21| 0| XCTFail() 22| 1| } 23| 1| expectation.fulfill() 24| 1| } 25| 1| 26| 1| waitForExpectations(timeout: 5, handler: nil) 27| 1| } 28| | 29| 1| func testInvalidData() { 30| 1| let expectation = self.expectation(description: #function) 31| 1| let downloader = Downloader(sessionConfiguration: .dummyDataProviding) 32| 1| 33| 1| _ = downloader.fetch(Self.url) { response in 34| 1| switch response { 35| 1| case .success: 36| 0| XCTFail() 37| 1| case .failure(let error): 38| 1| XCTAssertNotNil(error) 39| 1| } 40| 1| expectation.fulfill() 41| 1| } 42| 1| 43| 1| waitForExpectations(timeout: 5, handler: nil) 44| 1| } 45| | 46| 1| func testFailure() { 47| 1| let expectation = self.expectation(description: #function) 48| 1| let downloader = Downloader(sessionConfiguration: .failed) 49| 1| 50| 1| _ = downloader.fetch(Self.url) { response in 51| 1| switch response { 52| 1| case .success: 53| 0| XCTFail() 54| 1| case .failure(let error): 55| 1| XCTAssertNotNil(error) 56| 1| } 57| 1| expectation.fulfill() 58| 1| } 59| 1| 60| 1| waitForExpectations(timeout: 5, handler: nil) 61| 1| } 62| | 63| 1| func testConcurrentDownloads() { 64| 1| let downloader = Downloader(sessionConfiguration: .dummyDataProviding) 65| 1| 66| 1| let firstExpectation = expectation(description: "first") 67| 1| _ = downloader.fetch(Self.url) { response in 68| 1| switch response { 69| 1| case .success(let data): 70| 1| XCTAssertNotNil(data) 71| 1| case .failure: 72| 0| XCTFail() 73| 1| } 74| 1| firstExpectation.fulfill() 75| 1| } 76| 1| 77| 1| let secondExpectation = expectation(description: "second") 78| 1| _ = downloader.fetch(Self.url) { response in 79| 1| switch response { 80| 1| case .success(let data): 81| 1| XCTAssertNotNil(data) 82| 1| case .failure: 83| 0| XCTFail() 84| 1| } 85| 1| secondExpectation.fulfill() 86| 1| } 87| 1| 88| 1| waitForExpectations(timeout: 5, handler: nil) 89| 1| } 90| | 91| 1| func testCancel() { 92| 1| let expectation = self.expectation(description: #function) 93| 1| let downloader = Downloader(sessionConfiguration: .failed) 94| 1| 95| 1| let downloadTask = downloader.fetch(Self.url) { response in 96| 1| switch response { 97| 1| case .failure(let error as DownloaderError): 98| 1| XCTAssertEqual(error, .canceled) 99| 1| case .success, .failure: 100| 0| XCTFail() 101| 1| } 102| 1| expectation.fulfill() 103| 1| } 104| 1| 105| 1| XCTAssertNotNil(downloadTask) 106| 1| downloadTask.cancel() 107| 1| 108| 1| waitForExpectations(timeout: 5, handler: nil) 109| 1| } 110| | 111| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBaconTests/ImageTransformingTests.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import MapleBacon 6| |import XCTest 7| | 8| |final class ImageTransformingTests: XCTestCase { 9| | 10| 1| func testComposition() throws { 11| 1| let first = FirstDummyTransformer() 12| 1| let second = SecondDummyTransformer() 13| 1| let third = ThirdDummyTransformer() 14| 1| 15| 1| let composed = first.appending(transformer: second).appending(transformer: third) 16| 1| 17| 1| XCTAssertTrue(composed.identifier.hasPrefix(first.identifier)) 18| 1| XCTAssertTrue(composed.identifier.contains(second.identifier)) 19| 1| XCTAssertTrue(composed.identifier.hasSuffix(third.identifier)) 20| 1| } 21| | 22| 1| func testOperatorComposition() { 23| 1| let first = FirstDummyTransformer() 24| 1| let second = SecondDummyTransformer() 25| 1| let third = ThirdDummyTransformer() 26| 1| 27| 1| let composed = first >>> second >>> third 28| 1| 29| 1| XCTAssertTrue(composed.identifier.hasPrefix(first.identifier)) 30| 1| XCTAssertTrue(composed.identifier.contains(second.identifier)) 31| 1| XCTAssertTrue(composed.identifier.hasSuffix(third.identifier)) 32| 1| } 33| | 34| 1| func testTransfomerCalling() { 35| 1| let first = FirstDummyTransformer() 36| 1| let second = SecondDummyTransformer() 37| 1| let third = ThirdDummyTransformer() 38| 1| 39| 1| let composed = first.appending(transformer: second).appending(transformer: third) 40| 1| _ = composed.transform(image: makeImage()) 41| 1| 42| 1| XCTAssertEqual(first.callCount, 1) 43| 1| XCTAssertEqual(second.callCount, 1) 44| 1| XCTAssertEqual(third.callCount, 1) 45| 1| } 46| | 47| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBaconTests/MapleBaconTests.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |#if canImport(Combine) 6| |import Combine 7| |#endif 8| |@testable import MapleBacon 9| |import XCTest 10| | 11| |final class MapleBaconTests: XCTestCase { 12| | 13| | private static let url = URL(string: "https://example.com/mapleBacon.png")! 14| | 15| 5| private let cache = Cache(name: "MapleBaconTests") 16| | 17| | @available(iOS 13.0, *) 18| | private lazy var subscriptions: Set = [] 19| | 20| 1| func testIntegration() { 21| 1| let expectation = self.expectation(description: #function) 22| 1| let mapleBacon = MapleBacon(cache: cache, sessionConfiguration: .imageDataProviding) 23| 1| 24| 1| let token = mapleBacon.image(with: Self.url) { result in 25| 1| switch result { 26| 1| case .success(let image): 27| 1| XCTAssertEqual(image.pngData(), makeImageData()) 28| 1| case .failure: 29| 0| XCTFail() 30| 1| } 31| 1| mapleBacon.clearCache(.all) { _ in 32| 1| expectation.fulfill() 33| 1| } 34| 1| } 35| 1| 36| 1| XCTAssertNotNil(token) 37| 1| waitForExpectations(timeout: 5, handler: nil) 38| 1| } 39| | 40| 1| func testError() { 41| 1| let expectation = self.expectation(description: #function) 42| 1| let mapleBacon = MapleBacon(cache: cache, sessionConfiguration: .failed) 43| 1| 44| 1| mapleBacon.image(with: Self.url) { result in 45| 1| switch result { 46| 1| case .success: 47| 0| XCTFail() 48| 1| case .failure(let error): 49| 1| XCTAssertNotNil(error) 50| 1| } 51| 1| mapleBacon.clearCache(.all) { _ in 52| 1| expectation.fulfill() 53| 1| } 54| 1| } 55| 1| 56| 1| waitForExpectations(timeout: 5, handler: nil) 57| 1| } 58| | 59| 1| func testTransformer() { 60| 1| let expectation = self.expectation(description: #function) 61| 1| let transformer = FirstDummyTransformer() 62| 1| let mapleBacon = MapleBacon(cache: cache, sessionConfiguration: .imageDataProviding) 63| 1| 64| 1| mapleBacon.image(with: Self.url, imageTransformer: transformer) { result in 65| 1| switch result { 66| 1| case .success(let image): 67| 1| XCTAssertEqual(image.pngData(), makeImageData()) 68| 1| XCTAssertEqual(transformer.callCount, 1) 69| 1| case .failure: 70| 0| XCTFail() 71| 1| } 72| 1| mapleBacon.clearCache(.all) { _ in 73| 1| expectation.fulfill() 74| 1| } 75| 1| } 76| 1| 77| 1| waitForExpectations(timeout: 5, handler: nil) 78| 1| } 79| | 80| 1| func testCancel() { 81| 1| let expectation = self.expectation(description: #function) 82| 1| let mapleBacon = MapleBacon(cache: cache, sessionConfiguration: .failed) 83| 1| 84| 1| let downloadTask = mapleBacon.image(with: Self.url) { result in 85| 1| switch result { 86| 1| case .failure(let error as DownloaderError): 87| 1| XCTAssertEqual(error, .canceled) 88| 1| case .success, .failure: 89| 0| XCTFail() 90| 1| } 91| 1| mapleBacon.clearCache(.all) { _ in 92| 1| expectation.fulfill() 93| 1| } 94| 1| } 95| 1| 96| 1| XCTAssertNotNil(downloadTask) 97| 1| downloadTask?.cancel() 98| 1| 99| 1| waitForExpectations(timeout: 5, handler: nil) 100| 1| } 101| | 102| |} 103| | 104| |#if canImport(Combine) 105| | 106| |@available(iOS 13.0, *) 107| |extension MapleBaconTests { 108| | 109| 1| func testIntegrationPublisher() { 110| 1| let expectation = self.expectation(description: #function) 111| 1| let mapleBacon = MapleBacon(cache: cache, sessionConfiguration: .imageDataProviding) 112| 1| 113| 1| mapleBacon.image(with: Self.url) 114| 1| .sink(receiveCompletion: { _ in 115| 1| mapleBacon.clearCache(.all) { _ in 116| 1| expectation.fulfill() 117| 1| } 118| 1| }, receiveValue: { image in 119| 1| XCTAssertEqual(image.pngData(), makeImageData()) 120| 1| }) 121| 1| .store(in: &self.subscriptions) 122| 1| 123| 1| waitForExpectations(timeout: 5, handler: nil) 124| 1| } 125| | 126| |} 127| | 128| |#endif /Users/runner/work/MapleBacon/MapleBacon/MapleBaconTests/MemoryCacheTests.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |@testable import MapleBacon 6| |import XCTest 7| | 8| |final class MemoryCacheTests: XCTestCase { 9| | 10| 1| func testStorage() { 11| 1| let cache = MemoryCache() 12| 1| 13| 1| cache["foo"] = "bar" 14| 1| cache["baz"] = "bat" 15| 1| 16| 1| XCTAssertEqual(cache["foo"], "bar") 17| 1| XCTAssertEqual(cache["baz"], "bat") 18| 1| XCTAssertNil(cache["nothing"]) 19| 1| } 20| | 21| 1| func testRemoval() { 22| 1| let cache = MemoryCache() 23| 1| 24| 1| cache["foo"] = "bar" 25| 1| cache["foo"] = nil 26| 1| 27| 1| XCTAssertNil(cache["foo"]) 28| 1| } 29| | 30| 1| func testNamedCaches() { 31| 1| let defaultCache = MemoryCache() 32| 1| defaultCache["foo"] = "bar" 33| 1| 34| 1| let bazCache = MemoryCache(name: "baz") 35| 1| bazCache["foo"] = "baz" 36| 1| 37| 1| XCTAssertNotEqual(defaultCache["foo"], bazCache["foo"]) 38| 1| } 39| | 40| 1| func testClear() { 41| 1| let cache = MemoryCache() 42| 1| cache["foo"] = "bar" 43| 1| 44| 1| cache.clear() 45| 1| 46| 1| XCTAssertNil(cache["foo"]) 47| 1| } 48| | 49| 1| func testIsCached() { 50| 1| let cache = MemoryCache() 51| 1| cache["foo"] = "bar" 52| 1| 53| 1| XCTAssertTrue(cache.isCached(forKey: "foo")) 54| 1| XCTAssertFalse(cache.isCached(forKey: "bar")) 55| 1| } 56| | 57| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBaconTests/MockURLSessionConfiguration.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import Foundation 6| | 7| |extension URLSessionConfiguration { 8| 4| static var failed: URLSessionConfiguration { 9| 4| final class MockURLProtocol: URLProtocol { 10| 4| override class func canInit(with request: URLRequest) -> Bool { 11| 4| true 12| 4| } 13| 4| 14| 4| override class func canonicalRequest(for request: URLRequest) -> URLRequest { 15| 4| request 16| 4| } 17| 4| 18| 4| override func startLoading() { 19| 3| let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil) 20| 3| client?.urlProtocol(self, didFailWithError: error) 21| 3| } 22| 4| 23| 4| override func stopLoading() {} 24| 4| } 25| 4| 26| 4| let configuration: URLSessionConfiguration = .ephemeral 27| 4| configuration.protocolClasses = [MockURLProtocol.self] 28| 4| return configuration 29| 4| } 30| | 31| 3| static var dummyDataProviding: URLSessionConfiguration { 32| 3| final class MockURLProtocol: URLProtocol { 33| 3| override class func canInit(with request: URLRequest) -> Bool { 34| 3| true 35| 3| } 36| 3| 37| 3| override class func canonicalRequest(for request: URLRequest) -> URLRequest { 38| 3| request 39| 3| } 40| 3| 41| 3| override func startLoading() { 42| 3| client?.urlProtocol(self, didReceive: HTTPURLResponse(), cacheStoragePolicy: .notAllowed) 43| 3| client?.urlProtocol(self, didLoad: dummyData()) 44| 3| client?.urlProtocolDidFinishLoading(self) 45| 3| } 46| 3| 47| 3| override func stopLoading() {} 48| 3| } 49| 3| 50| 3| let configuration: URLSessionConfiguration = .ephemeral 51| 3| configuration.protocolClasses = [MockURLProtocol.self] 52| 3| return configuration 53| 3| } 54| | 55| 3| static var imageDataProviding: URLSessionConfiguration { 56| 3| final class MockURLProtocol: URLProtocol { 57| 3| override class func canInit(with request: URLRequest) -> Bool { 58| 3| true 59| 3| } 60| 3| 61| 3| override class func canonicalRequest(for request: URLRequest) -> URLRequest { 62| 3| request 63| 3| } 64| 3| 65| 3| override func startLoading() { 66| 3| client?.urlProtocol(self, didReceive: HTTPURLResponse(), cacheStoragePolicy: .notAllowed) 67| 3| client?.urlProtocol(self, didLoad: makeImage().pngData()!) 68| 3| client?.urlProtocolDidFinishLoading(self) 69| 3| } 70| 3| 71| 3| override func stopLoading() {} 72| 3| } 73| 3| 74| 3| let configuration: URLSessionConfiguration = .ephemeral 75| 3| configuration.protocolClasses = [MockURLProtocol.self] 76| 3| return configuration 77| 3| } 78| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBaconTests/TestHelpers.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| |import MapleBacon 7| | 8| 13|func dummyData() -> Data { 9| 13| let string = #function + #file 10| 13| return Data(string.utf8) 11| 13|} 12| | 13| 7|func makeImage() -> UIImage { 14| 7| let renderer = UIGraphicsImageRenderer(size: .init(width: 10, height: 10)) 15| 7| return renderer.image { context in 16| 7| UIColor.black.setFill() 17| 7| context.fill(renderer.format.bounds) 18| 7| } 19| 7|} 20| | 21| 3|func makeImageData() -> Data { 22| 3| makeImage().pngData()! 23| 3|} 24| | 25| |final class FirstDummyTransformer: ImageTransforming { 26| | 27| | let identifier = "com.schnaub.FirstDummyTransformer" 28| | 29| | var callCount = 0 30| | 31| 2| func transform(image: UIImage) -> UIImage? { 32| 2| callCount += 1 33| 2| return image 34| 2| } 35| | 36| |} 37| | 38| |final class SecondDummyTransformer: ImageTransforming { 39| | 40| | let identifier = "com.schnaub.SecondDummyTransformer" 41| | 42| | var callCount = 0 43| | 44| 1| func transform(image: UIImage) -> UIImage? { 45| 1| callCount += 1 46| 1| return image 47| 1| } 48| | 49| |} 50| | 51| |final class ThirdDummyTransformer: ImageTransforming { 52| | 53| | let identifier = "com.schnaub.ThirdDummyTransformer" 54| | 55| | var callCount = 0 56| | 57| 1| func transform(image: UIImage) -> UIImage? { 58| 1| callCount += 1 59| 1| return image 60| 1| } 61| | 62| |} <<<<<< EOF # path=./MapleBacon.framework.coverage.txt /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Core/Cache/Cache.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| | 7| |enum CacheError: Error { 8| | case dataConversion 9| |} 10| | 11| |final class Cache where T.Result == T { 12| | 13| | typealias CacheCompletion = (Result, Error>) -> Void 14| | 15| | var maxCacheAgeSeconds: TimeInterval { 16| 0| get { 17| 0| diskCache.maxCacheAgeSeconds 18| 0| } 19| 0| set { 20| 0| diskCache.maxCacheAgeSeconds = newValue 21| 0| } 22| | } 23| | 24| | private let memoryCache: MemoryCache 25| | private let diskCache: DiskCache 26| | 27| 11| init(name: String) { 28| 11| self.memoryCache = MemoryCache(name: name) 29| 11| self.diskCache = DiskCache(name: name) 30| 11| 31| 11| let notifications = [UIApplication.willTerminateNotification, UIApplication.didEnterBackgroundNotification] 32| 22| notifications.forEach { notification in 33| 22| NotificationCenter.default.addObserver(self, selector: #selector(cleanDiskOnNotification), name: notification, object: nil) 34| 22| } 35| 11| } 36| | 37| 9| func store(value: T, forKey key: String, completion: ((Error?) -> Void)? = nil) { 38| 9| let safeKey = safeCacheKey(key) 39| 9| memoryCache[safeKey] = value.toData() 40| 9| diskCache.insert(value.toData(), forKey: safeKey, completion: completion) 41| 9| } 42| | 43| 5| func value(forKey key: String, completion: CacheCompletion? = nil) { 44| 5| let safeKey = safeCacheKey(key) 45| 5| 46| 5| if let value = memoryCache[safeKey] { 47| 2| completion?(convertToTargetType(value, type: .memory)) 48| 5| } else { 49| 3| diskCache.value(forKey: safeKey) { [weak self] result in 50| 3| guard let self = self else { 51| 0| return 52| 3| } 53| 3| 54| 3| switch result { 55| 3| case .success(let data): 56| 2| // Promote to in-memory cache for faster access the next time 57| 2| self.memoryCache[safeKey] = data 58| 2| 59| 2| completion?(self.convertToTargetType(data, type: .disk)) 60| 3| case .failure(let error): 61| 1| completion?(.failure(error)) 62| 3| } 63| 3| } 64| 5| } 65| 5| } 66| | 67| 15| func clear(_ options: CacheClearOptions, completion: ((Error?) -> Void)? = nil) { 68| 15| if options.contains(.memory) { 69| 15| memoryCache.clear() 70| 15| if !options.contains(.disk) { 71| 3| completion?(nil) 72| 15| } 73| 15| } 74| 15| if options.contains(.disk) { 75| 12| diskCache.clear(completion) 76| 15| } 77| 15| } 78| | 79| 7| func isCached(forKey key: String) throws -> Bool { 80| 7| let safeKey = safeCacheKey(key) 81| 7| if memoryCache.isCached(forKey: safeKey) { 82| 1| return true 83| 6| } 84| 6| return try diskCache.isCached(forKey: safeKey) 85| 7| } 86| | 87| 0| @objc private func cleanDiskOnNotification() { 88| 0| clear(.disk) 89| 0| } 90| | 91| |} 92| | 93| |private extension Cache { 94| | 95| 4| func convertToTargetType(_ data: Data, type: CacheType) -> Result, Error> { 96| 4| guard let targetType = T.convert(from: data) else { 97| 0| return .failure(CacheError.dataConversion) 98| 4| } 99| 4| return .success(.init(value: targetType, type: type)) 100| 4| } 101| | 102| 21| func safeCacheKey(_ key: String) -> String { 103| 21| #if canImport(CryptoKit) 104| 21| if #available(iOS 13.0, *) { 105| 21| return cryptoSafeCacheKey(key) 106| 21| } 107| 0| #endif 108| 0| return key.components(separatedBy: CharacterSet(charactersIn: "()/")).joined(separator: "-") 109| 21| } 110| | 111| |} 112| | 113| |#if canImport(CryptoKit) 114| |import CryptoKit 115| | 116| |@available(iOS 13.0, *) 117| |private extension Cache { 118| | 119| 21| func cryptoSafeCacheKey(_ key: String) -> String { 120| 21| let hash = Insecure.MD5.hash(data: Data(key.utf8)) 121| 336| return hash.compactMap { String.init(format: "%02x", $0) }.joined() 122| 21| } 123| | 124| |} 125| |#endif /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Core/Cache/CacheClearOptions.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import Foundation 6| | 7| |public struct CacheClearOptions: OptionSet { 8| | public let rawValue: Int 9| | 10| 99| public init(rawValue: Int) { 11| 99| self.rawValue = rawValue 12| 99| } 13| | 14| | public static let memory = CacheClearOptions(rawValue: 1 << 0) 15| | public static let disk = CacheClearOptions(rawValue: 1 << 1) 16| | 17| | public static let all: CacheClearOptions = [.memory, .disk] 18| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Core/Cache/DiskCache.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import Foundation 6| | 7| |final class DiskCache { 8| | 9| | private static let domain = "com.schnaub.DiskCache" 10| | 11| 17| var maxCacheAgeSeconds: TimeInterval = 7.days 12| | 13| | private let diskQueue: DispatchQueue 14| | private let cacheName: String 15| | 16| 17| init(name: String) { 17| 17| let queueLabel = "\(Self.domain).\(name)" 18| 17| self.diskQueue = DispatchQueue(label: queueLabel) 19| 17| self.cacheName = "\(Self.domain).\(name)" 20| 17| } 21| | 22| 13| func insert(_ data: Data, forKey key: String, completion: ((Error?) -> Void)? = nil) { 23| 13| diskQueue.async { 24| 13| var diskError: Error? 25| 13| defer { 26| 13| completion?(diskError) 27| 13| } 28| 13| do { 29| 13| try self.store(data: data, key: key) 30| 13| } catch { 31| 0| diskError = error 32| 13| } 33| 13| } 34| 13| } 35| | 36| 5| func value(forKey key: String, completion: ((Result) -> Void)? = nil) { 37| 5| diskQueue.async { 38| 5| var diskError: Error? 39| 5| defer { 40| 5| if let error = diskError { 41| 5| completion?(.failure(error)) 42| 5| } 43| 5| } 44| 5| do { 45| 5| let url = try self.cacheDirectory().appendingPathComponent(key) 46| 5| let data = try FileManager.default.fileContents(at: url) 47| 5| completion?(.success(data)) 48| 5| } catch { 49| 2| diskError = error 50| 5| } 51| 5| } 52| 5| } 53| | 54| 19| func clear(_ completion: ((Error?) -> Void)? = nil) { 55| 19| diskQueue.async { 56| 19| var diskError: Error? 57| 19| defer { 58| 19| completion?(diskError) 59| 19| } 60| 19| do { 61| 19| let cacheDirectory = try self.cacheDirectory() 62| 19| try FileManager.default.removeItem(at: cacheDirectory) 63| 19| } catch { 64| 0| diskError = error 65| 19| } 66| 19| } 67| 19| } 68| | 69| 1| func clearExpired(_ completion: ((Error?) -> Void)? = nil) { 70| 1| diskQueue.async { 71| 1| var diskError: Error? 72| 1| defer { 73| 1| completion?(diskError) 74| 1| } 75| 1| do { 76| 1| let expiredFiles = try self.expiredFileURLs() 77| 1| try expiredFiles.forEach { url in 78| 1| _ = try FileManager.default.removeItem(at: url) 79| 1| } 80| 1| } catch { 81| 0| diskError = error 82| 1| } 83| 1| } 84| 1| } 85| | 86| 3| func expiredFileURLs() throws -> [URL] { 87| 3| let cacheDirectory = try self.cacheDirectory() 88| 3| 89| 3| let keys: Set = [.isDirectoryKey, .contentModificationDateKey] 90| 3| let contents = try? FileManager.default.contentsOfDirectory(at: cacheDirectory, includingPropertiesForKeys: Array(keys), 91| 3| options: .skipsHiddenFiles) 92| 3| guard let files = contents else { 93| 0| return [] 94| 3| } 95| 3| 96| 3| let expirationDate = Date(timeIntervalSinceNow: -maxCacheAgeSeconds) 97| 3| let expiredFileUrls = files.filter { url in 98| 2| let resource = try? url.resourceValues(forKeys: keys) 99| 2| let isDirectory = resource?.isDirectory 100| 2| guard let lastAccessDate = resource?.contentAccessDate else { 101| 2| return true 102| 2| } 103| 0| return isDirectory == false && lastAccessDate < expirationDate 104| 2| } 105| 3| return expiredFileUrls 106| 3| } 107| | 108| 7| func isCached(forKey key: String) throws -> Bool { 109| 7| let url = try self.cacheDirectory().appendingPathComponent(key) 110| 7| return FileManager.default.fileExists(atPath: url.path) 111| 7| } 112| | 113| |} 114| | 115| |private extension DiskCache { 116| | 117| 13| func store(data: Data, key: String) throws { 118| 13| let cacheDirectory = try self.cacheDirectory() 119| 13| let fileURL = cacheDirectory.appendingPathComponent(key) 120| 13| try data.write(to: fileURL) 121| 13| } 122| | 123| 47| func cacheDirectory() throws -> URL { 124| 47| let fileManger = FileManager.default 125| 47| 126| 47| let folderURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 127| 47| let cacheDirectory = folderURL.appendingPathComponent(cacheName, isDirectory: true) 128| 47| guard !fileManger.fileExists(atPath: cacheDirectory.absoluteString) else { 129| 0| return cacheDirectory 130| 47| } 131| 47| try fileManger.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil) 132| 47| return cacheDirectory 133| 47| } 134| | 135| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Core/Cache/MemoryCache.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import Foundation 6| | 7| |final class MemoryCache { 8| | 9| 17| private let backingCache = NSCache() 10| | 11| | subscript(key: Key) -> Value? { 12| 21| get { 13| 21| value(forKey: key) 14| 21| } 15| 19| set { 16| 19| guard let value = newValue else { 17| 1| removeValue(forKey: key) 18| 1| return 19| 18| } 20| 18| insert(value, forKey: key) 21| 18| } 22| | } 23| | 24| 17| init(name: String = "") { 25| 17| backingCache.name = name 26| 17| } 27| | 28| 9| func isCached(forKey key: Key) -> Bool { 29| 9| self[key] != nil 30| 9| } 31| | 32| 16| func clear() { 33| 16| backingCache.removeAllObjects() 34| 16| } 35| | 36| |} 37| | 38| |private extension MemoryCache { 39| 18| func insert(_ value: Value, forKey key: Key) { 40| 18| backingCache.setObject(Entry(value: value), forKey: WrappedKey(key: key)) 41| 18| } 42| | 43| 21| func value(forKey key: Key) -> Value? { 44| 21| let entry = backingCache.object(forKey: WrappedKey(key: key)) 45| 21| return entry?.value 46| 21| } 47| | 48| 1| func removeValue(forKey key: Key) { 49| 1| backingCache.removeObject(forKey: WrappedKey(key: key)) 50| 1| } 51| |} 52| | 53| |extension MemoryCache { 54| | 55| | private class WrappedKey: NSObject { 56| | 57| | private let key: Key 58| | 59| 41| override var hash: Int { 60| 41| key.hashValue 61| 41| } 62| | 63| 40| init(key: Key) { 64| 40| self.key = key 65| 40| } 66| | 67| 10| override func isEqual(_ object: Any?) -> Bool { 68| 10| guard let value = object as? WrappedKey else { 69| 0| return false 70| 10| } 71| 10| return value.key == key 72| 10| } 73| | } 74| | 75| | private class Entry { 76| | 77| | let value: Value 78| | 79| 18| init(value: Value) { 80| 18| self.value = value 81| 18| } 82| | 83| | } 84| | 85| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Core/DisplayOptions.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import Foundation 6| | 7| |public struct DisplayOptions: OptionSet { 8| | 9| | public let rawValue: Int 10| | 11| 0| public init(rawValue: Int) { 12| 0| self.rawValue = rawValue 13| 0| } 14| | 15| | /// Scale the raw image to the target size 16| | public static let downsampled = DisplayOptions(rawValue: 1 << 0) 17| | 18| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Core/Download.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| | 7| |/// A download task – this wraps the internal download instance and can be used to cancel an inflight request 8| |public struct DownloadTask { 9| | 10| | let download: Download 11| | 12| | public let cancelToken: CancelToken 13| | 14| 2| public func cancel() { 15| 2| download.cancel(cancelToken: cancelToken) 16| 2| } 17| | 18| |} 19| | 20| |final class Download { 21| | 22| | typealias Completion = (Result) -> Void 23| | 24| | let task: URLSessionDataTask 25| | 26| 8| var completions: [Completion] { 27| 8| defer { 28| 8| lock.unlock() 29| 8| } 30| 8| lock.lock() 31| 8| return Array(tokenCompletions.values) 32| 8| } 33| | 34| 10| private let lock = NSLock() 35| | 36| 10| private(set) var data = Data() 37| | private var currentToken: CancelToken = 0 38| 10| private var backgroundTask: UIBackgroundTaskIdentifier = .invalid 39| 10| private var tokenCompletions: [CancelToken: Completion] = [:] 40| | 41| 10| init(task: URLSessionDataTask) { 42| 10| self.task = task 43| 10| } 44| | 45| 10| deinit { 46| 10| invalidateBackgroundTask() 47| 10| } 48| | 49| 11| func addCompletion(_ completion: @escaping Completion) -> CancelToken { 50| 11| defer { 51| 11| currentToken += 1 52| 11| lock.unlock() 53| 11| } 54| 11| lock.lock() 55| 11| tokenCompletions[currentToken] = completion 56| 11| return currentToken 57| 11| } 58| | 59| 2| func removeCompletion(for token: CancelToken) -> Completion? { 60| 2| defer { 61| 2| lock.unlock() 62| 2| } 63| 2| lock.lock() 64| 2| guard let completion = tokenCompletions[token] else { 65| 0| return nil 66| 2| } 67| 2| tokenCompletions[token] = nil 68| 2| return completion 69| 2| } 70| | 71| 6| func appendData(_ data: Data) { 72| 6| self.data.append(data) 73| 6| } 74| | 75| 10| func start() { 76| 10| backgroundTask = UIApplication.shared.beginBackgroundTask { 77| 0| self.invalidateBackgroundTask() 78| 0| } 79| 10| } 80| | 81| 8| func finish() { 82| 8| invalidateBackgroundTask() 83| 8| } 84| | 85| 2| func cancel(cancelToken: CancelToken) { 86| 2| guard let completion = removeCompletion(for: cancelToken) else { 87| 0| return 88| 2| } 89| 2| if tokenCompletions.isEmpty { 90| 2| task.cancel() 91| 2| } 92| 2| completion(.failure(DownloaderError.canceled)) 93| 2| } 94| | 95| |} 96| | 97| |private extension Download { 98| | 99| 18| func invalidateBackgroundTask() { 100| 18| UIApplication.shared.endBackgroundTask(backgroundTask) 101| 18| backgroundTask = .invalid 102| 18| } 103| | 104| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Core/Downloader.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| | 7| |enum DownloaderError: Error { 8| | case dataConversion 9| | case canceled 10| |} 11| | 12| |final class Downloader { 13| | 14| | let session: URLSession 15| | 16| | private let sessionDelegate: SessionDelegate 17| 10| private let lock = NSLock() 18| | 19| 10| private var downloads: [URL: Download] = [:] 20| | 21| | fileprivate subscript(_ url: URL) -> Download? { 22| 33| get { 23| 33| defer { 24| 33| lock.unlock() 25| 33| } 26| 33| lock.lock() 27| 33| return downloads[url] 28| 33| } 29| 18| set { 30| 18| defer { 31| 18| lock.unlock() 32| 18| } 33| 18| lock.lock() 34| 18| downloads[url] = newValue 35| 18| } 36| | } 37| | 38| 10| init(sessionConfiguration: URLSessionConfiguration = .default) { 39| 10| self.sessionDelegate = SessionDelegate() 40| 10| self.session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: .main) 41| 10| self.sessionDelegate.downloader = self 42| 10| } 43| | 44| 10| deinit { 45| 10| session.invalidateAndCancel() 46| 10| } 47| | 48| 11| func fetch(_ url: URL, completion: @escaping (Result) -> Void) -> DownloadTask { 49| 11| if let download = self[url] { 50| 1| let token = download.addCompletion(completion) 51| 1| return DownloadTask(download: download, cancelToken: token) 52| 10| } 53| 10| 54| 10| let task = session.dataTask(with: url) 55| 10| let download = Download(task: task) 56| 10| let token = download.addCompletion(completion) 57| 10| download.start() 58| 10| self[url] = download 59| 10| task.resume() 60| 10| 61| 10| return DownloadTask(download: download, cancelToken: token) 62| 11| } 63| | 64| |} 65| | 66| |private final class SessionDelegate: NSObject, URLSessionDataDelegate { 67| | 68| | weak var downloader: Downloader? 69| | 70| 6| func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 71| 6| guard let url = dataTask.originalRequest?.url, let download = downloader?[url] else { 72| 0| return 73| 6| } 74| 6| download.appendData(data) 75| 6| } 76| | 77| 10| func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 78| 10| guard let url = task.originalRequest?.url, let download = downloader?[url] else { 79| 2| return 80| 8| } 81| 8| 82| 9| downloader?[url]?.completions.forEach { completion in 83| 9| if let error = error { 84| 2| completion(.failure(error)) 85| 2| return 86| 7| } 87| 7| guard let value = T.convert(from: download.data) else { 88| 1| completion(.failure(DownloaderError.dataConversion)) 89| 1| return 90| 6| } 91| 6| completion(.success(value)) 92| 6| } 93| 8| downloader?[url] = nil 94| 8| download.finish() 95| 8| } 96| | 97| | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, 98| | willCacheResponse proposedResponse: CachedURLResponse, 99| 0| completionHandler: @escaping (CachedURLResponse?) -> Void) { 100| 0| completionHandler(nil) 101| 0| } 102| | 103| |} 104| | /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Core/ImageTransforming.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| | 7| |public protocol ImageTransforming { 8| | /// The transformer's identifier. Any unique string. 9| | var identifier: String { get } 10| | 11| | /// The transform function to apply 12| | /// 13| | /// - Parameter image: The image to transform 14| | /// - Returns: The transformed image 15| | func transform(image: UIImage) -> UIImage? 16| |} 17| | 18| |infix operator >>>: AdditionPrecedence 19| | 20| 2|public func >>>(transformer1: ImageTransforming, transformer2: ImageTransforming) -> ImageTransforming { 21| 2| transformer1.appending(transformer: transformer2) 22| 2|} 23| | 24| |public extension ImageTransforming { 25| | 26| | /// Appends one transformer to another 27| | /// 28| | /// - Parameter transformer: The transformer to append 29| | /// - Returns: A new transformer that will run both transformers after one another 30| 6| func appending(transformer: ImageTransforming) -> ImageTransforming { 31| 6| let chainIdentifier = identifier.appending(" -> \(transformer.identifier)") 32| 6| 33| 6| return BaseComposableImageTransformer(identifier: chainIdentifier) { image in 34| 2| guard let image = self.transform(image: image) else { 35| 0| return nil 36| 2| } 37| 2| return transformer.transform(image: image) 38| 2| } 39| 6| } 40| | 41| |} 42| | 43| |private class BaseComposableImageTransformer: ImageTransforming { 44| | 45| | let identifier: String 46| | private let call: (UIImage) -> UIImage? 47| | 48| 6| init(identifier: String, call: @escaping (UIImage) -> UIImage?) { 49| 6| self.identifier = identifier 50| 6| self.call = call 51| 6| } 52| | 53| 2| func transform(image: UIImage) -> UIImage? { 54| 2| call(image) 55| 2| } 56| | 57| |} 58| | 59| 0|func ==(lhs: ImageTransforming, rhs: ImageTransforming) -> Bool { 60| 0| lhs.identifier == rhs.identifier 61| 0|} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Core/MapleBacon.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| | 7| |public enum MapleBaconError: Error { 8| | case imageTransformingError 9| |} 10| | 11| |public typealias CancelToken = Int 12| | 13| |public final class MapleBacon { 14| | 15| | public typealias ImageCompletion = (Result) -> Void 16| | 17| | /// The shared instance of MapleBacon 18| | public static let shared = MapleBacon() 19| | 20| | private static let queueLabel = "com.schnaub.MapleBacon.transformer" 21| | 22| | public var maxCacheAgeSeconds: TimeInterval { 23| 0| get { 24| 0| cache.maxCacheAgeSeconds 25| 0| } 26| 0| set { 27| 0| cache.maxCacheAgeSeconds = newValue 28| 0| } 29| | } 30| | 31| | private let cache: Cache 32| | private let downloader: Downloader 33| | private let transformerQueue: DispatchQueue 34| | 35| | /// Initialise a custom instance of MapleBacon 36| | /// - Parameters: 37| | /// - name: The name to give this instance. Internally this reflects a distinct cache region. 38| | /// - sessionConfiguration: The URLSessionConfiguration to use. Uses `.default` when no parameter is supplied. 39| 0| public convenience init(name: String = "", sessionConfiguration: URLSessionConfiguration = .default) { 40| 0| self.init(cache: Cache(name: name), sessionConfiguration: sessionConfiguration) 41| 0| } 42| | 43| 5| init(cache: Cache, sessionConfiguration: URLSessionConfiguration) { 44| 5| self.cache = cache 45| 5| self.downloader = Downloader(sessionConfiguration: sessionConfiguration) 46| 5| self.transformerQueue = DispatchQueue(label: Self.queueLabel, attributes: .concurrent) 47| 5| } 48| | 49| | /// Return an image for the passed URL. This will check the in-memory and disk cache and fetch the image over the network if nothing is in either cache yet. 50| | /// After a successful download, the image will be stored in both caches. 51| | /// - Parameters: 52| | /// - url: The URL of the image 53| | /// - imageTransformer: An optional image transformer 54| | /// - completion: The completion to call with the image result 55| | /// - Returns: An optional `DownloadTask` if needs to fetch the image over the network. The task can be used to cancel an inflight request 56| | @discardableResult 57| 5| public func image(with url: URL, imageTransformer: ImageTransforming? = nil, completion: @escaping ImageCompletion) -> DownloadTask? { 58| 5| if (try? isCached(with: url, imageTransformer: imageTransformer)) == true { 59| 0| fetchImageFromCache(with: url, imageTransformer: imageTransformer, completion: completion) 60| 0| return nil 61| 5| } 62| 5| 63| 5| return fetchImageFromNetworkAndCache(with: url, imageTransformer: imageTransformer, completion: completion) 64| 5| } 65| | 66| | /// Hydrate the cache 67| | /// - Parameter url: The URL to fetch 68| 0| public func hydrateCache(url: URL) { 69| 0| if (try? isCached(with: url, imageTransformer: nil)) == false { 70| 0| _ = self.fetchImageFromNetworkAndCache(with: url, imageTransformer: nil, completion: { _ in }) 71| 0| } 72| 0| } 73| | 74| | /// Hydrate the cache 75| | /// - Parameter urls: An array of URLs to fetch 76| 0| public func hydrateCache(urls: [URL]) { 77| 0| for url in urls { 78| 0| if (try? isCached(with: url, imageTransformer: nil)) == false { 79| 0| _ = self.fetchImageFromNetworkAndCache(with: url, imageTransformer: nil, completion: { _ in }) 80| 0| } 81| 0| } 82| 0| } 83| | 84| | /// Clear the cache 85| | /// - Parameters: 86| | /// - options: The `CacheClearOptions`. Clear either only memory, disk or both caches. 87| | /// - completion: The completion to call after clearing the cache 88| 5| public func clearCache(_ options: CacheClearOptions, completion: ((Error?) -> Void)? = nil) { 89| 5| cache.clear(options, completion: completion) 90| 5| } 91| | 92| |} 93| | 94| |private extension MapleBacon { 95| | 96| 5| func isCached(with url: URL, imageTransformer: ImageTransforming?) throws -> Bool { 97| 5| let cacheKey = makeCacheKey(for: url, imageTransformer: imageTransformer) 98| 5| return try cache.isCached(forKey: cacheKey) 99| 5| } 100| | 101| 0| func fetchImageFromCache(with url: URL, imageTransformer: ImageTransforming?, completion: @escaping ImageCompletion) { 102| 0| let cacheKey = makeCacheKey(for: url, imageTransformer: imageTransformer) 103| 0| cache.value(forKey: cacheKey) { result in 104| 0| DispatchQueue.main.optionalAsync { 105| 0| completion(result.flatMap { .success($0.value) }) 106| 0| } 107| 0| } 108| 0| } 109| | 110| 5| func fetchImageFromNetworkAndCache(with url: URL, imageTransformer: ImageTransforming?, completion: @escaping ImageCompletion) -> DownloadTask { 111| 5| fetchImageFromNetwork(with: url) { result in 112| 5| switch result { 113| 5| case .success(let image): 114| 3| if let transformer = imageTransformer { 115| 1| let cacheKey = self.makeCacheKey(for: url, imageTransformer: transformer) 116| 1| self.transformImageAndCache(image, cacheKey: cacheKey, imageTransformer: transformer, completion: completion) 117| 3| } else { 118| 2| self.cache.store(value: image, forKey: url.absoluteString) 119| 2| DispatchQueue.main.optionalAsync { 120| 2| completion(.success(image)) 121| 2| } 122| 5| } 123| 5| case .failure(let error): 124| 2| DispatchQueue.main.optionalAsync { 125| 2| completion(.failure(error)) 126| 2| } 127| 5| } 128| 5| } 129| 5| } 130| | 131| 5| func fetchImageFromNetwork(with url: URL, completion: @escaping ImageCompletion) -> DownloadTask { 132| 5| downloader.fetch(url, completion: completion) 133| 5| } 134| | 135| 1| func transformImageAndCache(_ image: UIImage, cacheKey: String, imageTransformer: ImageTransforming, completion: @escaping ImageCompletion) { 136| 1| transformImage(image, imageTransformer: imageTransformer) { result in 137| 1| switch result { 138| 1| case .success(let image): 139| 1| self.cache.store(value: image, forKey: cacheKey) 140| 1| DispatchQueue.main.optionalAsync { 141| 1| completion(.success(image)) 142| 1| } 143| 1| case .failure(let error): 144| 0| DispatchQueue.main.optionalAsync { 145| 0| completion(.failure(error)) 146| 0| } 147| 1| } 148| 1| } 149| 1| } 150| | 151| 1| func transformImage(_ image: UIImage, imageTransformer: ImageTransforming, completion: @escaping ImageCompletion) { 152| 1| transformerQueue.async { 153| 1| guard let image = imageTransformer.transform(image: image) else { 154| 0| completion(.failure(MapleBaconError.imageTransformingError)) 155| 0| return 156| 1| } 157| 1| completion(.success(image)) 158| 1| } 159| 1| } 160| | 161| 6| func makeCacheKey(for url: URL, imageTransformer: ImageTransforming?) -> String { 162| 6| guard let imageTransformer = imageTransformer else { 163| 4| return url.absoluteString 164| 4| } 165| 2| return url.absoluteString + imageTransformer.identifier 166| 6| } 167| |} 168| | 169| |#if canImport(Combine) 170| |import Combine 171| | 172| |@available(iOS 13.0, *) 173| |extension MapleBacon { 174| | 175| | /// The Combine way to fetch an image for the passed URL. This will check the in-memory and disk cache and fetch the image over the network if nothing is in either cache yet. 176| | /// After a successful download, the image will be stored in both caches. 177| | /// - Parameters: 178| | /// - url: The URL of the image 179| | /// - imageTransformer: An optional image transformer 180| | /// - Returns: `AnyPublisher` 181| 1| public func image(with url: URL, imageTransformer: ImageTransforming? = nil) -> AnyPublisher { 182| 1| Deferred { 183| 1| Future { resolve in 184| 1| self.image(with: url, imageTransformer: imageTransformer) { result in 185| 1| resolve(result) 186| 1| } 187| 1| } 188| 1| }.eraseToAnyPublisher() 189| 1| } 190| | 191| |} 192| | 193| |#endif /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Extensions/CGSize+MapleBacon.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| | 7| |extension CGSize { 8| 0| static func * (size: CGSize, scale: CGFloat) -> CGSize { 9| 0| size.applying(CGAffineTransform(scaleX: scale, y: scale)) 10| 0| } 11| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Extensions/Data+MapleBacon.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import Foundation 6| | 7| |extension Data { 8| | 9| 5| init(from inputStream: InputStream) { 10| 5| self.init() 11| 5| 12| 5| defer { 13| 5| inputStream.close() 14| 5| } 15| 5| inputStream.open() 16| 5| 17| 5| let bufferSize = 1024 18| 5| let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) 19| 11| while inputStream.hasBytesAvailable { 20| 6| let read = inputStream.read(buffer, maxLength: bufferSize) 21| 6| append(buffer, count: read) 22| 6| } 23| 5| buffer.deallocate() 24| 5| } 25| | 26| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Extensions/DataConvertible.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| | 7| |public protocol DataConvertible { 8| | associatedtype Result 9| | 10| | static func convert(from data: Data) -> Result? 11| | 12| | func toData() -> Data 13| |} 14| | 15| |extension Data: DataConvertible { 16| 7| public static func convert(from data: Data) -> Data? { 17| 7| data 18| 7| } 19| | 20| 12| public func toData() -> Data { 21| 12| self 22| 12| } 23| |} 24| | 25| |extension UIImage: DataConvertible { 26| | 27| 6| private var hasAlphaChannel: Bool { 28| 6| guard let alphaInfo = cgImage?.alphaInfo else { 29| 0| return false 30| 6| } 31| 6| switch alphaInfo { 32| 6| case .first, .last, .premultipliedFirst, .premultipliedLast, .alphaOnly: 33| 6| return true 34| 6| case .none, .noneSkipFirst, .noneSkipLast: 35| 0| return false 36| 6| @unknown default: 37| 0| fatalError("Unkown alphaInfo \(alphaInfo)") 38| 6| } 39| 6| } 40| | 41| 4| public static func convert(from data: Data) -> UIImage? { 42| 4| UIImage(data: data, scale: UIScreen.main.scale) 43| 4| } 44| | 45| 6| public func toData() -> Data { 46| 6| hasAlphaChannel ? pngData()! : jpegData(compressionQuality: 1)! 47| 6| } 48| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Extensions/DispatchQueue+MapleBacon.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import Foundation 6| | 7| |extension DispatchQueue { 8| 5| func optionalAsync(_ block: @escaping () -> Void) { 9| 5| if self === DispatchQueue.main && Thread.isMainThread { 10| 4| block() 11| 5| } else { 12| 1| async { 13| 1| block() 14| 1| } 15| 5| } 16| 5| } 17| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Extensions/DownsamplingImageTransformer.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| | 7| |public final class DownsamplingImageTransformer: ImageTransforming { 8| | 9| 0| public var identifier: String { 10| 0| "com.schnaub.DownsamplingImageTransformer@\(targetSize)" 11| 0| } 12| | 13| | private let targetSize: CGSize 14| | 15| 0| init(size: CGSize) { 16| 0| targetSize = size * UIScreen.main.scale 17| 0| } 18| | 19| 0| public func transform(image: UIImage) -> UIImage? { 20| 0| let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary 21| 0| guard let data = image.pngData(), let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else { 22| 0| return image 23| 0| } 24| 0| 25| 0| let maxDimensionInPixels = max(targetSize.width, targetSize.height) 26| 0| let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true, 27| 0| kCGImageSourceShouldCacheImmediately: true, 28| 0| kCGImageSourceCreateThumbnailWithTransform: true, 29| 0| kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary 30| 0| 31| 0| guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else { 32| 0| return image 33| 0| } 34| 0| return UIImage(cgImage: downsampledImage) 35| 0| } 36| | 37| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Extensions/FileManager.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import Foundation 6| | 7| |enum MapleBaconInputStreamError: Error { 8| | case uninitializedInputStream 9| | case emptyFile 10| |} 11| | 12| |extension FileManager { 13| 5| func fileContents(at url: URL) throws -> Data { 14| 5| guard let inputStream = InputStream(url: url) else { 15| 0| throw MapleBaconInputStreamError.uninitializedInputStream 16| 5| } 17| 5| let data = Data(from: inputStream) 18| 5| guard data.count > 0 else { 19| 2| throw MapleBaconInputStreamError.emptyFile 20| 3| } 21| 3| return data 22| 5| } 23| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Extensions/TimePeriod.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import Foundation 6| | 7| |enum TimePeriod { 8| | case seconds(Int) 9| | case minutes(Int) 10| | case hours(Int) 11| | case days(Int) 12| | 13| 18| var timeInterval: TimeInterval { 14| 18| switch self { 15| 18| case .seconds(let value): 16| 1| return TimeInterval(value) 17| 18| case .minutes(let value): 18| 0| return TimeInterval(value * 60) 19| 18| case .hours(let value): 20| 0| return TimeInterval(value * 60 * 60) 21| 18| case .days(let value): 22| 17| return TimeInterval(value * 60 * 60 * 24) 23| 18| } 24| 18| } 25| |} 26| | 27| |extension Int { 28| 0| var second: TimeInterval { 29| 0| TimePeriod.seconds(self).timeInterval 30| 0| } 31| 1| var seconds: TimeInterval { 32| 1| TimePeriod.seconds(self).timeInterval 33| 1| } 34| 0| var minutes: TimeInterval { 35| 0| TimePeriod.minutes(self).timeInterval 36| 0| } 37| 0| var hour: TimeInterval { 38| 0| TimePeriod.hours(self).timeInterval 39| 0| } 40| 0| var hours: TimeInterval { 41| 0| TimePeriod.hours(self).timeInterval 42| 0| } 43| 17| var days: TimeInterval { 44| 17| TimePeriod.days(self).timeInterval 45| 17| } 46| |} /Users/runner/work/MapleBacon/MapleBacon/MapleBacon/Extensions/UIImageView+MapleBacon.swift: 1| |// 2| |// Copyright © 2020 Schnaub. All rights reserved. 3| |// 4| | 5| |import UIKit 6| | 7| |private var baconImageUrlKey: UInt8 = 0 8| |private var downloadKey: UInt8 = 1 9| | 10| |extension UIImageView { 11| | 12| | private var baconImageUrl: URL? { 13| 0| get { 14| 0| objc_getAssociatedObject(self, &baconImageUrlKey) as? URL 15| 0| } 16| 0| set { 17| 0| objc_setAssociatedObject(self, &baconImageUrlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 18| 0| } 19| | } 20| | 21| | private var downloadTask: DownloadTask? { 22| 0| get { 23| 0| objc_getAssociatedObject(self, &downloadKey) as? DownloadTask 24| 0| } 25| 0| set { 26| 0| objc_setAssociatedObject(self, &downloadKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 27| 0| } 28| | } 29| | 30| | /// Set remote image 31| | /// - Parameters: 32| | /// - url: The URL of the image 33| | /// - placeholder: An optional placeholder image to set while fetching the remote image 34| | /// - displayOptions: `DisplayOptions` 35| | /// - imageTransformer: An optional image transformer 36| | /// - completion: An optional completion to call when the image is set 37| | /// - Returns: An optional `DownloadTask` if needs to fetch the image over the network. The task can be used to cancel an inflight request 38| | @discardableResult 39| | public func setImage(with url: URL?, 40| | placeholder: UIImage? = nil, 41| | displayOptions: [DisplayOptions] = [], 42| | imageTransformer: ImageTransforming? = nil, 43| 0| completion: ((UIImage?) -> Void)? = nil) -> DownloadTask? { 44| 0| cancelDownload() 45| 0| baconImageUrl = url 46| 0| image = placeholder 47| 0| guard let url = url else { 48| 0| return nil 49| 0| } 50| 0| 51| 0| let transformer = makeTransformer(displayOptions: displayOptions, imageTransformer: imageTransformer) 52| 0| 53| 0| let task = MapleBacon.shared.image(with: url, imageTransformer: transformer) { [weak self] result in 54| 0| var resultImage: UIImage? 55| 0| defer { 56| 0| self?.baconImageUrl = nil 57| 0| self?.downloadTask = nil 58| 0| completion?(resultImage) 59| 0| } 60| 0| guard case let Result.success(image) = result, let self = self, url == self.baconImageUrl else { 61| 0| return 62| 0| } 63| 0| resultImage = image 64| 0| self.image = image 65| 0| } 66| 0| downloadTask = task 67| 0| return task 68| 0| } 69| | 70| | /// Cancel a running download 71| 0| public func cancelDownload() { 72| 0| downloadTask?.cancel() 73| 0| } 74| | 75| 0| private func makeTransformer(displayOptions: [DisplayOptions] = [], imageTransformer: ImageTransforming?) -> ImageTransforming? { 76| 0| guard displayOptions.contains(.downsampled) else { 77| 0| return imageTransformer 78| 0| } 79| 0| 80| 0| let downsampler = DownsamplingImageTransformer(size: bounds.size) 81| 0| if let imageTransformer = imageTransformer { 82| 0| return downsampler >>> imageTransformer 83| 0| } 84| 0| return downsampler 85| 0| } 86| | 87| |} <<<<<< EOF # path=fixes ./Example/MapleBacon Example/AppDelegate.swift:4,6,9,10 ./Example/MapleBacon Example/Controllers/PrefetchViewController.swift:4,7,9,11,14,18,19,24,25,28,29,35,36,37,38,40,44,45,46 ./Example/MapleBacon Example/Controllers/ImageTransformerViewController.swift:4,7,9,12,15,18,19,24,25,28,29,35,36,37,38,40,42,45,49,54,55,57,58,59 ./Example/MapleBacon Example/Controllers/EntryViewController.swift:4,7,9,12,13,14 ./Example/MapleBacon Example/Controllers/DownsamplingViewController.swift:4,7,9,11,14,17,18,23,24,27,28,34,35,36 ./Example/MapleBacon Example/Controllers/CollectionViewController.swift:4,7,9,11,14,17,18,23,24,27,28,34,35,36 ./Example/MapleBacon Example/Helpers/UICollectionView+MapleBacon.swift:4,6,11,12 ./Example/MapleBacon Example/Helpers/ImageCollectionViewCell.swift:4,6,8,10,11 ./Example/MapleBacon Example/SceneDelegate.swift:4,6,8,10,11 ./MapleBacon/Core/DisplayOptions.swift:4,6,8,10,13,14,17,18 ./MapleBacon/Core/Downloader.swift:4,6,10,11,13,15,18,20,25,28,32,35,36,37,42,43,46,47,52,53,60,62,63,64,65,67,69,73,75,76,80,81,86,90,92,95,96,101,102,103,104 ./MapleBacon/Core/Cache/CacheClearOptions.swift:4,6,9,12,13,16,18 ./MapleBacon/Core/Cache/MemoryCache.swift:4,6,8,10,14,19,21,22,23,26,27,30,31,34,35,36,37,41,42,46,47,50,51,52,54,56,58,61,62,65,66,70,72,73,74,76,78,81,82,83,84,85 ./MapleBacon/Core/Cache/DiskCache.swift:4,6,8,10,12,15,20,21,27,32,33,34,35,42,43,50,51,52,53,59,65,66,67,68,74,79,82,83,84,85,88,94,95,102,104,106,107,111,112,113,114,116,121,122,125,130,133,134,135 ./MapleBacon/Core/Cache/Cache.swift:4,6,9,10,12,14,18,21,22,23,26,30,34,35,36,41,42,45,52,53,58,62,63,64,65,66,72,73,76,77,78,83,85,86,89,90,91,92,94,98,100,101,106,109,110,111,112,115,118,122,123,124 ./MapleBacon/Core/Cache/CacheResult.swift:4,6,10,11,14 ./MapleBacon/Core/Download.swift:4,6,9,11,13,16,17,18,19,21,23,25,29,32,33,35,40,43,44,47,48,53,57,58,62,66,69,70,73,74,78,79,80,83,84,88,91,93,94,95,96,98,102,103,104 ./MapleBacon/Core/MapleBacon.swift:4,6,9,10,12,14,16,19,21,25,28,29,30,34,41,42,47,48,61,62,64,65,71,72,73,80,81,82,83,90,91,92,93,95,99,100,106,107,108,109,121,122,126,127,128,129,130,133,134,142,146,147,148,149,150,156,158,159,160,164,166,167,168,171,174,186,187,189,190,191,192 ./MapleBacon/Core/ImageTransforming.swift:4,6,10,16,17,19,22,23,25,32,36,38,39,40,41,42,44,47,51,52,55,56,57,58,61 ./MapleBacon/Extensions/Data+MapleBacon.swift:4,6,8,11,14,16,22,24,25,26 ./MapleBacon/Extensions/DataConvertible.swift:4,6,9,11,13,14,18,19,22,23,24,26,30,38,39,40,43,44,47,48 ./MapleBacon/Extensions/TimePeriod.swift:4,6,12,23,24,25,26,30,33,36,39,42,45,46 ./MapleBacon/Extensions/FileManager.swift:4,6,10,11,16,20,22,23 ./MapleBacon/Extensions/DownsamplingImageTransformer.swift:4,6,8,11,12,14,17,18,23,24,30,33,35,36,37 ./MapleBacon/Extensions/DispatchQueue+MapleBacon.swift:4,6,14,15,16,17 ./MapleBacon/Extensions/CGSize+MapleBacon.swift:4,6,10,11 ./MapleBacon/Extensions/UIImageView+MapleBacon.swift:4,6,9,11,15,18,19,20,24,27,28,29,49,50,52,59,62,65,68,69,73,74,78,79,83,85,86,87 ./MapleBacon/MapleBacon.h:8,10,13,16,18,19 ./MapleBaconTests/MapleBaconTests.swift:4,10,12,14,16,19,23,30,33,34,35,38,39,43,50,53,54,55,57,58,63,71,74,75,76,78,79,83,90,93,94,95,98,100,101,102,103,105,108,112,117,122,124,125,126,127 ./MapleBaconTests/CacheTests.swift:4,7,9,11,13,16,17,20,22,26,27,29,30,33,35,44,46,47,48,50,51,54,56,59,66,68,69,70,72,73,76,78,81,89,91,92,93,95,96,99,101,104,114,115,117,118,119,121,122,125,127,130,134,135,136,138,139,140 ./MapleBaconTests/MemoryCacheTests.swift:4,7,9,12,15,19,20,23,26,28,29,33,36,38,39,43,45,47,48,52,55,56,57 ./MapleBaconTests/ImageTransformingTests.swift:4,7,9,14,16,20,21,26,28,32,33,38,41,45,46,47 ./MapleBaconTests/DownloaderTests.swift:4,7,9,11,15,22,24,25,27,28,32,39,41,42,44,45,49,56,58,59,61,62,65,73,75,76,84,86,87,89,90,94,101,103,104,107,109,110,111 ./MapleBaconTests/TestHelpers.swift:4,7,11,12,18,19,20,23,24,26,28,30,34,35,36,37,39,41,43,47,48,49,50,52,54,56,60,61,62 ./MapleBaconTests/MockURLSessionConfiguration.swift:4,6,12,13,16,17,21,22,24,25,29,30,35,36,39,40,45,46,48,49,53,54,59,60,63,64,69,70,72,73,77,78 ./MapleBaconTests/DiskCacheTests.swift:4,7,9,11,14,15,18,22,23,25,26,31,39,41,42,43,45,46,49,56,58,59,61,62,65,69,70,72,73,77,82,85,89,91,92,93,95,96,99,103,104,106,107,108 ./Package.swift:3,5 <<<<<< EOF