added iOS source code
[wl-app.git] / iOS / WolneLektury / Screens / Search / SearchViewController.swift
1 //
2 //  SearchViewController.swift
3 //  WolneLektury
4 //
5 //  Created by Pawel Dabrowski on 13/06/2018.
6 //  Copyright © 2018 Fundacja Nowoczesna Polska. All rights reserved.
7 //
8
9 import UIKit
10 import Toast_Swift
11
12 enum SearchViewControllerType{
13     case search
14 }
15
16 protocol DataLoaderDelegate: class{
17     func dataLoaderFetchingServerFinished()
18     func dataLoaderFetchingServerSuccess()
19     func dataLoaderFetchingServerFailed(showRefreshButton: Bool)
20 }
21
22 class DataLoader: NSObject{
23     var dataSource = [BookModel]()
24     var loadingMore = false
25     var canLoadMore = false
26     var currentParams =  FilterBooksParameters()
27     weak var delegate: DataLoaderDelegate?
28     
29     init(delegate: DataLoaderDelegate) {
30         self.delegate = delegate
31     }
32     
33     func setNewFilters(filterParams: FilterBooksParameters) {
34         let newParams = filterParams
35         newParams.search = currentParams.search
36         currentParams = newParams
37         dataSource = [BookModel]()
38     }
39     
40     func loadData(more: Bool){
41         
42         let params = currentParams
43         if more{
44             loadingMore = true
45             currentParams.after = dataSource.last?.key
46         }
47         
48         appDelegate.syncManager.filterBooks(params: params) { [weak self] (result) in
49             
50             guard let strongSelf = self else{
51                 return
52             }
53             
54             strongSelf.delegate?.dataLoaderFetchingServerFinished()
55             strongSelf.loadingMore = false
56             
57             guard strongSelf.currentParams == params else{
58                 print("strongSelf.currentParams != cp")
59                 return
60             }
61             
62             switch result {
63             case .success(let model):
64                 
65                 let array = model as! [BookModel]
66                 strongSelf.canLoadMore = array.count == FilterBooksParameters.SEARCH_ITEMS_COUNT
67                 strongSelf.dataSource.append(contentsOf: array)
68                 strongSelf.delegate?.dataLoaderFetchingServerSuccess()
69                 
70                 
71             case .failure(let error):
72                 if strongSelf.dataSource.count > 0{
73                     strongSelf.canLoadMore = false
74                     strongSelf.delegate?.dataLoaderFetchingServerFailed(showRefreshButton: false)
75                 }
76                 else{
77                     strongSelf.delegate?.dataLoaderFetchingServerFailed(showRefreshButton: true)
78                 }
79                 break
80             }
81         }
82     }
83 }
84
85 class SearchViewController: MainViewController {
86
87     @IBOutlet weak var filterCloseButton: UIButton!
88     @IBOutlet weak var filterContainer: UIView!
89     @IBOutlet weak var filterCollectionView: UICollectionView!
90     @IBOutlet weak var filterTopConstraint: NSLayoutConstraint!
91     @IBOutlet weak var tableView: UITableView!
92     @IBOutlet weak var footerViewActivityIndicator: UIActivityIndicatorView!
93     @IBOutlet weak var noDataLabel: UILabel!
94     @IBOutlet weak var refreshDataButton: ActivityIndicatorButton!
95     
96     var dataLoader: DataLoader!
97     var filtersManager : SearchFiltersManager!
98     
99     var controllerType = SearchViewControllerType.search
100     
101     lazy var searchBar = UISearchBar(frame: CGRect.zero)
102     var currentSearchQuery = ""
103     
104     class func instance(type: SearchViewControllerType) -> SearchViewController{
105         let controller = SearchViewController.instance()
106         controller.controllerType = type
107         controller.dataLoader = DataLoader(delegate: controller)
108         return controller
109     }
110     
111     override func viewDidLoad() {
112         super.viewDidLoad()
113         title = "nav_search".localized
114         
115         filterCloseButton.layer.cornerRadius = 14
116         filterCloseButton.tintColor = Constants.Colors.darkGreenBgColor()
117         
118         noDataLabel.text = "no_results".localized
119         noDataLabel.isHidden = true
120
121         refreshDataButton.tintColor = .black
122         
123         filtersManager = SearchFiltersManager(delegate: self)
124         setupTableView()
125         setupCollectionView()
126         filtersChanged(initially: true, reloadData: true)
127         setupSearchBar()
128 //        searchBar.becomeFirstResponder()
129     }
130     
131     func setupSearchBar() {
132         searchBar.placeholder = "search_placeholder".localized
133         searchBar.enablesReturnKeyAutomatically = false
134         UITextField.appearance(whenContainedInInstancesOf: [type(of: searchBar)]).tintColor = UIColor.gray
135         searchBar.delegate = self
136         searchBar.translatesAutoresizingMaskIntoConstraints = false
137
138         searchBar.layoutIfNeeded()
139         searchBar.sizeToFit()
140         searchBar.translatesAutoresizingMaskIntoConstraints = true // make nav bar happy
141
142         navigationItem.titleView = searchBar
143 //        if #available(iOS 11.0, *) {
144 ////            searchBar.heightAnchor.constraint(equalToConstant: 44).isActive = true
145 //        }
146         let singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.singleTap(sender:)))
147         singleTapGestureRecognizer.numberOfTapsRequired = 1
148         singleTapGestureRecognizer.isEnabled = true
149         singleTapGestureRecognizer.cancelsTouchesInView = false
150         self.view.addGestureRecognizer(singleTapGestureRecognizer)
151     }
152     
153     func setupTableView(){
154         tableView.registerNib(name: "BookTableViewCell")
155         tableView.separatorStyle = .none
156         tableView.delegate = self
157         tableView.dataSource = self
158         tableView.rowHeight = 137
159         var insets = tableView.contentInset
160         insets.top = 11
161         tableView.contentInset = insets
162         footerViewActivityIndicator.color = Constants.Colors.darkGreenBgColor()
163         footerViewActivityIndicator.hidesWhenStopped = true
164     }
165     
166     @objc func singleTap(sender: UITapGestureRecognizer) {
167         self.searchBar.resignFirstResponder()
168     }
169
170     @IBAction func filterCloseButtonAction(_ sender: Any) {
171         filtersManager.clearFilters()
172     }
173     
174     @IBAction func refreshDataButtonAction(_ sender: Any) {
175         loadData(more: false, fromRefreshControl: false)
176     }
177
178     func setupCollectionView(){
179         
180         filterCollectionView.backgroundColor = UIColor.clear
181         filterCollectionView.delegate = filtersManager
182         filterCollectionView.dataSource = filtersManager
183         filterCollectionView.register(UINib.init(nibName: "SearchFilterCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "SearchFilterCollectionViewCell")
184     }
185
186     func filtersChanged(initially: Bool = false, reloadData: Bool){
187         if reloadData{
188             filterCollectionView.reloadData()
189             
190             if filtersManager.numberOfFilters() == 0{
191                 if filterTopConstraint.constant != 0 && !initially{
192                     filterTopConstraint.constant = 0
193
194                     UIView.animate(withDuration:0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: [], animations: {
195                         self.view.layoutIfNeeded()
196                     }, completion: {
197                         [weak self] (value: Bool) in
198                         self?.filterContainer.isHidden = true
199                     })
200                 }
201                 else{
202                     filterTopConstraint.constant = 0
203                     filterContainer.isHidden = true
204                 }
205             }
206             else{
207                 filterContainer.isHidden = false
208                 filterTopConstraint.constant = 44
209             }
210         }
211         
212         dataLoader.setNewFilters(filterParams: filtersManager.getParametersForApi())
213         
214         tableView.reloadData()
215         
216         loadData(more: false, fromRefreshControl: false)
217     }
218     
219     func loadData(more: Bool, fromRefreshControl: Bool){
220         
221         refreshDataButton.setIndicatorButtonState(state: .hidden)
222         if more && dataLoader.loadingMore{
223             return
224         }
225         
226         if !fromRefreshControl{
227             footerViewActivityIndicator.startAnimating()
228         }
229         
230         dataLoader.loadData(more: more)
231     }
232     
233     @IBAction func filterButtonAction(_ sender: Any) {
234         self.navigationController?.present( WLNavigationController(rootViewController: FilterViewController.instance(delegate: self, selectedKindsArray: filtersManager.selectedKindsArray, selectedEpochsArray: filtersManager.selectedEpochsArray, selectedGenresArray: filtersManager.selectedGenresArray, onlyLectures: filtersManager.onlyLecturesCategory.checked, hasAudiobook: filtersManager.hasAudiobookCategory.checked)) , animated: true, completion: nil)
235     }
236     
237     @objc func getDataForCurrentSearchQuery(){
238         if currentSearchQuery.count > 0 {
239             dataLoader.currentParams.search = currentSearchQuery
240         }
241         else{
242             dataLoader.currentParams.search = nil
243         }
244         dataLoader.dataSource = [BookModel]()
245         tableView.reloadData()
246         loadData(more: false, fromRefreshControl: false)
247     }
248 }
249
250 extension SearchViewController: UITableViewDataSource{
251     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
252         return dataLoader.dataSource.count
253     }
254     
255     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
256         
257         let cell = tableView.dequeueReusableCell(withIdentifier: "BookTableViewCell", for: indexPath) as! BookTableViewCell
258         cell.setup(bookModel: dataLoader.dataSource[indexPath.row])
259         return cell
260     }
261     
262     func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
263         if indexPath.row > 0 && indexPath.row == (dataLoader.dataSource.count-1) && dataLoader.canLoadMore && !dataLoader.loadingMore{
264             let _ = loadData(more: true, fromRefreshControl: false)
265         }
266     }
267 }
268
269 extension SearchViewController: UITableViewDelegate{
270     func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
271         tableView.deselectRow(at: indexPath, animated: true)
272         searchBar.resignFirstResponder()
273         if dataLoader.dataSource.count > indexPath.row{
274             navigationController?.pushViewController(BookDetailsViewController.instance(bookSlug: dataLoader.dataSource[indexPath.row].slug) , animated: true)
275         }
276     }
277 }
278
279 extension SearchViewController: FilterViewControllerDelegate{
280     func filterViewControllerDidSelectItems(kindsArray: [CategoryModel], epochsArray: [CategoryModel], genresArray: [CategoryModel], onlyLectures: Bool, hasAudiobook: Bool, filterChanged: Bool) {
281         
282         if filterChanged{
283             filtersManager.selectedKindsArray = kindsArray
284             filtersManager.selectedEpochsArray = epochsArray
285             filtersManager.selectedGenresArray = genresArray
286             filtersManager.onlyLecturesCategory.checked = onlyLectures
287             filtersManager.hasAudiobookCategory.checked = hasAudiobook
288
289             filtersChanged(reloadData: true)
290         }
291         self.navigationController?.presentedViewController?.dismiss(animated: true, completion: nil)
292     }
293 }
294
295 extension SearchViewController: UISearchBarDelegate{
296     
297     func textFieldShouldReturn(_ textField: UITextField) -> Bool {// called when 'return' key pressed. return NO to ignore.
298         textField.resignFirstResponder()
299         return true
300     }
301
302     func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
303         var searchQueryCandidate = ""
304         if let txt = searchBar.text{
305             searchQueryCandidate = txt.trimmed
306         }
307
308         if(currentSearchQuery != searchQueryCandidate){
309             currentSearchQuery = searchQueryCandidate
310             getDataForCurrentSearchQuery()
311         }
312     }
313     
314     func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
315         searchBar.resignFirstResponder()
316     }
317     
318     func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
319         currentSearchQuery = searchText.trimmed
320         
321         NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(SearchViewController.getDataForCurrentSearchQuery), object: nil)
322         self.perform(#selector(SearchViewController.getDataForCurrentSearchQuery), with: nil, afterDelay: 0.8)
323     }
324 }
325
326 extension SearchViewController: SearchFiltersManagerDelegate{
327     func searchFiltersManagerFiltersChanged(reloadData: Bool) {
328         filtersChanged(reloadData: reloadData)
329     }
330 }
331
332 extension SearchViewController: DataLoaderDelegate{
333     func dataLoaderFetchingServerFinished() {
334         footerViewActivityIndicator.stopAnimating()
335     }
336     
337     func dataLoaderFetchingServerSuccess() {
338         noDataLabel.isHidden = dataLoader.dataSource.count > 0
339         tableView.reloadData()
340     }
341     
342     func dataLoaderFetchingServerFailed(showRefreshButton: Bool) {
343     
344         if showRefreshButton{
345             refreshDataButton.setIndicatorButtonState(state: .button)
346         }
347         view.makeToast("book_loading_error".localized, duration: 3.0, position: .bottom)
348     }
349 }