diff --git a/Classes/Repository/RepositoryIssuesViewController.swift b/Classes/Repository/RepositoryIssuesViewController.swift index ee5d4fd45..c3906885a 100644 --- a/Classes/Repository/RepositoryIssuesViewController.swift +++ b/Classes/Repository/RepositoryIssuesViewController.swift @@ -29,7 +29,8 @@ IndicatorInfoProvider { private let type: RepositoryIssuesType private let searchKey: ListDiffable = "searchKey" as ListDiffable private let debouncer = Debouncer() - private var previousSearchString = "is:open " + private var previousSearchString = "" + private var prefix = "is:open " private var label: String? init(client: GithubClient, owner: String, repo: String, type: RepositoryIssuesType, label: String? = nil) { @@ -103,6 +104,14 @@ IndicatorInfoProvider { debouncer.action = { [weak self] in self?.fetch(page: nil) } } + func didChangeSegment(sectionController: SearchBarSectionController, index: Int) { + switch index { + case 0: updateState(isOpen: true) + case 1: updateState(isOpen: false) + default: break + } + } + // MARK: BaseListViewControllerHeaderDataSource func headerModel(for adapter: ListSwiftAdapter) -> ListSwiftPair { @@ -110,7 +119,8 @@ IndicatorInfoProvider { SearchBarSectionController( placeholder: Constants.Strings.search, delegate: self, - query: previousSearchString + query: previousSearchString, + items: ["Open", "Closed"] ) }) } @@ -151,7 +161,12 @@ IndicatorInfoProvider { case .issues: typeQuery = "is:issue" case .pullRequests: typeQuery = "is:pr" } - return "repo:\(owner)/\(repo) \(typeQuery) \(previousSearchString)".lowercased() + return "repo:\(owner)/\(repo) \(typeQuery) \(prefix + previousSearchString)".lowercased() + } + + func updateState(isOpen: Bool = true) { + prefix = isOpen ? "is:open " : "is:closed " + debouncer.action = { [weak self] in self?.fetch(page: nil) } } // MARK: IndicatorInfoProvider diff --git a/Classes/Section Controllers/SearchBar/SearchBarCell.swift b/Classes/Section Controllers/SearchBar/SearchBarCell.swift index 2c60d84f0..47acc2e1d 100644 --- a/Classes/Section Controllers/SearchBar/SearchBarCell.swift +++ b/Classes/Section Controllers/SearchBar/SearchBarCell.swift @@ -9,11 +9,15 @@ import UIKit import SnapKit +protocol SearchableCell: class {} + protocol SearchBarCellDelegate: class { - func didChangeSearchText(cell: SearchBarCell, query: String) + func didChangeSearchText(cell: SearchableCell, query: String) + func didChangeSegment(cell: SearchableCell, index: Int) } -final class SearchBarCell: UICollectionViewCell, UISearchBarDelegate { + +final class SearchBarCell: UICollectionViewCell, SearchableCell, UISearchBarDelegate { weak var delegate: SearchBarCellDelegate? diff --git a/Classes/Section Controllers/SearchBar/SearchBarSectionController.swift b/Classes/Section Controllers/SearchBar/SearchBarSectionController.swift index 7ca7bbd43..2deaf5177 100644 --- a/Classes/Section Controllers/SearchBar/SearchBarSectionController.swift +++ b/Classes/Section Controllers/SearchBar/SearchBarSectionController.swift @@ -11,39 +11,64 @@ import IGListKit protocol SearchBarSectionControllerDelegate: class { func didChangeSelection(sectionController: SearchBarSectionController, query: String) + func didChangeSegment(sectionController: SearchBarSectionController, index: Int) } final class SearchBarSectionController: ListSwiftSectionController, SearchBarCellDelegate { public private(set) var query: String + public private(set) var index: Int + public private(set) var items: [String]? private weak var delegate: SearchBarSectionControllerDelegate? private let placeholder: String - init(placeholder: String, delegate: SearchBarSectionControllerDelegate?, query: String = "") { + init(placeholder: String, delegate: SearchBarSectionControllerDelegate?, query: String = "", index: Int = 0, items: [String]? = nil) { self.delegate = delegate self.placeholder = placeholder self.query = query + self.index = index + self.items = items super.init() } override func createBinders(from value: String) -> [ListBinder] { - return [ - binder(value, cellType: ListCellType.class(SearchBarCell.self), size: { - return $0.collection.cellSize(with: 56) - }, configure: { [weak self] (cell, _) in - guard let `self` = self else { return } - cell.delegate = self - cell.configure(query: self.query, placeholder: self.placeholder) - }) - ] + + if let items = items { + return [ + binder(value, cellType: ListCellType.class(SearchSegmentBarCell.self), size: { + return $0.collection.cellSize(with: 56) + }, configure: { [weak self] (cell, _) in + guard let `self` = self else { return } + cell.set(items: items) + cell.delegate = self + cell.configure(query: self.query, placeholder: self.placeholder) + }) + ] + } + else { + return [ + binder(value, cellType: ListCellType.class(SearchBarCell.self), size: { + return $0.collection.cellSize(with: 56) + }, configure: { [weak self] (cell, _) in + guard let `self` = self else { return } + cell.delegate = self + cell.configure(query: self.query, placeholder: self.placeholder) + }) + ] + } } // MARK: SearchBarSectionControllerDelegate - func didChangeSearchText(cell: SearchBarCell, query: String) { + func didChangeSearchText(cell: SearchableCell, query: String) { self.query = query self.delegate?.didChangeSelection(sectionController: self, query: query) } + func didChangeSegment(cell: SearchableCell, index: Int) { + self.index = index + self.delegate?.didChangeSegment(sectionController: self, index: index) + } + } diff --git a/Classes/Section Controllers/SearchBar/SearchSegmentBarCell.swift b/Classes/Section Controllers/SearchBar/SearchSegmentBarCell.swift new file mode 100644 index 000000000..a6772118e --- /dev/null +++ b/Classes/Section Controllers/SearchBar/SearchSegmentBarCell.swift @@ -0,0 +1,126 @@ +// +// SearchSegementBarCell.swift +// Freetime +// +// Created by Ehud Adler on 3/12/19. +// Copyright © 2019 Ryan Nystrom. All rights reserved. +// + +import UIKit +import SnapKit + +final class SearchSegmentBarCell: UICollectionViewCell, SearchableCell, UISearchBarDelegate { + + weak var delegate: SearchBarCellDelegate? + + private let searchBar = UISearchBar(frame: .zero) + private let segmentControl = UISegmentedControl(frame: .zero) + var segmentControlLeadingConstraint: Constraint! + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white + + searchBar.returnKeyType = .search + searchBar.enablesReturnKeyAutomatically = false + searchBar.searchBarStyle = .minimal + searchBar.delegate = self + + contentView.addSubview(searchBar) + contentView.addSubview(segmentControl) + + searchBar.snp.makeConstraints { make in + make.leading.centerY.equalTo(contentView) + make.trailing.equalTo(segmentControl.snp.leading) + } + + segmentControl.snp.makeConstraints { make in + segmentControlLeadingConstraint = make.leading.equalTo(contentView.snp.trailing).constraint + make.trailing.equalTo(contentView.safeAreaLayoutGuide).inset(15) + make.top.bottom.equalTo(contentView) + + } + + segmentControl.addTarget(self, action: #selector(updateSegement), for: .valueChanged) + + setSegmentControl() + segmentControlLeadingConstraint.deactivate() + searchBar.resignWhenKeyboardHides() + segmentControl.removeBorders() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + layoutContentView() + } + + // MARK: Public API + + func set(items: [String], selectedIndex: Int = 0) { + for title in items { + segmentControl.insertSegment( + withTitle: title, + at: segmentControl.numberOfSegments, + animated: true + ) + } + segmentControl.selectedSegmentIndex = selectedIndex + } + + func configure(query: String, placeholder: String) { + searchBar.text = query + searchBar.placeholder = placeholder + } + + // MARK: Private API + + func setSegmentControl() { + + segmentControl.backgroundColor = .clear + segmentControl.tintColor = .clear + + let normalFont: [AnyHashable: Any] = [ + NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.dark.color, + NSAttributedStringKey.font: UIFont.systemFont(ofSize: 15) + ] + let selectedFont: [AnyHashable: Any] = [ + NSAttributedStringKey.foregroundColor: Styles.Colors.Blue.medium.color, + NSAttributedStringKey.font: UIFont.systemFont(ofSize: 15) + ] + segmentControl.setTitleTextAttributes(normalFont, for: .normal) + segmentControl.setTitleTextAttributes(selectedFont, for: .selected) + } + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + delegate?.didChangeSearchText(cell: self, query: searchText) + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + searchBar.resignFirstResponder() + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + UIView.animate(withDuration: 0.3) { + self.segmentControlLeadingConstraint.deactivate() + self.segmentControl.alpha = 1 + self.layoutIfNeeded() + } + } + + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + UIView.animate(withDuration: 0.3) { + self.segmentControlLeadingConstraint.activate() + self.segmentControl.alpha = 0 + self.layoutIfNeeded() + } + + } + + @objc func updateSegement() { + delegate?.didChangeSegment(cell: self, index: self.segmentControl.selectedSegmentIndex) + } +} diff --git a/Classes/Utility/SegmentControl+Border.swift b/Classes/Utility/SegmentControl+Border.swift new file mode 100644 index 000000000..423bfc9cc --- /dev/null +++ b/Classes/Utility/SegmentControl+Border.swift @@ -0,0 +1,30 @@ +// +// SegmentControl+Border.swift +// Freetime +// +// Created by Ehud Adler on 3/12/19. +// Copyright © 2019 Ryan Nystrom. All rights reserved. +// + +import Foundation + +extension UISegmentedControl { + func removeBorders() { + setBackgroundImage(imageWithColor(color: backgroundColor ?? .white), for: .normal, barMetrics: .default) + setBackgroundImage(imageWithColor(color: tintColor ?? .white), for: .selected, barMetrics: .default) + setDividerImage(imageWithColor(color: UIColor.clear), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default) + } + + // create a 1x1 image with this color + private func imageWithColor(color: UIColor) -> UIImage { + let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0) + UIGraphicsBeginImageContext(rect.size) + if let context = UIGraphicsGetCurrentContext() { + context.setFillColor(color.cgColor) + context.fill(rect) + } + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image! + } +} diff --git a/Freetime.xcodeproj/project.pbxproj b/Freetime.xcodeproj/project.pbxproj index 8a7eb13af..a86b7560a 100644 --- a/Freetime.xcodeproj/project.pbxproj +++ b/Freetime.xcodeproj/project.pbxproj @@ -520,6 +520,8 @@ BDB6AA762165B8EA009BB73C /* SwitchBranches.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB6AA752165B8EA009BB73C /* SwitchBranches.swift */; }; C0E3CD4B21BAE49B00185B57 /* NSRegularExpression+StaticString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E3CD4A21BAE49B00185B57 /* NSRegularExpression+StaticString.swift */; }; C0E3CD4D21BAE65000185B57 /* UIImage+StaticString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E3CD4C21BAE65000185B57 /* UIImage+StaticString.swift */; }; + C0F0707A22386CC900C90A31 /* SearchSegmentBarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F0707922386CC900C90A31 /* SearchSegmentBarCell.swift */; }; + C0F0707C223882DD00C90A31 /* SegmentControl+Border.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F0707B223882DD00C90A31 /* SegmentControl+Border.swift */; }; D8BAD0601FDA0A1A00C41071 /* LabelListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BAD05F1FDA0A1A00C41071 /* LabelListCell.swift */; }; D8BAD0641FDF221900C41071 /* LabelListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BAD0631FDF221900C41071 /* LabelListView.swift */; }; D8BAD0661FDF224600C41071 /* WrappingStaticSpacingFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BAD0651FDF224600C41071 /* WrappingStaticSpacingFlowLayout.swift */; }; @@ -1129,6 +1131,8 @@ BDB6AA752165B8EA009BB73C /* SwitchBranches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchBranches.swift; sourceTree = ""; }; C0E3CD4A21BAE49B00185B57 /* NSRegularExpression+StaticString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+StaticString.swift"; sourceTree = ""; }; C0E3CD4C21BAE65000185B57 /* UIImage+StaticString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+StaticString.swift"; sourceTree = ""; }; + C0F0707922386CC900C90A31 /* SearchSegmentBarCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSegmentBarCell.swift; sourceTree = ""; }; + C0F0707B223882DD00C90A31 /* SegmentControl+Border.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SegmentControl+Border.swift"; sourceTree = ""; }; D396E0DA66FED629384A84BC /* Pods_FreetimeWatch.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FreetimeWatch.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D8BAD05F1FDA0A1A00C41071 /* LabelListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelListCell.swift; sourceTree = ""; }; D8BAD0631FDF221900C41071 /* LabelListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelListView.swift; sourceTree = ""; }; @@ -2213,6 +2217,7 @@ children = ( 7BBFEE561F8A8A0400C68E47 /* SearchBarCell.swift */, 7BBFEE581F8A8A0400C68E47 /* SearchBarSectionController.swift */, + C0F0707922386CC900C90A31 /* SearchSegmentBarCell.swift */, ); path = SearchBar; sourceTree = ""; @@ -2303,6 +2308,7 @@ 298C7E2721D80BAF00DD2A60 /* Error+GraphQLForbidden.swift */, 031E0240220B433C00A329F1 /* UIImage+Color.swift */, 03E8D823221D339200EB792A /* GithubURL.swift */, + C0F0707B223882DD00C90A31 /* SegmentControl+Border.swift */, ); path = Utility; sourceTree = ""; @@ -3028,6 +3034,7 @@ 292FCAF91EDFCC510026635E /* IssueCommentDetailCell.swift in Sources */, DC3238931F9BA29D007DD924 /* SearchQuery.swift in Sources */, 2967DC56211751CB00FD3683 /* UIContentSizeCategory+Preferred.swift in Sources */, + C0F0707C223882DD00C90A31 /* SegmentControl+Border.swift in Sources */, 7BBFEE5B1F8A8A0400C68E47 /* SearchBarSectionController.swift in Sources */, 292FCAFA1EDFCC510026635E /* IssueCommentDetailsViewModel.swift in Sources */, 29A10541216D912F004734A0 /* IssueRoute+RoutePerformable.swift in Sources */, @@ -3280,6 +3287,7 @@ 290CA778216AFAE600DE04F8 /* RoutePerformable.swift in Sources */, 29FE635B21AB86B700A07A86 /* BookmarkRepoCell.swift in Sources */, 295B51421FC26B8100C3993B /* PeopleCell.swift in Sources */, + C0F0707A22386CC900C90A31 /* SearchSegmentBarCell.swift in Sources */, 29316DBF1ECC95DB007CAE3F /* RootViewControllers.swift in Sources */, 29DA1E791F5DEE8F0050C64B /* SearchLoadingView.swift in Sources */, 290CA76A216AC82700DE04F8 /* SearchShortcutRoute+RoutePerformable.swift in Sources */,