Tusker/Tusker/Screens/Onboarding/InstanceSelectorTableViewCo...

225 rindas
8.1 KiB
Swift

//
// InstanceSelectorTableViewController.swift
// Tusker
//
// Created by Shadowfacts on 9/15/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Combine
import Pachyderm
protocol InstanceSelectorTableViewControllerDelegate {
func didSelectInstance(url: URL)
}
fileprivate let instanceCell = "instanceCell"
class InstanceSelectorTableViewController: UITableViewController {
var delegate: InstanceSelectorTableViewControllerDelegate?
var dataSource: DataSource!
var searchController: UISearchController!
var recommendedInstances: [InstanceSelector.Instance] = []
let urlCheckerSubject = PassthroughSubject<String?, Never>()
var urlHandler: AnyCancellable?
var currentQuery: String?
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .portrait
} else {
return .all
}
}
init() {
super.init(style: .grouped)
title = NSLocalizedString("Choose Your Instance", comment: "onboarding screen title")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 120
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
switch item {
case let .selected(instance):
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
cell.updateUI(instance: instance)
return cell
case let .recommended(instance):
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
cell.updateUI(instance: instance)
return cell
}
})
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.searchTextField.autocapitalizationType = .none
navigationItem.searchController = searchController
definesPresentationContext = true
urlHandler = urlCheckerSubject
.debounce(for: .seconds(1), scheduler: RunLoop.main)
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
.sink(receiveValue: updateSpecificInstance)
loadRecommendedInstances()
}
private func parseURLComponents(input: String) -> URLComponents {
// we can't just use the URLComponents(string:) initializer, because when given just a domain (w/o protocol), it interprets it as the path
var input = input
var components = URLComponents()
// extract protocol
if input.contains("://") {
let parts = input.components(separatedBy: "://")
components.scheme = parts.first!
input = parts.last!
}
if components.scheme != "https" && components.scheme != "http" {
components.scheme = "https"
}
// drop path
if input.contains("/") {
let parts = input.components(separatedBy: "/")
input = parts.first!
}
// parse port
if input.contains(":") {
let parts = input.components(separatedBy: ":")
input = parts.first!
components.port = Int(parts.last!)
}
components.host = input
return components
}
private func updateSpecificInstance(domain: String) {
let components = parseURLComponents(input: domain)
let client = Client(baseURL: components.url!)
let request = client.getInstance()
client.run(request) { (response) in
var snapshot = self.dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected))
if case let .success(instance, _) = response {
if !snapshot.sectionIdentifiers.contains(.selected) {
snapshot.appendSections([.selected])
}
snapshot.appendItems([.selected(instance)], toSection: .selected)
DispatchQueue.main.async {
self.dataSource.apply(snapshot)
}
}
}
}
private func loadRecommendedInstances() {
InstanceSelector.getInstances(category: nil) { (response) in
guard case let .success(instances, _) = response else { fatalError() }
self.recommendedInstances = instances
self.filterRecommendedResults()
}
}
func filterRecommendedResults() {
let filteredInstances: [InstanceSelector.Instance]
if let currentQuery = currentQuery, !currentQuery.isEmpty {
filteredInstances = recommendedInstances.filter {
$0.domain.contains(currentQuery) || $0.description.lowercased().contains(currentQuery)
}
} else {
filteredInstances = recommendedInstances
}
var snapshot = self.dataSource.snapshot()
snapshot.deleteSections([.recommendedInstances])
snapshot.appendSections([.recommendedInstances])
snapshot.appendItems(filteredInstances.map { Item.recommended($0) }, toSection: .recommendedInstances)
DispatchQueue.main.async {
self.dataSource.apply(snapshot)
}
}
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let delegate = delegate, let item = dataSource.itemIdentifier(for: indexPath) else {
return
}
switch item {
case let .selected(instance):
// we can't just turn the URI string from the API into a URL instance, because Mastodon only includes the domain in the "URI"
let components = parseURLComponents(input: instance.uri)
delegate.didSelectInstance(url: components.url!)
case let .recommended(instance):
var components = URLComponents()
components.scheme = "https"
components.host = instance.domain
components.path = "/"
delegate.didSelectInstance(url: components.url!)
}
}
}
extension InstanceSelectorTableViewController {
enum Section {
case selected
case recommendedInstances
}
enum Item: Equatable, Hashable {
case selected(Instance)
case recommended(InstanceSelector.Instance)
static func ==(lhs: Item, rhs: Item) -> Bool {
if case let .selected(instance) = lhs,
case let .selected(other) = rhs {
return instance.uri == other.uri
} else if case let .recommended(instance) = lhs,
case let .recommended(other) = rhs {
return instance.domain == other.domain
}
return false
}
func hash(into hasher: inout Hasher) {
switch self {
case let .selected(instance):
hasher.combine(Section.selected)
hasher.combine(instance.uri)
case let .recommended(instance):
hasher.combine(Section.recommendedInstances)
hasher.combine(instance.domain)
}
}
}
class DataSource: UITableViewDiffableDataSource<Section, Item> {
}
}
extension InstanceSelectorTableViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
currentQuery = searchController.searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
filterRecommendedResults()
urlCheckerSubject.send(currentQuery)
}
}