Skip to content

Commit

Permalink
Implement caching for Posts
Browse files Browse the repository at this point in the history
  • Loading branch information
sergdort committed Jul 22, 2017
1 parent 0aa09a0 commit 7b82e52
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 39 deletions.
84 changes: 51 additions & 33 deletions Network/Cache/Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ func abstractMethod() -> Never {
fatalError("abstract method")
}

class AbstractChache<T> {
class AbstractCache<T> {
func save(object: T) -> Completable {
abstractMethod()
}
func save(objects: [T]) -> Completable {
abstractMethod()
}

func fetchObject() -> Maybe<T> {
func fetch(withID id: String) -> Maybe<T> {
abstractMethod()
}

Expand All @@ -22,7 +22,7 @@ class AbstractChache<T> {
}
}

final class Cache<T: Encodable>: AbstractChache<T> where T == T.Encoder.DomainType {
final class Cache<T: Encodable>: AbstractCache<T> where T == T.Encoder.DomainType {
enum Error: Swift.Error {
case saveObject(T)
case saveObjects([T])
Expand All @@ -38,23 +38,22 @@ final class Cache<T: Encodable>: AbstractChache<T> where T == T.Encoder.DomainTy
}
}

private let objectPath: String
private let objectsPath: String
private let path: String
private let chacheScheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "com.CleanAchitecture.Network.Cache.queue")

init(objectPath: String, objectsPath: String) {
self.objectPath = objectsPath
self.objectsPath = objectsPath
init(path: String) {
self.path = path
}

override func save(object: T) -> Completable {
return Completable.create { (observer) -> Disposable in
guard let url = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask).first else {
observer(.error(Error.saveObject(object)))
observer(.completed)
return Disposables.create()
}
let path = url.appendingPathComponent(self.objectPath)
let path = url.appendingPathComponent(self.path)
.appendingPathComponent("\(object.uid)")
.appendingPathComponent(FileNames.objectFileName)
.absoluteString

Expand All @@ -70,33 +69,34 @@ final class Cache<T: Encodable>: AbstractChache<T> where T == T.Encoder.DomainTy

override func save(objects: [T]) -> Completable {
return Completable.create { (observer) -> Disposable in
guard let url = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask).first else {
observer(.error(Error.saveObjects(objects)))
return Disposables.create()
guard let directoryURL = self.directoryURL() else {
observer(.completed)
return Disposables.create()
}
let path = url.appendingPathComponent(self.objectsPath)
let path = directoryURL
.appendingPathComponent(FileNames.objectsFileName)
.absoluteString

if NSKeyedArchiver.archiveRootObject(objects.map{ $0.encoder } , toFile: path) {
self.createDirectoryIfNeeded(at: directoryURL)
do {
try NSKeyedArchiver.archivedData(withRootObject: objects.map{ $0.encoder })
.write(to: path)
observer(.completed)
} else {
observer(.error(Error.saveObjects(objects)))
} catch {
observer(.error(error))
}

return Disposables.create()
}.subscribeOn(chacheScheduler)
}

override func fetchObject() -> Maybe<T> {
override func fetch(withID id: String) -> Maybe<T> {
return Maybe<T>.create { (observer) -> Disposable in
guard let url = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask).first else {
observer(.completed)
return Disposables.create()
}
let path = url.appendingPathComponent(self.objectPath)
let path = url.appendingPathComponent(self.path)
.appendingPathComponent("\(id)")
.appendingPathComponent(FileNames.objectFileName)
.absoluteString

Expand All @@ -111,21 +111,39 @@ final class Cache<T: Encodable>: AbstractChache<T> where T == T.Encoder.DomainTy

override func fetchObjects() -> Maybe<[T]> {
return Maybe<[T]>.create { (observer) -> Disposable in
guard let url = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask).first else {
observer(.completed)
return Disposables.create()
guard let directoryURL = self.directoryURL() else {
observer(.completed)
return Disposables.create()
}
let path = url.appendingPathComponent(self.objectPath)
.appendingPathComponent(FileNames.objectFileName)
.absoluteString

guard let objects = NSKeyedUnarchiver.unarchiveObject(withFile: path) as? [T.Encoder] else {
let fileURL = directoryURL
.appendingPathComponent(FileNames.objectsFileName)
guard let objects = NSKeyedUnarchiver.unarchiveObject(withFile: fileURL.path) as? [T.Encoder] else {
observer(.completed)
return Disposables.create()
}
observer(MaybeEvent.success(objects.map { $0.asDomain() }))
let domainObjects = objects.map { obj in
return obj.asDomain()
}
observer(MaybeEvent.success(domainObjects))
return Disposables.create()
}.subscribeOn(chacheScheduler)
}.subscribeOn(chacheScheduler)
}

private func directoryURL() -> URL? {
return FileManager.default
.urls(for: .documentDirectory,
in: .userDomainMask)
.first?
.appendingPathComponent(path)
}

private func createDirectoryIfNeeded(at url: URL) {
do {
try FileManager.default.createDirectory(at: url,
withIntermediateDirectories: true,
attributes: nil)
} catch {
print("Cache Error createDirectoryIfNeeded \(error)")
}
}
}
8 changes: 6 additions & 2 deletions Network/Entries/Encodable/Encodable.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
protocol DomainConvertibleType {
associatedtype DomainType
associatedtype DomainType: Identifiable

init(with domain: DomainType)

func asDomain() -> DomainType
}

typealias DomainConvertibleCoding = NSCoding & DomainConvertibleType
protocol Identifiable {
var uid: String { get }
}

typealias DomainConvertibleCoding = DomainConvertibleType

protocol Encodable {
associatedtype Encoder: DomainConvertibleCoding
Expand Down
2 changes: 1 addition & 1 deletion Network/Entries/Post+Mapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Domain
import ObjectMapper

extension Post: ImmutableMappable {
extension Post: ImmutableMappable, Identifiable {

// JSON -> Object
public init(map: Map) throws {
Expand Down
24 changes: 22 additions & 2 deletions Network/UseCases/AllPostsUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,32 @@ import RxSwift

final class AllPostsUseCase: Domain.AllPostsUseCase {
private let network: PostsNetwork
private let cache: AbstractCache<Post>

init(network: PostsNetwork) {
init(network: PostsNetwork, cache: AbstractCache<Post>) {
self.network = network
self.cache = cache
}

func posts() -> Observable<[Post]> {
return network.fetchPosts()
let fetchPosts = cache.fetchObjects().asObservable()
let stored = network.fetchPosts()
.flatMap {
return self.cache.save(objects: $0)
.asObservable()
.map(to: [Post].self)
.concat(Observable.just($0))
}

return fetchPosts.concat(stored)
}
}

struct MapFromNever: Error {}
extension ObservableType where E == Never {
func map<T>(to: T.Type) -> Observable<T> {
return self.flatMap { _ in
return Observable<T>.error(MapFromNever())
}
}
}
3 changes: 2 additions & 1 deletion Network/UseCases/UseCaseProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public final class UseCaseProvider: Domain.UseCaseProvider {
}

public func makeAllPostsUseCase() -> Domain.AllPostsUseCase {
return AllPostsUseCase(network: networkProvider.makePostsNetwork())
return AllPostsUseCase(network: networkProvider.makePostsNetwork(),
cache: Cache<Post>(path: "allPosts"))
}

public func makeCreatePostUseCase() -> Domain.SavePostUseCase {
Expand Down

0 comments on commit 7b82e52

Please sign in to comment.