diff --git a/DebugScreen.xcodeproj/project.pbxproj b/DebugScreen.xcodeproj/project.pbxproj index 6c3b244..2de39b9 100644 --- a/DebugScreen.xcodeproj/project.pbxproj +++ b/DebugScreen.xcodeproj/project.pbxproj @@ -7,8 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 400C6A2B2F3F67C500678284 /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400C6A2A2F3F67C500678284 /* MenuItem.swift */; }; + 400C6A2D2F3F69A700678284 /* NestedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400C6A2C2F3F69A700678284 /* NestedScreen.swift */; }; + 400C6A322F3F700200678284 /* DebugScreenMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400C6A312F3F700100678284 /* DebugScreenMenuItem.swift */; }; + 400C6A342F3F700400678284 /* DebugScreenNestedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400C6A332F3F700300678284 /* DebugScreenNestedScreen.swift */; }; + 400C6A362F3F700600678284 /* MenuItemsSectionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400C6A352F3F700500678284 /* MenuItemsSectionBuilder.swift */; }; + 400C6A382F3F700800678284 /* NestedScreensSectionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400C6A372F3F700700678284 /* NestedScreensSectionBuilder.swift */; }; 9054BA4A2A5C6CCA0009D70D /* BaseNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9054BA492A5C6CCA0009D70D /* BaseNavigationController.swift */; }; 90985F5C2A5C58980066790A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B2E91F9A2A4C2308000E3E45 /* LaunchScreen.storyboard */; }; + 90C0C14C2F45CC4300D3C438 /* MainScreenType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90C0C14B2F45CC3D00D3C438 /* MainScreenType.swift */; }; A4301A65285A462F006B77C0 /* DebugScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43019E6285A3137006B77C0 /* DebugScreenTests.swift */; }; A47BDB42286A1249002326B8 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A47BDB38286A1249002326B8 /* Colors.xcassets */; }; A47BDB43286A1249002326B8 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47BDB39286A1249002326B8 /* Colors.swift */; }; @@ -174,7 +181,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 400C6A2A2F3F67C500678284 /* MenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItem.swift; sourceTree = ""; }; + 400C6A2C2F3F69A700678284 /* NestedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedScreen.swift; sourceTree = ""; }; + 400C6A312F3F700100678284 /* DebugScreenMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugScreenMenuItem.swift; sourceTree = ""; }; + 400C6A332F3F700300678284 /* DebugScreenNestedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugScreenNestedScreen.swift; sourceTree = ""; }; + 400C6A352F3F700500678284 /* MenuItemsSectionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemsSectionBuilder.swift; sourceTree = ""; }; + 400C6A372F3F700700678284 /* NestedScreensSectionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedScreensSectionBuilder.swift; sourceTree = ""; }; 9054BA492A5C6CCA0009D70D /* BaseNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationController.swift; sourceTree = ""; }; + 90C0C14B2F45CC3D00D3C438 /* MainScreenType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenType.swift; sourceTree = ""; }; A43019E6285A3137006B77C0 /* DebugScreenTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugScreenTests.swift; sourceTree = ""; }; A47BDB38286A1249002326B8 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; A47BDB39286A1249002326B8 /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; @@ -565,6 +579,7 @@ A4BDF4B628512FAA00651C61 /* Configurator */ = { isa = PBXGroup; children = ( + 90C0C14B2F45CC3D00D3C438 /* MainScreenType.swift */, A4BDF4B728512FAA00651C61 /* MainModuleConfigurator.swift */, ); path = Configurator; @@ -710,6 +725,8 @@ B28F97E92A271FC00089C032 /* CopiedText.swift */, B28F97E72A271FAF0089C032 /* FeatureToggle.swift */, B28F97EB2A271FF30089C032 /* SelectionList.swift */, + 400C6A2A2F3F67C500678284 /* MenuItem.swift */, + 400C6A2C2F3F69A700678284 /* NestedScreen.swift */, ); path = Content; sourceTree = ""; @@ -956,6 +973,8 @@ B2E91F772A4C2305000E3E45 /* ActionsSectionBuilder.swift */, B2E91F792A4C2305000E3E45 /* CopiedTextSectionBuilder.swift */, B2B1D4502A52FC3000CC5A11 /* InfoTablesSectionBuilder.swift */, + 400C6A352F3F700500678284 /* MenuItemsSectionBuilder.swift */, + 400C6A372F3F700700678284 /* NestedScreensSectionBuilder.swift */, B2E91F7A2A4C2305000E3E45 /* ServerSelectionSectionBuilder.swift */, B2E91F782A4C2305000E3E45 /* TogglesSectionBuilder.swift */, ); @@ -969,6 +988,8 @@ B2E91F7D2A4C2305000E3E45 /* CopiedTextItem.swift */, B2E91F7E2A4C2305000E3E45 /* DebugScreenActionList.swift */, B2E91F7F2A4C2305000E3E45 /* DebugScreenAction.swift */, + 400C6A312F3F700100678284 /* DebugScreenMenuItem.swift */, + 400C6A332F3F700300678284 /* DebugScreenNestedScreen.swift */, B2E91F802A4C2305000E3E45 /* ServersSelectionList.swift */, ); path = Models; @@ -1359,9 +1380,11 @@ B28F97F92A27313E0089C032 /* UIImage.swift in Sources */, A47BDB43286A1249002326B8 /* Colors.swift in Sources */, A4BDF49828512F7700651C61 /* UIApplication.swift in Sources */, + 90C0C14C2F45CC4300D3C438 /* MainScreenType.swift in Sources */, A4BDF4C528512FAA00651C61 /* MainViewController.swift in Sources */, B2610E242A16DCC300509FA1 /* SimpleAlertModuleConfigurator.swift in Sources */, B28F97E42A271F980089C032 /* Action.swift in Sources */, + 400C6A2D2F3F69A700678284 /* NestedScreen.swift in Sources */, 9054BA4A2A5C6CCA0009D70D /* BaseNavigationController.swift in Sources */, B2B1D4442A52DB7F00CC5A11 /* InfoTableModuleInput.swift in Sources */, B237FF652A2913DF0095FA33 /* LoggerSectionComponent.swift in Sources */, @@ -1375,6 +1398,7 @@ A4BDF4C728512FAA00651C61 /* MainModuleConfigurator.swift in Sources */, B2C65D122A4571E0004DC80D /* UITableViewCell.swift in Sources */, A4BDF49328512F7700651C61 /* Array.swift in Sources */, + 400C6A2B2F3F67C500678284 /* MenuItem.swift in Sources */, B2B1D4492A52DBA400CC5A11 /* InfoTableViewInput.swift in Sources */, A4BDF49428512F7700651C61 /* NSObject.swift in Sources */, A4D337562863A7AA002328F2 /* TableSection.swift in Sources */, @@ -1472,6 +1496,10 @@ B2E91FA42A4C2309000E3E45 /* UserDefaultsFeatureToggle.swift in Sources */, B2E91FB22A4C2309000E3E45 /* UIImage.swift in Sources */, B2E91FA72A4C2309000E3E45 /* DebugScreenAction.swift in Sources */, + 400C6A322F3F700200678284 /* DebugScreenMenuItem.swift in Sources */, + 400C6A342F3F700400678284 /* DebugScreenNestedScreen.swift in Sources */, + 400C6A362F3F700600678284 /* MenuItemsSectionBuilder.swift in Sources */, + 400C6A382F3F700800678284 /* NestedScreensSectionBuilder.swift in Sources */, B2E91FB12A4C2309000E3E45 /* UIButton.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DebugScreen/Common/Models/Common/MainTableBlock.swift b/DebugScreen/Common/Models/Common/MainTableBlock.swift index 79c2488..440ccc5 100644 --- a/DebugScreen/Common/Models/Common/MainTableBlock.swift +++ b/DebugScreen/Common/Models/Common/MainTableBlock.swift @@ -25,4 +25,8 @@ public enum MainTableBlock { case selectionList(model: SelectionList) /// Block with toggle. case toggle(model: FeatureToggle) + /// Block with a custom menu item that performs an action on tap. + case menuItem(model: MenuItem) + /// Block that opens a nested section screen with its own table sections. + case nestedScreen(model: NestedScreen) } diff --git a/DebugScreen/Common/Protocols/Content/MenuItem.swift b/DebugScreen/Common/Protocols/Content/MenuItem.swift new file mode 100644 index 0000000..1a27d09 --- /dev/null +++ b/DebugScreen/Common/Protocols/Content/MenuItem.swift @@ -0,0 +1,15 @@ +// +// MenuItem.swift +// DebugScreen +// +// Created by Aleksandr Potemkin on 13.02.2026. +// + +public protocol MenuItem { + /// Item title text (supports multiline). + var title: String { get } + /// Whether to show a disclosure indicator arrow. + var showsDisclosure: Bool { get } + /// Action performed on tap. + var block: (() -> Void)? { get } +} diff --git a/DebugScreen/Common/Protocols/Content/NestedScreen.swift b/DebugScreen/Common/Protocols/Content/NestedScreen.swift new file mode 100644 index 0000000..02b5c26 --- /dev/null +++ b/DebugScreen/Common/Protocols/Content/NestedScreen.swift @@ -0,0 +1,13 @@ +// +// NestedScreen.swift +// DebugScreen +// +// Created by Aleksandr Potemkin on 13.02.2026. +// + +public protocol NestedScreen { + /// Screen title displayed in the navigation bar. + var title: String { get } + /// Sections — same ``TableSection`` items as on the main screen. + var sections: [TableSection] { get } +} diff --git a/DebugScreen/Common/ReusableLayer/Cells/PlainTextCell/PlainTextCell.swift b/DebugScreen/Common/ReusableLayer/Cells/PlainTextCell/PlainTextCell.swift index a61a406..d1ec6c4 100644 --- a/DebugScreen/Common/ReusableLayer/Cells/PlainTextCell/PlainTextCell.swift +++ b/DebugScreen/Common/ReusableLayer/Cells/PlainTextCell/PlainTextCell.swift @@ -38,7 +38,6 @@ private extension PlainTextCell { } func configureContainer() { - accessoryType = .disclosureIndicator backgroundColor = DebugScreenConfiguration.shared.colorScheme.backgroundColor } diff --git a/DebugScreen/Flows/DebugScreenCoordinator.swift b/DebugScreen/Flows/DebugScreenCoordinator.swift index aac23da..6d244a9 100644 --- a/DebugScreen/Flows/DebugScreenCoordinator.swift +++ b/DebugScreen/Flows/DebugScreenCoordinator.swift @@ -21,7 +21,7 @@ final class DebugScreenCoordinator: BaseCoordinator { // MARK: - Methods override func start() { - showMainScreen() + showMainScreen(type: .root) } func open(module: ModuleType) { @@ -43,8 +43,15 @@ final class DebugScreenCoordinator: BaseCoordinator { private extension DebugScreenCoordinator { - func showMainScreen() { - let (view, output) = MainModuleConfigurator().configure() + func showMainScreen(type: MainScreenType) { + let (view, output): MainModuleComponents + switch type { + case .root: + (view, output) = MainModuleConfigurator().configureMain() + case .nested(let model): + (view, output) = MainModuleConfigurator().configureNestedModule(model: model) + } + output.onActionListShow = { [weak self] model in self?.showActionList(model: model) } @@ -54,13 +61,25 @@ private extension DebugScreenCoordinator { output.onInfoTableShow = { [weak self] model in self?.showInfoTable(with: model) } + output.onNestedScreenShow = { [weak self] model in + self?.showMainScreen(type: .nested(model: model)) + } output.didModuleClosed = { [weak self] in self?.router.dismissModule() } - output.didModuleDismissed = { [weak self] in - self?.completionHandler?() + + if case .root = type { + output.didModuleDismissed = { [weak self] in + self?.completionHandler?() + } + } + + switch type { + case .root: + router.present(view) + case .nested(_): + router.push(view) } - router.present(view) } func showActionList(model: ActionList) { diff --git a/DebugScreen/Flows/MainScreen/Configurator/MainModuleConfigurator.swift b/DebugScreen/Flows/MainScreen/Configurator/MainModuleConfigurator.swift index 3236790..6af2a91 100644 --- a/DebugScreen/Flows/MainScreen/Configurator/MainModuleConfigurator.swift +++ b/DebugScreen/Flows/MainScreen/Configurator/MainModuleConfigurator.swift @@ -13,17 +13,33 @@ final class MainModuleConfigurator { // MARK: - Methods - func configure() -> MainModuleComponents { - let viewController = UIViewController.instantiate(ofType: MainViewController.self) - let presenter = MainModulePresenter() + func configureMain() -> MainModuleComponents { + let view = UIViewController.instantiate(ofType: MainViewController.self) + let presenter = MainModulePresenter( + title: L10n.MainViewController.debugTitle, + sections: DebugScreenConfiguration.shared.sections + ) - presenter.view = viewController - viewController.output = presenter + presenter.view = view + view.output = presenter - let navController = BaseNavigationController(rootViewController: viewController) + let navController = BaseNavigationController(rootViewController: view) navController.modalPresentationStyle = .overFullScreen return (navController, presenter) } + func configureNestedModule(model: NestedScreen) -> MainModuleComponents { + let view = UIViewController.instantiate(ofType: MainViewController.self) + let presenter = MainModulePresenter( + title: model.title, + sections: model.sections + ) + + presenter.view = view + view.output = presenter + + return (view, presenter) + } + } diff --git a/DebugScreen/Flows/MainScreen/Configurator/MainScreenType.swift b/DebugScreen/Flows/MainScreen/Configurator/MainScreenType.swift new file mode 100644 index 0000000..815edeb --- /dev/null +++ b/DebugScreen/Flows/MainScreen/Configurator/MainScreenType.swift @@ -0,0 +1,13 @@ +// +// MainScreenType.swift +// DebugScreen +// +// Created by chausov on 18.02.2026. +// + +enum MainScreenType { + /// Main debug screen + case root + /// Nested debug screen + case nested(model: NestedScreen) +} diff --git a/DebugScreen/Flows/MainScreen/Presenter/MainModuleOutput.swift b/DebugScreen/Flows/MainScreen/Presenter/MainModuleOutput.swift index 54f8dbb..895e3ae 100644 --- a/DebugScreen/Flows/MainScreen/Presenter/MainModuleOutput.swift +++ b/DebugScreen/Flows/MainScreen/Presenter/MainModuleOutput.swift @@ -14,6 +14,8 @@ protocol MainModuleOutput: AnyObject { var onAlertShow: ((AlertModel) -> Void)? { get set } /// Call then show information table screen var onInfoTableShow: ((InfoTableModel) -> Void)? { get set } + /// Called when a section screen needs to be shown + var onNestedScreenShow: ((NestedScreen) -> Void)? { get set } /// Call then close module var didModuleClosed: (() -> Void)? { get set } /// Call after module deinit from memory diff --git a/DebugScreen/Flows/MainScreen/Presenter/MainModulePresenter.swift b/DebugScreen/Flows/MainScreen/Presenter/MainModulePresenter.swift index 6058a93..9ee0cd6 100644 --- a/DebugScreen/Flows/MainScreen/Presenter/MainModulePresenter.swift +++ b/DebugScreen/Flows/MainScreen/Presenter/MainModulePresenter.swift @@ -15,6 +15,7 @@ final class MainModulePresenter: MainModuleOutput { var onActionListShow: ((ActionList) -> Void)? var onAlertShow: ((AlertModel) -> Void)? var onInfoTableShow: ((InfoTableModel) -> Void)? + var onNestedScreenShow: ((NestedScreen) -> Void)? var didModuleClosed: (() -> Void)? var didModuleDismissed: (() -> Void)? @@ -22,6 +23,18 @@ final class MainModulePresenter: MainModuleOutput { weak var view: MainViewInput? + // MARK: - Private Properties + + private let title: String + private let sections: [TableSection] + + // MARK: - Initialization + + init(title: String, sections: [TableSection]) { + self.title = title + self.sections = sections + } + // MARK: - Deinitialization deinit { @@ -39,7 +52,10 @@ extension MainModulePresenter: MainModuleInput { } extension MainModulePresenter: MainViewOutput { func viewLoaded() { - view?.setupInitialState(sections: DebugScreenConfiguration.shared.sections) + view?.setupInitialState( + title: title, + sections: sections + ) } func didTapActionList(model: ActionList) { @@ -56,6 +72,10 @@ extension MainModulePresenter: MainViewOutput { debugPrint("✅ \(model.title) copied to clipboard") } + func didTapNestedScreen(model: NestedScreen) { + onNestedScreenShow?(model) + } + func didTapCloseButton() { didModuleClosed?() } diff --git a/DebugScreen/Flows/MainScreen/View/Adapter/MainAdapter.swift b/DebugScreen/Flows/MainScreen/View/Adapter/MainAdapter.swift index ee8df21..8c423f2 100644 --- a/DebugScreen/Flows/MainScreen/View/Adapter/MainAdapter.swift +++ b/DebugScreen/Flows/MainScreen/View/Adapter/MainAdapter.swift @@ -20,6 +20,7 @@ final class MainAdapter: NSObject { var onOpenActionList: ((ActionList) -> Void)? var onSelectableTextTap: ((CopiedText) -> Void)? var onOpenInfoTable: ((InfoTableModel) -> Void)? + var onOpenNestedScreen: ((NestedScreen) -> Void)? // MARK: - Private Properties @@ -81,11 +82,23 @@ extension MainAdapter: UITableViewDataSource { case .toggle(let model): return configureSwitcherCell(tableView, indexPath: indexPath, model: model) case .copiedText(let model): - return configurePlainTextCell(tableView, indexPath: indexPath, title: model.title) + let cell = configurePlainTextCell(tableView, indexPath: indexPath, title: model.title) + cell.accessoryType = .disclosureIndicator + return cell case .infoTable(let model): - return configurePlainTextCell(tableView, indexPath: indexPath, title: model.header) + let cell = configurePlainTextCell(tableView, indexPath: indexPath, title: model.header) + cell.accessoryType = .disclosureIndicator + return cell case .selectionList(let model): return configureSelectorCell(tableView, indexPath: indexPath, model: model) + case .menuItem(let model): + let cell = configurePlainTextCell(tableView, indexPath: indexPath, title: model.title) + cell.accessoryType = model.showsDisclosure ? .disclosureIndicator : .none + return cell + case .nestedScreen(let model): + let cell = configurePlainTextCell(tableView, indexPath: indexPath, title: model.title) + cell.accessoryType = .disclosureIndicator + return cell } } @@ -129,6 +142,10 @@ extension MainAdapter: UITableViewDelegate { onSelectableTextTap?(model) case .infoTable(let model): onOpenInfoTable?(model) + case .menuItem(let model): + model.block?() + case .nestedScreen(let model): + onOpenNestedScreen?(model) default: return } diff --git a/DebugScreen/Flows/MainScreen/View/MainViewController.swift b/DebugScreen/Flows/MainScreen/View/MainViewController.swift index e0f53e4..dda1e67 100644 --- a/DebugScreen/Flows/MainScreen/View/MainViewController.swift +++ b/DebugScreen/Flows/MainScreen/View/MainViewController.swift @@ -34,8 +34,8 @@ final class MainViewController: UIViewController { extension MainViewController: MainViewInput { - func setupInitialState(sections: [TableSection]) { - configureAppearance() + func setupInitialState(title: String, sections: [TableSection]) { + configureAppearance(title: title) adapter?.fill(with: sections) } @@ -45,15 +45,15 @@ extension MainViewController: MainViewInput { private extension MainViewController { - func configureAppearance() { + func configureAppearance(title: String) { view.backgroundColor = DebugScreenConfiguration.shared.colorScheme.backgroundColor - configureNavigationBar() + configureNavigationBar(title: title) configureTableView() configureAdapter() } - func configureNavigationBar() { - navigationItem.title = L10n.MainViewController.debugTitle + func configureNavigationBar(title: String) { + navigationItem.title = title configureCloseButton() } @@ -78,6 +78,9 @@ private extension MainViewController { adapter?.onSelectableTextTap = { [weak self] model in self?.output?.didTapSelectableText(model: model) } + adapter?.onOpenNestedScreen = { [weak self] model in + self?.output?.didTapNestedScreen(model: model) + } } func configureCloseButton() { diff --git a/DebugScreen/Flows/MainScreen/View/MainViewInput.swift b/DebugScreen/Flows/MainScreen/View/MainViewInput.swift index 81d51bb..c9ab392 100644 --- a/DebugScreen/Flows/MainScreen/View/MainViewInput.swift +++ b/DebugScreen/Flows/MainScreen/View/MainViewInput.swift @@ -8,5 +8,5 @@ import Foundation protocol MainViewInput: AnyObject { - func setupInitialState(sections: [TableSection]) + func setupInitialState(title: String, sections: [TableSection]) } diff --git a/DebugScreen/Flows/MainScreen/View/MainViewOutput.swift b/DebugScreen/Flows/MainScreen/View/MainViewOutput.swift index 1321088..0f8afd5 100644 --- a/DebugScreen/Flows/MainScreen/View/MainViewOutput.swift +++ b/DebugScreen/Flows/MainScreen/View/MainViewOutput.swift @@ -12,5 +12,6 @@ protocol MainViewOutput { func didTapActionList(model: ActionList) func didTapInfoTable(model: InfoTableModel) func didTapSelectableText(model: CopiedText) + func didTapNestedScreen(model: NestedScreen) func didTapCloseButton() } diff --git a/Example/DebugScreenExample/App/AppDelegate.swift b/Example/DebugScreenExample/App/AppDelegate.swift index e6ad83c..de95904 100644 --- a/Example/DebugScreenExample/App/AppDelegate.swift +++ b/Example/DebugScreenExample/App/AppDelegate.swift @@ -56,6 +56,8 @@ private extension AppDelegate { let togglesSection = TogglesSectionBuilder().build() let copiedTextSection = CopiedTextSectionBuilder().build() let infoTablesSection = InfoTablesSectionBuilder().build() + let menuItemsSection = MenuItemsSectionBuilder().build() + let nestedScreensSection = NestedScreensSectionBuilder().build() /// You can build logger section from ready-to-use components /// or create it manually like other sections by implementing `SectionBuilder` protocol @@ -69,6 +71,8 @@ private extension AppDelegate { DebugScreenConfiguration.shared.sections = [serverSelectionSection, actionsSection, togglesSection, + menuItemsSection, + nestedScreensSection, copiedTextSection, infoTablesSection, loggerSection] diff --git a/Example/DebugScreenExample/App/DebugScreenConfiguration/Builders/MenuItemsSectionBuilder.swift b/Example/DebugScreenExample/App/DebugScreenConfiguration/Builders/MenuItemsSectionBuilder.swift new file mode 100644 index 0000000..b3b1b7e --- /dev/null +++ b/Example/DebugScreenExample/App/DebugScreenConfiguration/Builders/MenuItemsSectionBuilder.swift @@ -0,0 +1,60 @@ +// +// MenuItemsSectionBuilder.swift +// Example +// +// Created by Aleksandr Potemkin on 13.02.2026. +// + +import DebugScreen + +final class MenuItemsSectionBuilder: SectionBuilder { + + // MARK: - Methods + + func build() -> TableSection { + let blocks = configureMenuItemBlocks() + + return .init(title: L10n.MenuItems.header, blocks: blocks) + } + +} + +// MARK: - Private Methods + +private extension MenuItemsSectionBuilder { + + func configureMenuItemBlocks() -> [MainTableBlock] { + let simpleItem = DebugScreenMenuItem( + title: L10n.MenuItems.simpleTitle + ) { + debugPrint("✅ Simple menu item tapped") + } + + let disclosureItem = DebugScreenMenuItem( + title: L10n.MenuItems.disclosureTitle, + showsDisclosure: true + ) { + let view = DestinationViewController() + DebugScreenPresenterService.shared.showCustomScreen(view) + } + + let multilineItem = DebugScreenMenuItem( + title: L10n.MenuItems.multilineTitle, + showsDisclosure: true + ) { + debugPrint("✅ Multiline menu item tapped") + } + + let noActionItem = DebugScreenMenuItem( + title: L10n.MenuItems.noActionTitle + ) + + return [ + .menuItem(model: simpleItem), + .menuItem(model: disclosureItem), + .menuItem(model: multilineItem), + .menuItem(model: noActionItem) + ] + } + +} diff --git a/Example/DebugScreenExample/App/DebugScreenConfiguration/Builders/NestedScreensSectionBuilder.swift b/Example/DebugScreenExample/App/DebugScreenConfiguration/Builders/NestedScreensSectionBuilder.swift new file mode 100644 index 0000000..57202aa --- /dev/null +++ b/Example/DebugScreenExample/App/DebugScreenConfiguration/Builders/NestedScreensSectionBuilder.swift @@ -0,0 +1,103 @@ +// +// NestedScreensSectionBuilder.swift +// Example +// +// Created by Aleksandr Potemkin on 13.02.2026. +// + +import DebugScreen + +final class NestedScreensSectionBuilder: SectionBuilder { + + // MARK: - Methods + + func build() -> TableSection { + let blocks = configureNestedScreenBlocks() + + return .init(title: L10n.NestedScreens.header, blocks: blocks) + } + +} + +// MARK: - Private Methods + +private extension NestedScreensSectionBuilder { + + func configureNestedScreenBlocks() -> [MainTableBlock] { + let settingsScreen = configureSettingsScreen() + let nestedScreen = configureNestedScreen() + + return [ + .nestedScreen(model: settingsScreen), + .nestedScreen(model: nestedScreen) + ] + } + + /// Simple section screen with toggles and copied text items inside. + func configureSettingsScreen() -> DebugScreenNestedScreen { + let toggle1 = UserDefaultsFeatureToggle( + title: L10n.NestedScreens.Settings.firstToggleTitle, + key: UserDefaultsKeys.isNeedShowTouches + ) + let toggle2 = UserDefaultsFeatureToggle( + title: L10n.NestedScreens.Settings.secondToggleTitle, + key: UserDefaultsKeys.isNeedUseSSH + ) + let togglesSection = TableSection( + title: L10n.NestedScreens.Settings.togglesSectionTitle, + blocks: [.toggle(model: toggle1), .toggle(model: toggle2)] + ) + + let copiedTextItem = CopiedTextItem( + title: L10n.NestedScreens.Settings.copiedTextTitle, + value: "example_api_key_12345" + ) + let copiedTextSection = TableSection( + title: L10n.NestedScreens.Settings.copiedTextSectionTitle, + blocks: [.copiedText(model: copiedTextItem)] + ) + + return .init( + title: L10n.NestedScreens.settingsTitle, + sections: [togglesSection, copiedTextSection] + ) + } + + /// Nested section screen that contains another section screen inside (recursive nesting demo). + func configureNestedScreen() -> DebugScreenNestedScreen { + // Deepest level – a screen with simple menu items + let deepMenuItem = DebugScreenMenuItem( + title: L10n.NestedScreens.Nested.deepMenuItemTitle + ) { + debugPrint("✅ Deep nested menu item tapped") + } + let deepInfoTable: InfoTableModel = .appInfoTable() + let deepScreen = DebugScreenNestedScreen( + title: L10n.NestedScreens.Nested.deepScreenTitle, + sections: [ + .init(title: L10n.NestedScreens.Nested.deepMenuSectionTitle, + blocks: [.menuItem(model: deepMenuItem)]), + .init(title: L10n.NestedScreens.Nested.deepInfoSectionTitle, + blocks: [.infoTable(model: deepInfoTable)]) + ] + ) + + // Middle level – contains a link to the deepest screen and its own content + let middleMenuItem = DebugScreenMenuItem( + title: L10n.NestedScreens.Nested.middleMenuItemTitle, + ) { + debugPrint("✅ Middle nested menu item tapped") + } + + return .init( + title: L10n.NestedScreens.nestedTitle, + sections: [ + .init(title: L10n.NestedScreens.Nested.middleMenuSectionTitle, + blocks: [.menuItem(model: middleMenuItem)]), + .init(title: L10n.NestedScreens.Nested.nestedScreenSectionTitle, + blocks: [.nestedScreen(model: deepScreen)]) + ] + ) + } + +} diff --git a/Example/DebugScreenExample/App/DebugScreenConfiguration/Models/DebugScreenMenuItem.swift b/Example/DebugScreenExample/App/DebugScreenConfiguration/Models/DebugScreenMenuItem.swift new file mode 100644 index 0000000..ff0c492 --- /dev/null +++ b/Example/DebugScreenExample/App/DebugScreenConfiguration/Models/DebugScreenMenuItem.swift @@ -0,0 +1,26 @@ +// +// DebugScreenMenuItem.swift +// Example +// +// Created by Aleksandr Potemkin on 13.02.2026. +// + +import DebugScreen + +final class DebugScreenMenuItem: MenuItem { + + // MARK: - Properties + + let title: String + let showsDisclosure: Bool + let block: (() -> Void)? + + // MARK: - Initialization + + init(title: String, showsDisclosure: Bool = false, block: (() -> Void)? = nil) { + self.title = title + self.showsDisclosure = showsDisclosure + self.block = block + } + +} diff --git a/Example/DebugScreenExample/App/DebugScreenConfiguration/Models/DebugScreenNestedScreen.swift b/Example/DebugScreenExample/App/DebugScreenConfiguration/Models/DebugScreenNestedScreen.swift new file mode 100644 index 0000000..5d6a58f --- /dev/null +++ b/Example/DebugScreenExample/App/DebugScreenConfiguration/Models/DebugScreenNestedScreen.swift @@ -0,0 +1,24 @@ +// +// DebugScreenNestedScreen.swift +// Example +// +// Created by Aleksandr Potemkin on 13.02.2026. +// + +import DebugScreen + +final class DebugScreenNestedScreen: NestedScreen { + + // MARK: - Properties + + let title: String + let sections: [TableSection] + + // MARK: - Initialization + + init(title: String, sections: [TableSection]) { + self.title = title + self.sections = sections + } + +} diff --git a/Example/DebugScreenExample/Library/Resources/Strings/Strings.swift b/Example/DebugScreenExample/Library/Resources/Strings/Strings.swift index c14eaee..74dd00d 100644 --- a/Example/DebugScreenExample/Library/Resources/Strings/Strings.swift +++ b/Example/DebugScreenExample/Library/Resources/Strings/Strings.swift @@ -71,6 +71,57 @@ internal enum L10n { internal static let header = L10n.tr("Localizable", "InfoTable.header") } + internal enum MenuItems { + /// Открыть экран + internal static let disclosureTitle = L10n.tr("Localizable", "MenuItems.disclosureTitle") + /// Пункты меню + internal static let header = L10n.tr("Localizable", "MenuItems.header") + /// Многострочный пункт меню + /// с очень длинным описанием, которое занимает несколько строк + internal static let multilineTitle = L10n.tr("Localizable", "MenuItems.multilineTitle") + /// Информационный пункт (без действия) + internal static let noActionTitle = L10n.tr("Localizable", "MenuItems.noActionTitle") + /// Простое действие + internal static let simpleTitle = L10n.tr("Localizable", "MenuItems.simpleTitle") + } + + internal enum NestedScreens { + /// Вложенные экраны + internal static let header = L10n.tr("Localizable", "NestedScreens.header") + /// Вложенность экранов + internal static let nestedTitle = L10n.tr("Localizable", "NestedScreens.nestedTitle") + /// Настройки + internal static let settingsTitle = L10n.tr("Localizable", "NestedScreens.settingsTitle") + internal enum Nested { + /// Информация + internal static let deepInfoSectionTitle = L10n.tr("Localizable", "NestedScreens.Nested.deepInfoSectionTitle") + /// Действие глубокого уровня + internal static let deepMenuItemTitle = L10n.tr("Localizable", "NestedScreens.Nested.deepMenuItemTitle") + /// Действия + internal static let deepMenuSectionTitle = L10n.tr("Localizable", "NestedScreens.Nested.deepMenuSectionTitle") + /// Глубокий уровень + internal static let deepScreenTitle = L10n.tr("Localizable", "NestedScreens.Nested.deepScreenTitle") + /// Действие среднего уровня + internal static let middleMenuItemTitle = L10n.tr("Localizable", "NestedScreens.Nested.middleMenuItemTitle") + /// Действия + internal static let middleMenuSectionTitle = L10n.tr("Localizable", "NestedScreens.Nested.middleMenuSectionTitle") + /// Глубже + internal static let nestedScreenSectionTitle = L10n.tr("Localizable", "NestedScreens.Nested.nestedScreenSectionTitle") + } + internal enum Settings { + /// API ключи + internal static let copiedTextSectionTitle = L10n.tr("Localizable", "NestedScreens.Settings.copiedTextSectionTitle") + /// API ключ + internal static let copiedTextTitle = L10n.tr("Localizable", "NestedScreens.Settings.copiedTextTitle") + /// Показывать касания + internal static let firstToggleTitle = L10n.tr("Localizable", "NestedScreens.Settings.firstToggleTitle") + /// Использовать SSH + internal static let secondToggleTitle = L10n.tr("Localizable", "NestedScreens.Settings.secondToggleTitle") + /// Переключатели + internal static let togglesSectionTitle = L10n.tr("Localizable", "NestedScreens.Settings.togglesSectionTitle") + } + } + internal enum ServerSelection { /// Продовый internal static let firstServerTitle = L10n.tr("Localizable", "ServerSelection.firstServerTitle") diff --git a/Example/DebugScreenExample/Library/Resources/Strings/en.lproj/Localizable.strings b/Example/DebugScreenExample/Library/Resources/Strings/en.lproj/Localizable.strings index 63d20e4..856a2d1 100644 --- a/Example/DebugScreenExample/Library/Resources/Strings/en.lproj/Localizable.strings +++ b/Example/DebugScreenExample/Library/Resources/Strings/en.lproj/Localizable.strings @@ -47,6 +47,32 @@ "InfoTable.AboutDevice" = "Device Info"; "InfoTable.Custom" = "Custom Data"; +// MARK: - MenuItems + +"MenuItems.header" = "Menu Items"; +"MenuItems.simpleTitle" = "Simple action"; +"MenuItems.disclosureTitle" = "Open Screen"; +"MenuItems.multilineTitle" = "This is a multiline menu item\nwith a very long description that spans multiple lines"; +"MenuItems.noActionTitle" = "Informational item (no action)"; + +// MARK: - NestedScreens + +"NestedScreens.header" = "Section Screens"; +"NestedScreens.settingsTitle" = "Settings"; +"NestedScreens.nestedTitle" = "Nested Screens"; +"NestedScreens.Settings.togglesSectionTitle" = "Toggles"; +"NestedScreens.Settings.firstToggleTitle" = "Show touches"; +"NestedScreens.Settings.secondToggleTitle" = "Use SSH"; +"NestedScreens.Settings.copiedTextSectionTitle" = "API Keys"; +"NestedScreens.Settings.copiedTextTitle" = "API Key"; +"NestedScreens.Nested.middleMenuSectionTitle" = "Actions"; +"NestedScreens.Nested.middleMenuItemTitle" = "Middle level action"; +"NestedScreens.Nested.nestedScreenSectionTitle" = "Go Deeper"; +"NestedScreens.Nested.deepScreenTitle" = "Deep Level"; +"NestedScreens.Nested.deepMenuSectionTitle" = "Deep Actions"; +"NestedScreens.Nested.deepMenuItemTitle" = "Deep nested action"; +"NestedScreens.Nested.deepInfoSectionTitle" = "Info"; + // MARK: - Destination Controller "DestinationController.title" = "Destination controller"; diff --git a/Example/DebugScreenExample/Library/Resources/Strings/ru.lproj/Localizable.strings b/Example/DebugScreenExample/Library/Resources/Strings/ru.lproj/Localizable.strings index 4924b45..b7363da 100644 --- a/Example/DebugScreenExample/Library/Resources/Strings/ru.lproj/Localizable.strings +++ b/Example/DebugScreenExample/Library/Resources/Strings/ru.lproj/Localizable.strings @@ -47,6 +47,32 @@ "InfoTable.AboutDevice" = "Информация об устройстве"; "InfoTable.Custom" = "Произвольные данные"; +// MARK: - MenuItems + +"MenuItems.header" = "Пункты меню"; +"MenuItems.simpleTitle" = "Простое действие"; +"MenuItems.disclosureTitle" = "Открыть экран"; +"MenuItems.multilineTitle" = "Многострочный пункт меню\nс очень длинным описанием, которое занимает несколько строк"; +"MenuItems.noActionTitle" = "Информационный пункт (без действия)"; + +// MARK: - NestedScreens + +"NestedScreens.header" = "Вложенные экраны"; +"NestedScreens.settingsTitle" = "Настройки"; +"NestedScreens.nestedTitle" = "Вложенность экранов"; +"NestedScreens.Settings.togglesSectionTitle" = "Переключатели"; +"NestedScreens.Settings.firstToggleTitle" = "Показывать касания"; +"NestedScreens.Settings.secondToggleTitle" = "Использовать SSH"; +"NestedScreens.Settings.copiedTextSectionTitle" = "API ключи"; +"NestedScreens.Settings.copiedTextTitle" = "API ключ"; +"NestedScreens.Nested.middleMenuSectionTitle" = "Действия"; +"NestedScreens.Nested.middleMenuItemTitle" = "Действие среднего уровня"; +"NestedScreens.Nested.nestedScreenSectionTitle" = "Глубже"; +"NestedScreens.Nested.deepScreenTitle" = "Глубокий уровень"; +"NestedScreens.Nested.deepMenuSectionTitle" = "Действия"; +"NestedScreens.Nested.deepMenuItemTitle" = "Действие глубокого уровня"; +"NestedScreens.Nested.deepInfoSectionTitle" = "Информация"; + // MARK: - Destination Controller "DestinationController.title" = "Новый экран";