From 93f6f8e4b6002d250d6b6f8b0b14139c76c227b2 Mon Sep 17 00:00:00 2001 From: Felizia Bernutz Date: Sat, 1 Feb 2020 10:30:59 +0100 Subject: [PATCH 1/8] Add SearchTokenCell (WIP) --- Cineaste.xcodeproj/project.pbxproj | 4 ++ .../Storyboards/Base.lproj/Search.storyboard | 37 +++++++++- .../SearchMovies/SearchMovieDataSource.swift | 68 ++++++++++++++++--- ...oviesViewController+SearchController.swift | 12 ++++ .../SearchMoviesViewController.swift | 2 + .../SearchMovies/SearchTokenCell.swift | 22 ++++++ 6 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 Cineaste/ViewController/SearchMovies/SearchTokenCell.swift diff --git a/Cineaste.xcodeproj/project.pbxproj b/Cineaste.xcodeproj/project.pbxproj index 6f5af700..b2e2a65b 100644 --- a/Cineaste.xcodeproj/project.pbxproj +++ b/Cineaste.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 3F7ABD5620F5C71E004B069E /* String+Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F941B6C20F527E5008A4407 /* String+Locale.swift */; }; 3F7B66C62353B81600464BCC /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F7B66C52353B81600464BCC /* ContextMenu.swift */; }; 3F7D1D4F23D4A84900BA530F /* SearchMovieDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F7D1D4E23D4A84900BA530F /* SearchMovieDataSource.swift */; }; + 3F7D1D5123D4AEB400BA530F /* SearchTokenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F7D1D5023D4AEB400BA530F /* SearchTokenCell.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 */; }; @@ -252,6 +253,7 @@ 3F75665B2381737E007E2DE8 /* UIViewController+ShareMovie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+ShareMovie.swift"; sourceTree = ""; }; 3F7B66C52353B81600464BCC /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; 3F7D1D4E23D4A84900BA530F /* SearchMovieDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchMovieDataSource.swift; sourceTree = ""; }; + 3F7D1D5023D4AEB400BA530F /* SearchTokenCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTokenCell.swift; sourceTree = ""; }; 3F8065BB238AE3220087D6EA /* DateFormatter+Cineaste.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Cineaste.swift"; sourceTree = ""; }; 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 = ""; }; @@ -601,6 +603,7 @@ 6409298F22DBAB0C0011AB96 /* SearchMoviesViewController+SwipeHint.swift */, 3FD722CB20264B720046DEAC /* SearchMoviesViewController+UITableView.swift */, 3FD722CD20264BF80046DEAC /* SearchMoviesViewController+Webservice.swift */, + 3F7D1D5023D4AEB400BA530F /* SearchTokenCell.swift */, ); path = SearchMovies; sourceTree = ""; @@ -1089,6 +1092,7 @@ 3FF095D5210325D100ADFB86 /* SettingsViewController+UIDocumentPicker.swift in Sources */, 3FF7910220DE274A007B7D37 /* SettingsCell.swift in Sources */, 3FF7910420DE274A007B7D37 /* SettingsDetailViewController.swift in Sources */, + 3F7D1D5123D4AEB400BA530F /* SearchTokenCell.swift in Sources */, 4ED2FC7D22666D89007BDE06 /* CoreDataMigrator.swift in Sources */, 3F0C1D1322A4395E00F0F52A /* ShortcutIdentifier.swift in Sources */, 3FF7910620DE274A007B7D37 /* Model.xcdatamodeld in Sources */, diff --git a/Cineaste/Storyboards/Base.lproj/Search.storyboard b/Cineaste/Storyboards/Base.lproj/Search.storyboard index 0e6deab4..f0d97f78 100644 --- a/Cineaste/Storyboards/Base.lproj/Search.storyboard +++ b/Cineaste/Storyboards/Base.lproj/Search.storyboard @@ -130,6 +130,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -166,7 +201,7 @@ - + diff --git a/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift b/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift index 3a9db9f0..8827fdc2 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift @@ -9,27 +9,79 @@ import UIKit class SearchMovieDataSource: NSObject, UITableViewDataSource { + enum Mode { + case discover + case manualSearch + } + var mode: Mode = .discover var movies: [Movie] = [] var currentPage: Int? var totalResults: Int? + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + guard mode == .manualSearch else { return nil } + + switch section { + case .tokens: + return "Genres" + case .movies: + return "Movies" + default: + return nil + } + } + + func numberOfSections(in tableView: UITableView) -> Int { + switch mode { + case .discover: + return 1 + case .manualSearch: + return 2 + } + } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - movies.count + switch (mode, section) { + case (.discover, _), + (.manualSearch, .movies): + return movies.count + case (.manualSearch, .tokens): + return 1 + default: + return 0 + } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: SearchMoviesCell = tableView.dequeueCell(identifier: SearchMoviesCell.identifier) - let movie = movies[indexPath.row] + switch (mode, indexPath.section) { + case (.discover, _), + (.manualSearch, .movies): + let cell: SearchMoviesCell = tableView.dequeueCell(identifier: SearchMoviesCell.identifier) + + let movie = movies[indexPath.row] - cell.configure(with: movie) + cell.configure(with: movie) - if let numberOfMovies = totalResults, - indexPath.isLast(of: numberOfMovies) { - tableView.tableFooterView = UIView() + if let numberOfMovies = totalResults, + indexPath.isLast(of: numberOfMovies) { + tableView.tableFooterView = UIView() + } + + return cell + case (.manualSearch, .tokens): + let cell: SearchTokenCell = tableView.dequeueCell(identifier: SearchTokenCell.identifier) + cell.configure() + return cell + default: + fatalError("The impossible has happened") } - return cell } } + +private extension Int { + static let tokens = 0 + static let movies = 1 +} diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift index 831de0e1..b0e0040c 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift @@ -24,6 +24,18 @@ extension SearchMoviesViewController: UISearchControllerDelegate { } } +extension SearchMoviesViewController: UITextFieldDelegate { + func textFieldDidBeginEditing(_ textField: UITextField) { + dataSource.mode = .manualSearch + tableView.reloadData() + } + + func textFieldDidEndEditing(_ textField: UITextField) { + dataSource.mode = .discover + tableView.reloadData() + } +} + extension SearchMoviesViewController: UISearchResultsUpdating { internal func updateSearchResults(for searchController: UISearchController) { searchDelayTimer?.invalidate() diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift index a3c6c819..fd03b92b 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift @@ -140,6 +140,8 @@ class SearchMoviesViewController: UIViewController { navigationItem.hidesSearchBarWhenScrolling = false definesPresentationContext = true + + resultSearchController.searchBar.searchTextField.delegate = self } // MARK: - Custom functions diff --git a/Cineaste/ViewController/SearchMovies/SearchTokenCell.swift b/Cineaste/ViewController/SearchMovies/SearchTokenCell.swift new file mode 100644 index 00000000..cb545d23 --- /dev/null +++ b/Cineaste/ViewController/SearchMovies/SearchTokenCell.swift @@ -0,0 +1,22 @@ +// +// SearchTokenCell.swift +// Cineaste App +// +// Created by Felizia Bernutz on 19.01.20. +// Copyright © 2020 spacepandas.de. All rights reserved. +// + +import UIKit + +class SearchTokenCell: UITableViewCell { + static let identifier = "SearchTokenCell" + + @IBOutlet weak var tokenIcon: UIImageView! + @IBOutlet weak var title: UILabel! + + func configure() { + tokenIcon.image = UIImage.searchIcon + title.text = "Horror" + } + +} From beea813bba87b6baac129de1f67448083ed93d24 Mon Sep 17 00:00:00 2001 From: Xavier Lowmiller Date: Sat, 20 Jun 2020 17:37:53 +0200 Subject: [PATCH 2/8] Introduce specific search section struct --- Cineaste.xcodeproj/project.pbxproj | 4 ++++ .../SearchMovies/SearchMovieDataSource.swift | 23 ++++++++----------- .../SearchMovies/SearchSection.swift | 4 ++++ 3 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 Cineaste/ViewController/SearchMovies/SearchSection.swift diff --git a/Cineaste.xcodeproj/project.pbxproj b/Cineaste.xcodeproj/project.pbxproj index b2e2a65b..ce7debb0 100644 --- a/Cineaste.xcodeproj/project.pbxproj +++ b/Cineaste.xcodeproj/project.pbxproj @@ -146,6 +146,7 @@ 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 */; }; + 4E903D96249E5DE600EAA4E6 /* SearchSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E903D95249E5DE600EAA4E6 /* SearchSection.swift */; }; 4EA460EF23EAF0E50014B543 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA460EE23EAF0E50014B543 /* Kingfisher */; }; 4EA460F223EAF15B0014B543 /* ReSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA460F123EAF15B0014B543 /* ReSwift */; }; 4EA460F523EAF1760014B543 /* ReSwift-Thunk in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA460F423EAF1760014B543 /* ReSwift-Thunk */; }; @@ -318,6 +319,7 @@ 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 = ""; }; + 4E903D95249E5DE600EAA4E6 /* SearchSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSection.swift; sourceTree = ""; }; 4EABD57D2129C622007F20D9 /* SeparatorViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorViewTests.swift; sourceTree = ""; }; 4EB512012268B07600CBDC7E /* AppReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = ""; }; 4EB512032268B1FD00CBDC7E /* SelectionReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionReducer.swift; sourceTree = ""; }; @@ -604,6 +606,7 @@ 3FD722CB20264B720046DEAC /* SearchMoviesViewController+UITableView.swift */, 3FD722CD20264BF80046DEAC /* SearchMoviesViewController+Webservice.swift */, 3F7D1D5023D4AEB400BA530F /* SearchTokenCell.swift */, + 4E903D95249E5DE600EAA4E6 /* SearchSection.swift */, ); path = SearchMovies; sourceTree = ""; @@ -1062,6 +1065,7 @@ 3FF790F320DE274A007B7D37 /* StoredMovie.swift in Sources */, 3F1EE5952219FD1B00F6E6E2 /* MovieRefresher.swift in Sources */, 3FF790F420DE274A007B7D37 /* Int16+Formatter.swift in Sources */, + 4E903D96249E5DE600EAA4E6 /* SearchSection.swift in Sources */, 3F22920C225A3B5C001D4358 /* AppState.swift in Sources */, 3F187F952263491500B7A9CF /* SortDescriptor.swift in Sources */, 3F36F5D2238A8CCE00267280 /* UserDefaults+Cineaste.swift in Sources */, diff --git a/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift b/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift index 8827fdc2..4fe88e25 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift @@ -22,10 +22,10 @@ class SearchMovieDataSource: NSObject, UITableViewDataSource { func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { guard mode == .manualSearch else { return nil } - switch section { - case .tokens: + switch SearchSection(rawValue: section) { + case .tokens?: return "Genres" - case .movies: + case .movies?: return "Movies" default: return nil @@ -42,11 +42,11 @@ class SearchMovieDataSource: NSObject, UITableViewDataSource { } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch (mode, section) { + switch (mode, SearchSection(rawValue: section)) { case (.discover, _), - (.manualSearch, .movies): + (.manualSearch, .movies?): return movies.count - case (.manualSearch, .tokens): + case (.manualSearch, .tokens?): return 1 default: return 0 @@ -55,9 +55,9 @@ class SearchMovieDataSource: NSObject, UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - switch (mode, indexPath.section) { + switch (mode, SearchSection(rawValue: indexPath.section)) { case (.discover, _), - (.manualSearch, .movies): + (.manualSearch, .movies?): let cell: SearchMoviesCell = tableView.dequeueCell(identifier: SearchMoviesCell.identifier) let movie = movies[indexPath.row] @@ -70,7 +70,7 @@ class SearchMovieDataSource: NSObject, UITableViewDataSource { } return cell - case (.manualSearch, .tokens): + case (.manualSearch, .tokens?): let cell: SearchTokenCell = tableView.dequeueCell(identifier: SearchTokenCell.identifier) cell.configure() return cell @@ -80,8 +80,3 @@ class SearchMovieDataSource: NSObject, UITableViewDataSource { } } - -private extension Int { - static let tokens = 0 - static let movies = 1 -} diff --git a/Cineaste/ViewController/SearchMovies/SearchSection.swift b/Cineaste/ViewController/SearchMovies/SearchSection.swift new file mode 100644 index 00000000..167d5e6e --- /dev/null +++ b/Cineaste/ViewController/SearchMovies/SearchSection.swift @@ -0,0 +1,4 @@ +enum SearchSection: Int { + case tokens = 0 + case movies = 1 +} From 420ed96d4af8118df8cfe666d83ec02485c40e17 Mon Sep 17 00:00:00 2001 From: Xavier Lowmiller Date: Sat, 20 Jun 2020 17:38:15 +0200 Subject: [PATCH 3/8] Make manualSearch only available on iOS 13 --- .../ViewController/SearchMovies/SearchMovieDataSource.swift | 5 ++++- .../SearchMoviesViewController+SearchController.swift | 6 +++++- .../SearchMovies/SearchMoviesViewController.swift | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift b/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift index 4fe88e25..86d8bcb5 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift @@ -11,6 +11,7 @@ import UIKit class SearchMovieDataSource: NSObject, UITableViewDataSource { enum Mode { case discover + @available (iOS 13, *) case manualSearch } @@ -20,7 +21,9 @@ class SearchMovieDataSource: NSObject, UITableViewDataSource { var totalResults: Int? func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - guard mode == .manualSearch else { return nil } + guard #available(iOS 13, *), + mode == .manualSearch + else { return nil } switch SearchSection(rawValue: section) { case .tokens?: diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift index b0e0040c..9de2ee16 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift @@ -26,7 +26,11 @@ extension SearchMoviesViewController: UISearchControllerDelegate { extension SearchMoviesViewController: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { - dataSource.mode = .manualSearch + if #available(iOS 13, *) { + dataSource.mode = .manualSearch + } else { + dataSource.mode = .discover + } tableView.reloadData() } diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift index fd03b92b..ea16b247 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController.swift @@ -141,7 +141,9 @@ class SearchMoviesViewController: UIViewController { definesPresentationContext = true - resultSearchController.searchBar.searchTextField.delegate = self + if #available(iOS 13.0, *) { + resultSearchController.searchBar.searchTextField.delegate = self + } } // MARK: - Custom functions From 16c81688d4ce0c805f2246e2f6c0972adf9036cf Mon Sep 17 00:00:00 2001 From: Xavier Lowmiller Date: Sat, 20 Jun 2020 17:38:40 +0200 Subject: [PATCH 4/8] Show UISearchToken in search field --- ...archMoviesViewController+UITableView.swift | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift index 060b9182..3d061a9f 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift @@ -30,8 +30,25 @@ extension SearchMoviesViewController: UITableViewDataSourcePrefetching { extension SearchMoviesViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let selectedMovie = movies[indexPath.row] - store.dispatch(SelectionAction.select(movie: selectedMovie)) - perform(segue: .showMovieDetail, sender: self) + + switch (dataSource.mode, SearchSection(rawValue: indexPath.section)) { + case (.discover, _), + (.manualSearch, .movies?): + let selectedMovie = movies[indexPath.row] + store.dispatch(SelectionAction.select(movie: selectedMovie)) + perform(segue: .showMovieDetail, sender: self) + case (.manualSearch, .tokens?): + guard #available(iOS 13.0, *), + let cell = tableView.cellForRow(at: indexPath) as? SearchTokenCell, + let genre = cell.title.text + else { return } + + let searchField = resultSearchController.searchBar.searchTextField + let searchToken = UISearchToken(icon: UIImage.moreIcon, text: genre) + searchField.insertToken(searchToken, at: 0) + searchField.tokenBackgroundColor = .cineButton + default: + break + } } } From 807a0e73646d793b1e86e05173b09d71dd0d7f5d Mon Sep 17 00:00:00 2001 From: Felizia Bernutz Date: Sat, 20 Jun 2020 18:09:47 +0200 Subject: [PATCH 5/8] Add SearchState with Action and Reducer --- Cineaste.xcodeproj/project.pbxproj | 14 +++- Cineaste/ReSwift/Actions/SearchAction.swift | 16 ++++ Cineaste/ReSwift/AppState.swift | 8 ++ Cineaste/ReSwift/Reducer/AppReducer.swift | 3 +- Cineaste/ReSwift/Reducer/SearchReducer.swift | 28 +++++++ CineasteTests/SearchReducerTests.swift | 81 ++++++++++++++++++++ 6 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 Cineaste/ReSwift/Actions/SearchAction.swift create mode 100644 Cineaste/ReSwift/Reducer/SearchReducer.swift create mode 100644 CineasteTests/SearchReducerTests.swift diff --git a/Cineaste.xcodeproj/project.pbxproj b/Cineaste.xcodeproj/project.pbxproj index ce7debb0..6aefa895 100644 --- a/Cineaste.xcodeproj/project.pbxproj +++ b/Cineaste.xcodeproj/project.pbxproj @@ -91,6 +91,9 @@ 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 */; }; 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 */; }; @@ -314,6 +317,9 @@ 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 = ""; }; 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 = ""; }; @@ -484,10 +490,11 @@ 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 */, @@ -558,6 +565,7 @@ 4EB512012268B07600CBDC7E /* AppReducer.swift */, 3F22920F225A3CDE001D4358 /* MovieReducer.swift */, 4EB512032268B1FD00CBDC7E /* SelectionReducer.swift */, + 3FF3E961249E691F00CA472F /* SearchReducer.swift */, ); path = Reducer; sourceTree = ""; @@ -575,6 +583,7 @@ children = ( 3F22920D225A3C4B001D4358 /* MovieAction.swift */, 3FC1AA742385E4EA00E24C16 /* SelectionAction.swift */, + 3FF3E95F249E687600CA472F /* SearchAction.swift */, ); path = Actions; sourceTree = ""; @@ -1034,6 +1043,7 @@ 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 */, @@ -1053,6 +1063,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 */, @@ -1089,6 +1100,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 */, diff --git a/Cineaste/ReSwift/Actions/SearchAction.swift b/Cineaste/ReSwift/Actions/SearchAction.swift new file mode 100644 index 00000000..a07f9a57 --- /dev/null +++ b/Cineaste/ReSwift/Actions/SearchAction.swift @@ -0,0 +1,16 @@ +// +// SearchAction.swift +// Cineaste App +// +// Created by Felizia Bernutz on 20.06.20. +// Copyright © 2020 spacepandas.de. All rights reserved. +// + +import ReSwift + +enum SearchAction: Action { + case updateSearchQuery(query: String) + case selectGenre(genre: Genre) + case deselectGenre(genre: Genre) + case resetSearch +} diff --git a/Cineaste/ReSwift/AppState.swift b/Cineaste/ReSwift/AppState.swift index 3c429b9e..dad40091 100644 --- a/Cineaste/ReSwift/AppState.swift +++ b/Cineaste/ReSwift/AppState.swift @@ -19,8 +19,16 @@ 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 searchResult: Set = [] +} 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..55b5170d --- /dev/null +++ b/Cineaste/ReSwift/Reducer/SearchReducer.swift @@ -0,0 +1,28 @@ +// +// SearchReducer.swift +// Cineaste App +// +// Created by Felizia Bernutz on 20.06.20. +// Copyright © 2020 spacepandas.de. All rights reserved. +// + +import ReSwift + +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 + case .selectGenre(let genre): + state.selectedGenres.append(genre) + case .deselectGenre(let genre): + state.selectedGenres = state.selectedGenres.filter { $0 != genre } + case .resetSearch: + state = SearchState() + } + + return state +} diff --git a/CineasteTests/SearchReducerTests.swift b/CineasteTests/SearchReducerTests.swift new file mode 100644 index 00000000..3b4c5487 --- /dev/null +++ b/CineasteTests/SearchReducerTests.swift @@ -0,0 +1,81 @@ +// +// 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 expectedState = SearchState( + searchQuery: query + ) + + // When + let state = searchReducer(action: action, state: SearchState()) + + // Then + XCTAssertEqual(state, expectedState) + } + + func testSelectGenreActionShouldAppendGenre() { + // Given + let genre = Genre(id: 0, name: "Horror") + let action = SearchAction.selectGenre(genre: genre) + let expectedState = SearchState( + selectedGenres: [genre] + ) + + // When + let state = searchReducer(action: action, state: SearchState()) + + // Then + XCTAssertEqual(state, expectedState) + } + + func testDeselectGenreActionShouldRemoveGenre() { + // Given + let genre = Genre(id: 0, name: "Horror") + let action = SearchAction.deselectGenre(genre: genre) + let expectedState = SearchState() + + // When + let state = searchReducer( + action: action, + state: SearchState(selectedGenres: [genre]) + ) + + // Then + XCTAssertEqual(state, expectedState) + } + + func testResetSearchActionShouldResetSearch() { + // Given + let genre = Genre(id: 0, name: "Horror") + let query = "First Man" + let action = SearchAction.resetSearch + let expectedState = SearchState() + + // When + let state = searchReducer( + action: action, + state: SearchState( + selectedGenres: [genre], + searchQuery: query, + searchResult: [] + ) + ) + + // Then + XCTAssertEqual(state, expectedState) + } + +} From 45cbdb2e70770031a495bb7f026607a1235efc48 Mon Sep 17 00:00:00 2001 From: Felizia Bernutz Date: Sat, 20 Jun 2020 20:51:35 +0200 Subject: [PATCH 6/8] Add more stuff to SearchState --- Cineaste.xcodeproj/project.pbxproj | 8 +-- Cineaste/ReSwift/Actions/SearchAction.swift | 6 ++ Cineaste/ReSwift/AppState.swift | 27 +++++++- Cineaste/ReSwift/Reducer/SearchReducer.swift | 25 ++++++- Cineaste/ReSwift/Thunks/SearchThunks.swift | 63 +++++++++++++++++ .../SearchMovies/SearchMovieDataSource.swift | 7 -- ...oviesViewController+SearchController.swift | 27 +++----- ...archMoviesViewController+UITableView.swift | 17 +---- ...earchMoviesViewController+Webservice.swift | 49 ------------- .../SearchMoviesViewController.swift | 69 ++++--------------- CineasteTests/SearchReducerTests.swift | 5 +- 11 files changed, 151 insertions(+), 152 deletions(-) create mode 100644 Cineaste/ReSwift/Thunks/SearchThunks.swift delete mode 100644 Cineaste/ViewController/SearchMovies/SearchMoviesViewController+Webservice.swift diff --git a/Cineaste.xcodeproj/project.pbxproj b/Cineaste.xcodeproj/project.pbxproj index 6aefa895..ac06cb98 100644 --- a/Cineaste.xcodeproj/project.pbxproj +++ b/Cineaste.xcodeproj/project.pbxproj @@ -94,6 +94,7 @@ 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 */; }; @@ -121,7 +122,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 */; }; @@ -299,7 +299,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 = ""; }; @@ -320,6 +319,7 @@ 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 = ""; }; @@ -574,6 +574,7 @@ isa = PBXGroup; children = ( 3FC1AA6D2385DFA400E24C16 /* MovieThunks.swift */, + 3FF3E965249E6CCD00CA472F /* SearchThunks.swift */, ); path = Thunks; sourceTree = ""; @@ -613,7 +614,6 @@ 3FEEE8E6222BBC6A00EB1649 /* SearchMoviesViewController+SwipeAction.swift */, 6409298F22DBAB0C0011AB96 /* SearchMoviesViewController+SwipeHint.swift */, 3FD722CB20264B720046DEAC /* SearchMoviesViewController+UITableView.swift */, - 3FD722CD20264BF80046DEAC /* SearchMoviesViewController+Webservice.swift */, 3F7D1D5023D4AEB400BA530F /* SearchTokenCell.swift */, 4E903D95249E5DE600EAA4E6 /* SearchSection.swift */, ); @@ -1123,7 +1123,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/ReSwift/Actions/SearchAction.swift b/Cineaste/ReSwift/Actions/SearchAction.swift index a07f9a57..cc3170e0 100644 --- a/Cineaste/ReSwift/Actions/SearchAction.swift +++ b/Cineaste/ReSwift/Actions/SearchAction.swift @@ -7,10 +7,16 @@ // 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 dad40091..8faebb7c 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 = [] @@ -28,7 +29,29 @@ struct SelectedMovieState: Equatable { struct SearchState: Equatable { var selectedGenres: [Genre] = [] - var searchQuery: String? + var searchQuery: String = "" + var currentPage: Int = 1 { + didSet { print("page", currentPage) } + } + 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 searchResult: Set = [] + var isInitialSearch: Bool { + searchQuery.isEmpty && selectedGenres.isEmpty + } } diff --git a/Cineaste/ReSwift/Reducer/SearchReducer.swift b/Cineaste/ReSwift/Reducer/SearchReducer.swift index 55b5170d..d9cf4c24 100644 --- a/Cineaste/ReSwift/Reducer/SearchReducer.swift +++ b/Cineaste/ReSwift/Reducer/SearchReducer.swift @@ -16,12 +16,35 @@ func searchReducer(action: Action, state: SearchState?) -> SearchState { switch action { case .updateSearchQuery(let query): state.searchQuery = query + state.currentPage = 1 + state.totalResults = nil + state.currentRequest?.cancel() case .selectGenre(let genre): state.selectedGenres.append(genre) + state.searchQuery = "" + state.currentPage = 1 + state.totalResults = nil + state.currentRequest?.cancel() case .deselectGenre(let genre): state.selectedGenres = state.selectedGenres.filter { $0 != genre } + state.currentPage = 1 + 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: - state = SearchState() + 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..73f5f7f1 --- /dev/null +++ b/Cineaste/ReSwift/Thunks/SearchThunks.swift @@ -0,0 +1,63 @@ +// +// SearchThunks.swift +// Cineaste App +// +// Created by Felizia Bernutz on 20.06.20. +// Copyright © 2020 spacepandas.de. All rights reserved. +// + +import Dispatch +import ReSwift_Thunk + +let fetchSearchResults = Thunk { dispatch, getState in + guard let state = getState()?.searchState, + !state.hasLoadedAllMovies, + let storedIDs = getState()?.storedIDs, + !state.isLoading + else { return } + + let resource: Resource? + if state.isInitialSearch { + resource = Movie.latestReleases(page: state.currentPage) + } else { + // TODO: Extend search function to use genre and query + 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): + // self.showAlert(withMessage: Alert.loadingData(with: 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 86d8bcb5..4fafe460 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMovieDataSource.swift @@ -17,8 +17,6 @@ class SearchMovieDataSource: NSObject, UITableViewDataSource { var mode: Mode = .discover var movies: [Movie] = [] - var currentPage: Int? - var totalResults: Int? func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { guard #available(iOS 13, *), @@ -67,11 +65,6 @@ class SearchMovieDataSource: NSObject, UITableViewDataSource { cell.configure(with: movie) - if let numberOfMovies = totalResults, - indexPath.isLast(of: numberOfMovies) { - tableView.tableFooterView = UIView() - } - return cell case (.manualSearch, .tokens?): let cell: SearchTokenCell = tableView.dequeueCell(identifier: SearchTokenCell.identifier) diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift index 9de2ee16..c8f6081a 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+SearchController.swift @@ -7,20 +7,11 @@ // 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) } } @@ -43,13 +34,13 @@ extension SearchMoviesViewController: UITextFieldDelegate { 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 3d061a9f..a5b30752 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesViewController+UITableView.swift @@ -10,21 +10,10 @@ 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 - else { return } + guard indexPaths.contains(where: { $0.row >= movies.count - 1 }) else { return } + print("prefetching...", indexPaths.map(\.row)) - 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 ea16b247..8035214b 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) { @@ -148,30 +122,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 } @@ -185,15 +135,24 @@ 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 + if state.isLoading { + tableView.tableFooterView = loadingIndicatorView + } else { + tableView.tableFooterView = UIView() + } } } diff --git a/CineasteTests/SearchReducerTests.swift b/CineasteTests/SearchReducerTests.swift index 3b4c5487..e5e75e91 100644 --- a/CineasteTests/SearchReducerTests.swift +++ b/CineasteTests/SearchReducerTests.swift @@ -9,6 +9,7 @@ import XCTest @testable import Cineaste_App +//TODO: add more tests for SearchReducer class SearchReducerTests: XCTestCase { func testUpdateSearchQueryActionShouldUpdateSearchQuery() { @@ -26,7 +27,7 @@ class SearchReducerTests: XCTestCase { XCTAssertEqual(state, expectedState) } - func testSelectGenreActionShouldAppendGenre() { + func testSelectGenreActionShouldAppendGenreAndResetQuery() { // Given let genre = Genre(id: 0, name: "Horror") let action = SearchAction.selectGenre(genre: genre) @@ -35,7 +36,7 @@ class SearchReducerTests: XCTestCase { ) // When - let state = searchReducer(action: action, state: SearchState()) + let state = searchReducer(action: action, state: SearchState(searchQuery: "Hor")) // Then XCTAssertEqual(state, expectedState) From a3229fbf53cca2c4ddf258b6ec27357fefe80d5f Mon Sep 17 00:00:00 2001 From: Felizia Bernutz Date: Sat, 20 Jun 2020 20:55:33 +0200 Subject: [PATCH 7/8] Fix Search --- Cineaste/ReSwift/Reducer/SearchReducer.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cineaste/ReSwift/Reducer/SearchReducer.swift b/Cineaste/ReSwift/Reducer/SearchReducer.swift index d9cf4c24..12402058 100644 --- a/Cineaste/ReSwift/Reducer/SearchReducer.swift +++ b/Cineaste/ReSwift/Reducer/SearchReducer.swift @@ -19,17 +19,20 @@ func searchReducer(action: Action, state: SearchState?) -> SearchState { state.currentPage = 1 state.totalResults = nil state.currentRequest?.cancel() + state.searchResult = [] case .selectGenre(let genre): state.selectedGenres.append(genre) state.searchQuery = "" state.currentPage = 1 state.totalResults = nil state.currentRequest?.cancel() + state.searchResult = [] case .deselectGenre(let genre): state.selectedGenres = state.selectedGenres.filter { $0 != genre } state.currentPage = 1 state.totalResults = nil state.currentRequest?.cancel() + state.searchResult = [] case .showNextPage: if !state.hasLoadedAllMovies { state.currentPage += 1 From 3b3773c00daca03e53d3ba3a9c8c7a8a72e22049 Mon Sep 17 00:00:00 2001 From: Felizia Bernutz Date: Sun, 6 Dec 2020 11:21:16 +0100 Subject: [PATCH 8/8] Fix Pacakge Versions --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- Cineaste/Settings.bundle/Acknowledgements.latest_result.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cineaste.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cineaste.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 207f96a4..4a0e410b 100644 --- a/Cineaste.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cineaste.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", "state": { "branch": null, - "revision": "2fd91357e90efe82bfa6845d1e7d5bc2f5025d35", - "version": "1.8.1" + "revision": "c466812aa2e22898f27557e2e780d3aad7a27203", + "version": "1.8.2" } } ] diff --git a/Cineaste/Settings.bundle/Acknowledgements.latest_result.txt b/Cineaste/Settings.bundle/Acknowledgements.latest_result.txt index 0d309d9c..9e31ed69 100644 --- a/Cineaste/Settings.bundle/Acknowledgements.latest_result.txt +++ b/Cineaste/Settings.bundle/Acknowledgements.latest_result.txt @@ -4,7 +4,7 @@ name: ReSwift, nameSpecified: ReSwift, owner: ReSwift, version: 5.0.0 name: ReSwift-Thunk, nameSpecified: ReSwift-Thunk, owner: ReSwift, version: 1.2.0 -name: swift-snapshot-testing, nameSpecified: SnapshotTesting, owner: pointfreeco, version: 1.8.1 +name: swift-snapshot-testing, nameSpecified: SnapshotTesting, owner: pointfreeco, version: 1.8.2 add-version-numbers: false