diff --git a/Demo/DemoTests/Helpers/Snapshotting+WindowImage.swift b/Demo/DemoTests/Helpers/Snapshotting+WindowImage.swift index 10f492f..b72c192 100644 --- a/Demo/DemoTests/Helpers/Snapshotting+WindowImage.swift +++ b/Demo/DemoTests/Helpers/Snapshotting+WindowImage.swift @@ -7,7 +7,7 @@ extension Snapshotting where Value: UIViewController, Format == UIImage { Snapshotting.image(precision: precision).asyncPullback { vc in Async { callback in UIView.setAnimationsEnabled(false) - let window = UIApplication.shared.windows[0] + guard let window = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first?.windows.first else { return } window.rootViewController = vc action() DispatchQueue.main.async { @@ -15,6 +15,7 @@ extension Snapshotting where Value: UIViewController, Format == UIImage { window.drawHierarchy(in: window.bounds, afterScreenUpdates: true) } callback(image) + window.rootViewController = UIViewController() UIView.setAnimationsEnabled(true) } } diff --git a/Sources/XConfigs/DataSources/TableViewDataSource.swift b/Sources/XConfigs/DataSources/TableViewDataSource.swift index a12f837..e1275d2 100644 --- a/Sources/XConfigs/DataSources/TableViewDataSource.swift +++ b/Sources/XConfigs/DataSources/TableViewDataSource.swift @@ -1,9 +1,7 @@ -#if canImport(UIKit) - import UIKit +import UIKit - final class TableViewDataSource: UITableViewDiffableDataSource { - override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { - (snapshot().sectionIdentifiers[section] as? CustomStringConvertible)?.description - } +final class TableViewDataSource: UITableViewDiffableDataSource { + override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { + (snapshot().sectionIdentifiers[section] as? CustomStringConvertible)?.description } -#endif +} diff --git a/Sources/XConfigs/Extensions/UIView++.swift b/Sources/XConfigs/Extensions/UIView++.swift index 8c913e8..ea287cf 100644 --- a/Sources/XConfigs/Extensions/UIView++.swift +++ b/Sources/XConfigs/Extensions/UIView++.swift @@ -1,80 +1,78 @@ -#if canImport(UIKit) - import Combine - import UIKit +import Combine +import UIKit - // Provide a default `reuseIdentifier` equal to the class name. - private extension UITableViewCell { - static var reuseIdentifier: String { - String(describing: self) - } +// Provide a default `reuseIdentifier` equal to the class name. +private extension UITableViewCell { + static var reuseIdentifier: String { + String(describing: self) } +} - extension UITableView { - func registerCell(_ type: Cell.Type) { - register(type, forCellReuseIdentifier: type.reuseIdentifier) - } +extension UITableView { + func registerCell(_ type: Cell.Type) { + register(type, forCellReuseIdentifier: type.reuseIdentifier) + } - // MARK: Dequeue Table View Cell + // MARK: Dequeue Table View Cell - func dequeueCell(_ type: Cell.Type, for _: IndexPath) -> Cell { - guard let cell = dequeueReusableCell(withIdentifier: type.reuseIdentifier) as? Cell else { - fatalError("Unregistered cell: \(type.reuseIdentifier)") - } - return cell + func dequeueCell(_ type: Cell.Type, for _: IndexPath) -> Cell { + guard let cell = dequeueReusableCell(withIdentifier: type.reuseIdentifier) as? Cell else { + fatalError("Unregistered cell: \(type.reuseIdentifier)") } + return cell } +} - extension UIView { - func bind(to view: UIView, margins: UIEdgeInsets = .zero) { - translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - topAnchor.constraint(equalTo: view.topAnchor, constant: margins.top), - leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margins.left), - view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: margins.right), - view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: margins.bottom), - ]) - } +extension UIView { + func bind(to view: UIView, margins: UIEdgeInsets = .zero) { + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + topAnchor.constraint(equalTo: view.topAnchor, constant: margins.top), + leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margins.left), + view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: margins.right), + view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: margins.bottom), + ]) + } - func bindToSuperview(margins: UIEdgeInsets = .zero) { - guard let superview = superview else { return } - bind(to: superview, margins: margins) - } + func bindToSuperview(margins: UIEdgeInsets = .zero) { + guard let superview = superview else { return } + bind(to: superview, margins: margins) } +} - protocol ConfigurableView: UIView { - associatedtype ViewModel +protocol ConfigurableView: UIView { + associatedtype ViewModel - func configure(with viewModel: ViewModel) - } + func configure(with viewModel: ViewModel) +} - final class UIViewTableWrapperCell: UITableViewCell { - let mainView: MainView +final class UIViewTableWrapperCell: UITableViewCell { + let mainView: MainView - var subscriptions = Set() + var subscriptions = Set() - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - mainView = MainView() - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupUI() - } + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + mainView = MainView() + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - func configure(with viewModel: MainView.ViewModel) { - mainView.configure(with: viewModel) - } + func configure(with viewModel: MainView.ViewModel) { + mainView.configure(with: viewModel) + } - private func setupUI() { - contentView.addSubview(mainView) - mainView.bindToSuperview() - } + private func setupUI() { + contentView.addSubview(mainView) + mainView.bindToSuperview() + } - override func prepareForReuse() { - super.prepareForReuse() - subscriptions = .init() - } + override func prepareForReuse() { + super.prepareForReuse() + subscriptions = .init() } -#endif +} diff --git a/Sources/XConfigs/Extensions/UIViewController++.swift b/Sources/XConfigs/Extensions/UIViewController++.swift index 1a8d185..1bf40b1 100644 --- a/Sources/XConfigs/Extensions/UIViewController++.swift +++ b/Sources/XConfigs/Extensions/UIViewController++.swift @@ -1,20 +1,18 @@ -#if canImport(UIKit) - import UIKit +import UIKit - extension UIViewController { - func preferAsHalfSheet(preferredCornerRadius: CGFloat = 10) -> UIViewController { - if #available(iOS 15.0, *) { - if let sheet = sheetPresentationController { - sheet.detents = [.medium(), .large()] - sheet.prefersScrollingExpandsWhenScrolledToEdge = false - sheet.preferredCornerRadius = preferredCornerRadius - } +extension UIViewController { + func preferAsHalfSheet(preferredCornerRadius: CGFloat = 10) -> UIViewController { + if #available(iOS 15.0, *) { + if let sheet = sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersScrollingExpandsWhenScrolledToEdge = false + sheet.preferredCornerRadius = preferredCornerRadius } - return self } + return self + } - func wrapInsideNavVC() -> UINavigationController { - UINavigationController(rootViewController: self) - } + func wrapInsideNavVC() -> UINavigationController { + UINavigationController(rootViewController: self) } -#endif +} diff --git a/Sources/XConfigs/Models/SectionItemsModel.swift b/Sources/XConfigs/Models/SectionItemsModel.swift index 50b61b4..6ac1c89 100644 --- a/Sources/XConfigs/Models/SectionItemsModel.swift +++ b/Sources/XConfigs/Models/SectionItemsModel.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit.NSDiffableDataSourceSectionSnapshot struct SectionItemsModel: Hashable { var section: Section @@ -15,16 +16,12 @@ extension SectionItemsModel: Equatable { // MARK: - SectionItemsModel + NSDiffableDataSourceSnapshot -#if canImport(UIKit) - import UIKit - - extension Sequence { - @available(iOS 13.0, *) - func snapshot() -> NSDiffableDataSourceSnapshot where Element == SectionItemsModel { - reduce(into: NSDiffableDataSourceSnapshot()) { snapshot, sectionModel in - snapshot.appendSections([sectionModel.section]) - snapshot.appendItems(sectionModel.items, toSection: sectionModel.section) - } +extension Sequence { + @available(iOS 13.0, *) + func snapshot() -> NSDiffableDataSourceSnapshot where Element == SectionItemsModel { + reduce(into: NSDiffableDataSourceSnapshot()) { snapshot, sectionModel in + snapshot.appendSections([sectionModel.section]) + snapshot.appendItems(sectionModel.items, toSection: sectionModel.section) } } -#endif +} diff --git a/Sources/XConfigs/ViewControllers/InputValueViewController.swift b/Sources/XConfigs/ViewControllers/InputValueViewController.swift index ba82fad..3b5ecf3 100644 --- a/Sources/XConfigs/ViewControllers/InputValueViewController.swift +++ b/Sources/XConfigs/ViewControllers/InputValueViewController.swift @@ -1,88 +1,86 @@ -#if canImport(UIKit) - import Combine - import CombineCocoa - import Highlightr - import UIKit +import Combine +import CombineCocoa +import Highlightr +import UIKit - final class InputValueViewController: UIViewController, UITextViewDelegate { - typealias ViewModel = InputValueViewModel +final class InputValueViewController: UIViewController, UITextViewDelegate { + typealias ViewModel = InputValueViewModel - private lazy var textContainer = NSTextContainer().apply { - let textStorage = CodeAttributedString() - textStorage.language = "Javascript" - let layoutManager = NSLayoutManager() - textStorage.addLayoutManager(layoutManager) - layoutManager.addTextContainer($0) - textStorage.highlightr.setTheme(to: "vs") - } + private lazy var textContainer = NSTextContainer().apply { + let textStorage = CodeAttributedString() + textStorage.language = "Javascript" + let layoutManager = NSLayoutManager() + textStorage.addLayoutManager(layoutManager) + layoutManager.addTextContainer($0) + textStorage.highlightr.setTheme(to: "vs") + } - private lazy var textView = UITextView(frame: .zero, textContainer: textContainer).apply { - $0.font = .preferredFont(forTextStyle: .body) - $0.isEditable = true - } + private lazy var textView = UITextView(frame: .zero, textContainer: textContainer).apply { + $0.font = .preferredFont(forTextStyle: .body) + $0.isEditable = true + } - private let viewModel: ViewModel - private var subscriptions = Set() + private let viewModel: ViewModel + private var subscriptions = Set() - private var textSubject = PassthroughSubject() + private var textSubject = PassthroughSubject() - var valuePublisher: AnyPublisher { - textSubject.eraseToAnyPublisher() - } + var valuePublisher: AnyPublisher { + textSubject.eraseToAnyPublisher() + } - init(viewModel: ViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } + init(viewModel: ViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - handleViewModelOutput() - } + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + handleViewModelOutput() + } - private func setupUI() { - navigationItem.rightBarButtonItem = .init(systemItem: .done) - navigationItem.leftBarButtonItem = .init(systemItem: .cancel) - view.backgroundColor = .systemBackground - view.addSubview(textView) - textView.bindToSuperview(margins: .init(top: 20, left: 20, bottom: 20, right: 5)) - } + private func setupUI() { + navigationItem.rightBarButtonItem = .init(systemItem: .done) + navigationItem.leftBarButtonItem = .init(systemItem: .cancel) + view.backgroundColor = .systemBackground + view.addSubview(textView) + textView.bindToSuperview(margins: .init(top: 20, left: 20, bottom: 20, right: 5)) + } - private func handleViewModelOutput() { - guard let leftNavItem = navigationItem.leftBarButtonItem, - let rightNavITem = navigationItem.rightBarButtonItem - else { return } - let output = viewModel.transform(input: .init( - textPublisher: textView.textPublisher.compactMap { $0 }.eraseToAnyPublisher(), - dismissPublisher: leftNavItem.tapPublisher, - donePublisher: rightNavITem.tapPublisher - )) - output.title.sink { [weak self] title in - self?.title = title - } - .store(in: &subscriptions) + private func handleViewModelOutput() { + guard let leftNavItem = navigationItem.leftBarButtonItem, + let rightNavITem = navigationItem.rightBarButtonItem + else { return } + let output = viewModel.transform(input: .init( + textPublisher: textView.textPublisher.compactMap { $0 }.eraseToAnyPublisher(), + dismissPublisher: leftNavItem.tapPublisher, + donePublisher: rightNavITem.tapPublisher + )) + output.title.sink { [weak self] title in + self?.title = title + } + .store(in: &subscriptions) - output.value.sink { [weak self] value in - self?.textView.text = value - } - .store(in: &subscriptions) + output.value.sink { [weak self] value in + self?.textView.text = value + } + .store(in: &subscriptions) - output.action.sink { [weak self] action in - switch action { - case .cancel: - self?.dismiss(animated: true) - case let .done(text): - self?.textSubject.send(text) - self?.dismiss(animated: true) - } + output.action.sink { [weak self] action in + switch action { + case .cancel: + self?.dismiss(animated: true) + case let .done(text): + self?.textSubject.send(text) + self?.dismiss(animated: true) } - .store(in: &subscriptions) } + .store(in: &subscriptions) } -#endif +} diff --git a/Sources/XConfigs/ViewControllers/OptionViewController.swift b/Sources/XConfigs/ViewControllers/OptionViewController.swift index 5cacf54..3545d2d 100644 --- a/Sources/XConfigs/ViewControllers/OptionViewController.swift +++ b/Sources/XConfigs/ViewControllers/OptionViewController.swift @@ -1,90 +1,89 @@ -#if canImport(UIKit) - import Combine - import UIKit - final class OptionViewController: UITableViewController { - typealias ViewModel = OptionViewModel - typealias DataSource = UITableViewDiffableDataSource +import Combine +import UIKit - private let viewModel: ViewModel - private var subscriptions = Set() +final class OptionViewController: UITableViewController { + typealias ViewModel = OptionViewModel + typealias DataSource = UITableViewDiffableDataSource - private let itemSubject = PassthroughSubject() - var selectedItemPublisher: AnyPublisher { - itemSubject.eraseToAnyPublisher() - } + private let viewModel: ViewModel + private var subscriptions = Set() - private lazy var datasource: DataSource = { - var ds = DataSource(tableView: tableView) { [weak self] tableView, indexPath, item in - guard let self = self else { return .init() } - let cell = tableView.dequeueCell(UITableViewCell.self, for: indexPath) - cell.textLabel?.text = item.displayName - return cell - } - ds.defaultRowAnimation = .fade - return ds - }() + private let itemSubject = PassthroughSubject() + var selectedItemPublisher: AnyPublisher { + itemSubject.eraseToAnyPublisher() + } - init(viewModel: ViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) + private lazy var datasource: DataSource = { + var ds = DataSource(tableView: tableView) { [weak self] tableView, indexPath, item in + guard let self = self else { return .init() } + let cell = tableView.dequeueCell(UITableViewCell.self, for: indexPath) + cell.textLabel?.text = item.displayName + return cell } + ds.defaultRowAnimation = .fade + return ds + }() - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + init(viewModel: ViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - handleViewModelOutput() - } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + handleViewModelOutput() + } + + private func handleViewModelOutput() { + guard let leftNavItem = navigationItem.leftBarButtonItem else { return } + let output = viewModel.transform(input: .init( + reloadPublisher: Just(()).eraseToAnyPublisher(), + dismissPublisher: leftNavItem.tapPublisher, + selectItemPublisher: tableView.didSelectRowPublisher.compactMap { [weak self] indexPath -> ViewModel.Item? in + guard let self = self else { return nil } + return self.datasource.itemIdentifier(for: indexPath) + }.eraseToAnyPublisher() + )) - private func handleViewModelOutput() { - guard let leftNavItem = navigationItem.leftBarButtonItem else { return } - let output = viewModel.transform(input: .init( - reloadPublisher: Just(()).eraseToAnyPublisher(), - dismissPublisher: leftNavItem.tapPublisher, - selectItemPublisher: tableView.didSelectRowPublisher.compactMap { [weak self] indexPath -> ViewModel.Item? in - guard let self = self else { return nil } - return self.datasource.itemIdentifier(for: indexPath) - }.eraseToAnyPublisher() - )) + output.title.sink { [weak self] title in + self?.title = title + } + .store(in: &subscriptions) - output.title.sink { [weak self] title in - self?.title = title + output.sectionItemsModels + .sink { [weak self] secItems in + guard let self = self else { return } + self.datasource.apply(secItems.snapshot(), animatingDifferences: false) } .store(in: &subscriptions) - output.sectionItemsModels - .sink { [weak self] secItems in - guard let self = self else { return } - self.datasource.apply(secItems.snapshot(), animatingDifferences: false) - } - .store(in: &subscriptions) - - output.action - .sink { [weak self] action in - guard let self = self else { return } - switch action { - case .cancel: - self.dismiss(animated: true) - case let .select(item): - self.itemSubject.send(item.value) - self.dismiss(animated: true) - } + output.action + .sink { [weak self] action in + guard let self = self else { return } + switch action { + case .cancel: + self.dismiss(animated: true) + case let .select(item): + self.itemSubject.send(item.value) + self.dismiss(animated: true) } - .store(in: &subscriptions) - } + } + .store(in: &subscriptions) + } - override func tableView(_: UITableView, didSelectRowAt _: IndexPath) { - dismiss(animated: true) - } + override func tableView(_: UITableView, didSelectRowAt _: IndexPath) { + dismiss(animated: true) + } - private func setupUI() { - tableView.registerCell(UITableViewCell.self) - navigationItem.leftBarButtonItem = .init(systemItem: .cancel) - } + private func setupUI() { + tableView.registerCell(UITableViewCell.self) + navigationItem.leftBarButtonItem = .init(systemItem: .cancel) } -#endif +} diff --git a/Sources/XConfigs/ViewControllers/XConfigsViewController.swift b/Sources/XConfigs/ViewControllers/XConfigsViewController.swift index 2c5f003..98cd01b 100644 --- a/Sources/XConfigs/ViewControllers/XConfigsViewController.swift +++ b/Sources/XConfigs/ViewControllers/XConfigsViewController.swift @@ -1,205 +1,204 @@ -#if canImport(UIKit) - import Combine - import CombineCocoa - import UIKit - - final class XConfigsViewController: UITableViewController { - typealias ViewModel = XConfigsViewModel - typealias DataSource = TableViewDataSource - - private let viewModel: ViewModel - private var subscriptions = Set() - private var updateValueSubject = PassthroughSubject() - private var overrideConfigSubject = PassthroughSubject() - private var resetSubject = PassthroughSubject() - private var shouldAnimate = false - - private lazy var datasource: DataSource = { - var ds = DataSource(tableView: tableView) { [weak self] tableView, indexPath, item in - guard let self = self else { return .init() } - switch item { - case let .toggle(vm): - let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) - cell.configure(with: (vm.displayName, vm.value)) - cell.mainView.valueChangedPublisher - .map { KeyValue(key: vm.key, value: $0) } - .subscribe(self.updateValueSubject) - .store(in: &cell.subscriptions) - cell.selectionStyle = .none - return cell - case let .textInput(vm): - let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) - cell.configure(with: (vm.displayName, vm.value)) - return cell - case let .optionSelection(vm): - let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) - cell.configure(with: (vm.displayName, vm.value)) - cell.selectionStyle = .default - return cell - case let .actionButton(title, _): - let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) - cell.configure(with: title) - return cell - case let .inAppModification(title, val): - let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) - cell.configure(with: (title, val)) - cell.mainView.valueChangedPublisher - .subscribe(self.overrideConfigSubject) - .store(in: &cell.subscriptions) - cell.selectionStyle = .none - return cell - case let .nameValue(name, val): - let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) - cell.configure(with: (name, val)) - cell.selectionStyle = .none - return cell - } - } - ds.defaultRowAnimation = .fade - return ds - }() - private lazy var searchController = UISearchController(searchResultsController: nil).apply { - $0.obscuresBackgroundDuringPresentation = false - } +import Combine +import CombineCocoa +import UIKit + +final class XConfigsViewController: UITableViewController { + typealias ViewModel = XConfigsViewModel + typealias DataSource = TableViewDataSource - init(viewModel: XConfigsViewModel) { - self.viewModel = viewModel - if #available(iOS 13.0, *) { - super.init(style: .insetGrouped) - } else { - super.init(style: .grouped) + private let viewModel: ViewModel + private var subscriptions = Set() + private var updateValueSubject = PassthroughSubject() + private var overrideConfigSubject = PassthroughSubject() + private var resetSubject = PassthroughSubject() + private var shouldAnimate = false + + private lazy var datasource: DataSource = { + var ds = DataSource(tableView: tableView) { [weak self] tableView, indexPath, item in + guard let self = self else { return .init() } + switch item { + case let .toggle(vm): + let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) + cell.configure(with: (vm.displayName, vm.value)) + cell.mainView.valueChangedPublisher + .map { KeyValue(key: vm.key, value: $0) } + .subscribe(self.updateValueSubject) + .store(in: &cell.subscriptions) + cell.selectionStyle = .none + return cell + case let .textInput(vm): + let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) + cell.configure(with: (vm.displayName, vm.value)) + return cell + case let .optionSelection(vm): + let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) + cell.configure(with: (vm.displayName, vm.value)) + cell.selectionStyle = .default + return cell + case let .actionButton(title, _): + let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) + cell.configure(with: title) + return cell + case let .inAppModification(title, val): + let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) + cell.configure(with: (title, val)) + cell.mainView.valueChangedPublisher + .subscribe(self.overrideConfigSubject) + .store(in: &cell.subscriptions) + cell.selectionStyle = .none + return cell + case let .nameValue(name, val): + let cell = tableView.dequeueCell(UIViewTableWrapperCell.self, for: indexPath) + cell.configure(with: (name, val)) + cell.selectionStyle = .none + return cell } } + ds.defaultRowAnimation = .fade + return ds + }() - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + private lazy var searchController = UISearchController(searchResultsController: nil).apply { + $0.obscuresBackgroundDuringPresentation = false + } - override func viewDidLoad() { - super.viewDidLoad() - let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: nil) - navigationItem.rightBarButtonItem = doneButton - setupUI() - handleViewModelOutput() + init(viewModel: XConfigsViewModel) { + self.viewModel = viewModel + if #available(iOS 13.0, *) { + super.init(style: .insetGrouped) + } else { + super.init(style: .grouped) } + } - private func setupUI() { - navigationItem.searchController = searchController - setupTableView() - } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - private func setupTableView() { - tableView.registerCell(UIViewTableWrapperCell.self) - tableView.registerCell(UIViewTableWrapperCell.self) - tableView.registerCell(UIViewTableWrapperCell.self) - if #available(iOS 15.0, *) { - tableView.isPrefetchingEnabled = false - } - } + override func viewDidLoad() { + super.viewDidLoad() + let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: nil) + navigationItem.rightBarButtonItem = doneButton + setupUI() + handleViewModelOutput() + } + + private func setupUI() { + navigationItem.searchController = searchController + setupTableView() + } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - shouldAnimate = true + private func setupTableView() { + tableView.registerCell(UIViewTableWrapperCell.self) + tableView.registerCell(UIViewTableWrapperCell.self) + tableView.registerCell(UIViewTableWrapperCell.self) + if #available(iOS 15.0, *) { + tableView.isPrefetchingEnabled = false } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + shouldAnimate = true + } - private func handleViewModelOutput() { - guard let doneButton = navigationItem.rightBarButtonItem else { return } - let output = viewModel.transform( - input: .init( - searchPublisher: searchController.searchBar.textDidChangePublisher.prepend("").eraseToAnyPublisher(), - reloadPublisher: Just(()).eraseToAnyPublisher(), - updateValuePublisher: updateValueSubject.eraseToAnyPublisher(), - overrideConfigPublisher: overrideConfigSubject.eraseToAnyPublisher(), - resetPublisher: resetSubject.eraseToAnyPublisher(), - selectItemPublisher: tableView.didSelectRowPublisher.compactMap { [weak self] indexPath -> ViewModel.Item? in - guard let self = self else { return nil } - self.tableView.deselectRow(at: indexPath, animated: false) - return self.datasource.itemIdentifier(for: indexPath) - }.eraseToAnyPublisher(), - dismissPublisher: doneButton.tapPublisher - )) - - output.searchPlaceholderTitle.compactMap { $0 }.assign(to: \UISearchBar.placeholder, on: searchController.searchBar).store(in: &subscriptions) - output.title.compactMap { $0 }.assign(to: \UIViewController.title, on: self).store(in: &subscriptions) - - output.sectionItemsModels - .sink { [weak self] secItems in - guard let self = self else { return } - self.datasource.apply(secItems.snapshot(), animatingDifferences: self.shouldAnimate) - } - .store(in: &subscriptions) - - output.action.sink { [weak self] action in - self?.handleAction(action) + private func handleViewModelOutput() { + guard let doneButton = navigationItem.rightBarButtonItem else { return } + let output = viewModel.transform( + input: .init( + searchPublisher: searchController.searchBar.textDidChangePublisher.prepend("").eraseToAnyPublisher(), + reloadPublisher: Just(()).eraseToAnyPublisher(), + updateValuePublisher: updateValueSubject.eraseToAnyPublisher(), + overrideConfigPublisher: overrideConfigSubject.eraseToAnyPublisher(), + resetPublisher: resetSubject.eraseToAnyPublisher(), + selectItemPublisher: tableView.didSelectRowPublisher.compactMap { [weak self] indexPath -> ViewModel.Item? in + guard let self = self else { return nil } + self.tableView.deselectRow(at: indexPath, animated: false) + return self.datasource.itemIdentifier(for: indexPath) + }.eraseToAnyPublisher(), + dismissPublisher: doneButton.tapPublisher + )) + + output.searchPlaceholderTitle.compactMap { $0 }.assign(to: \UISearchBar.placeholder, on: searchController.searchBar).store(in: &subscriptions) + output.title.compactMap { $0 }.assign(to: \UIViewController.title, on: self).store(in: &subscriptions) + + output.sectionItemsModels + .sink { [weak self] secItems in + guard let self = self else { return } + self.datasource.apply(secItems.snapshot(), animatingDifferences: self.shouldAnimate) } .store(in: &subscriptions) - } - private func handleAction(_ action: ViewModel.Action) { - switch action { - case let .showOptionSelection(model): - showOptionSelection(for: model) - case let .showTextInput(model): - showTextInputViewController(model: model) - case let .showResetConfirmation(title): - let alertConfirmation = UIAlertController(title: title, message: nil, preferredStyle: .alert) - alertConfirmation.addAction(.init(title: "Reset", style: .destructive, handler: { [weak self] _ in - self?.resetSubject.send(()) - })) - alertConfirmation.addAction(.init(title: "Cancel", style: .cancel)) - present(alertConfirmation, animated: true) - case .dismiss: - dismiss(animated: true) - } + output.action.sink { [weak self] action in + self?.handleAction(action) } + .store(in: &subscriptions) + } - private func showTextInputViewController(model: TextInputModel) { - let textInputVC = InputValueViewController(viewModel: .init(model: model)) - textInputVC.valuePublisher - .map { KeyValue(key: model.key, value: $0) } - .subscribe(updateValueSubject) - .store(in: &subscriptions) - present(textInputVC.wrapInsideNavVC().preferAsHalfSheet(), animated: true) + private func handleAction(_ action: ViewModel.Action) { + switch action { + case let .showOptionSelection(model): + showOptionSelection(for: model) + case let .showTextInput(model): + showTextInputViewController(model: model) + case let .showResetConfirmation(title): + let alertConfirmation = UIAlertController(title: title, message: nil, preferredStyle: .alert) + alertConfirmation.addAction(.init(title: "Reset", style: .destructive, handler: { [weak self] _ in + self?.resetSubject.send(()) + })) + alertConfirmation.addAction(.init(title: "Cancel", style: .cancel)) + present(alertConfirmation, animated: true) + case .dismiss: + dismiss(animated: true) } + } - private func showOptionSelection(for model: OptionSelectionModel) { - let optionVC = OptionViewController(viewModel: .init(model: model)) - optionVC.selectedItemPublisher - .map { KeyValue(key: model.key, value: $0) } - .subscribe(updateValueSubject) - .store(in: &subscriptions) - present(optionVC.wrapInsideNavVC().preferAsHalfSheet(), animated: true) - } + private func showTextInputViewController(model: TextInputModel) { + let textInputVC = InputValueViewController(viewModel: .init(model: model)) + textInputVC.valuePublisher + .map { KeyValue(key: model.key, value: $0) } + .subscribe(updateValueSubject) + .store(in: &subscriptions) + present(textInputVC.wrapInsideNavVC().preferAsHalfSheet(), animated: true) + } - override func tableView(_: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point _: CGPoint) -> UIContextMenuConfiguration? { - guard let item = datasource.itemIdentifier(for: indexPath) else { return nil } - switch item { - case let .toggle(vm): - return createContextMenuConfiguration(title: vm.key, actions: [createCopyAction(vm.key)]) - case let .textInput(vm): - return createContextMenuConfiguration(title: vm.key, actions: [createCopyAction(vm.key), createCopyAction(vm.value)]) - case let .optionSelection(vm): - return createContextMenuConfiguration(title: vm.key, actions: [createCopyAction(vm.key), createCopyAction(vm.value)]) - default: - return nil - } - } + private func showOptionSelection(for model: OptionSelectionModel) { + let optionVC = OptionViewController(viewModel: .init(model: model)) + optionVC.selectedItemPublisher + .map { KeyValue(key: model.key, value: $0) } + .subscribe(updateValueSubject) + .store(in: &subscriptions) + present(optionVC.wrapInsideNavVC().preferAsHalfSheet(), animated: true) + } - private func createCopyAction(_ value: String) -> UIAction { - let copyAction = UIAction(title: "Copy \"\(value)\"") { _ in - let pasteboard = UIPasteboard.general - pasteboard.string = value - } - return copyAction + override func tableView(_: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point _: CGPoint) -> UIContextMenuConfiguration? { + guard let item = datasource.itemIdentifier(for: indexPath) else { return nil } + switch item { + case let .toggle(vm): + return createContextMenuConfiguration(title: vm.key, actions: [createCopyAction(vm.key)]) + case let .textInput(vm): + return createContextMenuConfiguration(title: vm.key, actions: [createCopyAction(vm.key), createCopyAction(vm.value)]) + case let .optionSelection(vm): + return createContextMenuConfiguration(title: vm.key, actions: [createCopyAction(vm.key), createCopyAction(vm.value)]) + default: + return nil } + } - @available(iOS 13.0, *) - private func createContextMenuConfiguration(title: String, actions: [UIAction]) -> UIContextMenuConfiguration { - UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in - UIMenu(title: title, children: actions) - }) + private func createCopyAction(_ value: String) -> UIAction { + let copyAction = UIAction(title: "Copy \"\(value)\"") { _ in + let pasteboard = UIPasteboard.general + pasteboard.string = value } + return copyAction + } + + @available(iOS 13.0, *) + private func createContextMenuConfiguration(title: String, actions: [UIAction]) -> UIContextMenuConfiguration { + UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in + UIMenu(title: title, children: actions) + }) } -#endif +} diff --git a/Sources/XConfigs/Views/ActionView.swift b/Sources/XConfigs/Views/ActionView.swift index 9d1b38f..392833c 100644 --- a/Sources/XConfigs/Views/ActionView.swift +++ b/Sources/XConfigs/Views/ActionView.swift @@ -1,35 +1,33 @@ -#if canImport(UIKit) - import UIKit +import UIKit - final class ActionView: UIView, ConfigurableView { - typealias ViewModel = String +final class ActionView: UIView, ConfigurableView { + typealias ViewModel = String - private let keyLabel = UILabel().apply { - if #available(iOS 13.0, *) { - $0.textColor = .link - } - $0.heightAnchor.constraint(greaterThanOrEqualToConstant: 31).isActive = true - $0.numberOfLines = 0 + private let keyLabel = UILabel().apply { + if #available(iOS 13.0, *) { + $0.textColor = .link } + $0.heightAnchor.constraint(greaterThanOrEqualToConstant: 31).isActive = true + $0.numberOfLines = 0 + } - init() { - super.init(frame: .zero) - setupUI() - } + init() { + super.init(frame: .zero) + setupUI() + } - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - func configure(with viewModel: ViewModel) { - keyLabel.text = viewModel - } + func configure(with viewModel: ViewModel) { + keyLabel.text = viewModel + } - private func setupUI() { - translatesAutoresizingMaskIntoConstraints = false - addSubview(keyLabel) - keyLabel.bindToSuperview(margins: .init(top: 7, left: 20, bottom: 7, right: 20)) - } + private func setupUI() { + translatesAutoresizingMaskIntoConstraints = false + addSubview(keyLabel) + keyLabel.bindToSuperview(margins: .init(top: 7, left: 20, bottom: 7, right: 20)) } -#endif +} diff --git a/Sources/XConfigs/Views/KeyValueView.swift b/Sources/XConfigs/Views/KeyValueView.swift index 1ec8e39..dfde647 100644 --- a/Sources/XConfigs/Views/KeyValueView.swift +++ b/Sources/XConfigs/Views/KeyValueView.swift @@ -1,47 +1,46 @@ -#if canImport(UIKit) - import UIKit - final class KeyValueView: UIView, ConfigurableView { - typealias ViewModel = (String, String) +import UIKit - private let keyLabel = UILabel().apply { - $0.numberOfLines = 0 - $0.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - } +final class KeyValueView: UIView, ConfigurableView { + typealias ViewModel = (String, String) - private let valueLabel = UILabel().apply { - $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - $0.textAlignment = .right - $0.widthAnchor.constraint(lessThanOrEqualToConstant: 160).isActive = true - $0.heightAnchor.constraint(greaterThanOrEqualToConstant: 31).isActive = true - } + private let keyLabel = UILabel().apply { + $0.numberOfLines = 0 + $0.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } - init() { - super.init(frame: .zero) - setupUI() - } + private let valueLabel = UILabel().apply { + $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + $0.textAlignment = .right + $0.widthAnchor.constraint(lessThanOrEqualToConstant: 160).isActive = true + $0.heightAnchor.constraint(greaterThanOrEqualToConstant: 31).isActive = true + } - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + init() { + super.init(frame: .zero) + setupUI() + } - func configure(with viewModel: ViewModel) { - keyLabel.text = viewModel.0 - valueLabel.text = viewModel.1 - keyLabel.layoutIfNeeded() - } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(with viewModel: ViewModel) { + keyLabel.text = viewModel.0 + valueLabel.text = viewModel.1 + keyLabel.layoutIfNeeded() + } - private func setupUI() { - translatesAutoresizingMaskIntoConstraints = false - let stackview = UIStackView(arrangedSubviews: [ - keyLabel, - valueLabel, - ]).apply { - $0.spacing = 10 - } - addSubview(stackview) - stackview.bindToSuperview(margins: .init(top: 7, left: 20, bottom: 7, right: 20)) + private func setupUI() { + translatesAutoresizingMaskIntoConstraints = false + let stackview = UIStackView(arrangedSubviews: [ + keyLabel, + valueLabel, + ]).apply { + $0.spacing = 10 } + addSubview(stackview) + stackview.bindToSuperview(margins: .init(top: 7, left: 20, bottom: 7, right: 20)) } -#endif +} diff --git a/Sources/XConfigs/Views/ToggleView.swift b/Sources/XConfigs/Views/ToggleView.swift index 30320a5..1bf20d3 100644 --- a/Sources/XConfigs/Views/ToggleView.swift +++ b/Sources/XConfigs/Views/ToggleView.swift @@ -1,54 +1,53 @@ -#if canImport(UIKit) - import Combine - import CombineCocoa - import CombineExt - import UIKit - - final class ToggleView: UIView, ConfigurableView { - typealias ViewModel = (String, Bool) - - private let switchView = UISwitch() - private let keyLabel = UILabel().apply { - $0.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - $0.numberOfLines = 0 - } - var valueChangedPublisher: AnyPublisher { - switchView.controlEventPublisher(for: .valueChanged).map { self.switchView.isOn }.eraseToAnyPublisher() - } +import Combine +import CombineCocoa +import CombineExt +import UIKit - init() { - super.init(frame: .zero) - setupUI() - } +final class ToggleView: UIView, ConfigurableView { + typealias ViewModel = (String, Bool) - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + private let switchView = UISwitch() + private let keyLabel = UILabel().apply { + $0.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + $0.numberOfLines = 0 + } - // MARK: - Private - - private func setupUI() { - translatesAutoresizingMaskIntoConstraints = false - let stackview = UIStackView(arrangedSubviews: [ - keyLabel, - switchView, - ]).apply { - $0.distribution = .fill - $0.spacing = 10 - $0.alignment = .center - } - addSubview(stackview) - stackview.bindToSuperview(margins: .init(top: 7, left: 20, bottom: 7, right: 20)) - } + var valueChangedPublisher: AnyPublisher { + switchView.controlEventPublisher(for: .valueChanged).map { self.switchView.isOn }.eraseToAnyPublisher() + } - // MARK: - Internal + init() { + super.init(frame: .zero) + setupUI() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - func configure(with viewModel: ViewModel) { - keyLabel.text = viewModel.0 - switchView.isOn = viewModel.1 - keyLabel.layoutIfNeeded() + // MARK: - Private + + private func setupUI() { + translatesAutoresizingMaskIntoConstraints = false + let stackview = UIStackView(arrangedSubviews: [ + keyLabel, + switchView, + ]).apply { + $0.distribution = .fill + $0.spacing = 10 + $0.alignment = .center } + addSubview(stackview) + stackview.bindToSuperview(margins: .init(top: 7, left: 20, bottom: 7, right: 20)) + } + + // MARK: - Internal + + func configure(with viewModel: ViewModel) { + keyLabel.text = viewModel.0 + switchView.isOn = viewModel.1 + keyLabel.layoutIfNeeded() } -#endif +} diff --git a/Sources/XConfigs/XConfigs.swift b/Sources/XConfigs/XConfigs.swift index de497fd..f7bfdab 100644 --- a/Sources/XConfigs/XConfigs.swift +++ b/Sources/XConfigs/XConfigs.swift @@ -1,7 +1,6 @@ -#if canImport(UIKit) - import UIKit -#endif + import Foundation +import UIKit var defaultConfigUseCase: XConfigUseCase! @@ -24,16 +23,14 @@ public struct XConfigs { ) } - #if canImport(UIKit) - public static func configsViewController() throws -> UIViewController { - guard defaultConfigUseCase.keyValueStore != nil else { throw ConfigError.inAppModificationIsNotAllowed } - return XConfigsViewController(viewModel: .init(useCase: defaultConfigUseCase)) - } + public static func configsViewController() throws -> UIViewController { + guard defaultConfigUseCase.keyValueStore != nil else { throw ConfigError.inAppModificationIsNotAllowed } + return XConfigsViewController(viewModel: .init(useCase: defaultConfigUseCase)) + } - public static func show(from vc: UIViewController, animated: Bool = true) throws { - try vc.present(configsViewController().wrapInsideNavVC(), animated: animated, completion: nil) - } - #endif + public static func show(from vc: UIViewController, animated: Bool = true) throws { + try vc.present(configsViewController().wrapInsideNavVC(), animated: animated, completion: nil) + } /// A method to allow in-app modification or not. public static func setInAppModification(enable: Bool) throws {