Commit c9991001 authored by Jose Blaya's avatar Jose Blaya
Browse files

Merge branch '223-piax-region-selection-add-search-bar' into...

Merge branch '223-piax-region-selection-add-search-bar' into '222-piax-region-selection-update-cell-style'

Resolve "PIAX. Region Selection. Add search bar"

See merge request ios/vpn-ios!341
parents 5fac29eb d03d6d52
This diff is collapsed.
......@@ -14,6 +14,7 @@ import SwiftyBeaver
private let log = SwiftyBeaver.self
class AppPreferences {
private struct Entries {
static let version = "Version"
......@@ -28,6 +29,10 @@ class AppPreferences {
static let lastVPNConnectionStatus = "LastVPNConnectionStatus"
static let piaSocketType = "PIASocketType"
static let favoriteServerIdentifiers = "FavoriteServerIdentifiers"
static let regionFilter = "RegionFilter"
}
static let shared = AppPreferences()
......@@ -104,6 +109,30 @@ class AppPreferences {
}
}
var favoriteServerIdentifiers: [String] {
get {
if let serverIdentifiers = defaults.array(forKey: Entries.favoriteServerIdentifiers) as? [String] {
return serverIdentifiers
}
return []
}
set {
defaults.set(newValue, forKey: Entries.favoriteServerIdentifiers)
}
}
var regionFilter: RegionFilter {
get {
guard let rawValue = defaults.string(forKey: Entries.regionFilter) else {
return .name
}
return RegionFilter(rawValue: rawValue) ?? .name
}
set {
defaults.set(newValue.rawValue, forKey: Entries.regionFilter)
}
}
private init() {
guard let defaults = UserDefaults(suiteName: AppConstants.appGroup) else {
fatalError("Unable to initialize app preferences")
......@@ -113,6 +142,8 @@ class AppPreferences {
defaults.register(defaults: [
Entries.version: AppPreferences.currentVersion,
Entries.launched: false,
Entries.regionFilter: RegionFilter.name.rawValue,
Entries.favoriteServerIdentifiers: [],
Entries.didAskToEnableNotifications: false,
Entries.themeCode: ThemeCode.light.rawValue
])
......
{
"images" : [
{
"idiom" : "universal",
"filename" : "iconRedHeart6Copy2.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"images" : [
{
"idiom" : "universal",
"filename" : "group92.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"images" : [
{
"idiom" : "universal",
"filename" : "15IllustrationNoResultDarkCopy2.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"images" : [
{
"idiom" : "universal",
"filename" : "15IllustrationNoResultLightCopy2.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
......@@ -14,7 +14,7 @@ private struct PIAConnectionButtonSettings {
static let outsideBorderWidth: CGFloat = 10.0
static let outsideBorderLightColor = UIColor.piaGrey2.cgColor
static let outsideBorderDarkColor = UIColor.piaGrey10.cgColor
static let animatedShapeWidth: CGFloat = 2.0
static let animatedShapeWidth: CGFloat = 4.0
static let startAngle: CGFloat = -0.25 * 2 * .pi
static let endAngle: CGFloat = PIAConnectionButtonSettings.startAngle + 2 * .pi
static let udpateColorAnimationDuration = 0.3
......
//
// PropertyStoring.swift
// PIA VPN
//
// Created by Jose Antonio Blaya Garcia on 28/12/2018.
// Copyright © 2018 London Trust Media. All rights reserved.
//
import Foundation
protocol PropertyStoring {
associatedtype T
func getAssociatedObject(_ key: UnsafeRawPointer!, defaultValue: T) -> T
}
extension PropertyStoring {
func getAssociatedObject(_ key: UnsafeRawPointer!, defaultValue: T) -> T {
guard let value = objc_getAssociatedObject(self, key) as? T else {
return defaultValue
}
return value
}
}
......@@ -22,12 +22,14 @@ class RegionCell: UITableViewCell, Restylable {
@IBOutlet private weak var selectedRegionImageView: UIImageView!
private var isFavorite: Bool!
private weak var server: Server!
override func setSelected(_ selected: Bool, animated: Bool) {
}
func fill(withServer server: Server, isSelected: Bool) {
viewShouldRestyle()
self.server = server
imvFlag.setImage(fromServer: server)
labelRegion.text = server.name
......@@ -49,10 +51,11 @@ class RegionCell: UITableViewCell, Restylable {
}
accessibilityIdentifier = "uitests.regions.region_name"
self.favoriteImageView.alpha = CGFloat(NSNumber(booleanLiteral: server.name != L10n.Global.automatic).floatValue)
self.favoriteButton.alpha = CGFloat(NSNumber(booleanLiteral: server.name != L10n.Global.automatic).floatValue)
self.favoriteImageView.image = self.favoriteImageView.image?.withRenderingMode(.alwaysTemplate)
self.favoriteImageView.alpha = CGFloat(NSNumber(booleanLiteral: !server.isAutomatic).floatValue)
self.favoriteButton.alpha = CGFloat(NSNumber(booleanLiteral: !server.isAutomatic).floatValue)
self.isFavorite = isSelected
self.isFavorite = server.isFavorite
self.updateFavoriteImage()
self.setSelected(false, animated: false)
......@@ -65,13 +68,19 @@ class RegionCell: UITableViewCell, Restylable {
self.backgroundColor = Theme.current.palette.lightBackground
self.contentView.backgroundColor = Theme.current.palette.lightBackground
Theme.current.applyList(labelRegion, appearance: .dark)
Theme.current.applySettingsCellTitle(labelRegion, appearance: .dark)
Theme.current.applyTag(labelPingTime, appearance: .dark)
Theme.current.applyFavoriteUnselectedImage(self.favoriteImageView)
if Theme.current.palette.appearance! == .dark {
self.favoriteImageView.tintColor = UIColor.piaGrey10
}
}
@IBAction func favoriteServer(_ sender: UIButton) {
self.isFavorite = !self.isFavorite
self.isFavorite ? self.server.favorite() : self.server.unfavorite()
self.animateFavoriteImage()
}
......@@ -87,8 +96,8 @@ class RegionCell: UITableViewCell, Restylable {
}
private func updateFavoriteImage() {
self.favoriteImageView.image = self.isFavorite ?
Asset.Piax.Global.favoriteSelected.image :
Asset.Piax.Global.favoriteUnselected.image
self.isFavorite ?
self.favoriteImageView.image = Asset.Piax.Global.favoriteSelected.image :
Theme.current.applyFavoriteUnselectedImage(self.favoriteImageView)
}
}
//
// RegionFilter.swift
// PIA VPN
//
// Created by Jose Antonio Blaya Garcia on 28/12/2018.
// Copyright © 2018 London Trust Media. All rights reserved.
//
import Foundation
public enum RegionFilter: String {
case name
case latency
case favorite
}
......@@ -8,6 +8,8 @@
import UIKit
import PIALibrary
import DZNEmptyDataSet
import PopupDialog
class RegionsViewController: AutolayoutViewController {
private struct Cells {
......@@ -17,9 +19,11 @@ class RegionsViewController: AutolayoutViewController {
@IBOutlet private weak var tableView: UITableView!
private var servers: [Server] = []
private var filteredServers = [Server]()
private var selectedServer: Server!
let searchController = UISearchController(searchResultsController: nil)
deinit {
NotificationCenter.default.removeObserver(self)
}
......@@ -30,19 +34,76 @@ class RegionsViewController: AutolayoutViewController {
title = L10n.Menu.Item.region
var servers = Client.providers.serverProvider.currentServers
servers.insert(Server.automatic, at: 0)
let favoriteServers = AppPreferences.shared.favoriteServerIdentifiers
for server in servers {
server.isFavorite = favoriteServers.contains(server.identifier)
}
self.servers = servers
filterServers()
selectedServer = Client.preferences.displayedServer
NotificationCenter.default.addObserver(self, selector: #selector(pingsDidComplete(notification:)), name: .PIADaemonsDidPingServers, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(viewHasRotated), name: .UIDeviceOrientationDidChange, object: nil)
setupSearchBarController()
stylePopupDialog()
tableView.emptyDataSetSource = self
tableView.emptyDataSetDelegate = self
}
private func setupRightBarButton() {
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: Asset.Piax.Global.iconFilter.image,
style: .plain,
target: self,
action: #selector(showFilter(_:))
)
navigationItem.rightBarButtonItem?.accessibilityLabel = L10n.Region.Accessibility.filter
}
private func stylePopupDialog() {
let dialogAppearance = PopupDialogDefaultView.appearance()
dialogAppearance.backgroundColor = Theme.current.palette.appearance == .dark ? UIColor.piaGrey6 : .white
dialogAppearance.titleFont = TextStyle.textStyle12.font!
dialogAppearance.titleColor = Theme.current.palette.appearance == .dark ? .white : TextStyle.textStyle12.color
let containerAppearance = PopupDialogContainerView.appearance()
containerAppearance.cornerRadius = 0
containerAppearance.shadowEnabled = false
let overlayAppearance = PopupDialogOverlayView.appearance()
overlayAppearance.color = .black
overlayAppearance.blurEnabled = false
overlayAppearance.liveBlurEnabled = false
overlayAppearance.opacity = 0.5
let buttonAppearance = DefaultButton.appearance()
buttonAppearance.titleFont = TextStyle.textStyle21.font!
buttonAppearance.titleColor = TextStyle.textStyle21.color
buttonAppearance.buttonColor = Theme.current.palette.appearance == .dark ? UIColor.piaGrey6 : .white
buttonAppearance.separatorColor = Theme.current.palette.appearance == .dark ? UIColor.piaGrey10 : UIColor.piaGrey1
}
private func setupSearchBarController() {
searchController.searchResultsUpdater = self
if #available(iOS 9.1, *) {
searchController.obscuresBackgroundDuringPresentation = false
}
searchController.searchBar.placeholder = L10n.Region.Search.placeholder
self.tableView.tableHeaderView = self.searchController.searchBar
definesPresentationContext = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
styleNavigationBarWithTitle(L10n.Menu.Item.region)
setupRightBarButton()
let selectedRow = servers.index { (server) -> Bool in
return (server.identifier == selectedServer.identifier)
}
......@@ -54,6 +115,53 @@ class RegionsViewController: AutolayoutViewController {
}
// MARK: Actions
@objc private func showFilter(_ sender: Any?) {
let popup = PopupDialog(title: L10n.Region.Filter.sortby.uppercased(),
message: nil)
let buttonName = DefaultButton(title: L10n.Region.Filter.name.uppercased(), dismissOnTap: true) {
AppPreferences.shared.regionFilter = .name
self.filterServers()
}
let buttonLatency = DefaultButton(title: L10n.Region.Filter.latency.uppercased(), dismissOnTap: true) {
AppPreferences.shared.regionFilter = .latency
self.filterServers()
}
let buttonFavorites = DefaultButton(title: L10n.Region.Filter.favorites.uppercased(), dismissOnTap: true) {
AppPreferences.shared.regionFilter = .favorite
self.filterServers()
}
switch AppPreferences.shared.regionFilter {
case .name:
buttonName.titleColor = UIColor.piaGreenDark20
case .latency:
buttonLatency.titleColor = UIColor.piaGreenDark20
default:
buttonFavorites.titleColor = UIColor.piaGreenDark20
}
popup.addButtons([buttonName, buttonLatency, buttonFavorites])
self.present(popup, animated: true, completion: nil)
}
private func filterServers() {
self.servers = servers.filter({ !$0.isAutomatic })
switch AppPreferences.shared.regionFilter {
case .name:
self.servers = self.servers.sorted(by: { $0.name < $1.name })
case .latency:
self.servers = self.servers.sorted(by: { $0.pingTime ?? 0 < $1.pingTime ?? 0 })
default:
self.servers = self.servers.sorted(by: { $0.isFavorite && !$1.isFavorite })
}
tableView.reloadData()
self.servers.insert(Server.automatic, at: 0)
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
}
@objc private func viewHasRotated() {
styleNavigationBarWithTitle(L10n.Menu.Item.region)
}
......@@ -86,7 +194,14 @@ class RegionsViewController: AutolayoutViewController {
guard let indexPath = tableView.indexPath(for: sender as! UITableViewCell) else {
fatalError("Segue triggered without an input cell?")
}
let newSelectedServer = servers[indexPath.row]
let newSelectedServer: Server
if isFiltering() {
newSelectedServer = filteredServers[indexPath.row]
} else {
newSelectedServer = servers[indexPath.row]
}
selectedServer = newSelectedServer
tableView.reloadData()
......@@ -114,14 +229,26 @@ class RegionsViewController: AutolayoutViewController {
Theme.current.applyLightBackground(view)
Theme.current.applyLightBackground(viewContainer)
}
searchController.view.backgroundColor = .clear
Theme.current.applyLightBackground(tableView)
Theme.current.applyDividerToSeparator(tableView)
Theme.current.applySearchBarStyle(searchController.searchBar)
let bgView = UIView()
bgView.backgroundColor = .clear
self.tableView.backgroundView = bgView
tableView.reloadData()
}
}
extension RegionsViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
return filteredServers.count
}
return servers.count
}
......@@ -129,10 +256,63 @@ extension RegionsViewController: UITableViewDataSource, UITableViewDelegate {
let cell = tableView.dequeueReusableCell(withIdentifier: Cells.region, for: indexPath) as! RegionCell
cell.selectionStyle = .none
cell.separatorInset = .zero
let server = servers[indexPath.row]
let server: Server
if isFiltering() {
server = filteredServers[indexPath.row]
} else {
server = servers[indexPath.row]
}
let isSelected = (server.identifier == selectedServer.identifier)
cell.fill(withServer: server, isSelected: isSelected)
return cell
}
}
extension RegionsViewController: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
// MARK: - Private instance methods
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredServers = servers.filter({( server : Server) -> Bool in
return server.name.lowercased().contains(searchText.lowercased())
})
tableView.reloadData()
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
}
extension RegionsViewController: DZNEmptyDataSetSource, DZNEmptyDataSetDelegate {
func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! {
return Theme.current.noResultsImage()
}
func emptyDataSetWillAppear(_ scrollView: UIScrollView!) {
tableView.separatorStyle = .none
}
func emptyDataSetWillDisappear(_ scrollView: UIScrollView!) {
tableView.separatorStyle = .singleLine
}
func emptyDataSet(_ scrollView: UIScrollView!, didTap view: UIView!) {
searchController.searchBar.resignFirstResponder()
}
}
//
// Server+Favorite.swift
// PIA VPN
//
// Created by Jose Antonio Blaya Garcia on 28/12/2018.
// Copyright © 2018 London Trust Media. All rights reserved.
//
import Foundation
import PIALibrary
protocol Favoritable {
/// Favorite this server and update the cached servers
func favorite()
/// Unfavorite this server and update the cached servers
func unfavorite()
}
extension Server: Favoritable, PropertyStoring {
typealias T = Bool
private struct CustomProperties {
static var isFavorite = false
}
var isFavorite: Bool {
get {
return getAssociatedObject(&CustomProperties.isFavorite, defaultValue: CustomProperties.isFavorite)
}
set {
return objc_setAssociatedObject(self, &CustomProperties.isFavorite, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
func favorite() {
self.isFavorite = true
var currentFavorites = AppPreferences.shared.favoriteServerIdentifiers
currentFavorites.append(self.identifier)
AppPreferences.shared.favoriteServerIdentifiers = currentFavorites
}
func unfavorite() {
self.isFavorite = false
let currentFavorites = AppPreferences.shared.favoriteServerIdentifiers
let filteredFavorites = currentFavorites.filter({$0 != self.identifier})
AppPreferences.shared.favoriteServerIdentifiers = filteredFavorites
}
}
......@@ -50,14 +50,20 @@ enum Asset {
}
enum Global {
static let favoriteSelected = ImageAsset(name: "favorite-selected")
static let favoriteUnselectedDark = ImageAsset(name: "favorite-unselected-dark")
static let favoriteUnselected = ImageAsset(name: "favorite-unselected")
static let iconBack = ImageAsset(name: "icon-back")
static let iconFilter = ImageAsset(name: "icon-filter")
static let pagecontrolSelectedDot = ImageAsset(name: "pagecontrol-selected-dot")
static let pagecontrolUnselectedDot = ImageAsset(name: "pagecontrol-unselected-dot")
static let regionSelected = ImageAsset(name: "region-selected")
static let scrollableMapDark = ImageAsset(name: "scrollableMap-dark")
static let scrollableMapLight = ImageAsset(name: "scrollableMap-light")
}
enum Regions {
static let noResultsDark = ImageAsset(name: "no-results-dark")
static let noResultsLight = ImageAsset(name: "no-results-light")
}
enum Splash {
static let darkSplash = ImageAsset(name: "dark-splash")
static let lightSplash = ImageAsset(name: "light-splash")
......@@ -323,13 +329,17 @@ enum Asset {
static let allImages: [ImageAsset] = [
Piax.Dashboard.vpnButton,
Piax.Global.favoriteSelected,
Piax.Global.favoriteUnselectedDark,
Piax.Global.favoriteUnselected,
Piax.Global.iconBack,
Piax.Global.iconFilter,
Piax.Global.pagecontrolSelectedDot,
Piax.Global.pagecontrolUnselectedDot,
Piax.Global.regionSelected,
Piax.Global.scrollableMapDark,
Piax.Global.scrollableMapLight,
Piax.Regions.noResultsDark,
Piax.Regions.noResultsLight,
Piax.Splash.darkSplash,
Piax.Splash.lightSplash,
accessoryExpire,
......
......@@ -278,6 +278,27 @@ internal enum L10n {