Settings: Switch to UIKit

This commit is contained in:
James Magahern
2021-06-29 18:09:42 -07:00
parent c60df21c54
commit 656cf55b96
9 changed files with 424 additions and 26 deletions

View File

@@ -386,8 +386,9 @@ class BrowserViewController: UIViewController
let userActivity = NSUserActivity(activityType: SessionActivityType.SettingsWindow.rawValue)
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: userActivity, options: .none, errorHandler: nil)
#else
let settingsVC = SettingsViewController()
present(settingsVC, animated: true, completion: nil)
let settingsVC = SettingsViewController(windowScene: view.window!.windowScene!)
let wrapperNC = UINavigationController(rootViewController: settingsVC)
present(wrapperNC, animated: true, completion: nil)
#endif
}

View File

@@ -0,0 +1,47 @@
//
// GenericContentView.swift
// GenericContentView
//
// Created by James Magahern on 6/29/21.
//
import UIKit
class GenericContentView<View, Configuration> : UIView, UIContentView
where View: UIView, Configuration: UIContentConfiguration
{
typealias Applicator = (Configuration, View) -> Void
var insets: UIEdgeInsets = .zero
let applicator: Applicator
let view: View
var configuration: UIContentConfiguration {
didSet {
guard let config = configuration as? Configuration else { return }
apply(config)
}
}
init(configuration: Configuration, view: View, applicator: @escaping Applicator) {
self.configuration = configuration
self.view = view
self.applicator = applicator
super.init(frame: .zero)
addSubview(view)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
view.frame = bounds.inset(by: insets)
}
internal func apply(_ configuration: Configuration) {
applicator(configuration, view)
}
}

View File

@@ -25,10 +25,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let window = UIWindow(windowScene: windowScene)
if let userActivity = connectionOptions.userActivities.first {
if userActivity.activityType == SessionActivityType.SettingsWindow.rawValue {
let settingsViewController = SettingsViewController()
let settingsViewController = SettingsViewController(windowScene: windowScene)
self.settingsViewController = settingsViewController
window.rootViewController = settingsViewController
windowScene.sizeRestrictions?.maximumSize = CGSize(width: 500.0, height: 1200.0)
windowScene.sizeRestrictions?.maximumSize = CGSize(width: 760.0, height: 400.0)
}
} else {
let browserViewController = BrowserViewController()
@@ -39,15 +39,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let url = urlContext.url
browserViewController.tab.beginLoadingURL(url)
}
#if targetEnvironment(macCatalyst)
windowScene.titlebar?.titleVisibility = .hidden
windowScene.titlebar?.separatorStyle = .none
#endif
}
window.makeKeyAndVisible()
self.window = window
#if targetEnvironment(macCatalyst)
windowScene.titlebar?.titleVisibility = .hidden
windowScene.titlebar?.separatorStyle = .none
#endif
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)

View File

