diff --git a/Cineaste.xcodeproj/project.pbxproj b/Cineaste.xcodeproj/project.pbxproj index 6f5af700..b3a73ef3 100644 --- a/Cineaste.xcodeproj/project.pbxproj +++ b/Cineaste.xcodeproj/project.pbxproj @@ -61,7 +61,6 @@ 3F7D1D4F23D4A84900BA530F /* SearchMovieDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F7D1D4E23D4A84900BA530F /* SearchMovieDataSource.swift */; }; 3F8065BC238AE3220087D6EA /* DateFormatter+Cineaste.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8065BB238AE3220087D6EA /* DateFormatter+Cineaste.swift */; }; 3F80F27222359E5C007E03C5 /* UIView+SlideIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F80F27122359E5C007E03C5 /* UIView+SlideIn.swift */; }; - 3F85488A21039778007A322E /* IndexPath+Last.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F85488821039778007A322E /* IndexPath+Last.swift */; }; 3F8CEBEE2250A229001916BD /* Screenshots+ScrollToElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8CEBED2250A229001916BD /* Screenshots+ScrollToElement.swift */; }; 3F98C82621D262A500F926E9 /* Importer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F98C82521D262A500F926E9 /* Importer.swift */; }; 3F9FACF423A8290000D93464 /* SpotlightIndexing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F9FACF323A8290000D93464 /* SpotlightIndexing.swift */; }; @@ -90,6 +89,10 @@ 3FF095D22103258D00ADFB86 /* SettingsViewController+MFMailCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF095D02103258D00ADFB86 /* SettingsViewController+MFMailCompose.swift */; }; 3FF095D5210325D100ADFB86 /* SettingsViewController+UIDocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF095D3210325D100ADFB86 /* SettingsViewController+UIDocumentPicker.swift */; }; 3FF0A2F821D3C2BD00BF9E83 /* ImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF0A2F721D3C2BD00BF9E83 /* ImporterTests.swift */; }; + 3FF3E960249E687600CA472F /* SearchAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF3E95F249E687600CA472F /* SearchAction.swift */; }; + 3FF3E962249E692000CA472F /* SearchReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF3E961249E691F00CA472F /* SearchReducer.swift */; }; + 3FF3E964249E6A8E00CA472F /* SearchReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF3E963249E6A8E00CA472F /* SearchReducerTests.swift */; }; + 3FF3E966249E6CCD00CA472F /* SearchThunks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF3E965249E6CCD00CA472F /* SearchThunks.swift */; }; 3FF790E620DE274A007B7D37 /* Instantiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F83BE771FF39DFC00E584E9 /* Instantiable.swift */; }; 3FF790E720DE274A007B7D37 /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FD886451FF2C46400A86ACF /* Appearance.swift */; }; 3FF790E820DE274A007B7D37 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953182F6203E17AF00A331A0 /* Constants.swift */; }; @@ -117,7 +120,6 @@ 3FF7910B20DE274A007B7D37 /* PosterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FD4FF8220B74C6500538BA3 /* PosterViewController.swift */; }; 3FF7910C20DE274A007B7D37 /* UITableView+GenericDequeue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1F29E920B1C4AB00A9282B /* UITableView+GenericDequeue.swift */; }; 3FF7910F20DE274A007B7D37 /* WatchlistMovieCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1292F91FD7CC1100ED5593 /* WatchlistMovieCell.swift */; }; - 3FF7911120DE274A007B7D37 /* SearchMoviesViewController+Webservice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FD722CD20264BF80046DEAC /* SearchMoviesViewController+Webservice.swift */; }; 3FF7911220DE274A007B7D37 /* SearchMoviesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959E8B761FABC6F50004E8C3 /* SearchMoviesViewController.swift */; }; 3FF7911520DE274A007B7D37 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AE259F1F9531E20067F5F5 /* AppDelegate.swift */; }; 3FF7911620DE274A007B7D37 /* AlertMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FDB14532025B80F0063E980 /* AlertMessage.swift */; }; @@ -141,7 +143,6 @@ 3FF7913220DE274A007B7D37 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3FD6AA7820BA986900B72ABC /* Localizable.stringsdict */; }; 3FF798422205A9830055CCBB /* SettingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5E0D90203090C300E091BE /* SettingItem.swift */; }; 3FF798432205A9870055CCBB /* TextViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5E0D922030988E00E091BE /* TextViewType.swift */; }; - 3FF7F1E223894A8E00EC44DD /* SearchMoviesViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF7F1E123894A8E00EC44DD /* SearchMoviesViewControllerTests.swift */; }; 3FFEC6CD236DCF91005E7EED /* Storyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFEC6CC236DCF91005E7EED /* Storyboard.swift */; }; 4E20FD25226851D3006EB4B8 /* PersistenceSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E20FD24226851D3006EB4B8 /* PersistenceSubscriber.swift */; }; 4E779FC122690151007B278D /* SelectionReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E779FC022690151007B278D /* SelectionReducerTests.swift */; }; @@ -256,7 +257,6 @@ 3F80F27122359E5C007E03C5 /* UIView+SlideIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+SlideIn.swift"; sourceTree = ""; }; 3F83BE771FF39DFC00E584E9 /* Instantiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instantiable.swift; sourceTree = ""; }; 3F83BE971FF3BF7000E584E9 /* Segue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segue.swift; sourceTree = ""; }; - 3F85488821039778007A322E /* IndexPath+Last.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IndexPath+Last.swift"; sourceTree = ""; }; 3F8644A81FFA69E70046114A /* MoviesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesViewController.swift; sourceTree = ""; }; 3F8644AA1FFA72EE0046114A /* MoviesTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesTabBarController.swift; sourceTree = ""; }; 3F8644B21FFA96370046114A /* MoviesViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesViewControllerTests.swift; sourceTree = ""; }; @@ -293,7 +293,6 @@ 3FD4FF8220B74C6500538BA3 /* PosterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterViewController.swift; sourceTree = ""; }; 3FD6AA7920BA986900B72ABC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; 3FD722CB20264B720046DEAC /* SearchMoviesViewController+UITableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchMoviesViewController+UITableView.swift"; sourceTree = ""; }; - 3FD722CD20264BF80046DEAC /* SearchMoviesViewController+Webservice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchMoviesViewController+Webservice.swift"; sourceTree = ""; }; 3FD886451FF2C46400A86ACF /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; 3FDB14532025B80F0063E980 /* AlertMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertMessage.swift; sourceTree = ""; }; 3FDEF4742103368300955129 /* DebugPrint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPrint.swift; sourceTree = ""; }; @@ -311,8 +310,11 @@ 3FF095D02103258D00ADFB86 /* SettingsViewController+MFMailCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsViewController+MFMailCompose.swift"; sourceTree = ""; }; 3FF095D3210325D100ADFB86 /* SettingsViewController+UIDocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsViewController+UIDocumentPicker.swift"; sourceTree = ""; }; 3FF0A2F721D3C2BD00BF9E83 /* ImporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImporterTests.swift; sourceTree = ""; }; + 3FF3E95F249E687600CA472F /* SearchAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAction.swift; sourceTree = ""; }; + 3FF3E961249E691F00CA472F /* SearchReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchReducer.swift; sourceTree = ""; }; + 3FF3E963249E6A8E00CA472F /* SearchReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchReducerTests.swift; sourceTree = ""; }; + 3FF3E965249E6CCD00CA472F /* SearchThunks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchThunks.swift; sourceTree = ""; }; 3FF7913720DE274A007B7D37 /* Cineaste App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Cineaste App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3FF7F1E123894A8E00EC44DD /* SearchMoviesViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchMoviesViewControllerTests.swift; sourceTree = ""; }; 3FFEC6CC236DCF91005E7EED /* Storyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storyboard.swift; sourceTree = ""; }; 4E20FD24226851D3006EB4B8 /* PersistenceSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceSubscriber.swift; sourceTree = ""; }; 4E779FC022690151007B278D /* SelectionReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionReducerTests.swift; sourceTree = ""; }; @@ -480,10 +482,10 @@ 3F229213225A41F6001D4358 /* MovieReducerTests.swift */, 3F8644B21FFA96370046114A /* MoviesViewControllerTests.swift */, 3F02493C238893C100BA545C /* MovieTests.swift */, - 3F4701C5238C8312006C1577 /* PagedMovieResultTests.swift */, 3FC1AA6F2385E23B00E24C16 /* MovieThunksTests.swift */, + 3F4701C5238C8312006C1577 /* PagedMovieResultTests.swift */, 4ED2FC822266F586007BDE06 /* PersistenceTests.swift */, - 3FF7F1E123894A8E00EC44DD /* SearchMoviesViewControllerTests.swift */, + 3FF3E963249E6A8E00CA472F /* SearchReducerTests.swift */, 4E779FC022690151007B278D /* SelectionReducerTests.swift */, 4EABD57D2129C622007F20D9 /* SeparatorViewTests.swift */, 3F585B96203B5A58002F6E07 /* SettingsDetailViewControllerTests.swift */, @@ -516,7 +518,6 @@ children = ( 3FBD8F911FF3FC5B00AD5F02 /* Date+Formatter.swift */, 3F8065BB238AE3220087D6EA /* DateFormatter+Cineaste.swift */, - 3F85488821039778007A322E /* IndexPath+Last.swift */, 3FAA3BBB20826B1D00726DE2 /* Int16+Formatter.swift */, 3FEC13E02391C16A0078637B /* KeyedDecodingContainerProtocol+Decoding.swift */, 3F9806372055250F00B92A27 /* String+Cineaste.swift */, @@ -554,6 +555,7 @@ 4EB512012268B07600CBDC7E /* AppReducer.swift */, 3F22920F225A3CDE001D4358 /* MovieReducer.swift */, 4EB512032268B1FD00CBDC7E /* SelectionReducer.swift */, + 3FF3E961249E691F00CA472F /* SearchReducer.swift */, ); path = Reducer; sourceTree = ""; @@ -562,6 +564,7 @@ isa = PBXGroup; children = ( 3FC1AA6D2385DFA400E24C16 /* MovieThunks.swift */, + 3FF3E965249E6CCD00CA472F /* SearchThunks.swift */, ); path = Thunks; sourceTree = ""; @@ -571,6 +574,7 @@ children = ( 3F22920D225A3C4B001D4358 /* MovieAction.swift */, 3FC1AA742385E4EA00E24C16 /* SelectionAction.swift */, + 3FF3E95F249E687600CA472F /* SearchAction.swift */, ); path = Actions; sourceTree = ""; @@ -600,7 +604,6 @@ 3FEEE8E6222BBC6A00EB1649 /* SearchMoviesViewController+SwipeAction.swift */, 6409298F22DBAB0C0011AB96 /* SearchMoviesViewController+SwipeHint.swift */, 3FD722CB20264B720046DEAC /* SearchMoviesViewController+UITableView.swift */, - 3FD722CD20264BF80046DEAC /* SearchMoviesViewController+Webservice.swift */, ); path = SearchMovies; sourceTree = ""; @@ -1027,7 +1030,7 @@ 3FC1AA702385E23B00E24C16 /* MovieThunksTests.swift in Sources */, 3F701B40218640F80033562B /* MoviesViewControllerTests.swift in Sources */, 3F187F8F22632DC000B7A9CF /* Movie+Testing.swift in Sources */, - 3FF7F1E223894A8E00EC44DD /* SearchMoviesViewControllerTests.swift in Sources */, + 3FF3E964249E6A8E00CA472F /* SearchReducerTests.swift in Sources */, 4ED2FC832266F586007BDE06 /* PersistenceTests.swift in Sources */, 3F229214225A41F6001D4358 /* MovieReducerTests.swift in Sources */, 3F1F419E2381499800B1C0FD /* LocalizedReleaseDateTests.swift in Sources */, @@ -1047,6 +1050,7 @@ 3FF790E820DE274A007B7D37 /* Constants.swift in Sources */, 3FC1AA6E2385DFA400E24C16 /* MovieThunks.swift in Sources */, 3FA6C9D920F7DCEC0050368E /* CustomSafariViewController.swift in Sources */, + 3FF3E960249E687600CA472F /* SearchAction.swift in Sources */, 3FF790E920DE274A007B7D37 /* PagedMovieResult.swift in Sources */, 3F7ABD5520F5C710004B069E /* Movie+Networking.swift in Sources */, 3F3E6BE9222B0C5500F317E8 /* SearchMoviesCell.swift in Sources */, @@ -1074,7 +1078,6 @@ 3FF790F920DE274A007B7D37 /* TextView.swift in Sources */, 3FF790FA20DE274A007B7D37 /* Label.swift in Sources */, 3FEC13E12391C16A0078637B /* KeyedDecodingContainerProtocol+Decoding.swift in Sources */, - 3F85488A21039778007A322E /* IndexPath+Last.swift in Sources */, 3FF790FC20DE274A007B7D37 /* SearchMoviesViewController+UITableView.swift in Sources */, 3FA48FA020FA7C14002F7665 /* SearchController.swift in Sources */, 3FF790FD20DE274A007B7D37 /* String+VariantFittingPresentationWidth.swift in Sources */, @@ -1082,6 +1085,7 @@ 3FBD9F71222C7A6200DD9248 /* WatchState.swift in Sources */, 3FF798432205A9870055CCBB /* TextViewType.swift in Sources */, 3F80F27222359E5C007E03C5 /* UIView+SlideIn.swift in Sources */, + 3FF3E962249E692000CA472F /* SearchReducer.swift in Sources */, 3FF790FF20DE274A007B7D37 /* Resource.swift in Sources */, 3FF7910020DE274A007B7D37 /* ApiKeyStore.swift in Sources */, 3F8065BC238AE3220087D6EA /* DateFormatter+Cineaste.swift in Sources */, @@ -1103,7 +1107,7 @@ 3FF7910C20DE274A007B7D37 /* UITableView+GenericDequeue.swift in Sources */, 3FF7910F20DE274A007B7D37 /* WatchlistMovieCell.swift in Sources */, 3FF798422205A9830055CCBB /* SettingItem.swift in Sources */, - 3FF7911120DE274A007B7D37 /* SearchMoviesViewController+Webservice.swift in Sources */, + 3FF3E966249E6CCD00CA472F /* SearchThunks.swift in Sources */, 3FF7911220DE274A007B7D37 /* SearchMoviesViewController.swift in Sources */, 3FF095CF2103256100ADFB86 /* SettingsViewController+UITableView.swift in Sources */, 3FF7911520DE274A007B7D37 /* AppDelegate.swift in Sources */, diff --git a/Cineaste/Extension/IndexPath+Last.swift b/Cineaste/Extension/IndexPath+Last.swift deleted file mode 100644 index c9bddc8a..00000000 --- a/Cineaste/Extension/IndexPath+Last.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// IndexPath+Last.swift -// Cineaste -// -// Created by Felizia Bernutz on 21.07.18. -// Copyright © 2018 spacepandas.de. All rights reserved. -// - -import Foundation - -extension IndexPath { - func isLast(of numberOfElements: Int) -> Bool { - row == numberOfElements - 1 - } -} diff --git a/Cineaste/ReSwift/Actions/SearchAction.swift b/Cineaste/ReSwift/Actions/SearchAction.swift new file mode 100644 index 00000000..cc3170e0 --- /dev/null +++ b/Cineaste/ReSwift/Actions/SearchAction.swift @@ -0,0 +1,22 @@ +// +// SearchAction.swift +// Cineaste App +// +// Created by Felizia Bernutz on 20.06.20. +// Copyright © 2020 spacepandas.de. All rights reserved. +// + +import ReSwift +import Foundation + +enum SearchAction: Action { + case updateSearchQuery(query: String) + case selectGenre(genre: Genre) + case deselectGenre(genre: Genre) + case showNextPage + case setInitialSearchResult(result: [Movie]) + case updateSearchResult(result: [Movie]) + case updateTotalResults(Int) + case updateNetworkRequest(URLSessionTask?) + case resetSearch +} diff --git a/Cineaste/ReSwift/AppState.swift b/Cineaste/ReSwift/AppState.swift index 3c429b9e..9a22a90e 100644 --- a/Cineaste/ReSwift/AppState.swift +++ b/Cineaste/ReSwift/AppState.swift @@ -7,6 +7,7 @@ // import ReSwift +import Foundation struct AppState: StateType, Equatable { var movies: Set = [] @@ -19,8 +20,36 @@ struct AppState: StateType, Equatable { } var selectedMovieState = SelectedMovieState() + var searchState = SearchState() } struct SelectedMovieState: Equatable { var movie: Movie? } + +struct SearchState: Equatable { + var selectedGenres: [Genre] = [] + var searchQuery: String = "" + var currentPage: Int = 1 + var initialSearchResult: [Movie] = [] + var searchResult: [Movie] = [] + var totalResults: Int? + weak var currentRequest: URLSessionTask? + + var isLoading: Bool { + currentRequest != nil + } + + var hasLoadedAllMovies: Bool { + guard let totalResults = totalResults else { return false } + return moviesToDisplay.count >= totalResults + } + + var moviesToDisplay: [Movie] { + isInitialSearch ? initialSearchResult : searchResult + } + + var isInitialSearch: Bool { + searchQuery.isEmpty && selectedGenres.isEmpty + } +} diff --git a/Cineaste/ReSwift/Reducer/AppReducer.swift b/Cineaste/ReSwift/Reducer/AppReducer.swift index 67fe2076..8552c6c3 100644 --- a/Cineaste/ReSwift/Reducer/AppReducer.swift +++ b/Cineaste/ReSwift/Reducer/AppReducer.swift @@ -11,6 +11,7 @@ import ReSwift func appReducer(action: Action, state: AppState?) -> AppState { AppState( movies: movieReducer(action: action, state: state?.movies), - selectedMovieState: selectionReducer(action: action, state: state?.selectedMovieState) + selectedMovieState: selectionReducer(action: action, state: state?.selectedMovieState), + searchState: searchReducer(action: action, state: state?.searchState) ) } diff --git a/Cineaste/ReSwift/Reducer/SearchReducer.swift b/Cineaste/ReSwift/Reducer/SearchReducer.swift new file mode 100644 index 00000000..6cd4288a --- /dev/null +++ b/Cineaste/ReSwift/Reducer/SearchReducer.swift @@ -0,0 +1,56 @@ +// +// SearchReducer.swift +// Cineaste App +// +// Created by Felizia Bernutz on 20.06.20. +// Copyright © 2020 spacepandas.de. All rights reserved. +// + +import ReSwift + +// swiftlint:disable:next cyclomatic_complexity +func searchReducer(action: Action, state: SearchState?) -> SearchState { + var state = state ?? SearchState() + + guard let action = action as? SearchAction + else { return state } + + switch action { + case .updateSearchQuery(let query): + state.searchQuery = query + state.currentPage = 1 + state.searchResult = [] + state.totalResults = nil + state.currentRequest?.cancel() + case .selectGenre(let genre): + state.selectedGenres.append(genre) + state.searchQuery = "" + state.currentPage = 1 + state.searchResult = [] + state.totalResults = nil + state.currentRequest?.cancel() + case .deselectGenre(let genre): + state.selectedGenres = state.selectedGenres.filter { $0 != genre } + state.currentPage = 1 + state.searchResult = [] + state.totalResults = nil + state.currentRequest?.cancel() + case .showNextPage: + if !state.hasLoadedAllMovies { + state.currentPage += 1 + } + case .updateSearchResult(let result): + state.searchResult += result + case .updateTotalResults(let totalResults): + state.totalResults = totalResults + case .updateNetworkRequest(let task): + state.currentRequest = task + case .setInitialSearchResult(let result): + state.initialSearchResult += result + case .resetSearch: + let initialSearchResult = state.initialSearchResult + state = SearchState(initialSearchResult: initialSearchResult) + } + + return state +} diff --git a/Cineaste/ReSwift/Thunks/SearchThunks.swift b/Cineaste/ReSwift/Thunks/SearchThunks.swift new file mode 100644 index 00000000..3871ff97 --- /dev/null +++ b/Cineaste/ReSwift/Thunks/SearchThunks.swift @@ -0,0 +1,62 @@ +// +// SearchThunks.swift +// Cineaste App +// +// Created by Felizia Bernutz on 20.06.20. +// Copyright © 2020 spacepandas.de. All rights reserved. +// + +import Dispatch +import ReSwift_Thunk + +// swiftlint:disable:next closure_body_length +let fetchSearchResults = Thunk { dispatch, getState in + guard let state = getState()?.searchState, + !state.hasLoadedAllMovies, + !state.isLoading, + let storedIDs = getState()?.storedIDs + else { return } + + let resource: Resource? + if state.isInitialSearch { + resource = Movie.latestReleases(page: state.currentPage) + } else { + resource = Movie.search(withQuery: state.searchQuery, page: state.currentPage) + } + + let task = Webservice.load(resource: resource) { result in + DispatchQueue.main.async { + dispatch(SearchAction.updateNetworkRequest(nil)) + switch result { + case .failure(let error): + break + case .success(let result): + let networkingMovies = result.results + let movies = merge(networkingMovies, with: storedIDs) + + dispatch(SearchAction.showNextPage) + dispatch(SearchAction.updateTotalResults(result.totalResults)) + let action: SearchAction = state.isInitialSearch + ? .setInitialSearchResult(result: movies) + : .updateSearchResult(result: movies) + dispatch(action) + } + } + } + dispatch(SearchAction.updateNetworkRequest(task)) +} + +private func merge(_ networkMovies: Set, with storedIDs: StoredMovieIDs) -> [Movie] { + networkMovies.map { movie in + var movie = movie + if storedIDs.watchListMovieIDs.contains(movie.id) { + movie.watched = false + } else if storedIDs.seenMovieIDs.contains(movie.id) { + movie.watched = true + } else { + movie.watched = nil + } + return movie + } + .sorted(by: SortDescriptor.sortByPopularity) +} diff --git a/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift b/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift index 3a9db9f0..496aae4b 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift @@ -9,10 +9,7 @@ import UIKit class SearchMovieDataSource: NSObject, UITableViewDataSource { - var movies: [Movie] = [] - var currentPage: Int? - var totalResults: Int? func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { movies.count @@ -22,14 +19,8 @@ class SearchMovieDataSource: NSObject, UITableViewDataSource { let cell: SearchMoviesCell = tableView.dequeueCell(identifier: SearchMoviesCell.identifier) let movie = movies[indexPath.row] - cell.configure(with: movie) - if let numberOfMovies = totalResults, - indexPath.isLast(of: numberOfMovies) { - tableView.tableFooterView = UIView() - } - return cell } } diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift index 831de0e1..e1a06159 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift @@ -7,33 +7,24 @@ // import UIKit +import ReSwift extension SearchMoviesViewController: UISearchControllerDelegate { func didDismissSearchController(_ searchController: UISearchController) { - guard let text = searchController.searchBar.text, !text.isEmpty else { return } - - dataSource.currentPage = nil - dataSource.totalResults = nil - - loadMovies { [weak self] movies in - DispatchQueue.main.async { - self?.moviesFromNetworking = movies - self?.scrollToTopCell(withAnimation: true) - } - } + store.dispatch(SearchAction.resetSearch) } } extension SearchMoviesViewController: UISearchResultsUpdating { internal func updateSearchResults(for searchController: UISearchController) { searchDelayTimer?.invalidate() - searchDelayTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { [weak self] _ in - self?.loadMovies(forQuery: searchController.searchBar.text) { movies in - DispatchQueue.main.async { - self?.moviesFromNetworking = movies - self?.scrollToTopCell(withAnimation: false) - } - } + searchDelayTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in + store.dispatch( + SearchAction.updateSearchQuery( + query: searchController.searchBar.text ?? "" + ) + ) + store.dispatch(fetchSearchResults) } } } diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift index 060b9182..ffd6c8b9 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift @@ -10,21 +10,9 @@ import UIKit extension SearchMoviesViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - guard indexPaths.first(where: { $0.isLast(of: movies.count) }) != nil, - let total = dataSource.totalResults + guard indexPaths.contains(where: { $0.row >= movies.count - 1 }) else { return } - - if total > movies.count && !isLoadingNextPage { - tableView.tableFooterView = loadingIndicatorView - isLoadingNextPage = true - - loadMovies(forQuery: resultSearchController.searchBar.text, nextPage: true) { movies in - DispatchQueue.main.async { - self.moviesFromNetworking.formUnion(movies) - self.isLoadingNextPage = false - } - } - } + store.dispatch(fetchSearchResults) } } diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+Webservice.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+Webservice.swift deleted file mode 100644 index 117eaffd..00000000 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+Webservice.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// SearchMoviesViewController+Webservice.swift -// Cineaste -// -// Created by Felizia Bernutz on 03.02.18. -// Copyright © 2018 notimeforthat.org. All rights reserved. -// - -import UIKit - -extension SearchMoviesViewController { - func loadMovies(forQuery query: String? = nil, nextPage: Bool = false, completion: @escaping (Set) -> Void) { - var pageToLoad = 1 - if let page = dataSource.currentPage, nextPage { - pageToLoad = page + 1 - } - - let resource: Resource? - if let query = query, !query.isEmpty { - resource = Movie.search(withQuery: query, page: pageToLoad) - } else { - resource = Movie.latestReleases(page: pageToLoad) - } - - Webservice.load(resource: resource) { result in - switch result { - case .failure(let error): - self.showAlert(withMessage: Alert.loadingData(with: error)) - completion([]) - case .success(let result): - self.dataSource.currentPage = result.page - self.dataSource.totalResults = result.totalResults - completion(result.results) - } - } - } - - func loadDetails(for movie: Movie, completion: @escaping (Movie?) -> Void) { - Webservice.load(resource: movie.get) { result in - switch result { - case .failure(let error): - self.showAlert(withMessage: Alert.loadingData(with: error)) - completion(nil) - case .success(let detailedMovie): - completion(detailedMovie) - } - } - } -} diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift index a3c6c819..2254b628 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift @@ -20,17 +20,6 @@ class SearchMoviesViewController: UIViewController { return resultSearchController }() - private var storedIDs = StoredMovieIDs(watchListMovieIDs: [], seenMovieIDs: []) { - didSet { - guard oldValue != storedIDs else { return } - - movies = updateMoviesWithWatchState( - with: storedIDs, - moviesFromNetworking: moviesFromNetworking - ) - } - } - let dataSource = SearchMovieDataSource() var movies: [Movie] = [] { @@ -38,18 +27,7 @@ class SearchMoviesViewController: UIViewController { guard oldValue != movies else { return } dataSource.movies = movies - updateUI() - } - } - - var moviesFromNetworking: Set = [] { - didSet { - guard oldValue != moviesFromNetworking else { return } - - movies = updateMoviesWithWatchState( - with: storedIDs, - moviesFromNetworking: moviesFromNetworking - ) + tableView.reloadData() } } @@ -63,11 +41,7 @@ class SearchMoviesViewController: UIViewController { configureTableViewController() configureSearchController() - loadMovies { [weak self] movies in - DispatchQueue.main.async { - self?.moviesFromNetworking = movies - } - } + store.dispatch(fetchSearchResults) } override func viewWillAppear(_ animated: Bool) { @@ -144,30 +118,6 @@ class SearchMoviesViewController: UIViewController { // MARK: - Custom functions - func updateMoviesWithWatchState(with storedIDs: StoredMovieIDs, moviesFromNetworking: Set) -> [Movie] { - - moviesFromNetworking.map { movie in - var movie = movie - if storedIDs.watchListMovieIDs.contains(movie.id) { - movie.watched = false - } else if storedIDs.seenMovieIDs.contains(movie.id) { - movie.watched = true - } else { - movie.watched = nil - } - return movie - } - .sorted(by: SortDescriptor.sortByPopularity) - } - - func updateUI() { - tableView.reloadData() - - if movies.isEmpty { - tableView.tableFooterView = UIView() - } - } - func scrollToTopCell(withAnimation: Bool) { guard !movies.isEmpty else { return } @@ -181,15 +131,23 @@ class SearchMoviesViewController: UIViewController { extension SearchMoviesViewController: StoreSubscriber { struct State: Equatable { - let storedIDs: StoredMovieIDs + let movies: [Movie] + let isLoading: Bool } private static func select(state: AppState) -> State { - .init(storedIDs: state.storedIDs) + .init( + movies: state.searchState.moviesToDisplay, + isLoading: state.searchState.isLoading + ) } func newState(state: State) { - storedIDs = state.storedIDs + movies = state.movies + + tableView.tableFooterView = state.isLoading + ? loadingIndicatorView + : UIView() } } diff --git a/CineasteTests/SearchMoviesViewControllerTests.swift b/CineasteTests/SearchMoviesViewControllerTests.swift deleted file mode 100644 index cfb6ade3..00000000 --- a/CineasteTests/SearchMoviesViewControllerTests.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// SearchMoviesViewControllerTests.swift -// CineasteTests -// -// Created by Felizia Bernutz on 23.11.19. -// Copyright © 2019 spacepandas.de. All rights reserved. -// - -import XCTest -@testable import Cineaste_App - -class SearchMoviesViewControllerTests: XCTestCase { - - func testMoviesWithWatchStatesShouldBeConfiguredCorrectlyForWatchlist() { - // Given - let viewController = SearchMoviesViewController.instantiate() - let movieToTest = Movie.testingWatchlist - - let storedIds = StoredMovieIDs( - watchListMovieIDs: [movieToTest.id], - seenMovieIDs: [] - ) - - var networkingMovie = movieToTest - networkingMovie.watched = false - networkingMovie.watchedDate = nil - let networkingMovies: Set = [networkingMovie] - - let expectedMovies = [networkingMovie] - - // When - let moviesWithWatchStates = viewController.updateMoviesWithWatchState( - with: storedIds, - moviesFromNetworking: networkingMovies - ) - - // Then - XCTAssertEqual(moviesWithWatchStates, expectedMovies) - } - - func testMoviesWithWatchStatesShouldBeConfiguredCorrectlyForUndefined() { - // Given - let viewController = SearchMoviesViewController.instantiate() - let movieToTest = Movie.testingWatchlist - - let storedIds = StoredMovieIDs( - watchListMovieIDs: [], - seenMovieIDs: [] - ) - - var networkingMovie = movieToTest - networkingMovie.watched = nil - networkingMovie.watchedDate = nil - let networkingMovies: Set = [networkingMovie] - - let expectedMovies = [networkingMovie] - - // When - let moviesWithWatchStates = viewController.updateMoviesWithWatchState( - with: storedIds, - moviesFromNetworking: networkingMovies - ) - - // Then - XCTAssertEqual(moviesWithWatchStates, expectedMovies) - } - - func testMoviesWithWatchStatesShouldBeConfiguredCorrectlyForSeen() { - // Given - let viewController = SearchMoviesViewController.instantiate() - let movieToTest = Movie.testingSeen - - let storedIds = StoredMovieIDs( - watchListMovieIDs: [], - seenMovieIDs: [movieToTest.id] - ) - - var networkingMovie = movieToTest - networkingMovie.watched = true - networkingMovie.watchedDate = Date(timeIntervalSince1970: 4) - let networkingMovies: Set = [networkingMovie] - - let expectedMovies = [networkingMovie] - - // When - let moviesWithWatchStates = viewController.updateMoviesWithWatchState( - with: storedIds, - moviesFromNetworking: networkingMovies - ) - - // Then - XCTAssertEqual(moviesWithWatchStates, expectedMovies) - } - -} diff --git a/CineasteTests/SearchReducerTests.swift b/CineasteTests/SearchReducerTests.swift new file mode 100644 index 00000000..2e1a3a53 --- /dev/null +++ b/CineasteTests/SearchReducerTests.swift @@ -0,0 +1,208 @@ +// +// SearchReducerTests.swift +// CineasteTests +// +// Created by Felizia Bernutz on 20.06.20. +// Copyright © 2020 spacepandas.de. All rights reserved. +// + +import XCTest +@testable import Cineaste_App + +class SearchReducerTests: XCTestCase { + + func testUpdateSearchQueryActionShouldUpdateSearchQuery() { + // Given + let query = "First Man" + let action = SearchAction.updateSearchQuery(query: query) + let state = SearchState( + currentPage: 3, + searchResult: [Movie.testing], + totalResults: 10, + currentRequest: URLSessionTask() + ) + let expectedState = SearchState( + searchQuery: query + ) + + // When + let newState = searchReducer(action: action, state: state) + + // Then + XCTAssertEqual(newState, expectedState) + } + + func testSelectGenreActionShouldAppendGenreAndResetQuery() { + // Given + let genre = Genre(id: 0, name: "Horror") + let action = SearchAction.selectGenre(genre: genre) + let state = SearchState( + searchQuery: "Hor", + currentPage: 3, + searchResult: [Movie.testing], + totalResults: 10, + currentRequest: URLSessionTask() + ) + let expectedState = SearchState( + selectedGenres: [genre] + ) + + // When + let newState = searchReducer(action: action, state: state) + + // Then + XCTAssertEqual(newState, expectedState) + } + + func testDeselectGenreActionShouldRemoveGenre() { + // Given + let genre = Genre(id: 0, name: "Horror") + let action = SearchAction.deselectGenre(genre: genre) + let state = SearchState( + selectedGenres: [genre], + currentPage: 3, + searchResult: [Movie.testing], + totalResults: 10, + currentRequest: URLSessionTask() + ) + let expectedState = SearchState() + + // When + let newState = searchReducer(action: action, state: state) + + // Then + XCTAssertEqual(newState, expectedState) + } + + func testShowNextPageActionShouldUpdateCurrentPageWhenNotLoadedAllMovies() { + // Given + let action = SearchAction.showNextPage + let state = SearchState( + currentPage: 1, + initialSearchResult: [Movie.testingWatchlist2], + searchResult: [Movie.testing], + totalResults: 10 + ) + var expectedState = state + expectedState.currentPage = 2 + + // When + let newState = searchReducer(action: action, state: state) + + // Then + XCTAssertEqual(newState, expectedState) + } + + func testShowNextPageActionShouldNotUpdateCurrentPageWhenLoadedAllMovies() { + // Given + let action = SearchAction.showNextPage + let state = SearchState( + currentPage: 1, + initialSearchResult: [Movie.testingWatchlist2], + searchResult: [Movie.testing], + totalResults: 1 + ) + let expectedState = state + + // When + let newState = searchReducer(action: action, state: state) + + // Then + XCTAssertEqual(newState, expectedState) + } + + func testUpdateSearchResultsShouldAppendSearchResults() { + // Given + let action = SearchAction.updateSearchResult(result: [Movie.testingWatchlist2]) + let state = SearchState( + searchResult: [Movie.testing] + ) + let expectedState = SearchState( + searchResult: [ + Movie.testing, + Movie.testingWatchlist2 + ] + ) + + // When + let newState = searchReducer(action: action, state: state) + + // Then + XCTAssertEqual(newState, expectedState) + } + + func testUpdateTotalResultsShouldUpdateTotalResults() { + // Given + let total = 3 + let action = SearchAction.updateTotalResults(total) + let state = SearchState( + totalResults: 5 + ) + let expectedState = SearchState( + totalResults: total + ) + + // When + let newState = searchReducer(action: action, state: state) + + // Then + XCTAssertEqual(newState, expectedState) + } + + func testUpdateNetworkRequestShouldUpdateNetworkRequest() { + // Given + let task = URLSessionTask() + let action = SearchAction.updateNetworkRequest(task) + let expectedState = SearchState( + currentRequest: task + ) + + // When + let state = searchReducer(action: action, state: SearchState()) + + // Then + XCTAssertEqual(state, expectedState) + } + + func testSetInitialSearchResultShouldAppendSearchResults() { + // Given + let action = SearchAction.setInitialSearchResult(result: [Movie.testingWatchlist2]) + let state = SearchState( + initialSearchResult: [Movie.testing] + ) + let expectedState = SearchState( + initialSearchResult: [ + Movie.testing, + Movie.testingWatchlist2 + ] + ) + + // When + let newState = searchReducer(action: action, state: state) + + // Then + XCTAssertEqual(newState, expectedState) + } + + func testResetSearchActionShouldResetSearch() { + // Given + let action = SearchAction.resetSearch + let state = SearchState( + selectedGenres: [Genre(id: 0, name: "Horror")], + searchQuery: "First Man", + currentPage: 3, + initialSearchResult: [Movie.testingWatchlist2], + searchResult: [Movie.testing], + totalResults: 10, + currentRequest: URLSessionTask() + ) + let expectedState = SearchState(initialSearchResult: [Movie.testingWatchlist2]) + + // When + let newState = searchReducer(action: action, state: state) + + // Then + XCTAssertEqual(newState, expectedState) + } + +}