added iOS source code
[wl-app.git] / iOS / WolneLektury / Screens / Search / SearchViewController.swift
diff --git a/iOS/WolneLektury/Screens/Search/SearchViewController.swift b/iOS/WolneLektury/Screens/Search/SearchViewController.swift
new file mode 100644 (file)
index 0000000..d2ff84e
--- /dev/null
@@ -0,0 +1,349 @@
+//
+//  SearchViewController.swift
+//  WolneLektury
+//
+//  Created by Pawel Dabrowski on 13/06/2018.
+//  Copyright © 2018 Fundacja Nowoczesna Polska. All rights reserved.
+//
+
+import UIKit
+import Toast_Swift
+
+enum SearchViewControllerType{
+    case search
+}
+
+protocol DataLoaderDelegate: class{
+    func dataLoaderFetchingServerFinished()
+    func dataLoaderFetchingServerSuccess()
+    func dataLoaderFetchingServerFailed(showRefreshButton: Bool)
+}
+
+class DataLoader: NSObject{
+    var dataSource = [BookModel]()
+    var loadingMore = false
+    var canLoadMore = false
+    var currentParams =  FilterBooksParameters()
+    weak var delegate: DataLoaderDelegate?
+    
+    init(delegate: DataLoaderDelegate) {
+        self.delegate = delegate
+    }
+    
+    func setNewFilters(filterParams: FilterBooksParameters) {
+        let newParams = filterParams
+        newParams.search = currentParams.search
+        currentParams = newParams
+        dataSource = [BookModel]()
+    }
+    
+    func loadData(more: Bool){
+        
+        let params = currentParams
+        if more{
+            loadingMore = true
+            currentParams.after = dataSource.last?.key
+        }
+        
+        appDelegate.syncManager.filterBooks(params: params) { [weak self] (result) in
+            
+            guard let strongSelf = self else{
+                return
+            }
+            
+            strongSelf.delegate?.dataLoaderFetchingServerFinished()
+            strongSelf.loadingMore = false
+            
+            guard strongSelf.currentParams == params else{
+                print("strongSelf.currentParams != cp")
+                return
+            }
+            
+            switch result {
+            case .success(let model):
+                
+                let array = model as! [BookModel]
+                strongSelf.canLoadMore = array.count == FilterBooksParameters.SEARCH_ITEMS_COUNT
+                strongSelf.dataSource.append(contentsOf: array)
+                strongSelf.delegate?.dataLoaderFetchingServerSuccess()
+                
+                
+            case .failure(let error):
+                if strongSelf.dataSource.count > 0{
+                    strongSelf.canLoadMore = false
+                    strongSelf.delegate?.dataLoaderFetchingServerFailed(showRefreshButton: false)
+                }
+                else{
+                    strongSelf.delegate?.dataLoaderFetchingServerFailed(showRefreshButton: true)
+                }
+                break
+            }
+        }
+    }
+}
+
+class SearchViewController: MainViewController {
+
+    @IBOutlet weak var filterCloseButton: UIButton!
+    @IBOutlet weak var filterContainer: UIView!
+    @IBOutlet weak var filterCollectionView: UICollectionView!
+    @IBOutlet weak var filterTopConstraint: NSLayoutConstraint!
+    @IBOutlet weak var tableView: UITableView!
+    @IBOutlet weak var footerViewActivityIndicator: UIActivityIndicatorView!
+    @IBOutlet weak var noDataLabel: UILabel!
+    @IBOutlet weak var refreshDataButton: ActivityIndicatorButton!
+    
+    var dataLoader: DataLoader!
+    var filtersManager : SearchFiltersManager!
+    
+    var controllerType = SearchViewControllerType.search
+    
+    lazy var searchBar = UISearchBar(frame: CGRect.zero)
+    var currentSearchQuery = ""
+    
+    class func instance(type: SearchViewControllerType) -> SearchViewController{
+        let controller = SearchViewController.instance()
+        controller.controllerType = type
+        controller.dataLoader = DataLoader(delegate: controller)
+        return controller
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        title = "nav_search".localized
+        
+        filterCloseButton.layer.cornerRadius = 14
+        filterCloseButton.tintColor = Constants.Colors.darkGreenBgColor()
+        
+        noDataLabel.text = "no_results".localized
+        noDataLabel.isHidden = true
+
+        refreshDataButton.tintColor = .black
+        
+        filtersManager = SearchFiltersManager(delegate: self)
+        setupTableView()
+        setupCollectionView()
+        filtersChanged(initially: true, reloadData: true)
+        setupSearchBar()
+//        searchBar.becomeFirstResponder()
+    }
+    
+    func setupSearchBar() {
+        searchBar.placeholder = "search_placeholder".localized
+        searchBar.enablesReturnKeyAutomatically = false
+        UITextField.appearance(whenContainedInInstancesOf: [type(of: searchBar)]).tintColor = UIColor.gray
+        searchBar.delegate = self
+        searchBar.translatesAutoresizingMaskIntoConstraints = false
+
+        searchBar.layoutIfNeeded()
+        searchBar.sizeToFit()
+        searchBar.translatesAutoresizingMaskIntoConstraints = true // make nav bar happy
+
+        navigationItem.titleView = searchBar
+//        if #available(iOS 11.0, *) {
+////            searchBar.heightAnchor.constraint(equalToConstant: 44).isActive = true
+//        }
+        let singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.singleTap(sender:)))
+        singleTapGestureRecognizer.numberOfTapsRequired = 1
+        singleTapGestureRecognizer.isEnabled = true
+        singleTapGestureRecognizer.cancelsTouchesInView = false
+        self.view.addGestureRecognizer(singleTapGestureRecognizer)
+    }
+    
+    func setupTableView(){
+        tableView.registerNib(name: "BookTableViewCell")
+        tableView.separatorStyle = .none
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.rowHeight = 137
+        var insets = tableView.contentInset
+        insets.top = 11
+        tableView.contentInset = insets
+        footerViewActivityIndicator.color = Constants.Colors.darkGreenBgColor()
+        footerViewActivityIndicator.hidesWhenStopped = true
+    }
+    
+    @objc func singleTap(sender: UITapGestureRecognizer) {
+        self.searchBar.resignFirstResponder()
+    }
+
+    @IBAction func filterCloseButtonAction(_ sender: Any) {
+        filtersManager.clearFilters()
+    }
+    
+    @IBAction func refreshDataButtonAction(_ sender: Any) {
+        loadData(more: false, fromRefreshControl: false)
+    }
+
+    func setupCollectionView(){
+        
+        filterCollectionView.backgroundColor = UIColor.clear
+        filterCollectionView.delegate = filtersManager
+        filterCollectionView.dataSource = filtersManager
+        filterCollectionView.register(UINib.init(nibName: "SearchFilterCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "SearchFilterCollectionViewCell")
+    }
+
+    func filtersChanged(initially: Bool = false, reloadData: Bool){
+        if reloadData{
+            filterCollectionView.reloadData()
+            
+            if filtersManager.numberOfFilters() == 0{
+                if filterTopConstraint.constant != 0 && !initially{
+                    filterTopConstraint.constant = 0
+
+                    UIView.animate(withDuration:0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: [], animations: {
+                        self.view.layoutIfNeeded()
+                    }, completion: {
+                        [weak self] (value: Bool) in
+                        self?.filterContainer.isHidden = true
+                    })
+                }
+                else{
+                    filterTopConstraint.constant = 0
+                    filterContainer.isHidden = true
+                }
+            }
+            else{
+                filterContainer.isHidden = false
+                filterTopConstraint.constant = 44
+            }
+        }
+        
+        dataLoader.setNewFilters(filterParams: filtersManager.getParametersForApi())
+        
+        tableView.reloadData()
+        
+        loadData(more: false, fromRefreshControl: false)
+    }
+    
+    func loadData(more: Bool, fromRefreshControl: Bool){
+        
+        refreshDataButton.setIndicatorButtonState(state: .hidden)
+        if more && dataLoader.loadingMore{
+            return
+        }
+        
+        if !fromRefreshControl{
+            footerViewActivityIndicator.startAnimating()
+        }
+        
+        dataLoader.loadData(more: more)
+    }
+    
+    @IBAction func filterButtonAction(_ sender: Any) {
+        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)
+    }
+    
+    @objc func getDataForCurrentSearchQuery(){
+        if currentSearchQuery.count > 0 {
+            dataLoader.currentParams.search = currentSearchQuery
+        }
+        else{
+            dataLoader.currentParams.search = nil
+        }
+        dataLoader.dataSource = [BookModel]()
+        tableView.reloadData()
+        loadData(more: false, fromRefreshControl: false)
+    }
+}
+
+extension SearchViewController: UITableViewDataSource{
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
+        return dataLoader.dataSource.count
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
+        
+        let cell = tableView.dequeueReusableCell(withIdentifier: "BookTableViewCell", for: indexPath) as! BookTableViewCell
+        cell.setup(bookModel: dataLoader.dataSource[indexPath.row])
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
+        if indexPath.row > 0 && indexPath.row == (dataLoader.dataSource.count-1) && dataLoader.canLoadMore && !dataLoader.loadingMore{
+            let _ = loadData(more: true, fromRefreshControl: false)
+        }
+    }
+}
+
+extension SearchViewController: UITableViewDelegate{
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        tableView.deselectRow(at: indexPath, animated: true)
+        searchBar.resignFirstResponder()
+        if dataLoader.dataSource.count > indexPath.row{
+            navigationController?.pushViewController(BookDetailsViewController.instance(bookSlug: dataLoader.dataSource[indexPath.row].slug) , animated: true)
+        }
+    }
+}
+
+extension SearchViewController: FilterViewControllerDelegate{
+    func filterViewControllerDidSelectItems(kindsArray: [CategoryModel], epochsArray: [CategoryModel], genresArray: [CategoryModel], onlyLectures: Bool, hasAudiobook: Bool, filterChanged: Bool) {
+        
+        if filterChanged{
+            filtersManager.selectedKindsArray = kindsArray
+            filtersManager.selectedEpochsArray = epochsArray
+            filtersManager.selectedGenresArray = genresArray
+            filtersManager.onlyLecturesCategory.checked = onlyLectures
+            filtersManager.hasAudiobookCategory.checked = hasAudiobook
+
+            filtersChanged(reloadData: true)
+        }
+        self.navigationController?.presentedViewController?.dismiss(animated: true, completion: nil)
+    }
+}
+
+extension SearchViewController: UISearchBarDelegate{
+    
+    func textFieldShouldReturn(_ textField: UITextField) -> Bool {// called when 'return' key pressed. return NO to ignore.
+        textField.resignFirstResponder()
+        return true
+    }
+
+    func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
+        var searchQueryCandidate = ""
+        if let txt = searchBar.text{
+            searchQueryCandidate = txt.trimmed
+        }
+
+        if(currentSearchQuery != searchQueryCandidate){
+            currentSearchQuery = searchQueryCandidate
+            getDataForCurrentSearchQuery()
+        }
+    }
+    
+    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
+        searchBar.resignFirstResponder()
+    }
+    
+    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
+        currentSearchQuery = searchText.trimmed
+        
+        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(SearchViewController.getDataForCurrentSearchQuery), object: nil)
+        self.perform(#selector(SearchViewController.getDataForCurrentSearchQuery), with: nil, afterDelay: 0.8)
+    }
+}
+
+extension SearchViewController: SearchFiltersManagerDelegate{
+    func searchFiltersManagerFiltersChanged(reloadData: Bool) {
+        filtersChanged(reloadData: reloadData)
+    }
+}
+
+extension SearchViewController: DataLoaderDelegate{
+    func dataLoaderFetchingServerFinished() {
+        footerViewActivityIndicator.stopAnimating()
+    }
+    
+    func dataLoaderFetchingServerSuccess() {
+        noDataLabel.isHidden = dataLoader.dataSource.count > 0
+        tableView.reloadData()
+    }
+    
+    func dataLoaderFetchingServerFailed(showRefreshButton: Bool) {
+    
+        if showRefreshButton{
+            refreshDataButton.setIndicatorButtonState(state: .button)
+        }
+        view.makeToast("book_loading_error".localized, duration: 3.0, position: .bottom)
+    }
+}