Skip to content

Commit

Permalink
Support removing cached files (#132)
Browse files Browse the repository at this point in the history
* Add empty demo app

* Display fake image

* Store response type for fake requests

* Create directory for images

* Remove hardcoded logic

* Add SwiftFormat to demo project

* Add a way to clear downloads folder

* Wrap up

* Move test

* Rename test method

* Rename
  • Loading branch information
3lvis authored Sep 28, 2016
1 parent 7b8091d commit ca86903
Show file tree
Hide file tree
Showing 19 changed files with 582 additions and 46 deletions.
191 changes: 188 additions & 3 deletions Demo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions Demo.xcodeproj/xcshareddata/xcschemes/iOSDemo.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0800"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "44DFD38C1D9A2D5A0014E9F2"
BuildableName = "iOSDemo.app"
BlueprintName = "iOSDemo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "44DFD38C1D9A2D5A0014E9F2"
BuildableName = "iOSDemo.app"
BlueprintName = "iOSDemo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "44DFD38C1D9A2D5A0014E9F2"
BuildableName = "iOSDemo.app"
BlueprintName = "iOSDemo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "44DFD38C1D9A2D5A0014E9F2"
BuildableName = "iOSDemo.app"
BlueprintName = "iOSDemo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
2 changes: 2 additions & 0 deletions Sources/NSFileManager+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ extension FileManager {

public func remove(at url: URL) {
let path = url.path
guard FileManager.default.isDeletableFile(atPath: url.path) else { return }

do {
try FileManager.default.removeItem(atPath: path)
} catch let error as NSError {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking+DELETE.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public extension Networking {
- parameter statusCode: By default it's 200, if you provide any status code that is between 200 and 299 the response object will be returned, otherwise we will return an error containig the provided status code.
*/
public func fakeDELETE(_ path: String, response: Any?, statusCode: Int = 200) {
self.fake(.DELETE, path: path, response: response, statusCode: statusCode)
self.fake(.DELETE, path: path, response: response, responseType: .json, statusCode: statusCode)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking+GET.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public extension Networking {
- parameter statusCode: By default it's 200, if you provide any status code that is between 200 and 299 the response object will be returned, otherwise we will return an error containig the provided status code.
*/
public func fakeGET(_ path: String, response: Any?, statusCode: Int = 200) {
self.fake(.GET, path: path, response: response, statusCode: statusCode)
self.fake(.GET, path: path, response: response, responseType: .json, statusCode: statusCode)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking+Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ public extension Networking {
- parameter image: An image that will be returned when there's a request to the registered path.
*/
public func fakeImageDownload(_ path: String, image: NetworkingImage?, statusCode: Int = 200) {
self.fake(.GET, path: path, response: image, statusCode: statusCode)
self.fake(.GET, path: path, response: image, responseType: .image, statusCode: statusCode)
}
}
2 changes: 1 addition & 1 deletion Sources/Networking+POST.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public extension Networking {
- parameter statusCode: By default it's 200, if you provide any status code that is between 200 and 299 the response object will be returned, otherwise we will return an error containig the provided status code.
*/
public func fakePOST(_ path: String, response: Any?, statusCode: Int = 200) {
self.fake(.POST, path: path, response: response, statusCode: statusCode)
self.fake(.POST, path: path, response: response, responseType: .json, statusCode: statusCode)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking+PUT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public extension Networking {
- parameter statusCode: By default it's 200, if you provide any status code that is between 200 and 299 the response object will be returned, otherwise we will return an error containig the provided status code.
*/
public func fakePUT(_ path: String, response: Any?, statusCode: Int = 200) {
self.fake(.PUT, path: path, response: response, statusCode: statusCode)
self.fake(.PUT, path: path, response: response, responseType: .json, statusCode: statusCode)
}

/**
Expand Down
60 changes: 38 additions & 22 deletions Sources/Networking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ public extension Int {
}

public class Networking {
static let ErrorDomain = "NetworkingErrorDomain"
static let domain = "com.3lvis.networking"

struct FakeRequest {
let response: Any?
let responseType: ResponseType
let statusCode: Int
}

Expand Down Expand Up @@ -189,7 +190,7 @@ public class Networking {
- returns: A NSURL generated after appending the path to the base URL.
*/
public func url(for path: String) -> URL {
guard let encodedPath = path.encodeUTF8() else { fatalError("Couldn't encode path to UTF8: \(path)") }
let encodedPath = path.encodeUTF8() ?? path
guard let url = URL(string: self.baseURL + encodedPath) else { fatalError("Couldn't create a url using baseURL: \(self.baseURL) and encodedPath: \(encodedPath)") }
return url
}
Expand All @@ -200,26 +201,28 @@ public class Networking {
- returns: A NSURL where a resource has been stored.
*/
public func destinationURL(for path: String, cacheName: String? = nil) throws -> URL {
#if os(tvOS)
let directory = FileManager.SearchPathDirectory.cachesDirectory
#else
let directory = TestCheck.isTesting ? FileManager.SearchPathDirectory.cachesDirectory : FileManager.SearchPathDirectory.documentDirectory
#endif
let finalPath = cacheName ?? self.url(for: path).absoluteString
let replacedPath = finalPath.replacingOccurrences(of: "/", with: "-")
if let url = URL(string: replacedPath) {
if let cachesURL = FileManager.default.urls(for: directory, in: .userDomainMask).first {
#if !os(tvOS)
try (cachesURL as NSURL).setResourceValue(true, forKey: URLResourceKey.isExcludedFromBackupKey)
#endif
let resourcesPath = cacheName ?? self.url(for: path).absoluteString
let normalizedResourcesPath = resourcesPath.replacingOccurrences(of: "/", with: "-")
let folderPath = Networking.domain
let finalPath = "\(folderPath)/\(normalizedResourcesPath)"

if let url = URL(string: finalPath) {
if let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
try (cachesURL as NSURL).setResourceValue(true, forKey: URLResourceKey.isExcludedFromBackupKey)
let folderURL = cachesURL.appendingPathComponent(URL(string: folderPath)!.absoluteString)

if FileManager.default.exists(at: folderURL) == false {
try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: false, attributes: nil)
}

let destinationURL = cachesURL.appendingPathComponent(url.absoluteString)

return destinationURL
} else {
throw NSError(domain: Networking.ErrorDomain, code: 9999, userInfo: [NSLocalizedDescriptionKey: "Couldn't normalize url"])
throw NSError(domain: Networking.domain, code: 9999, userInfo: [NSLocalizedDescriptionKey: "Couldn't normalize url"])
}
} else {
throw NSError(domain: Networking.ErrorDomain, code: 9999, userInfo: [NSLocalizedDescriptionKey: "Couldn't create a url using replacedPath: \(replacedPath)"])
throw NSError(domain: Networking.domain, code: 9999, userInfo: [NSLocalizedDescriptionKey: "Couldn't create a url using replacedPath: \(finalPath)"])
}
}

Expand Down Expand Up @@ -320,6 +323,19 @@ public class Networking {

return object as? Data
}

/**
Deletes the downloaded/cached files.
*/
public static func deleteCachedFiles() {
if let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
let folderURL = cachesURL.appendingPathComponent(URL(string: Networking.domain)!.absoluteString)

if FileManager.default.exists(at: folderURL) {
FileManager.default.remove(at: folderURL)
}
}
}
}

extension Networking {
Expand Down Expand Up @@ -374,7 +390,7 @@ extension Networking {
func fake(_ requestType: RequestType, path: String, fileName: String, bundle: Bundle = Bundle.main) {
do {
if let result = try JSON.from(fileName, bundle: bundle) {
self.fake(requestType, path: path, response: result, statusCode: 200)
self.fake(requestType, path: path, response: result, responseType: .json, statusCode: 200)
}
} catch ParsingError.notFound {
fatalError("We couldn't find \(fileName), are you sure is there?")
Expand All @@ -383,21 +399,21 @@ extension Networking {
}
}

func fake(_ requestType: RequestType, path: String, response: Any?, statusCode: Int) {
func fake(_ requestType: RequestType, path: String, response: Any?, responseType: ResponseType, statusCode: Int) {
var fakeRequests = self.fakeRequests[requestType] ?? [String: FakeRequest]()
fakeRequests[path] = FakeRequest(response: response, statusCode: statusCode)
fakeRequests[path] = FakeRequest(response: response, responseType: responseType, statusCode: statusCode)
self.fakeRequests[requestType] = fakeRequests
}

@discardableResult
func request(_ requestType: RequestType, path: String, cacheName: String? = nil, parameterType: ParameterType?, parameters: Any?, parts: [FormDataPart]?, responseType: ResponseType, completion: @escaping(_ response: Any?, _ headers: [AnyHashable: Any], _ error: NSError?) -> ()) -> String {
var requestID = UUID().uuidString

if let responses = self.fakeRequests[requestType], let fakeRequest = responses[path] {
if let fakeRequests = self.fakeRequests[requestType], let fakeRequest = fakeRequests[path] {
if fakeRequest.statusCode.statusCodeType() == .successful {
completion(fakeRequest.response, [String: Any](), nil)
} else {
let error = NSError(domain: Networking.ErrorDomain, code: fakeRequest.statusCode, userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: fakeRequest.statusCode)])
let error = NSError(domain: Networking.domain, code: fakeRequest.statusCode, userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: fakeRequest.statusCode)])
completion(fakeRequest.response, [String: Any](), error)
}
} else {
Expand Down Expand Up @@ -552,7 +568,7 @@ extension Networking {
returnedData = data
}
} else {
connectionError = NSError(domain: Networking.ErrorDomain, code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)])
connectionError = NSError(domain: Networking.domain, code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)])
}
}

Expand Down
4 changes: 2 additions & 2 deletions Tests/GETTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,14 @@ class GETTests: XCTestCase {
var statusCode = 300
networking.GET("/status/\(statusCode)") { JSON, error in
XCTAssertNil(JSON)
let connectionError = NSError(domain: Networking.ErrorDomain, code: statusCode, userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)])
let connectionError = NSError(domain: Networking.domain, code: statusCode, userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)])
XCTAssertEqual(error, connectionError)
}

statusCode = 400
networking.GET("/status/\(statusCode)") { JSON, error in
XCTAssertNil(JSON)
let connectionError = NSError(domain: Networking.ErrorDomain, code: statusCode, userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)])
let connectionError = NSError(domain: Networking.domain, code: statusCode, userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)])
XCTAssertEqual(error, connectionError)
}
}
Expand Down
14 changes: 1 addition & 13 deletions Tests/ImageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class ImageTests: XCTestCase {
func testImageFromCacheForPathInCache() {
let networking = Networking(baseURL: self.baseURL)
let path = "/image/png"
Helper.removeFileIfNeeded(networking, path: path)
Networking.deleteCachedFiles()
networking.downloadImage(path) { image, error in
let image = networking.imageFromCache(path)
let pigImage = NetworkingImage.find(named: "pig.png", inBundle: Bundle(for: ImageTests.self))
Expand Down Expand Up @@ -243,16 +243,4 @@ class ImageTests: XCTestCase {
XCTAssertNil(image)
}
}

func testCacheRetrieval() {
let cache = NSCache<AnyObject, AnyObject>()
let networking = Networking(baseURL: "http://store.storeimages.cdn-apple.com", cache: cache)
let path = "/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/i/pa/ipad/pro/ipad-pro-201603-gallery3?wid=4000&amp%3Bhei=1536&amp%3Bfmt=jpeg&amp%3Bqlt=95&amp%3Bop_sharpen=0&amp%3BresMode=bicub&amp%3Bop_usm=0.5%2C0.5%2C0%2C0&amp%3BiccEmbed=0&amp%3Blayer=comp&amp%3B.v=Y7wkx0&hei=3072"

networking.downloadData(for: path) { downloadData, error in
let cacheKey = path.components(separatedBy: "?").first!
let cacheData = networking.dataFromCache(for: cacheKey)
XCTAssert(downloadData == cacheData!)
}
}
}
22 changes: 21 additions & 1 deletion Tests/NetworkingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,27 @@ class NetworkingTests: XCTestCase {
XCTAssertTrue(synchronous)
}

// Needs tests

func testDataFromCache() {
let cache = NSCache<AnyObject, AnyObject>()
let networking = Networking(baseURL: "http://store.storeimages.cdn-apple.com", cache: cache)
let path = "/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/i/pa/ipad/pro/ipad-pro-201603-gallery3?wid=4000&amp%3Bhei=1536&amp%3Bfmt=jpeg&amp%3Bqlt=95&amp%3Bop_sharpen=0&amp%3BresMode=bicub&amp%3Bop_usm=0.5%2C0.5%2C0%2C0&amp%3BiccEmbed=0&amp%3Blayer=comp&amp%3B.v=Y7wkx0&hei=3072"

networking.downloadData(for: path) { downloadData, error in
let cacheKey = path.components(separatedBy: "?").first!
let cacheData = networking.dataFromCache(for: cacheKey)
XCTAssert(downloadData == cacheData!)
}
}

func testDeleteDownloadedFiles() {
let networking = Networking(baseURL: self.baseURL)
networking.downloadImage("/image/png") { image, error in
let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let folderURL = cachesURL.appendingPathComponent(URL(string: Networking.domain)!.absoluteString)
XCTAssertTrue(FileManager.default.exists(at: folderURL))
Networking.deleteCachedFiles()
XCTAssertFalse(FileManager.default.exists(at: folderURL))
}
}
}
20 changes: 20 additions & 0 deletions iOSDemo/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder {
var window: UIWindow?
}

extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)

let controller = OptionsController(nibName: nil, bundle: nil)
let navigationController = UINavigationController(rootViewController: controller)
self.window?.rootViewController = navigationController

self.window?.makeKeyAndVisible()

return true
}
}
Loading

0 comments on commit ca86903

Please sign in to comment.