@@ -19,7 +19,7 @@ struct SettingsCategoryCell: View {
}
}
struct SettingsView: View {
struct AmberSettingsView: View {
@Environment(\.presentationMode)
@Binding private var presentationMode
@@ -62,6 +62,6 @@ struct SettingsView: View {
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
AmberSettingsView()
}
}

View File

@@ -0,0 +1,22 @@
//
// SettingsViewController.swift
// App
//
// Created by James Magahern on 3/3/21.
//
import SwiftUI
import UIKit
class AmberSettingsViewController: UIHostingController<AmberSettingsView>
{
var settingsView = AmberSettingsView()
init() {
super.init(rootView: settingsView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@@ -0,0 +1,195 @@
//
// GeneralSettingsViewController.swift
// GeneralSettingsViewController
//
// Created by James Magahern on 6/25/21.
//
import UIKit
struct LabelContentConfiguration : UIContentConfiguration
{
var text: String = ""
var insets: UIEdgeInsets = .zero
var textAlignment: NSTextAlignment = .natural
func makeContentView() -> UIView & UIContentView {
let label = UILabel(frame: .zero)
let contentView = GenericContentView<UILabel, LabelContentConfiguration>(configuration: self, view: label) { config, label in
label.text = config.text
label.textAlignment = config.textAlignment
}
contentView.insets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
return contentView
}
func updated(for state: UIConfigurationState) -> Self {
self
}
}
struct ButtonContentConfiguration : UIContentConfiguration
{
var menu: UIMenu
func makeContentView() -> UIView & UIContentView {
let button = UIButton(primaryAction: nil)
return GenericContentView<UIButton, ButtonContentConfiguration>(configuration: self, view: button) { config, button in
button.menu = menu
button.showsMenuAsPrimaryAction = true
if #available(macCatalyst 15.0, iOS 15.0, *) {
var buttonConfiguration = UIButton.Configuration.plain()
buttonConfiguration.titleAlignment = .trailing
button.configuration = buttonConfiguration
button.changesSelectionAsPrimaryAction = true
}
}
}
func updated(for state: UIConfigurationState) -> ButtonContentConfiguration {
self
}
}
class GeneralSettingsViewController: UIViewController
{
enum Section: String, CaseIterable {
case searchEngine = "Search Engine"
}
typealias Item = String
static let SearchProviderPopupItem = "searchProvider.popup"
let dataSource: UICollectionViewDiffableDataSource<Section, Item>
let collectionView: UICollectionView
static func createLayout(forIdiom idiom: UIUserInterfaceIdiom) -> UICollectionViewLayout {
if idiom == .mac {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.60),
heightDimension: .absolute(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(1.0), top: nil, trailing: nil, bottom: nil)
let insets = NSDirectionalEdgeInsets(top: 24.0, leading: 64.0, bottom: 24.0, trailing: 64.0)
let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.40),
heightDimension: .estimated(44.0))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .topLeading,
absoluteOffset: CGPoint(x: insets.leading, y: insets.top)
)
sectionHeader.extendsBoundary = false
sectionHeader.contentInsets = insets
let section = NSCollectionLayoutSection(group: group)
// section.interGroupSpacing = spacing
section.contentInsets = insets
section.supplementariesFollowContentInsets = true
section.boundarySupplementaryItems = [ sectionHeader ]
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
} else {
var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
listConfiguration.headerMode = .supplementary
return UICollectionViewCompositionalLayout.list(using: listConfiguration)
}
}
static func sectionHeaderConfiguration(forIdiom idiom: UIUserInterfaceIdiom, sectionName: String) -> UIContentConfiguration {
if idiom == .mac {
return LabelContentConfiguration(
text: sectionName + ": ",
insets: UIEdgeInsets(top: 0, left: 10.0, bottom: 0, right: 10.0),
textAlignment: .right
)
} else {
var config = UIListContentConfiguration.plainHeader()
config.text = sectionName
return config
}
}
#if targetEnvironment(macCatalyst)
static let staticIdiom = UIUserInterfaceIdiom.mac
#else
static let staticIdiom = UIUserInterfaceIdiom.pad
#endif
init() {
let actionHandler = { (action: UIAction) in
let providerString = action.title
let provider = Settings.SearchProviderSetting(rawValue: providerString)!
Settings.shared.searchProvider = provider
}
let itemCellRegistry = UICollectionView.CellRegistration<UICollectionViewCell, Item> { cell, indexPath, identifier in
if identifier == Self.SearchProviderPopupItem {
let menu = UIMenu(children: Settings.SearchProviderSetting.allCases.map { provider in
let action = UIAction(title: provider.rawValue, handler: actionHandler)
action.state = Settings.shared.searchProvider == provider ? .on : .off
return action
})
cell.contentConfiguration = ButtonContentConfiguration(menu: menu)
#if !targetEnvironment(macCatalyst)
cell.backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
#endif
}
}
let sectionHeaderRegistry = UICollectionView.SupplementaryRegistration<UICollectionViewCell>(elementKind: UICollectionView.elementKindSectionHeader, handler: {
(cell, string, indexPath) in
let sectionName = Section.allCases[indexPath.section].rawValue
cell.contentConfiguration = Self.sectionHeaderConfiguration(forIdiom: Self.staticIdiom, sectionName: sectionName)
})
let layout = Self.createLayout(forIdiom: Self.staticIdiom)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .systemGroupedBackground
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
(collectionView, indexPath, identifier) in
return collectionView.dequeueConfiguredReusableCell(using: itemCellRegistry, for: indexPath, item: identifier)
}
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderRegistry, for: indexPath)
}
super.init(nibName: nil, bundle: nil)
tabBarItem.title = "General"
tabBarItem.image = UIImage(systemName: "gear")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = collectionView
}
override func viewDidLoad() {
super.viewDidLoad()
var snapshot = dataSource.snapshot()
snapshot.appendSections(Section.allCases)
// iOS
// snapshot.appendItems(Settings.SearchProviderSetting.allCases.map { $0.rawValue }, toSection: .searchEngine)
snapshot.appendItems([ Self.SearchProviderPopupItem ], toSection: .searchEngine)
dataSource.apply(snapshot, animatingDifferences: false)
}
}

