// // RedirectRulesSettingsViewController.swift // RedirectRulesSettingsViewController // // Created by James Magahern on 6/25/21. // import UIKit class RedirectRulesSettingsViewController: UICollectionViewController { private enum Section { case rules case addButton } private enum Items: String { case addButton } private lazy var dataSource: UICollectionViewDiffableDataSource = { let registry = UICollectionView.CellRegistration { [unowned self] cell, indexPath, item in var config = cell.defaultContentConfiguration() if item == Items.addButton.rawValue { config.text = "Add Redirect Rule" config.textProperties.font = .preferredFont(forTextStyle: .subheadline) config.textProperties.alignment = .center config.image = .init(systemName: "plus.circle") cell.accessories = [] } else { if let rule = Settings.shared.redirectRules[item] { config.text = "\(item) → \(rule)" } let deleteOptions = UICellAccessory.DeleteOptions(isHidden: false, reservedLayoutWidth: nil, tintColor: .red, backgroundColor: nil) cell.accessories = [ .delete(displayed: .always, options: deleteOptions, actionHandler: { [unowned self] in deleteRedirectRule(fromHost: item) }) ] } cell.contentConfiguration = config } return UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, itemIdentifier) in collectionView.dequeueConfiguredReusableCell(using: registry, for: indexPath, item: itemIdentifier) } }() init() { let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) #if targetEnvironment(macCatalyst) let layout = UICollectionViewCompositionalLayout { section, layoutEnvironment in let listSection = NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) listSection.contentInsets = NSDirectionalEdgeInsets(top: 24.0, leading: 164.0, bottom: 24.0, trailing: 164.0) return listSection } #else let layout = UICollectionViewCompositionalLayout.list(using: config) #endif super.init(collectionViewLayout: layout) tabBarItem.title = "Redirect Rules" tabBarItem.image = UIImage(systemName: "arrowshape.zigzag.forward") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { var snapshot = dataSource.snapshot() snapshot.appendSections([ .rules, .addButton ]) snapshot.appendItems(Settings.shared.redirectRules.keys.sorted(), toSection: .rules) snapshot.appendItems([ Items.addButton.rawValue ], toSection: .addButton) dataSource.applySnapshotUsingReloadData(snapshot) } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { collectionView.deselectItem(at: indexPath, animated: true) let item = dataSource.itemIdentifier(for: indexPath) if item == Items.addButton.rawValue { let createRuleViewController = CreateRedirectRuleViewController() createRuleViewController.finishedCreatingRule = { [unowned self] (fromHost, toHost) in if fromHost.count > 0 && toHost.count > 0 { addRedirectRule(fromHost: fromHost, toHost: toHost) } } let navController = UINavigationController(rootViewController: createRuleViewController) present(navController, animated: true, completion: nil) } } private func addRedirectRule(fromHost: String, toHost: String) { Settings.shared.redirectRules[fromHost] = toHost var snapshot = dataSource.snapshot() snapshot.appendItems([ fromHost ], toSection: .rules) dataSource.apply(snapshot, animatingDifferences: true) } private func deleteRedirectRule(fromHost: String) { Settings.shared.redirectRules.removeValue(forKey: fromHost) var snapshot = dataSource.snapshot() snapshot.deleteItems([ fromHost ]) dataSource.apply(snapshot, animatingDifferences: true) } } class CreateRedirectRuleViewController: UICollectionViewController { enum Section: Int { case header case fromHost case toHost } enum Item: Hashable { case header(section: Section) case textField(section: Section) } struct TextFieldCellConfiguration: UIContentConfiguration { var textField: UITextField func makeContentView() -> UIView & UIContentView { return GenericContentView(configuration: self, view: textField) { _,_ in } } func updated(for state: UIConfigurationState) -> TextFieldCellConfiguration { self } } struct ImageViewCellConfiguration: UIContentConfiguration { var imageView: UIImageView func makeContentView() -> UIView & UIContentView { return GenericContentView(configuration: self, view: imageView) { _,_ in } } func updated(for state: UIConfigurationState) -> Self { self } } public var finishedCreatingRule: ((_ fromHost: String, _ toHost: String) -> Void)? private let sectionHeaderImageView = UIImageView(image: .init(systemName: "arrowshape.zigzag.forward")) private let fromHostField = UITextField(frame: .zero) private let toHostField = UITextField(frame: .zero) private let doneButton = UIBarButtonItem(systemItem: .done, primaryAction: nil, menu: nil) private lazy var dataSource: UICollectionViewDiffableDataSource = { let listCellRegistry = UICollectionView.CellRegistration { [unowned self] cell, indexPath, item in let section = dataSource.snapshot().sectionIdentifier(containingItem: item) switch item { case .header(_): var contentConfig = cell.defaultContentConfiguration() contentConfig.text = section == .fromHost ? "From Host" : "To Host" cell.contentConfiguration = contentConfig case .textField(_): cell.contentConfiguration = TextFieldCellConfiguration(textField: section == .fromHost ? fromHostField : toHostField) } } let headerCellRegistry = UICollectionView.CellRegistration { [unowned self] cell, indexPath, item in let config = ImageViewCellConfiguration(imageView: sectionHeaderImageView) cell.contentConfiguration = config } return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] (collectionView, indexPath, itemIdentifier) in let section = self.dataSource.sectionIdentifier(for: indexPath.section) if section == .header { return collectionView.dequeueConfiguredReusableCell(using: headerCellRegistry, for: indexPath, item: itemIdentifier) } else { return collectionView.dequeueConfiguredReusableCell(using: listCellRegistry, for: indexPath, item: itemIdentifier) } } }() init() { var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) config.headerMode = .firstItemInSection let layout = UICollectionViewCompositionalLayout { (sectionIndex, environment) in let section = Section(rawValue: sectionIndex) if section == .header { let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(80.0)) let group = NSCollectionLayoutGroup.vertical( layoutSize: size, subitems: [ NSCollectionLayoutItem(layoutSize: size) ]) return NSCollectionLayoutSection(group: group) } else { return NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment) } } super.init(collectionViewLayout: layout) preferredContentSize = CGSize(width: -1, height: 320.0) self.title = "Add Redirect Rule" navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction { [unowned self] _ in dismiss(animated: true, completion: nil) }, menu: nil) doneButton.primaryAction = UIAction { [unowned self] _ in finishedCreatingRule?(fromHostField.text!, toHostField.text!) dismiss(animated: true, completion: nil) } navigationItem.rightBarButtonItem = doneButton sectionHeaderImageView.contentMode = .scaleAspectFit fromHostField.placeholder = "fromhostname.com" fromHostField.autocorrectionType = .no fromHostField.autocapitalizationType = .none fromHostField.keyboardType = .URL fromHostField.returnKeyType = .next fromHostField.delegate = self toHostField.placeholder = "tohostname.com" toHostField.autocorrectionType = .no toHostField.autocapitalizationType = .none toHostField.keyboardType = .URL toHostField.returnKeyType = .done toHostField.delegate = self } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { var snapshot = dataSource.snapshot() snapshot.appendSections([ Section.header, Section.fromHost, Section.toHost ]) snapshot.appendItems([ .header(section: .header) ], toSection: .header) snapshot.appendItems([ .header(section: .fromHost), .textField(section: .fromHost) ], toSection: .fromHost) snapshot.appendItems([ .header(section: .toHost), .textField(section: .toHost) ], toSection: .toHost) dataSource.applySnapshotUsingReloadData(snapshot) } override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { false } } extension CreateRedirectRuleViewController : UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { if textField == fromHostField { toHostField.becomeFirstResponder() } else if textField == toHostField { toHostField.resignFirstResponder() } return false } }