From a1ed10a37ea60c8c9ddba3c4b63d257ae1cbbf91 Mon Sep 17 00:00:00 2001 From: Wendy Liga <16457495+wendyliga@users.noreply.github.com> Date: Thu, 22 Feb 2024 01:02:18 +0800 Subject: [PATCH] TPTweak 2.0 (#14) * initial minimizable * disable peep if minimizable * update * fix compile error * fix error compile again --- CHANGELOG.md | 7 + Example/Example/AppDelegate.swift | 2 +- Example/Example/TPTweakEntry+Extension.swift | 2 +- .../TrackingHistoryViewController.swift | 2 +- Example/Example/ViewController.swift | 2 +- LICENSE.md | 2 +- README.md | 6 +- Sources/TPTweak/TPTweakEntry.swift | 46 +- .../TPTweakOptionsViewController.swift | 2 +- .../TPTweak/TPTweakPickerViewController.swift | 80 ++- Sources/TPTweak/TPTweakShakeWindow.swift | 20 +- Sources/TPTweak/TPTweakStore.swift | 8 +- Sources/TPTweak/TPTweakViewController.swift | 469 ++++++++++++++++-- TPTweak.podspec | 2 +- Tests/TPTweakTests/TPTweakTests.swift | 4 +- 15 files changed, 545 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6fd5e..64b5392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ +# 2.0.0 +- Minimizable TPTweakViewController with `TPTweakViewController.presentMinimizableTweaks` +- Hold navigation controller to peep the background(only available on non minimizable mode) +- now every option will have completions +- add Settings menu for setting up TPTweakViewController +- add an empty state message on the favourite page +- fix the favorite page not reflecting the latest value after modifying one of the cells. # 1.2.0 - Add Search functionality ([#11](https://github.com/tokopedia/ios-tptweak/pull/11)) diff --git a/Example/Example/AppDelegate.swift b/Example/Example/AppDelegate.swift index 0f62778..b5a6841 100644 --- a/Example/Example/AppDelegate.swift +++ b/Example/Example/AppDelegate.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Example/Example/TPTweakEntry+Extension.swift b/Example/Example/TPTweakEntry+Extension.swift index dcc2c07..a65e90a 100644 --- a/Example/Example/TPTweakEntry+Extension.swift +++ b/Example/Example/TPTweakEntry+Extension.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Example/Example/TrackingHistoryViewController.swift b/Example/Example/TrackingHistoryViewController.swift index b326f00..94b0656 100644 --- a/Example/Example/TrackingHistoryViewController.swift +++ b/Example/Example/TrackingHistoryViewController.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Example/Example/ViewController.swift b/Example/Example/ViewController.swift index 019314a..78f93e1 100644 --- a/Example/Example/ViewController.swift +++ b/Example/Example/ViewController.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/LICENSE.md b/LICENSE.md index 5065ae9..6c21786 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 Tokopedia + Copyright 2022-2024 Tokopedia Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 23d9846..c292250 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ https://github.com/tokopedia/ios-tptweak or manually add to your `Package.swift` ```swift -.package(url: "https://github.com/tokopedia/ios-tptweak", from: "1.0.0"), +.package(url: "https://github.com/tokopedia/ios-tptweak", from: "2.0.0"), ``` # Cocoapods add this to your `Podfile` ``` -pod 'TPTweak', '~> 1.0.0' +pod 'TPTweak', '~> 2.0.0' ``` # Nomenclature @@ -178,7 +178,7 @@ TPTweakEntry.enableTracking.setValue(true) # License ``` - Copyright 2022 Tokopedia. All rights reserved. + Copyright 2022-2024 Tokopedia. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Sources/TPTweak/TPTweakEntry.swift b/Sources/TPTweak/TPTweakEntry.swift index 391e012..caecc2d 100644 --- a/Sources/TPTweak/TPTweakEntry.swift +++ b/Sources/TPTweak/TPTweakEntry.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,16 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +#if canImport(UIKit) import Foundation +import UIKit /** Entry type, pick your poison */ public enum TPTweakEntryType { - case `switch`(defaultValue: Bool, closure: ((Bool) -> Void)? = nil) - case action(() -> Void) - case strings(item: [String], selected: String) - case numbers(item: [Double], selected: Double) + case `switch`(defaultValue: Bool, completion: ((Bool) -> Void)? = nil) + case action(accessoryType: UITableViewCell.AccessoryType = .disclosureIndicator, () -> Void) + case strings(item: [String], selected: String, completion: ((String) -> Void)? = nil) + case numbers(item: [Double], selected: Double, completion: ((Double) -> Void)? = nil) } /** @@ -34,26 +36,31 @@ public struct TPTweakEntry { public let section: String /// Cell will be the name of cell on second page table of TPTweak public let cell: String + /// icon on the left of the cell + public let cellLeftIcon: UIImage? /// Will only visible on second page / TPTweakPickerViewController public let footer: String? + /// type of Entry public let type: TPTweakEntryType - + public init( category: String, section: String, cell: String, - footer: String?, + cellLeftIcon: UIImage? = nil, + footer: String? = nil, type: TPTweakEntryType ) { self.category = category self.section = section self.cell = cell + self.cellLeftIcon = cellLeftIcon self.footer = footer self.type = type } - + /** Read current value of this entry on TPTweak @@ -113,4 +120,27 @@ extension TPTweakEntry { internal static var favourite: TPTweakEntry { TPTweakEntry(category: "tptweak", section: "internal", cell: "favourite", footer: nil, type: .action({})) } + + internal static var peepOpacity: TPTweakEntry { + TPTweakEntry( + category: "Settings", + section: "Interaction", + cell: "Hold Opacity", + footer: "The opacity when you hold the navigation bar", + type: .numbers(item: [0, 0.25, 0.5, 0.75, 1], selected: 0.25) + ) + } + + internal static var clearCache: TPTweakEntry { + TPTweakEntry( + category: "Settings", + section: "Miscellaneous", + cell: "Reset", + footer: "This will reset all Tweaks to default value", + type: .action(accessoryType: .none, { + TPTweak.resetAll() + }) + ) + } } +#endif diff --git a/Sources/TPTweak/TPTweakOptionsViewController.swift b/Sources/TPTweak/TPTweakOptionsViewController.swift index cfaec9a..24c4fdc 100644 --- a/Sources/TPTweak/TPTweakOptionsViewController.swift +++ b/Sources/TPTweak/TPTweakOptionsViewController.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/TPTweak/TPTweakPickerViewController.swift b/Sources/TPTweak/TPTweakPickerViewController.swift index 0ca76a4..a540d24 100644 --- a/Sources/TPTweak/TPTweakPickerViewController.swift +++ b/Sources/TPTweak/TPTweakPickerViewController.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -69,6 +69,7 @@ internal final class TPTweakPickerViewController: UIViewController { view.translatesAutoresizingMaskIntoConstraints = false view.delegate = self view.dataSource = self + view.keyboardDismissMode = .onDrag view.register(UITableViewCell.self, forCellReuseIdentifier: "cell") return view @@ -149,6 +150,7 @@ internal final class TPTweakPickerViewController: UIViewController { favourites.insert(identifier) TPTweakEntry.favourite.setValue(favourites) + table.reloadData() } private func removeFavourite(identifier: String) { @@ -156,6 +158,26 @@ internal final class TPTweakPickerViewController: UIViewController { favourites.remove(identifier) TPTweakEntry.favourite.setValue(favourites) + + // update data + for (offset, section) in zip(_data.indices, _data) { + for (rowOffset, row) in zip(section.cells.indices, section.cells) where row.identifer == identifier { + // create new cell without the removed favourite + var newCells = section.cells + newCells.removeAll(where: { $0.identifer == identifier }) + + if newCells.isEmpty { + // if section does not have any cells, remove section + _data.removeAll(where: { $0.name == section.name }) + } else { + // update section with new cells + guard let index = _data.firstIndex(where: { $0.name == section.name && $0.footer == section.footer }) else { continue } + _data[index].cells = newCells + } + } + } + + table.reloadData() } private func createFavouriteSwipeButton(identifier: String) -> UIContextualAction { @@ -166,7 +188,7 @@ internal final class TPTweakPickerViewController: UIViewController { } if #available(iOS 13.0, *) { - action.image = UIImage(systemName: "star.slash") + action.image = UIImage(systemName: "heart.slash") } action.backgroundColor = .systemRed @@ -179,7 +201,7 @@ internal final class TPTweakPickerViewController: UIViewController { } if #available(iOS 13.0, *) { - action.image = UIImage(systemName: "star") + action.image = UIImage(systemName: "heart") } action.backgroundColor = .systemBlue @@ -193,7 +215,7 @@ internal final class TPTweakPickerViewController: UIViewController { if Self.isFavourite(identifier: identifier) { return UIAction( title: "Unfavourite", - image: UIImage(systemName: "star.slash"), + image: UIImage(systemName: "heart.slash"), identifier: nil, attributes: .destructive ) { [weak self] _ in @@ -202,7 +224,7 @@ internal final class TPTweakPickerViewController: UIViewController { } else { return UIAction( title: "Favourite", - image: UIImage(systemName: "star"), + image: UIImage(systemName: "heart"), identifier: nil ) { [weak self] _ in self?.setFavourite(identifier: identifier) @@ -213,7 +235,22 @@ internal final class TPTweakPickerViewController: UIViewController { extension TPTweakPickerViewController: UITableViewDataSource, UITableViewDelegate { internal func numberOfSections(in _: UITableView) -> Int { - data.count + let count = data.count + + // handling empty state + if count == 0 { + let emptyLabel = UILabel(frame: .zero) + emptyLabel.text = "You can Favorite a Tweaks by swipe or long press on the cell" + emptyLabel.textAlignment = .center + emptyLabel.numberOfLines = 0 + emptyLabel.font = .boldSystemFont(ofSize: 16) + + self.table.backgroundView = emptyLabel + } else { + self.table.backgroundView = nil + } + + return count } internal func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -226,12 +263,14 @@ extension TPTweakPickerViewController: UITableViewDataSource, UITableViewDelegat } let cellData = data[indexPath.section].cells[indexPath.row] - + cell.imageView?.image = cellData.leftIcon + switch cellData.type { case .action: cell.textLabel?.text = cellData.name cell.detailTextLabel?.text = nil - cell.accessoryType = .disclosureIndicator + // custom accessoryType only available for action type + cell.accessoryType = cellData.accessoryType case .switch: let switcher = UISwitch() switcher.isOn = TPTweakStore.read(type: Bool.self, identifier: cellData.identifer) ?? false @@ -240,14 +279,14 @@ extension TPTweakPickerViewController: UITableViewDataSource, UITableViewDelegat cell.textLabel?.text = cellData.name cell.detailTextLabel?.text = nil cell.accessoryView = switcher - case let .strings(_, defaultValue): + case let .strings(_, defaultValue, _): let currentValue = TPTweakStore.read(type: String.self, identifier: cellData.identifer) ?? defaultValue cell = UITableViewCell(style: .value1, reuseIdentifier: "cell") cell.detailTextLabel?.text = currentValue cell.textLabel?.text = cellData.name cell.accessoryType = .disclosureIndicator - case let .numbers(_, defaultValue): + case let .numbers(_, defaultValue, _): let currentValue = TPTweakStore.read(type: Double.self, identifier: cellData.identifer) ?? defaultValue cell = UITableViewCell(style: .value1, reuseIdentifier: "cell") @@ -276,16 +315,16 @@ extension TPTweakPickerViewController: UITableViewDataSource, UITableViewDelegat let cellData = data[indexPath.section].cells[indexPath.row] switch cellData.type { - case let .action(closure): - closure() - case let .switch(_, closure): + case let .action(_, completion): + completion() + case let .switch(_, completion): var value = TPTweakStore.read(type: Bool.self, identifier: cellData.identifer) ?? false value.toggle() TPTweakStore.set(value, identifier: cellData.identifer) tableView.reloadRows(at: [indexPath], with: .none) // to update cell value after action - closure?(value) - case let .numbers(item, defaultValue): + completion?(value) + case let .numbers(item, defaultValue, completion): let viewController = TPTweakOptionsViewController( title: cellData.name, data: item.map { TPTweakOptionsViewController.Cell(name: String($0), value: $0) }, @@ -295,6 +334,7 @@ extension TPTweakPickerViewController: UITableViewDataSource, UITableViewDelegat viewController.didChoose = { [weak tableView, weak self] newValue in TPTweakStore.set(newValue, identifier: cellData.identifer) tableView?.reloadRows(at: [indexPath], with: .automatic) // to update cell value after action + completion?(newValue) if let self = self { self.closeDetail(viewController: viewController) // back to picker @@ -302,7 +342,7 @@ extension TPTweakPickerViewController: UITableViewDataSource, UITableViewDelegat } openDetail(viewController: viewController) - case let .strings(item, defaultValue): + case let .strings(item, defaultValue, completion): let viewController = TPTweakOptionsViewController( title: cellData.name, data: item.map { TPTweakOptionsViewController.Cell(name: $0, value: $0) }, @@ -312,7 +352,8 @@ extension TPTweakPickerViewController: UITableViewDataSource, UITableViewDelegat viewController.didChoose = { [weak tableView, weak self] newValue in TPTweakStore.set(newValue, identifier: cellData.identifer) tableView?.reloadRows(at: [indexPath], with: .automatic) // to update cell value after action - + completion?(newValue) + if let self = self { self.closeDetail(viewController: viewController) // back to picker } @@ -323,7 +364,6 @@ extension TPTweakPickerViewController: UITableViewDataSource, UITableViewDelegat } internal func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let cellData = data[indexPath.section].cells[indexPath.row] let action = createFavouriteSwipeButton(identifier: cellData.identifer) return UISwipeActionsConfiguration(actions: [action]) @@ -361,14 +401,16 @@ extension TPTweakPickerViewController { internal struct Section { internal let name: String internal let footer: String? - internal let cells: [Cell] + internal var cells: [Cell] } internal struct Cell { internal let name: String internal let identifer: String internal let type: TPTweakEntryType + internal let leftIcon: UIImage? internal let footer: String? + internal let accessoryType: UITableViewCell.AccessoryType } } #endif diff --git a/Sources/TPTweak/TPTweakShakeWindow.swift b/Sources/TPTweak/TPTweakShakeWindow.swift index 19fdd81..58db3e5 100644 --- a/Sources/TPTweak/TPTweakShakeWindow.swift +++ b/Sources/TPTweak/TPTweakShakeWindow.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -37,6 +37,10 @@ public class TPTweakShakeWindow: UIWindow { return false #endif } + + private var tweakViewController: UIViewController? + private var realViewController: UIViewController? + private var bubbleView: UIView? // MARK: - Life Cylce @@ -107,19 +111,9 @@ public class TPTweakShakeWindow: UIWindow { private func applicationDidBecomeActiveWithNotification() { active = true } - + private func presentTweaks() { - var visibleViewController = rootViewController - - if visibleViewController?.presentedViewController != nil { - visibleViewController = visibleViewController?.presentedViewController - } - - // prevent double-presenting the tweaks view controller - guard (visibleViewController is TPTweakWithNavigatationViewController) == false else { return } - - let viewController = TPTweakWithNavigatationViewController() - visibleViewController?.present(viewController, animated: true, completion: nil) + TPTweakViewController.presentMinimizableTweaks() } } #endif diff --git a/Sources/TPTweak/TPTweakStore.swift b/Sources/TPTweak/TPTweakStore.swift index 2a88085..0aa4d4b 100644 --- a/Sources/TPTweak/TPTweakStore.swift +++ b/Sources/TPTweak/TPTweakStore.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +#if canImport(UIKit) import Foundation internal typealias TPTweak = TPTweakStore @@ -122,9 +123,9 @@ public enum TPTweakStore { switch entry.type { case let .switch(defaultValue, _): set(defaultValue, identifier: identifier) - case let .numbers(_, defaultValue): + case let .numbers(_, defaultValue, _): set(defaultValue, identifier: identifier) - case let .strings(_, defaultValue): + case let .strings(_, defaultValue, _): set(defaultValue, identifier: identifier) case .action: break @@ -271,3 +272,4 @@ internal struct TPTweakStoreEnvironment { ) } } +#endif diff --git a/Sources/TPTweak/TPTweakViewController.swift b/Sources/TPTweak/TPTweakViewController.swift index c2a8036..6302900 100644 --- a/Sources/TPTweak/TPTweakViewController.swift +++ b/Sources/TPTweak/TPTweakViewController.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,13 +15,19 @@ #if canImport(UIKit) import UIKit +internal var __tweakViewController: TPTweakWithNavigatationViewController? +internal var __realViewController: UIViewController? +internal var __bubbleView: UIView? + public final class TPTweakWithNavigatationViewController: UINavigationController { + internal var tweakViewController: TPTweakViewController = TPTweakViewController() + public init() { if #available(iOS 13.0, *) { - super.init(rootViewController: TPTweakViewController()) + super.init(rootViewController: tweakViewController) } else { super.init(nibName: nil, bundle: nil) - viewControllers = [TPTweakViewController()] + viewControllers = [tweakViewController] } if #available(iOS 12.0, *) { @@ -43,15 +49,41 @@ public final class TPTweakWithNavigatationViewController: UINavigationController public required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } + + public override func pushViewController(_ viewController: UIViewController, animated: Bool) { + super.pushViewController(viewController, animated: animated) + + /// automatically add minimizable on every children if enable + if let tptweakviewController = __tweakViewController, + viewController != tptweakviewController.tweakViewController, + tptweakviewController.tweakViewController.minimizable + { + if (viewController.navigationItem.rightBarButtonItems?.count ?? 0) > 0 { + viewController.navigationItem.rightBarButtonItems?.append(tptweakviewController.tweakViewController.minimizeBarButtonItem) + } else { + viewController.navigationItem.rightBarButtonItems = [ + tptweakviewController.tweakViewController.minimizeBarButtonItem + ] + } + + + } + } } /** Page will show all TPTweak Category */ public final class TPTweakViewController: UIViewController { + // MARK: - Interfaces + + /// enable this true if you want to use `TPTweakViewController.presentMinimizableTweaks()` + public var minimizable: Bool = false + // MARK: - Values private var data: [Row] = [] + private var didSetUpHoldToPeepRecognizer = false // MARK: - Views @@ -94,17 +126,74 @@ public final class TPTweakViewController: UIViewController { return searchController }() - - private lazy var doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissSelf)) - private lazy var resetBarButtonItem = UIBarButtonItem(title: "Reset", style: .plain, target: self, action: #selector(resetAll)) + internal lazy var doneBarButtonItem: UIBarButtonItem = { + let button = { + if #available(iOS 13.0, *) { + return UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .plain, target: self, action: #selector(dismissSelf)) + } else { + return UIBarButtonItem(title: "Close", style: .plain , target: self, action: #selector(dismissSelf)) + } + }() + button.tintColor = .gray + + return button + }() + + private lazy var closeSearchBarButtonItem: UIBarButtonItem = { + let button = { + if #available(iOS 13.0, *) { + return UIBarButtonItem(image: UIImage(systemName: "arrow.backward"), style: .plain, target: self, action: #selector(closeSearch)) + } else { + return UIBarButtonItem(title: "Cancel", style: .plain , target: self, action: #selector(closeSearch)) + } + }() + button.tintColor = .gray + + return button + }() + + /// expose to reuse on other VC + internal lazy var minimizeBarButtonItem: UIBarButtonItem = { + let button = { + if #available(iOS 13.0, *) { + return UIBarButtonItem(image: UIImage(systemName: "arrow.down.right.and.arrow.up.left"), style: .plain, target: self, action: #selector(minimize)) + } else { + return UIBarButtonItem(title: "Minimize", style: .plain , target: self, action: #selector(minimize)) + } + }() + button.tintColor = .gray + + return button + }() + + private lazy var settingBarButtonItem: UIBarButtonItem = { + let button = { + if #available(iOS 13.0, *) { + return UIBarButtonItem(image: UIImage(systemName: "gearshape.fill"), style: .plain, target: self, action: #selector(openSettings)) + } else { + return UIBarButtonItem(title: "Settings", style: .plain , target: self, action: #selector(openSettings)) + } + }() + button.tintColor = .gray + + return button + }() + private lazy var favouriteBarButtonItem: UIBarButtonItem = { - if #available(iOS 13.0, *) { - return UIBarButtonItem(image: UIImage(systemName: "star.fill"), style: .plain, target: self, action: #selector(openFavourite)) - } else { - return UIBarButtonItem(title: "Favourite", style: .plain , target: self, action: #selector(openFavourite)) - } + let button = { + if #available(iOS 13.0, *) { + return UIBarButtonItem(image: UIImage(systemName: "heart.fill"), style: .plain, target: self, action: #selector(openFavourite)) + } else { + return UIBarButtonItem(title: "Favourite", style: .plain , target: self, action: #selector(openFavourite)) + } + }() + button.tintColor = .gray + + return button }() - + + private lazy var holdToPeepTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(holdToPeep)) + // MARK: - Life Cycle public init() { @@ -112,24 +201,35 @@ public final class TPTweakViewController: UIViewController { title = "TPTweaks" view.backgroundColor = .white - - setupView() } override public func viewDidLoad() { super.viewDidLoad() - data = fetchData() + setupView() + + data = fetchUserDefinedRows() searchResultViewController.setData(data: []) + setupDefaultNavigationBarItems() + table.reloadData() - - navigationItem.leftBarButtonItem = doneBarButtonItem - navigationItem.rightBarButtonItems = [resetBarButtonItem, favouriteBarButtonItem] navigationItem.searchController = searchController navigationItem.hidesSearchBarWhenScrolling = false } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + setupDefaultNavigationBarItems() + + if !didSetUpHoldToPeepRecognizer { + navigationController?.navigationBar.isUserInteractionEnabled = true + navigationController?.navigationBar.addGestureRecognizer(holdToPeepTapRecognizer) + didSetUpHoldToPeepRecognizer = true + } + } public required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -147,8 +247,23 @@ public final class TPTweakViewController: UIViewController { table.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } + + private func setupDefaultNavigationBarItems() { + navigationItem.leftBarButtonItems = [doneBarButtonItem] + + if minimizable { + navigationItem.leftBarButtonItems?.append(minimizeBarButtonItem) + } + + navigationItem.rightBarButtonItems = [settingBarButtonItem, favouriteBarButtonItem] + } + + private func setupSearchNavigationBarItems() { + navigationItem.leftBarButtonItems = [closeSearchBarButtonItem] + navigationItem.rightBarButtonItems = [] + } - private func fetchData() -> [Row] { + private func fetchUserDefinedRows() -> [Row] { var normalizedEntries = [String: [TPTweakEntry]]() TPTweakStore.entries @@ -194,7 +309,16 @@ public final class TPTweakViewController: UIViewController { name: entry.cell, identifer: entry.getIdentifier(), type: entry.type, - footer: entry.footer + leftIcon: entry.cellLeftIcon, + footer: entry.footer, + accessoryType: { + // custom accessory type only available for action type + if case let .action(accessoryType, _) = entry.type { + return accessoryType + } else { + return .disclosureIndicator + } + }() )) if let footer = entry.footer { @@ -207,42 +331,87 @@ public final class TPTweakViewController: UIViewController { return data } + @objc private func dismissSelf() { - dismiss(animated: true) + if minimizable { + guard let tweakViewController = __tweakViewController else { return } + let tweakView = tweakViewController.view! + + UIView.animate( + withDuration: 0.3, + animations: { + let scale = CGAffineTransform(scaleX: 0.1, y: 0.1) + tweakView.transform = scale + tweakView.alpha = 0.3 + tweakView.layoutIfNeeded() + }, + completion: { _ in + tweakView.alpha = 0 + let window = UIApplication.shared.keyWindow + window?.rootViewController = __realViewController + + __realViewController = nil + __tweakViewController = nil + } + ) + } else { + self.dismiss(animated: true) + } } - - @objc - private func resetAll() { - func showLoading() -> UIViewController { - let alert = UIAlertController(title: nil, message: "Please wait...", preferredStyle: .alert) - - let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50)) - loadingIndicator.hidesWhenStopped = true - loadingIndicator.style = UIActivityIndicatorView.Style.gray - loadingIndicator.startAnimating() - - alert.view.addSubview(loadingIndicator) - present(alert, animated: true, completion: nil) - return alert + + static func topMostController() -> UIViewController? { + guard let window = UIApplication.shared.keyWindow, let rootViewController = window.rootViewController else { + return nil } - let confirmationDialog = UIAlertController(title: "Are you Sure", message: "This action will clear all custom tweaks value, and revert it all back to default value.", preferredStyle: .alert) - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) - let confirmAction = UIAlertAction(title: "Confirm", style: .destructive) { _ in - let loading = showLoading() + var topController = rootViewController - TPTweak.resetAll { - loading.dismiss(animated: true) - } + while let newTopController = topController.presentedViewController { + topController = newTopController } - confirmationDialog.addAction(confirmAction) - confirmationDialog.addAction(cancelAction) - - present(confirmationDialog, animated: true) + return topController + } + + @objc + private func closeSearch() { + searchController.isActive = false } + @objc + private func openSettings() { + let entries: [TPTweakEntry] = [ + .peepOpacity, + .clearCache + ] + + let data = convertRowToSection(row: Row(name: "", entries: entries)) + let viewController = TPTweakPickerViewController(data: data) + viewController.title = "Settings" + self.navigationController?.pushViewController(viewController, animated: true) + } + + @objc + private func holdToPeep(_ sender: UILongPressGestureRecognizer) { + // only avalable if not minimizable + guard minimizable == false else { return } + + if (sender.state == .began) { + let opacity = TPTweakEntry.peepOpacity.getValue(Double.self) ?? 0.25 + + navigationController?.navigationBar.alpha = opacity + navigationController?.viewControllers.forEach({ vc in + vc.view.alpha = opacity + }) + } else if sender.state == .ended { + navigationController?.navigationBar.alpha = 1 + navigationController?.viewControllers.forEach({ vc in + vc.view.alpha = 1 + }) + } + } + @objc private func openFavourite() { var favouriteEntries = [TPTweakEntry]() @@ -257,30 +426,40 @@ public final class TPTweakViewController: UIViewController { } @objc - private func dismissKeyboard() { - searchController.searchBar.endEditing(true) + private func minimize() { + Self.minimize() } } extension TPTweakViewController: UITableViewDataSource, UITableViewDelegate { - public func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - data.count + public func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { + let count = data.count + + if count <= 0 { + let emptyLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height)) + emptyLabel.text = "No Data" + emptyLabel.textAlignment = NSTextAlignment.center + self.table.backgroundView = emptyLabel + } else { + self.table.backgroundView = nil + } + + return count } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") else { return UITableViewCell() } - + cell.textLabel?.text = data[indexPath.row].name cell.accessoryType = .disclosureIndicator - return cell } public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - + let cell = data[indexPath.row] let data = convertRowToSection(row: cell) @@ -290,13 +469,14 @@ extension TPTweakViewController: UITableViewDataSource, UITableViewDelegate { } public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - return "Tweaks" + "Tweaks" } } - extension TPTweakViewController: UISearchControllerDelegate { public func presentSearchController(_ searchController: UISearchController) { + setupSearchNavigationBarItems() + var sections = [TPTweakPickerViewController.Section]() for row in data { @@ -305,6 +485,10 @@ extension TPTweakViewController: UISearchControllerDelegate { searchResultViewController.setData(data: sections) } + + public func willDismissSearchController(_ searchController: UISearchController) { + setupDefaultNavigationBarItems() + } } extension TPTweakViewController { @@ -313,4 +497,179 @@ extension TPTweakViewController { internal let entries: [TPTweakEntry] } } + +// minimizable +extension TPTweakViewController { + /// run this command to show TPTweakViewController that have minimizable capability + public static func presentMinimizableTweaks() { + let window = UIApplication.shared.keyWindow + __realViewController = window?.rootViewController + window?.rootViewController = nil + + let tweakViewController = TPTweakWithNavigatationViewController() + tweakViewController.tweakViewController.minimizable = true + __tweakViewController = tweakViewController + window?.rootViewController = tweakViewController + + let tweakView = tweakViewController.view! + tweakView.alpha = 0 + + UIView.animate( + withDuration: 0, + animations: { + let scale = CGAffineTransform(scaleX: 0.8, y: 0.8) + tweakView.transform = scale + tweakView.layoutIfNeeded() + }, + completion: { _ in + UIView.animate( + withDuration: 0.3, + animations: { + tweakView.alpha = 1 + tweakView.transform = CGAffineTransform.identity + tweakView.layoutIfNeeded() + } + ) + } + ) + } + + @objc + internal static func minimize() { + guard + let tweakViewController = __tweakViewController, + let realViewController = __realViewController + else { return } + let tweakView = tweakViewController.view! + + UIView.animate( + withDuration: 0.3, + animations: { + let bubblePosition = self.getBubblePosition() + + let scale = CGAffineTransform(scaleX: 0.1, y: 0.1) + let move = CGAffineTransform(translationX: bubblePosition.x, y: bubblePosition.y) + let hybrid = scale.concatenating(move) + + tweakView.transform = hybrid + tweakView.alpha = 0.3 + tweakView.layoutIfNeeded() + }, + completion: { _ in + tweakView.alpha = 0 + let window = UIApplication.shared.keyWindow + window?.rootViewController = realViewController + + setupBubble() + } + ) + } + + @objc + private static func restoreTweaks() { + guard let tweakViewController = __tweakViewController else { return } + let tweakView = tweakViewController.view! + + UIView.animate( + withDuration: 0.3, + animations: { + __bubbleView?.alpha = 0 + tweakView.transform = CGAffineTransform.identity + tweakView.alpha = 1 + }, + completion: { _ in + __bubbleView?.alpha = 0 + __bubbleView?.removeFromSuperview() + __bubbleView = nil + + let window = UIApplication.shared.keyWindow + window?.rootViewController = tweakViewController + } + ) + } +} + +// bubble view +extension TPTweakViewController { + private static func getBubblePosition() -> CGPoint { + let x = UserDefaults.standard.object(forKey: "panel_frame_x") as? CGFloat ?? 0 + let y = UserDefaults.standard.object(forKey: "panel_frame_y") as? CGFloat ?? 500 + + return CGPoint(x: x, y: y) + } + + private static func getVisibleViewController() -> UIViewController? { + var visibleViewController = UIApplication.shared.keyWindow?.rootViewController + + if visibleViewController?.presentedViewController != nil { + visibleViewController = visibleViewController?.presentedViewController + } + + // prevent double-presenting the tweaks view controller + guard let visibleViewController = visibleViewController, (visibleViewController is TPTweakWithNavigatationViewController) == false else { return nil } + return visibleViewController + } + + private static func setupBubble() { + guard let visibleViewController = getVisibleViewController() else { return } + + let subview: UIView + if #available(iOS 13.0, *) { + let image = UIImageView(frame: .init(x: 0, y: 0, width: 50, height: 50)) + image.contentMode = .center + image.image = UIImage(systemName: "arrow.up.left.and.arrow.down.right", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18)) + image.tintColor = .white + subview = image + } else { + let label = UILabel(frame: .init(x: 0, y: 0, width: 50, height: 50)) + label.text = "T" + label.textAlignment = .center + subview = label + } + + let lastPosition = getBubblePosition() + let bubble = UIView(frame: .init(x: lastPosition.x, y: lastPosition.y, width: 50, height: 50)) + if #available(iOS 13.0, *) { + bubble.backgroundColor = .secondarySystemBackground + } else { + bubble.backgroundColor = .gray + } + bubble.alpha = 0.9 + bubble.layer.cornerRadius = 25 + bubble.addSubview(subview) + + __bubbleView = bubble + + let pan = UIPanGestureRecognizer(target: self, action: #selector(panEvent)) + bubble.addGestureRecognizer(pan) + + let tap = UITapGestureRecognizer(target: self, action: #selector(tapEvent)) + bubble.addGestureRecognizer(tap) + + // show + visibleViewController.view.addSubview(bubble) + } + + @objc + private static func panEvent(ges: UIPanGestureRecognizer) { + if let view = ges.view { + view.alpha = 0.3 + let point = ges.location(in: nil) + let screenWidth = UIScreen.main.bounds.width + if ges.state == .ended { + view.alpha = 0.9 + view.center = .init(x: point.x < screenWidth / 2 ? (25) : (screenWidth - 25), y: point.y) + UserDefaults.standard.setValue(view.frame.origin.x, forKey: "panel_frame_x") + UserDefaults.standard.setValue(view.frame.origin.y, forKey: "panel_frame_y") + } else { + view.center = point + } + } + } + + @objc + private static func tapEvent(ges: UITapGestureRecognizer) { + restoreTweaks() + } +} #endif diff --git a/TPTweak.podspec b/TPTweak.podspec index 2bc82d9..20e9e1a 100644 --- a/TPTweak.podspec +++ b/TPTweak.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "TPTweak" - spec.version = "1.2.0" + spec.version = "2.0.0" spec.summary = "TPTweak is a debugging tool to help adjust your iOS app on the fly without recompile" spec.license = { :type => "Apache 2.0", :file => "LICENSE.md" } diff --git a/Tests/TPTweakTests/TPTweakTests.swift b/Tests/TPTweakTests/TPTweakTests.swift index 8c4b925..25ec81f 100644 --- a/Tests/TPTweakTests/TPTweakTests.swift +++ b/Tests/TPTweakTests/TPTweakTests.swift @@ -1,4 +1,4 @@ -// Copyright 2022 Tokopedia. All rights reserved. +// Copyright 2022-2024 Tokopedia. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +#if canImport(UIKit) @testable import TPTweak import XCTest @@ -113,3 +114,4 @@ internal final class TPTweakTests: XCTestCase { XCTAssertEqual(dummyEntry.getValue(Bool.self), nil) } } +#endif