View File

@@ -0,0 +1,27 @@
//
// RedirectRulesSettingsViewController.swift
// RedirectRulesSettingsViewController
//
// Created by James Magahern on 6/25/21.
//
import UIKit
class RedirectRulesSettingsViewController: UIViewController
{
init() {
super.init(nibName: nil, bundle: nil)
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() {
view.backgroundColor = .red
}
}

View File

@@ -1,22 +1,104 @@
//
// SettingsViewController.swift
// App
// SettingsViewController
//
// Created by James Magahern on 3/3/21.
// Created by James Magahern on 6/25/21.
//
import SwiftUI
import UIKit
class SettingsViewController: UIHostingController<SettingsView>
#if targetEnvironment(macCatalyst)
fileprivate extension NSToolbarItem.Identifier {
static let categories = NSToolbarItem.Identifier("preferences.categories")
}
#endif
#if !targetEnvironment(macCatalyst)
protocol NSToolbarDelegate {}
#endif
class SettingsViewController: UITabBarController, NSToolbarDelegate
{
var settingsView = SettingsView()
#if targetEnvironment(macCatalyst)
let toolbar = NSToolbar(identifier: NSToolbar.Identifier("net.buzzert.rossler-attix.preferences-toolbar"))
#endif
init() {
super.init(rootView: settingsView)
init(windowScene: UIWindowScene) {
super.init(nibName: nil, bundle: nil)
self.title = "Preferences"
self.viewControllers = [
GeneralSettingsViewController(),
RedirectRulesSettingsViewController(),
]
navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction { [unowned self] _ in
self.dismiss(animated: true, completion: nil)
}, menu: nil)
#if targetEnvironment(macCatalyst)
configureTabsForMacIdiom(windowScene: windowScene)
#endif
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#if targetEnvironment(macCatalyst)
internal func configureTabsForMacIdiom(windowScene: UIWindowScene) {
self.tabBar.isHidden = true
guard let titlebar = windowScene.titlebar else { return }
titlebar.toolbarStyle = .preference
toolbar.delegate = self
toolbar.displayMode = .iconAndLabel
titlebar.toolbar = toolbar
windowScene.title = self.title
toolbar.selectedItemIdentifier = toolbarAllowedItemIdentifiers(toolbar).first
}
// MARK: NSToolbarDelegate
@objc
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return viewControllers!.map { NSToolbarItem.Identifier($0.tabBarItem.title!) }
}
@objc
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return toolbarAllowedItemIdentifiers(toolbar)
}
@objc
func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return toolbarAllowedItemIdentifiers(toolbar)
}
@objc
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem?
{
guard let viewController = viewControllers!.first(where: { $0.tabBarItem.title == itemIdentifier.rawValue })
else { return nil }
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.label = viewController.tabBarItem.title!
item.image = viewController.tabBarItem.image!
item.action = #selector(didSelectToolbarItem(_:))
return item
}
@objc
internal func didSelectToolbarItem(_ sender: NSToolbarItem) {
guard let viewController = viewControllers!.first(where: { $0.tabBarItem.title == sender.itemIdentifier.rawValue }) else { return }
selectedViewController = viewController
toolbar.selectedItemIdentifier = sender.itemIdentifier
}
#endif // targetEnvironment(macCatalyst)
}