Skip to content

Commit 1c2d11a

Browse files
committed
Added data caching for movies response
1 parent 00890ef commit 1c2d11a

File tree

29 files changed

+430
-172
lines changed

29 files changed

+430
-172
lines changed

ExampleMVVM.xcodeproj/project.pbxproj

+74-30
Large diffs are not rendered by default.

ExampleMVVM/Application/DIContainer/MoviesSceneDIContainer.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ final class MoviesSceneDIContainer {
1919

2020
// MARK: - Persistent Storage
2121
lazy var moviesQueriesStorage: MoviesQueriesStorage = CoreDataMoviesQueriesStorage(maxStorageLimit: 10)
22-
22+
lazy var moviesResponseCache: MoviesResponseStorage = CoreDataMoviesResponseStorage()
23+
2324
init(dependencies: Dependencies) {
2425
self.dependencies = dependencies
2526
}
@@ -40,7 +41,7 @@ final class MoviesSceneDIContainer {
4041

4142
// MARK: - Repositories
4243
func makeMoviesRepository() -> MoviesRepository {
43-
return DefaultMoviesRepository(dataTransferService: dependencies.apiDataTransferService)
44+
return DefaultMoviesRepository(dataTransferService: dependencies.apiDataTransferService, moviesResponseCache: moviesResponseCache)
4445
}
4546
func makeMoviesQueriesRepository() -> MoviesQueriesRepository {
4647
return DefaultMoviesQueriesRepository(dataTransferService: dependencies.apiDataTransferService,

ExampleMVVM/Data/Network/DataMapping/MoviesRequestDTO+Mappings.swift renamed to ExampleMVVM/Data/Network/DataMapping/MoviesRequestDTO+Mapping.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// MoviesRequestDTO+Mappings.swift
2+
// MoviesRequestDTO+Mapping.swift
33
// ExampleMVVM
44
//
55
// Created by Oleh Kudinov on 22/03/2020.

ExampleMVVM/Data/Network/DataMapping/MoviesResponseDTO+Mappings.swift renamed to ExampleMVVM/Data/Network/DataMapping/MoviesResponseDTO+Mapping.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// MoviesPage+Decodable.swift
2+
// MoviesResponseDTO+Mapping.swift
33
// Data
44
//
55
// Created by Oleh Kudinov on 12.08.19.
@@ -31,9 +31,9 @@ extension MoviesResponseDTO {
3131
case releaseDate = "release_date"
3232
}
3333
let id: Int
34-
let title: String
34+
let title: String?
3535
let posterPath: String?
36-
let overview: String
36+
let overview: String?
3737
let releaseDate: String?
3838
}
3939
}

ExampleMVVM/Data/PersistentStorages/CoreDataStorage/CoreDataStorage.swift

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77

88
import CoreData
99

10+
enum CoreDataStorageError: Error {
11+
case readError(Error)
12+
case writeError(Error)
13+
case deleteError(Error)
14+
}
15+
1016
final class CoreDataStorage {
1117

1218
static let shared = CoreDataStorage()
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18G59b" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
2+
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
33
<entity name="MovieQueryEntity" representedClassName="MovieQueryEntity" syncable="YES" codeGenerationType="class">
4-
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
5-
<attribute name="query" optional="YES" attributeType="String" syncable="YES"/>
4+
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
5+
<attribute name="query" optional="YES" attributeType="String"/>
6+
</entity>
7+
<entity name="MovieResponseEntity" representedClassName="MovieResponseEntity" syncable="YES" codeGenerationType="class">
8+
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
9+
<attribute name="overview" optional="YES" attributeType="String"/>
10+
<attribute name="posterPath" optional="YES" attributeType="String"/>
11+
<attribute name="releaseDate" optional="YES" attributeType="String"/>
12+
<attribute name="title" optional="YES" attributeType="String"/>
13+
<relationship name="moviesResponse" maxCount="1" deletionRule="Nullify" destinationEntity="MoviesResponseEntity" inverseName="movies" inverseEntity="MoviesResponseEntity"/>
14+
</entity>
15+
<entity name="MoviesRequestEntity" representedClassName="MoviesRequestEntity" syncable="YES" codeGenerationType="class">
16+
<attribute name="page" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
17+
<attribute name="query" optional="YES" attributeType="String"/>
18+
<relationship name="response" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MoviesResponseEntity" inverseName="request" inverseEntity="MoviesResponseEntity"/>
19+
</entity>
20+
<entity name="MoviesResponseEntity" representedClassName="MoviesResponseEntity" syncable="YES" codeGenerationType="class">
21+
<attribute name="page" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
22+
<attribute name="totalPages" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
23+
<relationship name="movies" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MovieResponseEntity" inverseName="moviesResponse" inverseEntity="MovieResponseEntity"/>
24+
<relationship name="request" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MoviesRequestEntity" inverseName="response" inverseEntity="MoviesRequestEntity"/>
625
</entity>
726
<elements>
8-
<element name="MovieQueryEntity" positionX="-63" positionY="-18" width="128" height="75"/>
27+
<element name="MovieQueryEntity" positionX="6913.73828125" positionY="-748.890625" width="128" height="73"/>
28+
<element name="MoviesResponseEntity" positionX="6993.70703125" positionY="-578.1640625" width="128" height="103"/>
29+
<element name="MoviesRequestEntity" positionX="6822.30859375" positionY="-559.6484375" width="128" height="88"/>
30+
<element name="MovieResponseEntity" positionX="7148.09765625" positionY="-579.2265625" width="128" height="133"/>
931
</elements>
1032
</model>

ExampleMVVM/Data/PersistentStorages/CoreDataStorage/CoreDataMoviesQueriesStorage.swift renamed to ExampleMVVM/Data/PersistentStorages/MoviesQueriesStorage/CoreDataStorage/CoreDataMoviesQueriesStorage.swift

+11-17
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@
88
import Foundation
99
import CoreData
1010

11-
enum CoreDataMoviesQueriesStorageError: Error {
12-
case readError(Error)
13-
case writeError(Error)
14-
case deleteError(Error)
15-
}
16-
1711
final class CoreDataMoviesQueriesStorage {
1812

1913
private let maxStorageLimit: Int
@@ -27,13 +21,13 @@ final class CoreDataMoviesQueriesStorage {
2721
// MARK: - Private
2822
private func cleanUpQueries(for query: MovieQuery, inContext context: NSManagedObjectContext) throws {
2923

30-
let request: NSFetchRequest<MovieQueryEntity> = MovieQueryEntity.fetchRequest()
24+
let request: NSFetchRequest = MovieQueryEntity.fetchRequest()
3125
request.sortDescriptors = [NSSortDescriptor(key: #keyPath(MovieQueryEntity.createdAt),
3226
ascending: false)]
33-
let resut = try context.fetch(request)
34-
resut.filter { $0.query == query.query }.forEach { context.delete($0) }
35-
if resut.count > maxStorageLimit - 1 {
36-
Array(resut[maxStorageLimit - 1..<resut.count]).forEach { context.delete($0) }
27+
let result = try context.fetch(request)
28+
result.filter { $0.query == query.query }.forEach { context.delete($0) }
29+
if result.count > maxStorageLimit - 1 {
30+
Array(result[maxStorageLimit - 1..<result.count]).forEach { context.delete($0) }
3731
}
3832
}
3933
}
@@ -44,15 +38,15 @@ extension CoreDataMoviesQueriesStorage: MoviesQueriesStorage {
4438

4539
coreDataStorage.performBackgroundTask { context in
4640
do {
47-
let request: NSFetchRequest<MovieQueryEntity> = MovieQueryEntity.fetchRequest()
41+
let request: NSFetchRequest = MovieQueryEntity.fetchRequest()
4842
request.sortDescriptors = [NSSortDescriptor(key: #keyPath(MovieQueryEntity.createdAt),
4943
ascending: false)]
5044
request.fetchLimit = maxCount
51-
let resut = try context.fetch(request).map(MovieQuery.init)
45+
let result = try context.fetch(request).map { $0.mapToDomain() }
5246

53-
completion(.success(resut))
47+
completion(.success(result))
5448
} catch {
55-
completion(.failure(CoreDataMoviesQueriesStorageError.readError(error)))
49+
completion(.failure(CoreDataStorageError.readError(error)))
5650
print(error)
5751
}
5852
}
@@ -67,9 +61,9 @@ extension CoreDataMoviesQueriesStorage: MoviesQueriesStorage {
6761
let entity = MovieQueryEntity(movieQuery: query, insertInto: context)
6862
try context.save()
6963

70-
completion(.success(MovieQuery(movieQueryEntity: entity)))
64+
completion(.success(entity.mapToDomain()))
7165
} catch {
72-
completion(.failure(CoreDataMoviesQueriesStorageError.writeError(error)))
66+
completion(.failure(CoreDataStorageError.writeError(error)))
7367
print(error)
7468
}
7569
}

ExampleMVVM/Data/PersistentStorages/CoreDataStorage/EntityMapping/MovieQueryEntity+Mappings.swift renamed to ExampleMVVM/Data/PersistentStorages/MoviesQueriesStorage/CoreDataStorage/EntityMapping/MovieQueryEntity+Mapping.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// MovieQueryEntityMapping.swift
2+
// MovieQueryEntity+Mapping.swift
33
// ExampleMVVM
44
//
55
// Created by Oleh Kudinov on 16.08.19.
@@ -16,8 +16,8 @@ extension MovieQueryEntity {
1616
}
1717
}
1818

19-
extension MovieQuery {
20-
init(movieQueryEntity: MovieQueryEntity) {
21-
query = movieQueryEntity.query ?? ""
19+
extension MovieQueryEntity {
20+
func mapToDomain() -> MovieQuery {
21+
return .init(query: query ?? "")
2222
}
2323
}

ExampleMVVM/Data/PersistentStorages/UserDefaultsStorage/DataMapping/MovieQueryUDS+Mappings.swift renamed to ExampleMVVM/Data/PersistentStorages/MoviesQueriesStorage/UserDefaultsStorage/DataMapping/MovieQueryUDS+Mapping.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ extension MovieQueryUDS {
2222
}
2323
}
2424

25-
extension MovieQuery {
26-
init(movieQueryUDS: MovieQueryUDS) {
27-
query = movieQueryUDS.query
25+
extension MovieQueryUDS {
26+
func mapToDomain() -> MovieQuery {
27+
return .init(query: query)
2828
}
2929
}

ExampleMVVM/Data/PersistentStorages/UserDefaultsStorage/UserDefaultsMoviesQueriesStorage.swift renamed to ExampleMVVM/Data/PersistentStorages/MoviesQueriesStorage/UserDefaultsStorage/UserDefaultsMoviesQueriesStorage.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ final class UserDefaultsMoviesQueriesStorage {
1919

2020
private func fetchMoviesQuries() -> [MovieQuery] {
2121
if let queriesData = userDefaults.object(forKey: recentsMoviesQueriesKey) as? Data {
22-
let decoder = JSONDecoder()
23-
if let movieQueryList = try? decoder.decode(MovieQueriesListUDS.self, from: queriesData) {
24-
return movieQueryList.list.map(MovieQuery.init)
22+
if let movieQueryList = try? JSONDecoder().decode(MovieQueriesListUDS.self, from: queriesData) {
23+
return movieQueryList.list.map { $0.mapToDomain() }
2524
}
2625
}
2726
return []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// CoreDataMoviesResponseStorage.swift
3+
// ExampleMVVM
4+
//
5+
// Created by Oleh Kudinov on 05/04/2020.
6+
//
7+
8+
import Foundation
9+
import CoreData
10+
11+
final class CoreDataMoviesResponseStorage {
12+
13+
private let coreDataStorage: CoreDataStorage
14+
15+
init(coreDataStorage: CoreDataStorage = CoreDataStorage.shared) {
16+
self.coreDataStorage = coreDataStorage
17+
}
18+
19+
// MARK: - Private
20+
21+
private func fetchRequest(for requestDto: MoviesRequestDTO) -> NSFetchRequest<MoviesRequestEntity> {
22+
let request: NSFetchRequest = MoviesRequestEntity.fetchRequest()
23+
request.predicate = NSPredicate(format: "query = %@ AND page = %d", requestDto.query, requestDto.page)
24+
return request
25+
}
26+
27+
private func deleteResponse(for requestDto: MoviesRequestDTO, in context: NSManagedObjectContext) {
28+
let request = fetchRequest(for: requestDto)
29+
30+
do {
31+
if let result = try context.fetch(request).first {
32+
context.delete(result)
33+
}
34+
} catch {
35+
print(error)
36+
}
37+
}
38+
}
39+
40+
extension CoreDataMoviesResponseStorage: MoviesResponseStorage {
41+
42+
func fetchMoviesResponse(for requestDto: MoviesRequestDTO, completion: @escaping (Result<MoviesResponseDTO?, CoreDataStorageError>) -> Void) {
43+
coreDataStorage.performBackgroundTask { context in
44+
do {
45+
let request = self.fetchRequest(for: requestDto)
46+
47+
let result = try context.fetch(request).first
48+
49+
completion(.success(result?.response?.toDTO()))
50+
} catch {
51+
completion(.failure(CoreDataStorageError.readError(error)))
52+
print(error)
53+
}
54+
}
55+
}
56+
57+
func saveMoviesResponse(_ responseDto: MoviesResponseDTO, for requestDto: MoviesRequestDTO) {
58+
coreDataStorage.performBackgroundTask { context in
59+
do {
60+
self.deleteResponse(for: requestDto, in: context)
61+
62+
let requestEntity = requestDto.toEntity(in: context)
63+
requestEntity.response = responseDto.toEntity(in: context)
64+
65+
try context.save()
66+
} catch {
67+
print(error)
68+
}
69+
}
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// MoviesResponseEntity+Mapping.swift
3+
// ExampleMVVM
4+
//
5+
// Created by Oleh Kudinov on 05/04/2020.
6+
//
7+
8+
import Foundation
9+
import CoreData
10+
11+
extension MoviesResponseEntity {
12+
func toDTO() -> MoviesResponseDTO {
13+
return .init(page: Int(page),
14+
totalPages: Int(totalPages),
15+
movies: movies?.allObjects.map { ($0 as! MovieResponseEntity).toDTO() } ?? [])
16+
}
17+
}
18+
19+
extension MovieResponseEntity {
20+
func toDTO() -> MoviesResponseDTO.MovieDTO {
21+
return .init(id: Int(id),
22+
title: title,
23+
posterPath: posterPath,
24+
overview: overview,
25+
releaseDate: releaseDate)
26+
}
27+
}
28+
29+
extension MoviesRequestDTO {
30+
func toEntity(in context: NSManagedObjectContext) -> MoviesRequestEntity {
31+
let entity: MoviesRequestEntity = .init(context: context)
32+
entity.query = query
33+
entity.page = Int32(page)
34+
return entity
35+
}
36+
}
37+
38+
extension MoviesResponseDTO {
39+
func toEntity(in context: NSManagedObjectContext) -> MoviesResponseEntity {
40+
let entity: MoviesResponseEntity = .init(context: context)
41+
entity.page = Int32(page)
42+
entity.totalPages = Int32(totalPages)
43+
movies.forEach {
44+
entity.addToMovies($0.toEntity(in: context))
45+
}
46+
return entity
47+
}
48+
}
49+
50+
extension MoviesResponseDTO.MovieDTO {
51+
func toEntity(in context: NSManagedObjectContext) -> MovieResponseEntity {
52+
let entity: MovieResponseEntity = .init(context: context)
53+
entity.id = Int64(id)
54+
entity.title = title
55+
entity.posterPath = posterPath
56+
entity.overview = overview
57+
entity.releaseDate = releaseDate
58+
return entity
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// MoviesResponseStorage.swift
3+
// ExampleMVVM
4+
//
5+
// Created by Oleh Kudinov on 05/04/2020.
6+
//
7+
8+
import Foundation
9+
10+
protocol MoviesResponseStorage {
11+
func fetchMoviesResponse(for request: MoviesRequestDTO, completion: @escaping (Result<MoviesResponseDTO?, CoreDataStorageError>) -> Void)
12+
func saveMoviesResponse(_ responseDto: MoviesResponseDTO, for requestDto: MoviesRequestDTO)
13+
}

0 commit comments

Comments
 (0)