From 37818d3153e7061ff135cb81d106364ff3b959a1 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 27 Sep 2022 20:00:44 +0300 Subject: [PATCH 01/27] feat: basic logger --- TILogging/Sources/Logger.swift | 52 +++++++++++++++++++++ TILogging/Sources/LoggerRepresentable.swift | 5 ++ TILogging/TILogging.podspec | 15 ++++++ 3 files changed, 72 insertions(+) create mode 100644 TILogging/Sources/Logger.swift create mode 100644 TILogging/Sources/LoggerRepresentable.swift create mode 100644 TILogging/TILogging.podspec diff --git a/TILogging/Sources/Logger.swift b/TILogging/Sources/Logger.swift new file mode 100644 index 00000000..5eb345c1 --- /dev/null +++ b/TILogging/Sources/Logger.swift @@ -0,0 +1,52 @@ +import os + +public struct TILogger: LoggerRepresentable { + + // MARK: - Properties + + public static let defaultLogger = TILogger(subsystem: .defaultSubsystem ?? "", category: .pointsOfInterest) + + public let logInfo: OSLog + + // MARK: - Init + + public init(subsystem: String, category: String) { + self.logInfo = .init(subsystem: subsystem, category: category) + } + + public init(subsystem: String, category: OSLog.Category) { + self.logInfo = .init(subsystem: subsystem, category: category) + } + + // MARK: - LoggerRepresentable + + public func log(_ message: StaticString, log: OSLog?, type: OSLogType, _ arguments: CVarArg...) { + os_log(message, log: log ?? logInfo, type: type, arguments) + } + + // MARK: - Public methods + + public func verbose(_ message: StaticString, _ arguments: CVarArg...) { + self.log(message, log: logInfo, type: .default, arguments) + } + + public func info(_ message: StaticString, _ arguments: CVarArg...) { + self.log(message, log: logInfo, type: .info, arguments) + } + + public func debug(_ message: StaticString, _ arguments: CVarArg...) { + self.log(message, log: logInfo, type: .debug, arguments) + } + + public func error(_ message: StaticString, _ arguments: CVarArg...) { + self.log(message, log: logInfo, type: .error, arguments) + } + + public func fault(_ message: StaticString, _ arguments: CVarArg...) { + self.log(message, log: logInfo, type: .fault, arguments) + } +} + +private extension String { + static let defaultSubsystem = Bundle.main.bundleIdentifier +} diff --git a/TILogging/Sources/LoggerRepresentable.swift b/TILogging/Sources/LoggerRepresentable.swift new file mode 100644 index 00000000..c5a95beb --- /dev/null +++ b/TILogging/Sources/LoggerRepresentable.swift @@ -0,0 +1,5 @@ +import os + +public protocol LoggerRepresentable { + func log(_ message: StaticString, log: OSLog?, type: OSLogType, _ arguments: CVarArg...) +} diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec new file mode 100644 index 00000000..cbcd20ef --- /dev/null +++ b/TILogging/TILogging.podspec @@ -0,0 +1,15 @@ +Pod::Spec.new do |s| + s.name = 'TILogging' + s.version = '1.27.0' + s.summary = 'Logging API' + s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } + s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '10.0' + s.swift_versions = ['5.3'] + + s.source_files = s.name + '/Sources/**/*' + +end From 3bb9e74461751737041825f4b211e8c0801e2f6a Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 27 Sep 2022 20:05:28 +0300 Subject: [PATCH 02/27] fix: package file update --- Package.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Package.swift b/Package.swift index 0dc55fe0..111a8ba5 100644 --- a/Package.swift +++ b/Package.swift @@ -20,6 +20,7 @@ let package = Package( .library(name: "TIFoundationUtils", targets: ["TIFoundationUtils"]), .library(name: "TIKeychainUtils", targets: ["TIKeychainUtils"]), .library(name: "TITableKitUtils", targets: ["TITableKitUtils"]), + .library(name: "TILogging", targets: ["TILogging"]), // MARK: - Networking @@ -64,6 +65,7 @@ let package = Package( .target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils"), .target(name: "TIKeychainUtils", dependencies: ["TIFoundationUtils", "KeychainAccess"], path: "TIKeychainUtils/Sources"), .target(name: "TITableKitUtils", dependencies: ["TIUIElements", "TableKit"], path: "TITableKitUtils/Sources"), + .target(name: "TILogging", dependencies: [], path: "TILogging/Sources"), // MARK: - Networking From 31582b3bc5f927b16f700666e0d6fa1dc34d6087 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 27 Sep 2022 20:12:55 +0300 Subject: [PATCH 03/27] fix: errors --- TILogging/Sources/Logger.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TILogging/Sources/Logger.swift b/TILogging/Sources/Logger.swift index 5eb345c1..56aa4cbe 100644 --- a/TILogging/Sources/Logger.swift +++ b/TILogging/Sources/Logger.swift @@ -1,9 +1,11 @@ +import Foundation import os public struct TILogger: LoggerRepresentable { // MARK: - Properties + @available(iOS 12, *) public static let defaultLogger = TILogger(subsystem: .defaultSubsystem ?? "", category: .pointsOfInterest) public let logInfo: OSLog @@ -14,6 +16,7 @@ public struct TILogger: LoggerRepresentable { self.logInfo = .init(subsystem: subsystem, category: category) } + @available(iOS 12, *) public init(subsystem: String, category: OSLog.Category) { self.logInfo = .init(subsystem: subsystem, category: category) } From ff720fca0df305cd91bd99e39411d21c8b0ab7da Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Thu, 29 Sep 2022 15:44:27 +0300 Subject: [PATCH 04/27] feat: initial list view version --- TILogging/Sources/LogEntryTableViewCell.swift | 10 ++ TILogging/Sources/LogsListView.swift | 118 ++++++++++++++++++ TILogging/Sources/LogsStorageViewModel.swift | 80 ++++++++++++ TILogging/TILogging.podspec | 4 + 4 files changed, 212 insertions(+) create mode 100644 TILogging/Sources/LogEntryTableViewCell.swift create mode 100644 TILogging/Sources/LogsListView.swift create mode 100644 TILogging/Sources/LogsStorageViewModel.swift diff --git a/TILogging/Sources/LogEntryTableViewCell.swift b/TILogging/Sources/LogEntryTableViewCell.swift new file mode 100644 index 00000000..729c1f18 --- /dev/null +++ b/TILogging/Sources/LogEntryTableViewCell.swift @@ -0,0 +1,10 @@ +import TIUIKitCore +import TIUIElements +import OSLog + +@available(iOS 15, *) +open class LogEntryTableViewCell: BaseInitializableCell, ConfigurableView { + open func configure(with entry: OSLogEntryLog) { + textLabel?.text = entry.composedMessage + } +} diff --git a/TILogging/Sources/LogsListView.swift b/TILogging/Sources/LogsListView.swift new file mode 100644 index 00000000..c9833c5c --- /dev/null +++ b/TILogging/Sources/LogsListView.swift @@ -0,0 +1,118 @@ +import TIUIKitCore +import OSLog +import UIKit + +@available(iOS 15, *) +open class LogsListView: BaseInitializeableViewController, LogsListViewOutput { + + public enum TableSection: String, Hashable { + case main + } + + private let searchView = UITextField() + private let segmentView = UISegmentedControl() + private let tableView = UITableView() + + lazy private var dataSource = createDataSource() + + public typealias DataSource = UITableViewDiffableDataSource + public typealias Snapshot = NSDiffableDataSourceSnapshot + + public let viewModel = LogsStorageViewModel() + + // MARK: - Life cycle + + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + applySnapshot() + } + + open override func addViews() { + super.addViews() + + view.addSubview(searchView) + view.addSubview(segmentView) + view.addSubview(tableView) + } + + open override func configureLayout() { + super.configureLayout() + + [searchView, segmentView, tableView] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + searchView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), + searchView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + searchView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + searchView.heightAnchor.constraint(equalToConstant: 32), + + segmentView.topAnchor.constraint(equalTo: searchView.bottomAnchor), + segmentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + segmentView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + segmentView.heightAnchor.constraint(equalToConstant: 32), + + tableView.topAnchor.constraint(equalTo: segmentView.bottomAnchor, constant: 8), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + open override func bindViews() { + super.bindViews() + + viewModel.logsListView = self + tableView.register(LogEntryTableViewCell.self, forCellReuseIdentifier: "identifier") + } + + open override func configureAppearance() { + super.configureAppearance() + + configureSegmentView() + view.backgroundColor = .systemBackground + tableView.backgroundColor = .systemBackground + } + + // MARK: - LogsListViewOutput + + open func reloadTableView() { + applySnapshot() + } + + // MARK: - Open methods + + open func createDataSource() -> DataSource { + let cellProvider: DataSource.CellProvider = { collectionView, indexPath, itemIdentifier in + let cell = collectionView.dequeueReusableCell(withIdentifier: "identifier", + for: indexPath) as? LogEntryTableViewCell + + cell?.configure(with: itemIdentifier) + + return cell + } + + return .init(tableView: tableView, cellProvider: cellProvider) + } + + open func applySnapshot() { + var snapshot = Snapshot() + + snapshot.appendSections([TableSection.main.rawValue]) + snapshot.appendItems(viewModel.filteredLogs, toSection: TableSection.main.rawValue) + + dataSource.apply(snapshot, animatingDifferences: true) + } + + // MARK: - Private methods + + private func configureSegmentView() { + for (index, segment) in LogsStorageViewModel.LevelType.allCases.enumerated() { + let action = UIAction(title: segment.rawValue, handler: viewModel.actionHandler(for:)) + segmentView.insertSegment(action: action, at: index, animated: false) + } + + segmentView.selectedSegmentIndex = 0 + } +} diff --git a/TILogging/Sources/LogsStorageViewModel.swift b/TILogging/Sources/LogsStorageViewModel.swift new file mode 100644 index 00000000..9a586af0 --- /dev/null +++ b/TILogging/Sources/LogsStorageViewModel.swift @@ -0,0 +1,80 @@ +import TISwiftUtils +import UIKit +import OSLog + +public protocol LogsListViewOutput: AnyObject { + func reloadTableView() +} + +@available(iOS 15, *) +open class LogsStorageViewModel { + + public enum LevelType: String, CaseIterable { + case all + case `default` + case info + case debug + case error + case fault + } + + private var allLogs: [OSLogEntryLog] = [] { + didSet { + filterLogs() + } + } + + public var filteredLogs: [OSLogEntryLog] = [] { + didSet { + logsListView?.reloadTableView() + } + } + + weak public var logsListView: LogsListViewOutput? + + public init() { + loadLogs() + } + + open func loadLogs() { + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + let logStore = try? OSLogStore(scope: .currentProcessIdentifier) + let entries = try? logStore?.getEntries() + + let logs = entries? + .compactMap { $0 as? OSLogEntryLog } + + DispatchQueue.main.async { + self?.allLogs = logs ?? [] + } + } + } + + open func filterLogs(filter: Closure? = nil) { + filteredLogs = allLogs.filter { filter?($0) ?? true } + } + + open func actionHandler(for action: UIAction) { + guard let level = LevelType(rawValue: action.title) else { return } + + switch level { + case .all: + loadLogs() + + case .info: + filterLogs(filter: { $0.level == .info }) + + case .default: + filterLogs(filter: { $0.level == .notice }) + + case .debug: + filterLogs(filter: { $0.level == .debug }) + + case .error: + filterLogs(filter: { $0.level == .error }) + + case .fault: + filterLogs(filter: { $0.level == .fault }) + } + } +} diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index cbcd20ef..f5804527 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -12,4 +12,8 @@ Pod::Spec.new do |s| s.source_files = s.name + '/Sources/**/*' + s.dependency 'TIUIKitCore', s.version.to_s + s.dependency 'TISwiftUtils', s.version.to_s + s.dependency 'TIUIElements', s.version.to_s + end From e9edf3ab210cae444c6b76f091c9f191e3c929f9 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Sun, 2 Oct 2022 21:11:52 +0300 Subject: [PATCH 05/27] feat: added logger views --- Package.swift | 2 +- TILogging/Sources/LogEntryTableViewCell.swift | 10 - TILogging/Sources/{ => Logger}/Logger.swift | 0 .../{ => Logger}/LoggerRepresentable.swift | 0 TILogging/Sources/LoggingPresenter.swift | 51 ++++ TILogging/Sources/LogsListView.swift | 118 ------- TILogging/Sources/LogsStorageViewModel.swift | 80 ----- .../LoggerList/LogEntryTableViewCell.swift | 119 ++++++++ .../Views/LoggerList/LogsListView.swift | 289 ++++++++++++++++++ .../LoggingTogglingViewController.swift | 127 ++++++++ .../LoggerWindow/LoggingTogglingWindow.swift | 54 ++++ .../Sources/Views/Utilities/FileManager.swift | 63 ++++ .../ViewModels/LogsStorageViewModel.swift | 161 ++++++++++ .../Wrappers/ContainerTableViewCell.swift | 2 +- 14 files changed, 866 insertions(+), 210 deletions(-) delete mode 100644 TILogging/Sources/LogEntryTableViewCell.swift rename TILogging/Sources/{ => Logger}/Logger.swift (100%) rename TILogging/Sources/{ => Logger}/LoggerRepresentable.swift (100%) create mode 100644 TILogging/Sources/LoggingPresenter.swift delete mode 100644 TILogging/Sources/LogsListView.swift delete mode 100644 TILogging/Sources/LogsStorageViewModel.swift create mode 100644 TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift create mode 100644 TILogging/Sources/Views/LoggerList/LogsListView.swift create mode 100644 TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift create mode 100644 TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift create mode 100644 TILogging/Sources/Views/Utilities/FileManager.swift create mode 100644 TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift diff --git a/Package.swift b/Package.swift index 111a8ba5..e86d9dd7 100644 --- a/Package.swift +++ b/Package.swift @@ -65,7 +65,7 @@ let package = Package( .target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils"), .target(name: "TIKeychainUtils", dependencies: ["TIFoundationUtils", "KeychainAccess"], path: "TIKeychainUtils/Sources"), .target(name: "TITableKitUtils", dependencies: ["TIUIElements", "TableKit"], path: "TITableKitUtils/Sources"), - .target(name: "TILogging", dependencies: [], path: "TILogging/Sources"), + .target(name: "TILogging", dependencies: ["TIUIElements", "TISwiftUtils", "TIUIKitCore"], path: "TILogging/Sources"), // MARK: - Networking diff --git a/TILogging/Sources/LogEntryTableViewCell.swift b/TILogging/Sources/LogEntryTableViewCell.swift deleted file mode 100644 index 729c1f18..00000000 --- a/TILogging/Sources/LogEntryTableViewCell.swift +++ /dev/null @@ -1,10 +0,0 @@ -import TIUIKitCore -import TIUIElements -import OSLog - -@available(iOS 15, *) -open class LogEntryTableViewCell: BaseInitializableCell, ConfigurableView { - open func configure(with entry: OSLogEntryLog) { - textLabel?.text = entry.composedMessage - } -} diff --git a/TILogging/Sources/Logger.swift b/TILogging/Sources/Logger/Logger.swift similarity index 100% rename from TILogging/Sources/Logger.swift rename to TILogging/Sources/Logger/Logger.swift diff --git a/TILogging/Sources/LoggerRepresentable.swift b/TILogging/Sources/Logger/LoggerRepresentable.swift similarity index 100% rename from TILogging/Sources/LoggerRepresentable.swift rename to TILogging/Sources/Logger/LoggerRepresentable.swift diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift new file mode 100644 index 00000000..34a98f88 --- /dev/null +++ b/TILogging/Sources/LoggingPresenter.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +@available(iOS 15, *) +final public class LoggingPresenter { + + public static let shared = LoggingPresenter() + + private lazy var window: UIWindow = { + let window = LoggingTogglingWindow(frame: UIScreen.main.bounds) + window.rootViewController = loggingViewController + + return window + }() + + private lazy var loggingViewController: UIViewController = { + LoggingTogglingViewController() + }() + + private init() { } + + public func start(_ scene: UIWindowScene? = nil) { + window.windowScene = scene + window.isHidden = false + } + + public func stop() { + window.isHidden = true + } +} diff --git a/TILogging/Sources/LogsListView.swift b/TILogging/Sources/LogsListView.swift deleted file mode 100644 index c9833c5c..00000000 --- a/TILogging/Sources/LogsListView.swift +++ /dev/null @@ -1,118 +0,0 @@ -import TIUIKitCore -import OSLog -import UIKit - -@available(iOS 15, *) -open class LogsListView: BaseInitializeableViewController, LogsListViewOutput { - - public enum TableSection: String, Hashable { - case main - } - - private let searchView = UITextField() - private let segmentView = UISegmentedControl() - private let tableView = UITableView() - - lazy private var dataSource = createDataSource() - - public typealias DataSource = UITableViewDiffableDataSource - public typealias Snapshot = NSDiffableDataSourceSnapshot - - public let viewModel = LogsStorageViewModel() - - // MARK: - Life cycle - - open override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - applySnapshot() - } - - open override func addViews() { - super.addViews() - - view.addSubview(searchView) - view.addSubview(segmentView) - view.addSubview(tableView) - } - - open override func configureLayout() { - super.configureLayout() - - [searchView, segmentView, tableView] - .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } - - NSLayoutConstraint.activate([ - searchView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), - searchView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - searchView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - searchView.heightAnchor.constraint(equalToConstant: 32), - - segmentView.topAnchor.constraint(equalTo: searchView.bottomAnchor), - segmentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - segmentView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - segmentView.heightAnchor.constraint(equalToConstant: 32), - - tableView.topAnchor.constraint(equalTo: segmentView.bottomAnchor, constant: 8), - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - } - - open override func bindViews() { - super.bindViews() - - viewModel.logsListView = self - tableView.register(LogEntryTableViewCell.self, forCellReuseIdentifier: "identifier") - } - - open override func configureAppearance() { - super.configureAppearance() - - configureSegmentView() - view.backgroundColor = .systemBackground - tableView.backgroundColor = .systemBackground - } - - // MARK: - LogsListViewOutput - - open func reloadTableView() { - applySnapshot() - } - - // MARK: - Open methods - - open func createDataSource() -> DataSource { - let cellProvider: DataSource.CellProvider = { collectionView, indexPath, itemIdentifier in - let cell = collectionView.dequeueReusableCell(withIdentifier: "identifier", - for: indexPath) as? LogEntryTableViewCell - - cell?.configure(with: itemIdentifier) - - return cell - } - - return .init(tableView: tableView, cellProvider: cellProvider) - } - - open func applySnapshot() { - var snapshot = Snapshot() - - snapshot.appendSections([TableSection.main.rawValue]) - snapshot.appendItems(viewModel.filteredLogs, toSection: TableSection.main.rawValue) - - dataSource.apply(snapshot, animatingDifferences: true) - } - - // MARK: - Private methods - - private func configureSegmentView() { - for (index, segment) in LogsStorageViewModel.LevelType.allCases.enumerated() { - let action = UIAction(title: segment.rawValue, handler: viewModel.actionHandler(for:)) - segmentView.insertSegment(action: action, at: index, animated: false) - } - - segmentView.selectedSegmentIndex = 0 - } -} diff --git a/TILogging/Sources/LogsStorageViewModel.swift b/TILogging/Sources/LogsStorageViewModel.swift deleted file mode 100644 index 9a586af0..00000000 --- a/TILogging/Sources/LogsStorageViewModel.swift +++ /dev/null @@ -1,80 +0,0 @@ -import TISwiftUtils -import UIKit -import OSLog - -public protocol LogsListViewOutput: AnyObject { - func reloadTableView() -} - -@available(iOS 15, *) -open class LogsStorageViewModel { - - public enum LevelType: String, CaseIterable { - case all - case `default` - case info - case debug - case error - case fault - } - - private var allLogs: [OSLogEntryLog] = [] { - didSet { - filterLogs() - } - } - - public var filteredLogs: [OSLogEntryLog] = [] { - didSet { - logsListView?.reloadTableView() - } - } - - weak public var logsListView: LogsListViewOutput? - - public init() { - loadLogs() - } - - open func loadLogs() { - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - let logStore = try? OSLogStore(scope: .currentProcessIdentifier) - let entries = try? logStore?.getEntries() - - let logs = entries? - .compactMap { $0 as? OSLogEntryLog } - - DispatchQueue.main.async { - self?.allLogs = logs ?? [] - } - } - } - - open func filterLogs(filter: Closure? = nil) { - filteredLogs = allLogs.filter { filter?($0) ?? true } - } - - open func actionHandler(for action: UIAction) { - guard let level = LevelType(rawValue: action.title) else { return } - - switch level { - case .all: - loadLogs() - - case .info: - filterLogs(filter: { $0.level == .info }) - - case .default: - filterLogs(filter: { $0.level == .notice }) - - case .debug: - filterLogs(filter: { $0.level == .debug }) - - case .error: - filterLogs(filter: { $0.level == .error }) - - case .fault: - filterLogs(filter: { $0.level == .fault }) - } - } -} diff --git a/TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift b/TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift new file mode 100644 index 00000000..3be43074 --- /dev/null +++ b/TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift @@ -0,0 +1,119 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIUIKitCore +import TIUIElements +import UIKit +import OSLog + +open class LogCellView: BaseInitializableView { + + public let containerView = UIStackView() + public let timeLabel = UILabel() + public let processLabel = UILabel() + public let messageLabel = UILabel() + public let levelTypeView = UIView() + + // MARK: - Life cycle + + open override func addViews() { + super.addViews() + + containerView.addArrangedSubviews(timeLabel, processLabel) + addSubviews(containerView, + messageLabel, + levelTypeView) + } + + open override func configureLayout() { + super.configureLayout() + + [containerView, timeLabel, processLabel, messageLabel, levelTypeView] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + containerView.centerYAnchor.constraint(equalTo: centerYAnchor), + containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + containerView.topAnchor.constraint(equalTo: topAnchor, constant: 8), + containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), + + messageLabel.leadingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 8), + messageLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8), + messageLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), + messageLabel.trailingAnchor.constraint(equalTo: levelTypeView.leadingAnchor, constant: -8), + + levelTypeView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + levelTypeView.centerYAnchor.constraint(equalTo: centerYAnchor), + levelTypeView.heightAnchor.constraint(equalToConstant: 10), + levelTypeView.widthAnchor.constraint(equalToConstant: 10), + ]) + } + + open override func configureAppearance() { + super.configureAppearance() + + containerView.axis = .vertical + containerView.spacing = 4 + + timeLabel.font = .systemFont(ofSize: 10) + + processLabel.lineBreakMode = .byTruncatingTail + processLabel.font = .systemFont(ofSize: 12) + + messageLabel.numberOfLines = 0 + messageLabel.lineBreakMode = .byWordWrapping + messageLabel.font = .systemFont(ofSize: 12) + + levelTypeView.layer.cornerRadius = 3 + } +} + +@available(iOS 15, *) +open class LogEntryTableViewCell: ContainerTableViewCell, ConfigurableView { + + // MARK: - ConfigurableView + + open func configure(with entry: OSLogEntryLog) { + wrappedView.timeLabel.text = entry.date.formatted() + wrappedView.processLabel.text = entry.process + wrappedView.messageLabel.text = entry.composedMessage + + configureType(withLevel: entry.level) + } + + // MARK: - Open methods + + open func configureType(withLevel level: OSLogEntryLog.Level) { + switch level { + case .error: + wrappedView.levelTypeView.isHidden = false + wrappedView.levelTypeView.backgroundColor = .yellow + + case .fault: + wrappedView.levelTypeView.isHidden = false + wrappedView.levelTypeView.backgroundColor = .red + + default: + wrappedView.levelTypeView.isHidden = true + } + } +} diff --git a/TILogging/Sources/Views/LoggerList/LogsListView.swift b/TILogging/Sources/Views/LoggerList/LogsListView.swift new file mode 100644 index 00000000..303c5a4c --- /dev/null +++ b/TILogging/Sources/Views/LoggerList/LogsListView.swift @@ -0,0 +1,289 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIUIKitCore +import OSLog +import UIKit + +@available(iOS 15, *) +open class LogsListView: BaseInitializeableViewController, + LogsListViewOutput, + AlertPresentationContext, + UISearchBarDelegate, + UITextFieldDelegate { + + private var timer: Timer? + + public enum TableSection: String, Hashable { + case main + } + + public let activityView = UIActivityIndicatorView() + public let searchView = UISearchBar() + public let segmentView = UISegmentedControl() + public let refreshControl = UIRefreshControl() + public let shareButton = UIButton() + public let tableView = UITableView() + + lazy private var dataSource = createDataSource() + + public typealias DataSource = UITableViewDiffableDataSource + public typealias Snapshot = NSDiffableDataSourceSnapshot + + public let viewModel = LogsStorageViewModel() + + // MARK: - Life cycle + + open override func viewDidLoad() { + super.viewDidLoad() + + viewModel.loadLogs() + } + + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + applySnapshot() + } + + open override func addViews() { + super.addViews() + + tableView.addSubview(refreshControl) + + view.addSubviews(searchView, + segmentView, + shareButton, + tableView, + activityView) + } + + open override func configureLayout() { + super.configureLayout() + + [searchView, segmentView, shareButton, tableView, activityView] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + searchView.topAnchor.constraint(equalTo: view.topAnchor, constant: 16), + searchView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + searchView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + searchView.heightAnchor.constraint(equalToConstant: 32), + + segmentView.topAnchor.constraint(equalTo: searchView.bottomAnchor, constant: 8), + segmentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + segmentView.trailingAnchor.constraint(equalTo: shareButton.leadingAnchor, constant: -8), + segmentView.heightAnchor.constraint(equalToConstant: 32), + + shareButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + shareButton.centerYAnchor.constraint(equalTo: segmentView.centerYAnchor), + shareButton.heightAnchor.constraint(equalToConstant: 32), + shareButton.widthAnchor.constraint(equalToConstant: 32), + + tableView.topAnchor.constraint(equalTo: segmentView.bottomAnchor, constant: 8), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + activityView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor), + activityView.centerYAnchor.constraint(equalTo: tableView.centerYAnchor), + activityView.heightAnchor.constraint(equalToConstant: 32), + activityView.widthAnchor.constraint(equalToConstant: 32), + ]) + } + + open override func bindViews() { + super.bindViews() + + viewModel.logsListView = self + searchView.delegate = self + tableView.register(LogEntryTableViewCell.self, forCellReuseIdentifier: "identifier") + refreshControl.addTarget(self, action: #selector(reloadLogs), for: .valueChanged) + shareButton.addTarget(self, action: #selector(shareLogs), for: .touchUpInside) + } + + open override func configureAppearance() { + super.configureAppearance() + + configureSegmentView() + configureReloadButton() + configureSearchView() + + view.backgroundColor = .systemBackground + tableView.backgroundColor = .systemBackground + } + + // MARK: - LogsListViewOutput + + open func reloadTableView() { + applySnapshot() + } + + open func setLoadingState() { + view.subviews.forEach { $0.isUserInteractionEnabled = false } + startLoadingAnimation() + } + + open func setNormalState() { + view.subviews.forEach { $0.isUserInteractionEnabled = true } + stopLoadingAnimation() + } + + open func startSearch() { + [shareButton, segmentView, tableView] + .forEach { $0.isUserInteractionEnabled = false } + startLoadingAnimation() + } + + open func stopSearch() { + [shareButton, segmentView, tableView] + .forEach { $0.isUserInteractionEnabled = true } + stopLoadingAnimation() + } + + // MARK: - UISearchBarDelegate + UITextFieldDelegate + + open func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + guard !searchText.isEmpty else { + filterLogsByText() + return + } + + timer?.invalidate() + timer = Timer.scheduledTimer(timeInterval: 3, + target: self, + selector: #selector(filterLogsByText), + userInfo: nil, + repeats: false) + } + + open func searchBarResultsListButtonClicked(_ searchBar: UISearchBar) { + searchView.resignFirstResponder() + } + + open func textFieldDidEndEditing(_ textField: UITextField) { + viewModel.fileCreator = .init(fileName: textField.text ?? "", fileExtension: "log") + } + + // MARK: - Open methods + + open func createDataSource() -> DataSource { + let cellProvider: DataSource.CellProvider = { collectionView, indexPath, itemIdentifier in + let cell = collectionView.dequeueReusableCell(withIdentifier: "identifier", + for: indexPath) as? LogEntryTableViewCell + + cell?.configure(with: itemIdentifier) + + return cell + } + + return .init(tableView: tableView, cellProvider: cellProvider) + } + + open func applySnapshot() { + var snapshot = Snapshot() + + snapshot.appendSections([TableSection.main.rawValue]) + snapshot.appendItems(viewModel.filteredLogs, toSection: TableSection.main.rawValue) + + dataSource.apply(snapshot, animatingDifferences: true) + } + + open func startLoadingAnimation() { + activityView.isHidden = false + activityView.startAnimating() + } + + open func stopLoadingAnimation() { + activityView.isHidden = true + activityView.stopAnimating() + } + + // MARK: - Private methods + + private func configureSegmentView() { + for (index, segment) in LogsStorageViewModel.LevelType.allCases.enumerated() { + let action = UIAction(title: segment.rawValue, handler: viewModel.actionHandler(for:)) + segmentView.insertSegment(action: action, at: index, animated: false) + } + + segmentView.selectedSegmentIndex = 0 + } + + private func configureReloadButton() { + shareButton.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal) + } + + private func configureSearchView() { + searchView.placeholder = "Date, subcategory, message text, etc." + searchView.layer.backgroundColor = UIColor.systemBackground.cgColor + searchView.searchBarStyle = .minimal + } + + private func startSharingFlow() { + let alert = AlertFactory().alert(title: "Enter file name", actions: [ + .init(title: "Cancel", style: .cancel, action: nil), + .init(title: "Share", style: .default, action: { [weak self] in + self?.presentShareScreen() + }) + ]) + + alert.present(on: self, alertViewFactory: { configuration in + let alertController = UIAlertController(title: "", message: "", preferredStyle: .alert) + .configured(with: configuration) + + alertController.addTextField(configurationHandler: { textField in + textField.delegate = self + }) + + return alertController + }) + } + + private func presentShareScreen() { + guard let file = viewModel.getFileWithLogs() else { + AlertFactory().retryAlert(title: "Can't create a file with name: {\(viewModel.fileCreator?.fullFileName ?? "")}", + retryAction: { [weak self] in + self?.startSharingFlow() + }).present(on: self) + return + } + + let activityViewController = UIActivityViewController(activityItems: [file], + applicationActivities: nil) + present(activityViewController, animated: true, completion: nil) + } + + // MARK: - Actions + + @objc private func reloadLogs() { + viewModel.loadLogs(preCompletion: stopLoadingAnimation, postCompletion: refreshControl.endRefreshing) + } + + @objc private func shareLogs() { + startSharingFlow() + } + + @objc private func filterLogsByText() { + viewModel.filterLogs(byText: searchView.text ?? "") + } +} diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift new file mode 100644 index 00000000..f337709f --- /dev/null +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -0,0 +1,127 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIUIKitCore +import UIKit + +@available(iOS 15, *) +open class LoggingTogglingViewController: BaseInitializeableViewController { + + private var initialCenter = CGPoint() + + public lazy var button: UIButton = { + let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame + let button = UIButton(frame: .init(x: safeAreaFrame.minX, + y: safeAreaFrame.midY, + width: 70, + height: 32)) + + return button + }() + + // MARK: - Life cycle + + open override func addViews() { + super.addViews() + + view.addSubview(button) + } + + open override func bindViews() { + super.bindViews() + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:))) + + button.addGestureRecognizer(panGesture) + button.addTarget(self, action: #selector(openLoggingScreen), for: .touchUpInside) + } + + open override func configureAppearance() { + super.configureAppearance() + + view.backgroundColor = .clear + + button.setTitle("Logs", for: .normal) + button.setTitleColor(.black, for: .normal) + button.backgroundColor = .white + button.layer.cornerRadius = 10 + } + + // MARK: - Private methods + + private func clipButtonIfNeeded() { + let viewFrame = view.safeAreaLayoutGuide.layoutFrame + let buttonFrame = button.frame + + if buttonFrame.maxX > viewFrame.maxX { + button.frame = .init(x: viewFrame.maxX - buttonFrame.width, + y: buttonFrame.minY, + width: buttonFrame.width, + height: buttonFrame.height) + + } else if buttonFrame.minX < viewFrame.minX { + button.frame = .init(x: viewFrame.minX, + y: buttonFrame.minY, + width: buttonFrame.width, + height: buttonFrame.height) + } + + if buttonFrame.maxY > viewFrame.maxY { + button.frame = .init(x: button.frame.minX, + y: viewFrame.maxY - button.frame.height, + width: button.frame.width, + height: button.frame.height) + + } else if buttonFrame.minY < viewFrame.minY { + button.frame = .init(x: buttonFrame.minX, + y: viewFrame.minY, + width: buttonFrame.width, + height: buttonFrame.height) + } + } + + // MARK: - Actions + + @objc private func handlePanGesture(gesture: UIPanGestureRecognizer) { + let translation = gesture.translation(in: view) + + switch gesture.state { + case .began: + self.initialCenter = button.center + + case .changed, .ended: + let newCenter = CGPoint(x: initialCenter.x + translation.x, y: initialCenter.y + translation.y) + button.center = newCenter + clipButtonIfNeeded() + + case .cancelled: + button.center = initialCenter + + default: + break + } + } + + @objc private func openLoggingScreen() { + present(LogsListView(), animated: true) + } +} diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift new file mode 100644 index 00000000..15a82d82 --- /dev/null +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +@available(iOS 15, *) +class LoggingTogglingWindow: UIWindow { + + override init(frame: CGRect) { + super.init(frame: frame) + + windowLevel = .statusBar + backgroundColor = .clear + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + guard let rootView = rootViewController else { return false } + + if let loggingController = rootView.presentedViewController { + return loggingController.view.point(inside: point, with: event) + } + + if let button = rootView.view.subviews.first { + let buttonPoint = convert(point, to: button) + return button.point(inside: buttonPoint, with: event) + } + + return false + } +} diff --git a/TILogging/Sources/Views/Utilities/FileManager.swift b/TILogging/Sources/Views/Utilities/FileManager.swift new file mode 100644 index 00000000..d8977e3d --- /dev/null +++ b/TILogging/Sources/Views/Utilities/FileManager.swift @@ -0,0 +1,63 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public struct TIFileCreator { + + public var fileName: String + public var fileExtension: String + + public var fullFileName: String { + fileName + "." + fileExtension + } + + public init(fileName: String, fileExtension: String) { + self.fileName = fileName + self.fileExtension = fileExtension + } + + @discardableResult + public func createFile(withData data: Data) -> URL? { + guard var url = getDocumentsDirectory() else { + return nil + } + + url.appendPathComponent(fullFileName) + + do { + try data.write(to: url) + return url + + } catch { + return nil + } + + } + + public func getDocumentsDirectory() -> URL? { + try? FileManager.default.url(for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false) + } +} diff --git a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift new file mode 100644 index 00000000..d691d7b4 --- /dev/null +++ b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift @@ -0,0 +1,161 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TISwiftUtils +import UIKit +import OSLog + +public protocol LogsListViewOutput: AnyObject { + func reloadTableView() + func setLoadingState() + func setNormalState() + func startSearch() + func stopSearch() +} + +@available(iOS 15, *) +open class LogsStorageViewModel { + + public enum LevelType: String, CaseIterable { + case all + case `default` + case info + case debug + case error + case fault + } + + private var allLogs: [OSLogEntryLog] = [] { + didSet { + filterLogs() + } + } + + public var filteredLogs: [OSLogEntryLog] = [] { + didSet { + logsListView?.reloadTableView() + } + } + + public var fileCreator: TIFileCreator? + weak public var logsListView: LogsListViewOutput? + + public init() { } + + open func loadLogs(preCompletion: VoidClosure? = nil, postCompletion: VoidClosure? = nil) { + allLogs = [] + logsListView?.setLoadingState() + + preCompletion?() + + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + let logStore = try? OSLogStore(scope: .currentProcessIdentifier) + let entries = try? logStore?.getEntries() + + let logs = entries? + .reversed() + .compactMap { $0 as? OSLogEntryLog } + + DispatchQueue.main.async { + self?.allLogs = logs ?? [] + self?.logsListView?.setNormalState() + postCompletion?() + } + } + } + + open func filterLogs(filter: Closure? = nil) { + filteredLogs = allLogs.filter { filter?($0) ?? true } + } + + open func filterLogs(byText text: String) { + guard !text.isEmpty else { + filteredLogs = allLogs + return + } + + logsListView?.startSearch() + + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + let localFilteredLogs = self?.allLogs.filter { log in + let isDate = log.date.formatted().contains(text) + let isMessage = log.composedMessage.contains(text) + let isCategory = log.category.contains(text) + let isSubsystem = log.subsystem.contains(text) + let isProcess = log.process.contains(text) + + return isDate || isMessage || isCategory || isSubsystem || isProcess + } + + DispatchQueue.main.async { + self?.filteredLogs = localFilteredLogs ?? [] + self?.logsListView?.stopSearch() + } + } + } + + open func getFileWithLogs() -> URL? { + guard let data = encodeLogs() else { + return nil + } + + return fileCreator?.createFile(withData: data) + } + + func encodeLogs() -> Data? { + filteredLogs + .map { $0.getDescription() } + .joined(separator: "\n\n") + .data(using: .utf8) + } + + open func actionHandler(for action: UIAction) { + guard let level = LevelType(rawValue: action.title) else { return } + + switch level { + case .all: + filterLogs() + + case .info: + filterLogs(filter: { $0.level == .info }) + + case .default: + filterLogs(filter: { $0.level == .notice }) + + case .debug: + filterLogs(filter: { $0.level == .debug }) + + case .error: + filterLogs(filter: { $0.level == .error }) + + case .fault: + filterLogs(filter: { $0.level == .fault }) + } + } +} + +@available(iOS 15, *) +private extension OSLogEntryLog { + func getDescription() -> String { + return "[\(date)] - [\(category)]: \(composedMessage)" + } +} diff --git a/TIUIElements/Sources/Wrappers/ContainerTableViewCell.swift b/TIUIElements/Sources/Wrappers/ContainerTableViewCell.swift index f22aed9e..3892e327 100644 --- a/TIUIElements/Sources/Wrappers/ContainerTableViewCell.swift +++ b/TIUIElements/Sources/Wrappers/ContainerTableViewCell.swift @@ -41,7 +41,7 @@ open class ContainerTableViewCell: BaseInitializableCell, WrappedV override open func addViews() { super.addViews() - addSubview(wrappedView) + contentView.addSubview(wrappedView) } override open func configureLayout() { From f6f4d2f2144186f44d1db594f8b62a765465b405 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Sun, 2 Oct 2022 21:38:40 +0300 Subject: [PATCH 06/27] fix: added more visibility for logging presenter button --- .../Views/LoggerWindow/LoggingTogglingViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift index f337709f..4198e374 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -64,6 +64,8 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { button.setTitleColor(.black, for: .normal) button.backgroundColor = .white button.layer.cornerRadius = 10 + button.layer.borderWidth = 1 + button.layer.borderColor = UIColor.gray.cgColor } // MARK: - Private methods From 3af9cc5b35a9159b3b459e0711e7baf2a419067d Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Sun, 2 Oct 2022 22:28:13 +0300 Subject: [PATCH 07/27] refactor: remove blank space --- TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift index d691d7b4..f44ba491 100644 --- a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift +++ b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift @@ -71,7 +71,7 @@ open class LogsStorageViewModel { let logStore = try? OSLogStore(scope: .currentProcessIdentifier) let entries = try? logStore?.getEntries() - let logs = entries? + let logs = entries? .reversed() .compactMap { $0 as? OSLogEntryLog } From 2d9bb4a5d528cba09a9bd61cad61cfbb826c4a77 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Mon, 3 Oct 2022 08:03:25 +0300 Subject: [PATCH 08/27] refactor: added license and replace some helper objects --- TILogging/Sources/Logger/Logger.swift | 22 +++++++++++++++++++ .../Sources/Logger/LoggerRepresentable.swift | 22 +++++++++++++++++++ .../Sources/Helpers}/FileManager.swift | 0 3 files changed, 44 insertions(+) rename {TILogging/Sources/Views/Utilities => TISwiftUtils/Sources/Helpers}/FileManager.swift (100%) diff --git a/TILogging/Sources/Logger/Logger.swift b/TILogging/Sources/Logger/Logger.swift index 56aa4cbe..07507989 100644 --- a/TILogging/Sources/Logger/Logger.swift +++ b/TILogging/Sources/Logger/Logger.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + import Foundation import os diff --git a/TILogging/Sources/Logger/LoggerRepresentable.swift b/TILogging/Sources/Logger/LoggerRepresentable.swift index c5a95beb..5337fcfe 100644 --- a/TILogging/Sources/Logger/LoggerRepresentable.swift +++ b/TILogging/Sources/Logger/LoggerRepresentable.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + import os public protocol LoggerRepresentable { diff --git a/TILogging/Sources/Views/Utilities/FileManager.swift b/TISwiftUtils/Sources/Helpers/FileManager.swift similarity index 100% rename from TILogging/Sources/Views/Utilities/FileManager.swift rename to TISwiftUtils/Sources/Helpers/FileManager.swift From 54a01db21694927384b47653f7b69972df53aa3c Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 4 Oct 2022 11:02:05 +0300 Subject: [PATCH 09/27] feat: open logs on shacking motion + code review notes --- TILogging/Sources/LoggingPresenter.swift | 28 +++++- .../Views/LoggerList/LogEntryCellView.swift | 86 +++++++++++++++++++ .../LoggerList/LogEntryTableViewCell.swift | 64 +------------- .../LoggingTogglingViewController.swift | 45 +++++++++- .../{FileManager.swift => FileCreator.swift} | 0 5 files changed, 154 insertions(+), 69 deletions(-) create mode 100644 TILogging/Sources/Views/LoggerList/LogEntryCellView.swift rename TISwiftUtils/Sources/Helpers/{FileManager.swift => FileCreator.swift} (100%) diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift index 34a98f88..7f79b101 100644 --- a/TILogging/Sources/LoggingPresenter.swift +++ b/TILogging/Sources/LoggingPresenter.swift @@ -22,29 +22,51 @@ import UIKit +/// A presenter of a window on which list of logs can be opened. @available(iOS 15, *) final public class LoggingPresenter { public static let shared = LoggingPresenter() - private lazy var window: UIWindow = { + private lazy var window: LoggingTogglingWindow = { let window = LoggingTogglingWindow(frame: UIScreen.main.bounds) window.rootViewController = loggingViewController return window }() - private lazy var loggingViewController: UIViewController = { + private lazy var loggingViewController: LoggingTogglingViewController = { LoggingTogglingViewController() }() private init() { } + /// binds openning and closing of logging list view to a shacking motion. + public func bind(_ scene: UIWindowScene? = nil) { + if let scene = scene { + window.windowScene = scene + } + + window.makeKeyAndVisible() + loggingViewController.setVisible(isVisible: false) + } + + /// unbinds openning and closing of logging list view by shacking motion. + public func unbind() { + window.isHidden = true + loggingViewController.setVisible(isVisible: true) + } + + /// shows the UIWindow with a button that opens a logging list view. public func start(_ scene: UIWindowScene? = nil) { - window.windowScene = scene + if let scene = scene { + window.windowScene = scene + } + window.isHidden = false } + /// hides the UIWindow. public func stop() { window.isHidden = true } diff --git a/TILogging/Sources/Views/LoggerList/LogEntryCellView.swift b/TILogging/Sources/Views/LoggerList/LogEntryCellView.swift new file mode 100644 index 00000000..32e10e33 --- /dev/null +++ b/TILogging/Sources/Views/LoggerList/LogEntryCellView.swift @@ -0,0 +1,86 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIUIElements +import UIKit + +open class LogEntryCellView: BaseInitializableView { + + public let containerView = UIStackView() + public let timeLabel = UILabel() + public let processLabel = UILabel() + public let messageLabel = UILabel() + public let levelTypeView = UIView() + + // MARK: - Life cycle + + open override func addViews() { + super.addViews() + + containerView.addArrangedSubviews(timeLabel, processLabel) + addSubviews(containerView, + messageLabel, + levelTypeView) + } + + open override func configureLayout() { + super.configureLayout() + + [containerView, timeLabel, processLabel, messageLabel, levelTypeView] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + containerView.centerYAnchor.constraint(equalTo: centerYAnchor), + containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + containerView.topAnchor.constraint(equalTo: topAnchor, constant: 8), + containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), + + messageLabel.leadingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 8), + messageLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8), + messageLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), + messageLabel.trailingAnchor.constraint(equalTo: levelTypeView.leadingAnchor, constant: -8), + + levelTypeView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + levelTypeView.centerYAnchor.constraint(equalTo: centerYAnchor), + levelTypeView.heightAnchor.constraint(equalToConstant: 10), + levelTypeView.widthAnchor.constraint(equalToConstant: 10), + ]) + } + + open override func configureAppearance() { + super.configureAppearance() + + containerView.axis = .vertical + containerView.spacing = 4 + + timeLabel.font = .systemFont(ofSize: 10) + + processLabel.lineBreakMode = .byTruncatingTail + processLabel.font = .systemFont(ofSize: 12) + + messageLabel.numberOfLines = 0 + messageLabel.lineBreakMode = .byWordWrapping + messageLabel.font = .systemFont(ofSize: 12) + + levelTypeView.layer.cornerRadius = 3 + } +} diff --git a/TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift b/TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift index 3be43074..1e1fb78f 100644 --- a/TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift +++ b/TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift @@ -25,70 +25,8 @@ import TIUIElements import UIKit import OSLog -open class LogCellView: BaseInitializableView { - - public let containerView = UIStackView() - public let timeLabel = UILabel() - public let processLabel = UILabel() - public let messageLabel = UILabel() - public let levelTypeView = UIView() - - // MARK: - Life cycle - - open override func addViews() { - super.addViews() - - containerView.addArrangedSubviews(timeLabel, processLabel) - addSubviews(containerView, - messageLabel, - levelTypeView) - } - - open override func configureLayout() { - super.configureLayout() - - [containerView, timeLabel, processLabel, messageLabel, levelTypeView] - .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } - - NSLayoutConstraint.activate([ - containerView.centerYAnchor.constraint(equalTo: centerYAnchor), - containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), - containerView.topAnchor.constraint(equalTo: topAnchor, constant: 8), - containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), - - messageLabel.leadingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 8), - messageLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8), - messageLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), - messageLabel.trailingAnchor.constraint(equalTo: levelTypeView.leadingAnchor, constant: -8), - - levelTypeView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), - levelTypeView.centerYAnchor.constraint(equalTo: centerYAnchor), - levelTypeView.heightAnchor.constraint(equalToConstant: 10), - levelTypeView.widthAnchor.constraint(equalToConstant: 10), - ]) - } - - open override func configureAppearance() { - super.configureAppearance() - - containerView.axis = .vertical - containerView.spacing = 4 - - timeLabel.font = .systemFont(ofSize: 10) - - processLabel.lineBreakMode = .byTruncatingTail - processLabel.font = .systemFont(ofSize: 12) - - messageLabel.numberOfLines = 0 - messageLabel.lineBreakMode = .byWordWrapping - messageLabel.font = .systemFont(ofSize: 12) - - levelTypeView.layer.cornerRadius = 3 - } -} - @available(iOS 15, *) -open class LogEntryTableViewCell: ContainerTableViewCell, ConfigurableView { +open class LogEntryTableViewCell: ContainerTableViewCell, ConfigurableView { // MARK: - ConfigurableView diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift index 4198e374..7a68aad0 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -28,6 +28,18 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { private var initialCenter = CGPoint() + private var isRegisteredForShackingEvent: Bool { + !isVisibleState + } + + private(set) public var isLogsPresented = false + + private(set) public var isVisibleState = true { + didSet { + button.isHidden = !isVisibleState + } + } + public lazy var button: UIButton = { let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame let button = UIButton(frame: .init(x: safeAreaFrame.minX, @@ -38,6 +50,8 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { return button }() + open override var canBecomeFirstResponder: Bool { true } + // MARK: - Life cycle open override func addViews() { @@ -52,7 +66,10 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:))) button.addGestureRecognizer(panGesture) - button.addTarget(self, action: #selector(openLoggingScreen), for: .touchUpInside) + button.addTarget(self, action: #selector(openLoggingScreenAction), for: .touchUpInside) + + // Needed for catching shacking motion + becomeFirstResponder() } open override func configureAppearance() { @@ -68,6 +85,28 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { button.layer.borderColor = UIColor.gray.cgColor } + // MARK: - Overrided methods + + open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + if !isLogsPresented, isRegisteredForShackingEvent { + openLoggingScreen() + } + } + + // MARK: - Public methods + + public func setVisible(isVisible: Bool) { + isVisibleState = isVisible + } + + public func openLoggingScreen() { + present(LogsListView(), animated: true, completion: { [self] in + isLogsPresented = false + }) + + isLogsPresented = true + } + // MARK: - Private methods private func clipButtonIfNeeded() { @@ -123,7 +162,7 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { } } - @objc private func openLoggingScreen() { - present(LogsListView(), animated: true) + @objc private func openLoggingScreenAction() { + openLoggingScreen() } } diff --git a/TISwiftUtils/Sources/Helpers/FileManager.swift b/TISwiftUtils/Sources/Helpers/FileCreator.swift similarity index 100% rename from TISwiftUtils/Sources/Helpers/FileManager.swift rename to TISwiftUtils/Sources/Helpers/FileCreator.swift From f4dc72b61fb789dc0e96068717f7a2e52c60244e Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 4 Oct 2022 12:08:38 +0300 Subject: [PATCH 10/27] refactor: typos --- TILogging/Sources/LoggingPresenter.swift | 4 ++-- .../Views/LoggerWindow/LoggingTogglingViewController.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift index 7f79b101..11f85377 100644 --- a/TILogging/Sources/LoggingPresenter.swift +++ b/TILogging/Sources/LoggingPresenter.swift @@ -41,7 +41,7 @@ final public class LoggingPresenter { private init() { } - /// binds openning and closing of logging list view to a shacking motion. + /// binds openning and closing of logging list view to a shaking motion. public func bind(_ scene: UIWindowScene? = nil) { if let scene = scene { window.windowScene = scene @@ -51,7 +51,7 @@ final public class LoggingPresenter { loggingViewController.setVisible(isVisible: false) } - /// unbinds openning and closing of logging list view by shacking motion. + /// unbinds openning and closing of logging list view by shaking motion. public func unbind() { window.isHidden = true loggingViewController.setVisible(isVisible: true) diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift index 7a68aad0..c7f4e42f 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -28,7 +28,7 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { private var initialCenter = CGPoint() - private var isRegisteredForShackingEvent: Bool { + private var isRegisteredForShakingEvent: Bool { !isVisibleState } @@ -68,7 +68,7 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { button.addGestureRecognizer(panGesture) button.addTarget(self, action: #selector(openLoggingScreenAction), for: .touchUpInside) - // Needed for catching shacking motion + // Needed for catching shaking motion becomeFirstResponder() } @@ -88,7 +88,7 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { // MARK: - Overrided methods open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - if !isLogsPresented, isRegisteredForShackingEvent { + if motion == .motionShake, !isLogsPresented, isRegisteredForShakingEvent { openLoggingScreen() } } From 444d3b159d4157f06bb5f93b325ccbc67d13e352 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 4 Oct 2022 12:35:46 +0300 Subject: [PATCH 11/27] fix: code review notes --- .../Sources/Views/ViewModels}/FileCreator.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {TISwiftUtils/Sources/Helpers => TILogging/Sources/Views/ViewModels}/FileCreator.swift (100%) diff --git a/TISwiftUtils/Sources/Helpers/FileCreator.swift b/TILogging/Sources/Views/ViewModels/FileCreator.swift similarity index 100% rename from TISwiftUtils/Sources/Helpers/FileCreator.swift rename to TILogging/Sources/Views/ViewModels/FileCreator.swift From f230add1ed4dbdd03efdca75904f84cebddc0e9f Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 4 Oct 2022 12:41:32 +0300 Subject: [PATCH 12/27] refactor: naming changes --- TILogging/Sources/LoggingPresenter.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift index 11f85377..5a743238 100644 --- a/TILogging/Sources/LoggingPresenter.swift +++ b/TILogging/Sources/LoggingPresenter.swift @@ -42,7 +42,7 @@ final public class LoggingPresenter { private init() { } /// binds openning and closing of logging list view to a shaking motion. - public func bind(_ scene: UIWindowScene? = nil) { + public func displayOnShakeEvent(of scene: UIWindowScene? = nil) { if let scene = scene { window.windowScene = scene } @@ -51,14 +51,8 @@ final public class LoggingPresenter { loggingViewController.setVisible(isVisible: false) } - /// unbinds openning and closing of logging list view by shaking motion. - public func unbind() { - window.isHidden = true - loggingViewController.setVisible(isVisible: true) - } - /// shows the UIWindow with a button that opens a logging list view. - public func start(_ scene: UIWindowScene? = nil) { + public func addLogsButton(to scene: UIWindowScene? = nil) { if let scene = scene { window.windowScene = scene } @@ -67,7 +61,8 @@ final public class LoggingPresenter { } /// hides the UIWindow. - public func stop() { + public func hide() { window.isHidden = true + loggingViewController.setVisible(isVisible: true) } } From f60a443eed99df52c8fddc7d809cab1e95bd71a3 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 4 Oct 2022 15:32:42 +0300 Subject: [PATCH 13/27] fix: file creator prefix removed --- TILogging/Sources/Views/ViewModels/FileCreator.swift | 2 +- TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TILogging/Sources/Views/ViewModels/FileCreator.swift b/TILogging/Sources/Views/ViewModels/FileCreator.swift index d8977e3d..4faa30ec 100644 --- a/TILogging/Sources/Views/ViewModels/FileCreator.swift +++ b/TILogging/Sources/Views/ViewModels/FileCreator.swift @@ -22,7 +22,7 @@ import Foundation -public struct TIFileCreator { +public struct FileCreator { public var fileName: String public var fileExtension: String diff --git a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift index f44ba491..bda8398b 100644 --- a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift +++ b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift @@ -56,7 +56,7 @@ open class LogsStorageViewModel { } } - public var fileCreator: TIFileCreator? + public var fileCreator: FileCreator? weak public var logsListView: LogsListViewOutput? public init() { } From 8ffd2b589b3f4d3b210f5684fb826674b5cee7d3 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 8 Nov 2022 15:08:29 +0300 Subject: [PATCH 14/27] fix: check on shacking motion in toggling window --- TILogging/Sources/Logger/Logger.swift | 2 +- .../Views/LoggerWindow/LoggingTogglingWindow.swift | 12 ++++++++++++ .../Views/ViewModels/LogsStorageViewModel.swift | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/TILogging/Sources/Logger/Logger.swift b/TILogging/Sources/Logger/Logger.swift index 07507989..4b9e25cc 100644 --- a/TILogging/Sources/Logger/Logger.swift +++ b/TILogging/Sources/Logger/Logger.swift @@ -30,7 +30,7 @@ public struct TILogger: LoggerRepresentable { @available(iOS 12, *) public static let defaultLogger = TILogger(subsystem: .defaultSubsystem ?? "", category: .pointsOfInterest) - public let logInfo: OSLog + public let logInfo: NSLog // MARK: - Init diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift index 15a82d82..ff3b1995 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift @@ -37,6 +37,18 @@ class LoggingTogglingWindow: UIWindow { fatalError("init(coder:) has not been implemented") } + open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + guard let rootView = rootViewController else { return } + + guard let loggingController = rootView.presentedViewController as? LoggingTogglingViewController else { + return + } + + if motion == .motionShake { + loggingController.openLoggingScreen() + } + } + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { guard let rootView = rootViewController else { return false } diff --git a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift index bda8398b..5373c814 100644 --- a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift +++ b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift @@ -68,7 +68,7 @@ open class LogsStorageViewModel { preCompletion?() DispatchQueue.global(qos: .userInitiated).async { [weak self] in - let logStore = try? OSLogStore(scope: .currentProcessIdentifier) + let logStore = try? OSLogStore(scope: .system) let entries = try? logStore?.getEntries() let logs = entries? From 77abc2c5a550077729bf11cc9b0bbca00467e37f Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 8 Nov 2022 15:13:01 +0300 Subject: [PATCH 15/27] fix: typo --- TILogging/Sources/Logger/Logger.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TILogging/Sources/Logger/Logger.swift b/TILogging/Sources/Logger/Logger.swift index 4b9e25cc..07507989 100644 --- a/TILogging/Sources/Logger/Logger.swift +++ b/TILogging/Sources/Logger/Logger.swift @@ -30,7 +30,7 @@ public struct TILogger: LoggerRepresentable { @available(iOS 12, *) public static let defaultLogger = TILogger(subsystem: .defaultSubsystem ?? "", category: .pointsOfInterest) - public let logInfo: NSLog + public let logInfo: OSLog // MARK: - Init From 4ac0eace6658c554547e53e76d520d21fede1dbf Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 8 Nov 2022 15:15:48 +0300 Subject: [PATCH 16/27] fix: os log store scope --- TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift index 5373c814..bda8398b 100644 --- a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift +++ b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift @@ -68,7 +68,7 @@ open class LogsStorageViewModel { preCompletion?() DispatchQueue.global(qos: .userInitiated).async { [weak self] in - let logStore = try? OSLogStore(scope: .system) + let logStore = try? OSLogStore(scope: .currentProcessIdentifier) let entries = try? logStore?.getEntries() let logs = entries? From 69c2a857182e3f0a97e08af53a68c88171e40b53 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 8 Nov 2022 17:34:55 +0300 Subject: [PATCH 17/27] feat: change logic of registration on shaking motion --- TILogging/Sources/LoggingPresenter.swift | 28 +++++---- .../LoggingTogglingViewController.swift | 9 ++- .../LoggerWindow/LoggingTogglingWindow.swift | 57 ++++++++++++++----- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift index 5a743238..9427184c 100644 --- a/TILogging/Sources/LoggingPresenter.swift +++ b/TILogging/Sources/LoggingPresenter.swift @@ -41,26 +41,30 @@ final public class LoggingPresenter { private init() { } - /// binds openning and closing of logging list view to a shaking motion. + /// Binds openning and closing of logging list view to a shaking motion. public func displayOnShakeEvent(of scene: UIWindowScene? = nil) { + showWindow(withScene: scene) + loggingViewController.setVisible(isVisible: false) + loggingViewController.setRegistrationForShacking(isShackingEventAllowed: true) + } + + /// Shows the UIWindow with a button that opens a logging list view. + public func addLogsButton(to scene: UIWindowScene? = nil, isShakingMotionAllowed isShaking: Bool = true) { + showWindow(withScene: scene) + loggingViewController.setVisible(isVisible: true) + loggingViewController.setRegistrationForShacking(isShackingEventAllowed: isShaking) + } + + /// Shows the UIWindow + public func showWindow(withScene scene: UIWindowScene? = nil) { if let scene = scene { window.windowScene = scene } window.makeKeyAndVisible() - loggingViewController.setVisible(isVisible: false) } - /// shows the UIWindow with a button that opens a logging list view. - public func addLogsButton(to scene: UIWindowScene? = nil) { - if let scene = scene { - window.windowScene = scene - } - - window.isHidden = false - } - - /// hides the UIWindow. + /// Hides the UIWindow. public func hide() { window.isHidden = true loggingViewController.setVisible(isVisible: true) diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift index c7f4e42f..23b4b531 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -28,9 +28,7 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { private var initialCenter = CGPoint() - private var isRegisteredForShakingEvent: Bool { - !isVisibleState - } + private(set) public var isRegisteredForShakingEvent: Bool = false private(set) public var isLogsPresented = false @@ -95,6 +93,11 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { // MARK: - Public methods + public func setRegistrationForShacking(isShackingEventAllowed: Bool) { + isRegisteredForShakingEvent = isShackingEventAllowed + } + + /// Hides a button that opens logs list view controller. public func setVisible(isVisible: Bool) { isVisibleState = isVisible } diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift index ff3b1995..7c036bba 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift @@ -23,7 +23,7 @@ import UIKit @available(iOS 15, *) -class LoggingTogglingWindow: UIWindow { +final class LoggingTogglingWindow: UIWindow { override init(frame: CGRect) { super.init(frame: frame) @@ -37,18 +37,6 @@ class LoggingTogglingWindow: UIWindow { fatalError("init(coder:) has not been implemented") } - open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - guard let rootView = rootViewController else { return } - - guard let loggingController = rootView.presentedViewController as? LoggingTogglingViewController else { - return - } - - if motion == .motionShake { - loggingController.openLoggingScreen() - } - } - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { guard let rootView = rootViewController else { return false } @@ -64,3 +52,46 @@ class LoggingTogglingWindow: UIWindow { return false } } + +// MARK: - Registration for shaking event + +public extension UIWindow { + override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + if #available(iOS 15, *) { + guard let window = getLoggingWindow() else { + super.motionEnded(motion, with: event) + return + } + + checkForShakingMotion(motion, forWindow: window, with: event) + + } else { + super.motionEnded(motion, with: event) + } + } + + @available(iOS 15, *) + private func getLoggingWindow() -> LoggingTogglingWindow? { + guard let windows = windowScene?.windows else { + return nil + } + + return windows.compactMap { $0 as? LoggingTogglingWindow }.first + } + + @available(iOS 15, *) + private func checkForShakingMotion(_ motion: UIEvent.EventSubtype, + forWindow window: LoggingTogglingWindow, + with event: UIEvent?) { + guard let loggingController = window.rootViewController as? LoggingTogglingViewController else { + super.motionEnded(motion, with: event) + return + } + + if motion == .motionShake, loggingController.isRegisteredForShakingEvent { + loggingController.openLoggingScreen() + } else { + super.motionEnded(motion, with: event) + } + } +} From 13cf92c9c1a0932b6840f6842b4445362d2c387a Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 8 Nov 2022 17:47:01 +0300 Subject: [PATCH 18/27] refactor: change podspec version + changelog updated --- CHANGELOG.md | 5 +++++ LeadKit.podspec | 2 +- Package.swift | 2 +- TIAppleMapUtils/TIAppleMapUtils.podspec | 2 +- TIAuth/TIAuth.podspec | 2 +- TIEcommerce/TIEcommerce.podspec | 2 +- TIFoundationUtils/TIFoundationUtils.podspec | 2 +- TIGoogleMapUtils/TIGoogleMapUtils.podspec | 2 +- TIKeychainUtils/TIKeychainUtils.podspec | 2 +- TILogging/TILogging.podspec | 2 +- TIMapUtils/TIMapUtils.podspec | 2 +- TIMoyaNetworking/TIMoyaNetworking.podspec | 2 +- TINetworking/TINetworking.podspec | 2 +- TINetworkingCache/TINetworkingCache.podspec | 2 +- TIPagination/TIPagination.podspec | 2 +- TISwiftUICore/TISwiftUICore.podspec | 2 +- TISwiftUtils/TISwiftUtils.podspec | 2 +- TITableKitUtils/TITableKitUtils.podspec | 2 +- TITransitions/TITransitions.podspec | 2 +- TIUIElements/TIUIElements.podspec | 2 +- TIUIKitCore/TIUIKitCore.podspec | 2 +- TIYandexMapUtils/TIYandexMapUtils.podspec | 2 +- 22 files changed, 26 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06dab034..3b5f57c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### 1.28.0 + +- **Add**: `LoggingPresenter`to present list of logs with ability of sharing it +- **Add**: `TILogger` wrapper object to log events. + ### 1.27.0 - **Add**: Tag like filter collection view diff --git a/LeadKit.podspec b/LeadKit.podspec index 28b0aa87..369df172 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "LeadKit" - s.version = "1.27.0" + s.version = "1.28.0" s.summary = "iOS framework with a bunch of tools for rapid development" s.homepage = "https://github.com/TouchInstinct/LeadKit" s.license = "Apache License, Version 2.0" diff --git a/Package.swift b/Package.swift index e86d9dd7..4816a41b 100644 --- a/Package.swift +++ b/Package.swift @@ -86,7 +86,7 @@ let package = Package( .target(name: "TIAuth", dependencies: ["TIFoundationUtils"], path: "TIAuth/Sources"), //MARK: - Skolkovo - .target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking"], path: "TIEcommerce/Sources"), + .target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking", "TIUIKitCore", "TIUIElements"], path: "TIEcommerce/Sources"), // MARK: - Tests diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index f857ab3f..a88c29d5 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index f68d6ff7..edcb4345 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Login, registration, confirmation and other related actions' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index c09c9c2c..f5a239d0 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Cart, products, promocodes, bonuses and other related actions' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 035a2433..5acdb115 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index 752ffe82..c0c47b8a 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 8e778bb4..52436b16 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index f5804527..5ddb789e 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TILogging' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Logging API' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index 67345fd0..e6fad592 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Set of helpers for map objects clustering and interacting.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index e0a634e1..29255b7d 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index 20132c74..bf319b5d 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 8dc517e2..ae2c4a47 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index ac599fa7..03da19a7 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Generic pagination component.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index 64e3a7a0..0d0af9e3 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Core UI elements: protocols, views and helpers..' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index a428cafe..bc8fcac9 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index 67120a39..159dd3ab 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITransitions/TITransitions.podspec b/TITransitions/TITransitions.podspec index c580421c..b1b7dc29 100644 --- a/TITransitions/TITransitions.podspec +++ b/TITransitions/TITransitions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITransitions' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Set of custom transitions to present controller. ' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 1d7ed3ee..a724f255 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index dfa3cc61..e713c4ce 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index f11d9032..1efed4d0 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.27.0' + s.version = '1.28.0' s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } From d0576acd95e2d742cb76deec069b11ae7bfba0be Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 8 Nov 2022 18:06:32 +0300 Subject: [PATCH 19/27] fix: chacking for opend logging list view controller --- .../Sources/Views/LoggerWindow/LoggingTogglingWindow.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift index 7c036bba..7c8fea1e 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift @@ -88,7 +88,9 @@ public extension UIWindow { return } - if motion == .motionShake, loggingController.isRegisteredForShakingEvent { + if motion == .motionShake, + logginController.isLogsPresented, + loggingController.isRegisteredForShakingEvent { loggingController.openLoggingScreen() } else { super.motionEnded(motion, with: event) From e1c95960102fb8077a32f9f8833951322ea028cf Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 8 Nov 2022 18:11:39 +0300 Subject: [PATCH 20/27] fix: default value for presenting method --- TILogging/Sources/LoggingPresenter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift index 9427184c..03ccce6c 100644 --- a/TILogging/Sources/LoggingPresenter.swift +++ b/TILogging/Sources/LoggingPresenter.swift @@ -49,7 +49,7 @@ final public class LoggingPresenter { } /// Shows the UIWindow with a button that opens a logging list view. - public func addLogsButton(to scene: UIWindowScene? = nil, isShakingMotionAllowed isShaking: Bool = true) { + public func addLogsButton(to scene: UIWindowScene? = nil, isShakingMotionAllowed isShaking: Bool = false) { showWindow(withScene: scene) loggingViewController.setVisible(isVisible: true) loggingViewController.setRegistrationForShacking(isShackingEventAllowed: isShaking) From f8b79342040a8dce729e6fbc9cb6d7439376adf7 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Wed, 9 Nov 2022 21:14:43 +0300 Subject: [PATCH 21/27] fix: code review notes + change gcd on async/await --- TILogging/Sources/LoggingPresenter.swift | 20 +++---- .../Views/LoggerList/LogsListView.swift | 15 ++++- .../LoggingTogglingViewController.swift | 28 +++++----- .../LoggerWindow/LoggingTogglingWindow.swift | 16 +++--- .../Views/ViewModels/FileCreator.swift | 24 ++++---- .../Helpers/DefaultLogsListManipulator.swift | 47 ++++++++++++++++ .../Helpers/LogsListManipulatorProtocol.swift | 29 ++++++++++ .../ViewModels/LogsStorageViewModel.swift | 56 +++++++------------ TILogging/TILogging.podspec | 3 +- 9 files changed, 158 insertions(+), 80 deletions(-) create mode 100644 TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift create mode 100644 TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift index 03ccce6c..fbae8c71 100644 --- a/TILogging/Sources/LoggingPresenter.swift +++ b/TILogging/Sources/LoggingPresenter.swift @@ -28,31 +28,29 @@ final public class LoggingPresenter { public static let shared = LoggingPresenter() + private let loggingViewController = LoggingTogglingViewController() + private lazy var window: LoggingTogglingWindow = { - let window = LoggingTogglingWindow(frame: UIScreen.main.bounds) - window.rootViewController = loggingViewController + let window = LoggingTogglingWindow(frame: UIScreen.main.bounds, + loggingController: loggingViewController) return window }() - private lazy var loggingViewController: LoggingTogglingViewController = { - LoggingTogglingViewController() - }() - private init() { } /// Binds openning and closing of logging list view to a shaking motion. public func displayOnShakeEvent(of scene: UIWindowScene? = nil) { showWindow(withScene: scene) - loggingViewController.setVisible(isVisible: false) - loggingViewController.setRegistrationForShacking(isShackingEventAllowed: true) + loggingViewController.set(isVisible: false) + loggingViewController.set(isRegisteredForShakingEvent: true) } /// Shows the UIWindow with a button that opens a logging list view. public func addLogsButton(to scene: UIWindowScene? = nil, isShakingMotionAllowed isShaking: Bool = false) { showWindow(withScene: scene) - loggingViewController.setVisible(isVisible: true) - loggingViewController.setRegistrationForShacking(isShackingEventAllowed: isShaking) + loggingViewController.set(isVisible: true) + loggingViewController.set(isRegisteredForShakingEvent: isShaking) } /// Shows the UIWindow @@ -67,6 +65,6 @@ final public class LoggingPresenter { /// Hides the UIWindow. public func hide() { window.isHidden = true - loggingViewController.setVisible(isVisible: true) + loggingViewController.set(isVisible: true) } } diff --git a/TILogging/Sources/Views/LoggerList/LogsListView.swift b/TILogging/Sources/Views/LoggerList/LogsListView.swift index 303c5a4c..84716b42 100644 --- a/TILogging/Sources/Views/LoggerList/LogsListView.swift +++ b/TILogging/Sources/Views/LoggerList/LogsListView.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. // +import TISwiftUtils import TIUIKitCore import OSLog import UIKit @@ -56,7 +57,7 @@ open class LogsListView: BaseInitializeableViewController, open override func viewDidLoad() { super.viewDidLoad() - viewModel.loadLogs() + loadLogs() } open override func viewWillAppear(_ animated: Bool) { @@ -273,10 +274,16 @@ open class LogsListView: BaseInitializeableViewController, present(activityViewController, animated: true, completion: nil) } + private func loadLogs(preCompletion: VoidClosure? = nil, postCompletion: VoidClosure? = nil) { + Task { + await viewModel.loadLogs(preCompletion: preCompletion, postCompletion: postCompletion) + } + } + // MARK: - Actions @objc private func reloadLogs() { - viewModel.loadLogs(preCompletion: stopLoadingAnimation, postCompletion: refreshControl.endRefreshing) + loadLogs(preCompletion: stopLoadingAnimation, postCompletion: refreshControl.endRefreshing) } @objc private func shareLogs() { @@ -284,6 +291,8 @@ open class LogsListView: BaseInitializeableViewController, } @objc private func filterLogsByText() { - viewModel.filterLogs(byText: searchView.text ?? "") + Task { + await viewModel.filterLogs(byText: searchView.text ?? "") + } } } diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift index 23b4b531..27c128c8 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -28,7 +28,7 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { private var initialCenter = CGPoint() - private(set) public var isRegisteredForShakingEvent: Bool = false + private(set) public var isRegisteredForShakingEvent = false private(set) public var isLogsPresented = false @@ -38,15 +38,7 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { } } - public lazy var button: UIButton = { - let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame - let button = UIButton(frame: .init(x: safeAreaFrame.minX, - y: safeAreaFrame.midY, - width: 70, - height: 32)) - - return button - }() + public let button = UIButton() open override var canBecomeFirstResponder: Bool { true } @@ -58,6 +50,16 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { view.addSubview(button) } + open override func configureLayout() { + super.configureLayout() + + let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame + button.frame = .init(x: safeAreaFrame.minX, + y: safeAreaFrame.midY, + width: 70, + height: 32) + } + open override func bindViews() { super.bindViews() @@ -93,12 +95,12 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { // MARK: - Public methods - public func setRegistrationForShacking(isShackingEventAllowed: Bool) { - isRegisteredForShakingEvent = isShackingEventAllowed + public func set(isRegisteredForShakingEvent: Bool) { + self.isRegisteredForShakingEvent = isRegisteredForShakingEvent } /// Hides a button that opens logs list view controller. - public func setVisible(isVisible: Bool) { + public func set(isVisible: Bool) { isVisibleState = isVisible } diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift index 7c8fea1e..b5d8422a 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift @@ -25,9 +25,14 @@ import UIKit @available(iOS 15, *) final class LoggingTogglingWindow: UIWindow { - override init(frame: CGRect) { + let loggingController: LoggingTogglingViewController + + init(frame: CGRect, loggingController: LoggingTogglingViewController) { + self.loggingController = loggingController + super.init(frame: frame) + rootViewController = loggingController windowLevel = .statusBar backgroundColor = .clear } @@ -83,13 +88,10 @@ public extension UIWindow { private func checkForShakingMotion(_ motion: UIEvent.EventSubtype, forWindow window: LoggingTogglingWindow, with event: UIEvent?) { - guard let loggingController = window.rootViewController as? LoggingTogglingViewController else { - super.motionEnded(motion, with: event) - return - } + let loggingController = window.loggingController - if motion == .motionShake, - logginController.isLogsPresented, + if motion == .motionShake, + loggingController.isLogsPresented, loggingController.isRegisteredForShakingEvent { loggingController.openLoggingScreen() } else { diff --git a/TILogging/Sources/Views/ViewModels/FileCreator.swift b/TILogging/Sources/Views/ViewModels/FileCreator.swift index 4faa30ec..b785dd5a 100644 --- a/TILogging/Sources/Views/ViewModels/FileCreator.swift +++ b/TILogging/Sources/Views/ViewModels/FileCreator.swift @@ -37,27 +37,31 @@ public struct FileCreator { } @discardableResult - public func createFile(withData data: Data) -> URL? { - guard var url = getDocumentsDirectory() else { - return nil + public func createFile(withData data: Data) -> Result { + let result = getDocumentsDirectory() + + guard case var .success(url) = result else { + return result } url.appendPathComponent(fullFileName) do { try data.write(to: url) - return url + return .success(url) } catch { - return nil + return .failure(error) } } - public func getDocumentsDirectory() -> URL? { - try? FileManager.default.url(for: .documentDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: false) + public func getDocumentsDirectory() -> Result { + Result { + try FileManager.default.url(for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false) + } } } diff --git a/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift b/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift new file mode 100644 index 00000000..820367ac --- /dev/null +++ b/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift @@ -0,0 +1,47 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import OSLog + +@available(iOS 15, *) +public actor DefaultLogsListManipulator: LogsListManipulatorProtocol { + public func fetchLogs() async -> [OSLogEntryLog]? { + let logStore = try? OSLogStore(scope: .currentProcessIdentifier) + let entries = try? logStore?.getEntries() + + return entries? + .reversed() + .compactMap { $0 as? OSLogEntryLog } + } + + public func filter(_ logs: [OSLogEntryLog], byText text: String) async -> [OSLogEntryLog] { + logs.filter { log in + let isDate = log.date.formatted().contains(text) + let isMessage = log.composedMessage.contains(text) + let isCategory = log.category.contains(text) + let isSubsystem = log.subsystem.contains(text) + let isProcess = log.process.contains(text) + + return isDate || isMessage || isCategory || isSubsystem || isProcess + } + } +} diff --git a/TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift b/TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift new file mode 100644 index 00000000..dc11339d --- /dev/null +++ b/TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import OSLog + +@available(iOS 15, *) +public protocol LogsListManipulatorProtocol { + func fetchLogs() async -> [OSLogEntryLog]? + func filter(_ logs: [OSLogEntryLog], byText text: String) async -> [OSLogEntryLog] +} diff --git a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift index bda8398b..960a5a2e 100644 --- a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift +++ b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift @@ -44,6 +44,8 @@ open class LogsStorageViewModel { case fault } + private let logsManipulator: LogsListManipulatorProtocol + private var allLogs: [OSLogEntryLog] = [] { didSet { filterLogs() @@ -59,35 +61,27 @@ open class LogsStorageViewModel { public var fileCreator: FileCreator? weak public var logsListView: LogsListViewOutput? - public init() { } + public init(logsManipulator: LogsListManipulatorProtocol? = nil) { + self.logsManipulator = logsManipulator ?? DefaultLogsListManipulator() + } - open func loadLogs(preCompletion: VoidClosure? = nil, postCompletion: VoidClosure? = nil) { + @MainActor + open func loadLogs(preCompletion: VoidClosure? = nil, postCompletion: VoidClosure? = nil) async { allLogs = [] logsListView?.setLoadingState() - preCompletion?() - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - let logStore = try? OSLogStore(scope: .currentProcessIdentifier) - let entries = try? logStore?.getEntries() - - let logs = entries? - .reversed() - .compactMap { $0 as? OSLogEntryLog } - - DispatchQueue.main.async { - self?.allLogs = logs ?? [] - self?.logsListView?.setNormalState() - postCompletion?() - } - } + allLogs = await logsManipulator.fetchLogs() ?? [] + logsListView?.setNormalState() + postCompletion?() } open func filterLogs(filter: Closure? = nil) { filteredLogs = allLogs.filter { filter?($0) ?? true } } - open func filterLogs(byText text: String) { + @MainActor + open func filterLogs(byText text: String) async { guard !text.isEmpty else { filteredLogs = allLogs return @@ -95,22 +89,10 @@ open class LogsStorageViewModel { logsListView?.startSearch() - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - let localFilteredLogs = self?.allLogs.filter { log in - let isDate = log.date.formatted().contains(text) - let isMessage = log.composedMessage.contains(text) - let isCategory = log.category.contains(text) - let isSubsystem = log.subsystem.contains(text) - let isProcess = log.process.contains(text) + let localFilteredLogs = await logsManipulator.filter(allLogs, byText: text) - return isDate || isMessage || isCategory || isSubsystem || isProcess - } - - DispatchQueue.main.async { - self?.filteredLogs = localFilteredLogs ?? [] - self?.logsListView?.stopSearch() - } - } + filteredLogs = localFilteredLogs + logsListView?.stopSearch() } open func getFileWithLogs() -> URL? { @@ -118,10 +100,14 @@ open class LogsStorageViewModel { return nil } - return fileCreator?.createFile(withData: data) + if case let .success(url) = fileCreator?.createFile(withData: data) { + return url + } + + return nil } - func encodeLogs() -> Data? { + public func encodeLogs() -> Data? { filteredLogs .map { $0.getDescription() } .joined(separator: "\n\n") diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index 5ddb789e..c82ee99e 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -4,7 +4,8 @@ Pod::Spec.new do |s| s.summary = 'Logging API' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } - s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru', + 'castlele' => 'nikita.semenov@touchin.ru' } s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '10.0' From dffb4c6015c2a717fc827b05d11b3c574b7b2da0 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Thu, 10 Nov 2022 17:46:25 +0300 Subject: [PATCH 22/27] fix: code review notes --- .../LoggingTogglingViewController.swift | 31 +++++++++---------- .../Helpers/DefaultLogsListManipulator.swift | 18 ++++++++--- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift index 27c128c8..f0b7c549 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -117,32 +117,31 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { private func clipButtonIfNeeded() { let viewFrame = view.safeAreaLayoutGuide.layoutFrame let buttonFrame = button.frame + var x: CGFloat = buttonFrame.minX + var y: CGFloat = buttonFrame.minY if buttonFrame.maxX > viewFrame.maxX { - button.frame = .init(x: viewFrame.maxX - buttonFrame.width, - y: buttonFrame.minY, - width: buttonFrame.width, - height: buttonFrame.height) + x = viewFrame.maxX - buttonFrame.width + y = buttonFrame.minY } else if buttonFrame.minX < viewFrame.minX { - button.frame = .init(x: viewFrame.minX, - y: buttonFrame.minY, - width: buttonFrame.width, - height: buttonFrame.height) + x = viewFrame.minX + y = buttonFrame.minY } if buttonFrame.maxY > viewFrame.maxY { - button.frame = .init(x: button.frame.minX, - y: viewFrame.maxY - button.frame.height, - width: button.frame.width, - height: button.frame.height) + x = buttonFrame.minX + y = viewFrame.maxY - buttonFrame.height } else if buttonFrame.minY < viewFrame.minY { - button.frame = .init(x: buttonFrame.minX, - y: viewFrame.minY, - width: buttonFrame.width, - height: buttonFrame.height) + x = buttonFrame.minX + y = viewFrame.minY } + + button.frame = .init(x: x, + y: y, + width: buttonFrame.width, + height: buttonFrame.height) } // MARK: - Actions diff --git a/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift b/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift index 820367ac..26e1d842 100644 --- a/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift +++ b/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift @@ -25,12 +25,20 @@ import OSLog @available(iOS 15, *) public actor DefaultLogsListManipulator: LogsListManipulatorProtocol { public func fetchLogs() async -> [OSLogEntryLog]? { - let logStore = try? OSLogStore(scope: .currentProcessIdentifier) - let entries = try? logStore?.getEntries() + let logsResult = Result { try OSLogStore(scope: .currentProcessIdentifier) } + .flatMap { logStore in + Result { + try logStore + .getEntries() + .reversed() + .compactMap { $0 as? OSLogEntryLog } + } + } + if case let .success(logs) = logsResult { + return logs + } - return entries? - .reversed() - .compactMap { $0 as? OSLogEntryLog } + return nil } public func filter(_ logs: [OSLogEntryLog], byText text: String) async -> [OSLogEntryLog] { From 5159ee5a4d878023c495ab035aa4c37920c3bf91 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Fri, 11 Nov 2022 09:07:25 +0300 Subject: [PATCH 23/27] fix: code review notes --- .../LoggerWindow/LoggingTogglingViewController.swift | 4 ++-- .../ViewModels/Helpers/DefaultLogsListManipulator.swift | 9 ++------- .../ViewModels/Helpers/LogsListManipulatorProtocol.swift | 2 +- .../Sources/Views/ViewModels/LogsStorageViewModel.swift | 5 ++++- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift index f0b7c549..baaa75eb 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -117,8 +117,8 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { private func clipButtonIfNeeded() { let viewFrame = view.safeAreaLayoutGuide.layoutFrame let buttonFrame = button.frame - var x: CGFloat = buttonFrame.minX - var y: CGFloat = buttonFrame.minY + var x = buttonFrame.minX + var y = buttonFrame.minY if buttonFrame.maxX > viewFrame.maxX { x = viewFrame.maxX - buttonFrame.width diff --git a/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift b/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift index 26e1d842..07246611 100644 --- a/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift +++ b/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift @@ -24,8 +24,8 @@ import OSLog @available(iOS 15, *) public actor DefaultLogsListManipulator: LogsListManipulatorProtocol { - public func fetchLogs() async -> [OSLogEntryLog]? { - let logsResult = Result { try OSLogStore(scope: .currentProcessIdentifier) } + public func fetchLogs() async -> Result<[OSLogEntryLog], Error> { + Result { try OSLogStore(scope: .currentProcessIdentifier) } .flatMap { logStore in Result { try logStore @@ -34,11 +34,6 @@ public actor DefaultLogsListManipulator: LogsListManipulatorProtocol { .compactMap { $0 as? OSLogEntryLog } } } - if case let .success(logs) = logsResult { - return logs - } - - return nil } public func filter(_ logs: [OSLogEntryLog], byText text: String) async -> [OSLogEntryLog] { diff --git a/TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift b/TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift index dc11339d..2e229afd 100644 --- a/TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift +++ b/TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift @@ -24,6 +24,6 @@ import OSLog @available(iOS 15, *) public protocol LogsListManipulatorProtocol { - func fetchLogs() async -> [OSLogEntryLog]? + func fetchLogs() async -> Result<[OSLogEntryLog], Error> func filter(_ logs: [OSLogEntryLog], byText text: String) async -> [OSLogEntryLog] } diff --git a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift index 960a5a2e..27d53afe 100644 --- a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift +++ b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift @@ -71,7 +71,10 @@ open class LogsStorageViewModel { logsListView?.setLoadingState() preCompletion?() - allLogs = await logsManipulator.fetchLogs() ?? [] + if case let .success(logs) = await logsManipulator.fetchLogs() { + allLogs = logs + } + logsListView?.setNormalState() postCompletion?() } From 401d9365d0de9448408115e71bc22c538afac93a Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Thu, 17 Nov 2022 10:30:11 +0300 Subject: [PATCH 24/27] fix: code review notes --- TILogging/Sources/LoggingPresenter.swift | 2 +- ...iew.swift => LogsListViewController.swift} | 2 +- .../LoggingTogglingViewController.swift | 2 +- .../Views/ViewModels/LogsListViewOutput.swift | 29 +++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) rename TILogging/Sources/Views/LoggerList/{LogsListView.swift => LogsListViewController.swift} (99%) create mode 100644 TILogging/Sources/Views/ViewModels/LogsListViewOutput.swift diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift index fbae8c71..a173099f 100644 --- a/TILogging/Sources/LoggingPresenter.swift +++ b/TILogging/Sources/LoggingPresenter.swift @@ -63,7 +63,7 @@ final public class LoggingPresenter { } /// Hides the UIWindow. - public func hide() { + public func hideWindow() { window.isHidden = true loggingViewController.set(isVisible: true) } diff --git a/TILogging/Sources/Views/LoggerList/LogsListView.swift b/TILogging/Sources/Views/LoggerList/LogsListViewController.swift similarity index 99% rename from TILogging/Sources/Views/LoggerList/LogsListView.swift rename to TILogging/Sources/Views/LoggerList/LogsListViewController.swift index 84716b42..3ed827b9 100644 --- a/TILogging/Sources/Views/LoggerList/LogsListView.swift +++ b/TILogging/Sources/Views/LoggerList/LogsListViewController.swift @@ -26,7 +26,7 @@ import OSLog import UIKit @available(iOS 15, *) -open class LogsListView: BaseInitializeableViewController, +open class LogsListViewController: BaseInitializeableViewController, LogsListViewOutput, AlertPresentationContext, UISearchBarDelegate, diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift index baaa75eb..b3611150 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -105,7 +105,7 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { } public func openLoggingScreen() { - present(LogsListView(), animated: true, completion: { [self] in + present(LogsListViewController(), animated: true, completion: { [self] in isLogsPresented = false }) diff --git a/TILogging/Sources/Views/ViewModels/LogsListViewOutput.swift b/TILogging/Sources/Views/ViewModels/LogsListViewOutput.swift new file mode 100644 index 00000000..bc52d09a --- /dev/null +++ b/TILogging/Sources/Views/ViewModels/LogsListViewOutput.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol LogsListViewOutput: AnyObject { + func reloadTableView() + func setLoadingState() + func setNormalState() + func startSearch() + func stopSearch() +} \ No newline at end of file From d2ed1e837afab066c0f66f171efdb37a82864411 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Thu, 17 Nov 2022 10:32:04 +0300 Subject: [PATCH 25/27] fix: ambiguous usage of view output protocol --- .../Sources/Views/LoggerList/LogsListViewController.swift | 8 ++++---- .../Sources/Views/ViewModels/LogsStorageViewModel.swift | 8 -------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/TILogging/Sources/Views/LoggerList/LogsListViewController.swift b/TILogging/Sources/Views/LoggerList/LogsListViewController.swift index 3ed827b9..3c977dbb 100644 --- a/TILogging/Sources/Views/LoggerList/LogsListViewController.swift +++ b/TILogging/Sources/Views/LoggerList/LogsListViewController.swift @@ -27,10 +27,10 @@ import UIKit @available(iOS 15, *) open class LogsListViewController: BaseInitializeableViewController, - LogsListViewOutput, - AlertPresentationContext, - UISearchBarDelegate, - UITextFieldDelegate { + LogsListViewOutput, + AlertPresentationContext, + UISearchBarDelegate, + UITextFieldDelegate { private var timer: Timer? diff --git a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift index 27d53afe..fb04c0d1 100644 --- a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift +++ b/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift @@ -24,14 +24,6 @@ import TISwiftUtils import UIKit import OSLog -public protocol LogsListViewOutput: AnyObject { - func reloadTableView() - func setLoadingState() - func setNormalState() - func startSearch() - func stopSearch() -} - @available(iOS 15, *) open class LogsStorageViewModel { From 1c1fa1290b9db7b780b6be8c071e65686aad4181 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Fri, 18 Nov 2022 17:13:22 +0300 Subject: [PATCH 26/27] feat: added new system of registering for shacking motion event --- TILogging/Sources/LoggingPresenter.swift | 35 ++++--- .../LoggingTogglingViewController.swift | 4 + .../LoggerWindow/LoggingTogglingWindow.swift | 95 +++++++++++-------- 3 files changed, 75 insertions(+), 59 deletions(-) diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift index a173099f..a2c283ed 100644 --- a/TILogging/Sources/LoggingPresenter.swift +++ b/TILogging/Sources/LoggingPresenter.swift @@ -28,43 +28,40 @@ final public class LoggingPresenter { public static let shared = LoggingPresenter() - private let loggingViewController = LoggingTogglingViewController() - - private lazy var window: LoggingTogglingWindow = { - let window = LoggingTogglingWindow(frame: UIScreen.main.bounds, - loggingController: loggingViewController) - - return window - }() + private var window: LoggingTogglingWindow? private init() { } /// Binds openning and closing of logging list view to a shaking motion. - public func displayOnShakeEvent(of scene: UIWindowScene? = nil) { - showWindow(withScene: scene) - loggingViewController.set(isVisible: false) - loggingViewController.set(isRegisteredForShakingEvent: true) + public func displayOnShakeEvent(withWindow window: LoggingTogglingWindow) { + self.window = window + + window.set(isVisible: false) + window.set(isRegisteredForShakingEvent: true) } /// Shows the UIWindow with a button that opens a logging list view. - public func addLogsButton(to scene: UIWindowScene? = nil, isShakingMotionAllowed isShaking: Bool = false) { + public func addLogsButton(to scene: UIWindowScene? = nil) { + window = .init(frame: UIScreen.main.bounds) showWindow(withScene: scene) - loggingViewController.set(isVisible: true) - loggingViewController.set(isRegisteredForShakingEvent: isShaking) + + window?.setDefaultRootController() + window?.set(isVisible: true) + window?.set(isRegisteredForShakingEvent: false) } /// Shows the UIWindow public func showWindow(withScene scene: UIWindowScene? = nil) { if let scene = scene { - window.windowScene = scene + window?.windowScene = scene } - window.makeKeyAndVisible() + window?.makeKeyAndVisible() } /// Hides the UIWindow. public func hideWindow() { - window.isHidden = true - loggingViewController.set(isVisible: true) + window?.isHidden = true + window?.set(isVisible: true) } } diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift index b3611150..5a03f6fc 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift @@ -104,6 +104,10 @@ open class LoggingTogglingViewController: BaseInitializeableViewController { isVisibleState = isVisible } + public func set(isLogsPresented: Bool) { + self.isLogsPresented = isLogsPresented + } + public func openLoggingScreen() { present(LogsListViewController(), animated: true, completion: { [self] in isLogsPresented = false diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift index b5d8422a..d02514b8 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift @@ -23,16 +23,17 @@ import UIKit @available(iOS 15, *) -final class LoggingTogglingWindow: UIWindow { +public final class LoggingTogglingWindow: UIWindow { - let loggingController: LoggingTogglingViewController + let loggingController = LoggingTogglingViewController() - init(frame: CGRect, loggingController: LoggingTogglingViewController) { - self.loggingController = loggingController + override public init(windowScene: UIWindowScene) { + super.init(windowScene: windowScene) + } + override public init(frame: CGRect) { super.init(frame: frame) - rootViewController = loggingController windowLevel = .statusBar backgroundColor = .clear } @@ -42,7 +43,17 @@ final class LoggingTogglingWindow: UIWindow { fatalError("init(coder:) has not been implemented") } - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + public override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + if motion == .motionShake, + !loggingController.isLogsPresented { + openLoggingScreen() + + } else { + super.motionEnded(motion, with: event) + } + } + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { guard let rootView = rootViewController else { return false } if let loggingController = rootView.presentedViewController { @@ -56,46 +67,50 @@ final class LoggingTogglingWindow: UIWindow { return false } -} -// MARK: - Registration for shaking event + // MARK: - Public methdos -public extension UIWindow { - override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - if #available(iOS 15, *) { - guard let window = getLoggingWindow() else { - super.motionEnded(motion, with: event) - return - } + public func setDefaultRootController() { + rootViewController = loggingController + } - checkForShakingMotion(motion, forWindow: window, with: event) + public func set(isRegisteredForShakingEvent: Bool) { + loggingController.set(isRegisteredForShakingEvent: isRegisteredForShakingEvent) + } + public func set(isVisible: Bool) { + loggingController.set(isVisible: isVisible) + } + + // MARK: - Private methods + + private func openLoggingScreen() { + if loggingController.isRegisteredForShakingEvent { + openLoggingScreenOnTopViewController() } else { - super.motionEnded(motion, with: event) - } - } - - @available(iOS 15, *) - private func getLoggingWindow() -> LoggingTogglingWindow? { - guard let windows = windowScene?.windows else { - return nil - } - - return windows.compactMap { $0 as? LoggingTogglingWindow }.first - } - - @available(iOS 15, *) - private func checkForShakingMotion(_ motion: UIEvent.EventSubtype, - forWindow window: LoggingTogglingWindow, - with event: UIEvent?) { - let loggingController = window.loggingController - - if motion == .motionShake, - loggingController.isLogsPresented, - loggingController.isRegisteredForShakingEvent { loggingController.openLoggingScreen() - } else { - super.motionEnded(motion, with: event) } } + + private func openLoggingScreenOnTopViewController() { + guard let rootViewController = rootViewController else { + return + } + + let topViewController = getTopViewController(from: rootViewController) + + topViewController.present(LogsListViewController(), animated: true, completion: { [weak self] in + self?.loggingController.set(isLogsPresented: false) + }) + + loggingController.set(isLogsPresented: true) + } + + private func getTopViewController(from viewController: UIViewController) -> UIViewController { + guard let presentedViewController = viewController.presentedViewController else { + return viewController + } + + return getTopViewController(from: presentedViewController) + } } From 4778f2e70da1063e3f0b90e482efaf726ba34a6a Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Fri, 18 Nov 2022 18:53:28 +0300 Subject: [PATCH 27/27] feat: code review notes --- TILogging/Sources/LoggingPresenter.swift | 7 ++-- .../LoggerList/LogsListViewController.swift | 10 +++-- .../LoggerWindow/LoggingTogglingWindow.swift | 16 ++------ .../UIViewController+TopVisible.swift | 40 +++++++++++++++++++ 4 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 TIUIKitCore/Sources/Extensions/UIViewController/UIViewController+TopVisible.swift diff --git a/TILogging/Sources/LoggingPresenter.swift b/TILogging/Sources/LoggingPresenter.swift index a2c283ed..63884569 100644 --- a/TILogging/Sources/LoggingPresenter.swift +++ b/TILogging/Sources/LoggingPresenter.swift @@ -41,11 +41,12 @@ final public class LoggingPresenter { } /// Shows the UIWindow with a button that opens a logging list view. - public func addLogsButton(to scene: UIWindowScene? = nil) { - window = .init(frame: UIScreen.main.bounds) + public func addLogsButton(to scene: UIWindowScene? = nil, + windowRect rect: CGRect = UIScreen.main.bounds) { + + window = .init(frame: rect) showWindow(withScene: scene) - window?.setDefaultRootController() window?.set(isVisible: true) window?.set(isRegisteredForShakingEvent: false) } diff --git a/TILogging/Sources/Views/LoggerList/LogsListViewController.swift b/TILogging/Sources/Views/LoggerList/LogsListViewController.swift index 3c977dbb..7168311a 100644 --- a/TILogging/Sources/Views/LoggerList/LogsListViewController.swift +++ b/TILogging/Sources/Views/LoggerList/LogsListViewController.swift @@ -51,6 +51,7 @@ open class LogsListViewController: BaseInitializeableViewController, public typealias Snapshot = NSDiffableDataSourceSnapshot public let viewModel = LogsStorageViewModel() + public var logsFileExtension = "log" // MARK: - Life cycle @@ -117,7 +118,8 @@ open class LogsListViewController: BaseInitializeableViewController, viewModel.logsListView = self searchView.delegate = self - tableView.register(LogEntryTableViewCell.self, forCellReuseIdentifier: "identifier") + tableView.register(LogEntryTableViewCell.self, + forCellReuseIdentifier: LogEntryTableViewCell.reuseIdentifier) refreshControl.addTarget(self, action: #selector(reloadLogs), for: .valueChanged) shareButton.addTarget(self, action: #selector(shareLogs), for: .touchUpInside) } @@ -170,7 +172,7 @@ open class LogsListViewController: BaseInitializeableViewController, } timer?.invalidate() - timer = Timer.scheduledTimer(timeInterval: 3, + timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(filterLogsByText), userInfo: nil, @@ -182,14 +184,14 @@ open class LogsListViewController: BaseInitializeableViewController, } open func textFieldDidEndEditing(_ textField: UITextField) { - viewModel.fileCreator = .init(fileName: textField.text ?? "", fileExtension: "log") + viewModel.fileCreator = .init(fileName: textField.text ?? "", fileExtension: logsFileExtension) } // MARK: - Open methods open func createDataSource() -> DataSource { let cellProvider: DataSource.CellProvider = { collectionView, indexPath, itemIdentifier in - let cell = collectionView.dequeueReusableCell(withIdentifier: "identifier", + let cell = collectionView.dequeueReusableCell(withIdentifier: LogEntryTableViewCell.reuseIdentifier, for: indexPath) as? LogEntryTableViewCell cell?.configure(with: itemIdentifier) diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift index d02514b8..92f66cbc 100644 --- a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift +++ b/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. // +import TIUIKitCore import UIKit @available(iOS 15, *) @@ -34,6 +35,7 @@ public final class LoggingTogglingWindow: UIWindow { override public init(frame: CGRect) { super.init(frame: frame) + rootViewController = loggingController windowLevel = .statusBar backgroundColor = .clear } @@ -70,10 +72,6 @@ public final class LoggingTogglingWindow: UIWindow { // MARK: - Public methdos - public func setDefaultRootController() { - rootViewController = loggingController - } - public func set(isRegisteredForShakingEvent: Bool) { loggingController.set(isRegisteredForShakingEvent: isRegisteredForShakingEvent) } @@ -97,7 +95,7 @@ public final class LoggingTogglingWindow: UIWindow { return } - let topViewController = getTopViewController(from: rootViewController) + let topViewController = rootViewController.topVisibleViewController topViewController.present(LogsListViewController(), animated: true, completion: { [weak self] in self?.loggingController.set(isLogsPresented: false) @@ -105,12 +103,4 @@ public final class LoggingTogglingWindow: UIWindow { loggingController.set(isLogsPresented: true) } - - private func getTopViewController(from viewController: UIViewController) -> UIViewController { - guard let presentedViewController = viewController.presentedViewController else { - return viewController - } - - return getTopViewController(from: presentedViewController) - } } diff --git a/TIUIKitCore/Sources/Extensions/UIViewController/UIViewController+TopVisible.swift b/TIUIKitCore/Sources/Extensions/UIViewController/UIViewController+TopVisible.swift new file mode 100644 index 00000000..3c8266e7 --- /dev/null +++ b/TIUIKitCore/Sources/Extensions/UIViewController/UIViewController+TopVisible.swift @@ -0,0 +1,40 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +public extension UIViewController { + + /// Return top visible controller even if we have inner UI(Navigation/TabBar)Controller's inside + var topVisibleViewController: UIViewController { + switch self { + case let navController as UINavigationController: + return navController.visibleViewController?.topVisibleViewController ?? navController + + case let tabController as UITabBarController: + return tabController.selectedViewController?.topVisibleViewController ?? tabController + + default: + return self.presentedViewController?.topVisibleViewController ?? self + } + } +}