Merge pull request #327 from TouchInstinct/feature/logging_api

Логирование
This commit is contained in:
Nikita Semenov 2022-11-18 19:04:22 +03:00 committed by GitHub
commit 98ee540aac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1301 additions and 20 deletions

View File

@ -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.1
- **Fix**: Weak target reference in `RefreshControl`

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "LeadKit"
s.version = "1.27.1"
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"

View File

@ -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: ["TIUIElements", "TISwiftUtils", "TIUIKitCore"], path: "TILogging/Sources"),
// MARK: - Networking
@ -84,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

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIAppleMapUtils'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIAuth'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIEcommerce'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIFoundationUtils'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIGoogleMapUtils'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIKeychainUtils'
s.version = '1.27.1'
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' }

View File

@ -0,0 +1,77 @@
//
// 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
public struct TILogger: LoggerRepresentable {
// MARK: - Properties
@available(iOS 12, *)
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)
}
@available(iOS 12, *)
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
}

View File

@ -0,0 +1,27 @@
//
// 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 {
func log(_ message: StaticString, log: OSLog?, type: OSLogType, _ arguments: CVarArg...)
}

View File

@ -0,0 +1,68 @@
//
// 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
/// 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 var window: LoggingTogglingWindow?
private init() { }
/// Binds openning and closing of logging list view to a shaking motion.
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,
windowRect rect: CGRect = UIScreen.main.bounds) {
window = .init(frame: rect)
showWindow(withScene: scene)
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?.makeKeyAndVisible()
}
/// Hides the UIWindow.
public func hideWindow() {
window?.isHidden = true
window?.set(isVisible: true)
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,57 @@
//
// 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
@available(iOS 15, *)
open class LogEntryTableViewCell: ContainerTableViewCell<LogEntryCellView>, 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
}
}
}

View File

@ -0,0 +1,300 @@
//
// 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 TIUIKitCore
import OSLog
import UIKit
@available(iOS 15, *)
open class LogsListViewController: 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<String, OSLogEntryLog>
public typealias Snapshot = NSDiffableDataSourceSnapshot<String, OSLogEntryLog>
public let viewModel = LogsStorageViewModel()
public var logsFileExtension = "log"
// MARK: - Life cycle
open override func viewDidLoad() {
super.viewDidLoad()
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: LogEntryTableViewCell.reuseIdentifier)
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: 1,
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: logsFileExtension)
}
// MARK: - Open methods
open func createDataSource() -> DataSource {
let cellProvider: DataSource.CellProvider = { collectionView, indexPath, itemIdentifier in
let cell = collectionView.dequeueReusableCell(withIdentifier: LogEntryTableViewCell.reuseIdentifier,
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)
}
private func loadLogs(preCompletion: VoidClosure? = nil, postCompletion: VoidClosure? = nil) {
Task {
await viewModel.loadLogs(preCompletion: preCompletion, postCompletion: postCompletion)
}
}
// MARK: - Actions
@objc private func reloadLogs() {
loadLogs(preCompletion: stopLoadingAnimation, postCompletion: refreshControl.endRefreshing)
}
@objc private func shareLogs() {
startSharingFlow()
}
@objc private func filterLogsByText() {
Task {
await viewModel.filterLogs(byText: searchView.text ?? "")
}
}
}

View File

@ -0,0 +1,176 @@
//
// 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()
private(set) public var isRegisteredForShakingEvent = false
private(set) public var isLogsPresented = false
private(set) public var isVisibleState = true {
didSet {
button.isHidden = !isVisibleState
}
}
public let button = UIButton()
open override var canBecomeFirstResponder: Bool { true }
// MARK: - Life cycle
open override func addViews() {
super.addViews()
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()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
button.addGestureRecognizer(panGesture)
button.addTarget(self, action: #selector(openLoggingScreenAction), for: .touchUpInside)
// Needed for catching shaking motion
becomeFirstResponder()
}
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
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.gray.cgColor
}
// MARK: - Overrided methods
open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake, !isLogsPresented, isRegisteredForShakingEvent {
openLoggingScreen()
}
}
// MARK: - Public methods
public func set(isRegisteredForShakingEvent: Bool) {
self.isRegisteredForShakingEvent = isRegisteredForShakingEvent
}
/// Hides a button that opens logs list view controller.
public func set(isVisible: Bool) {
isVisibleState = isVisible
}
public func set(isLogsPresented: Bool) {
self.isLogsPresented = isLogsPresented
}
public func openLoggingScreen() {
present(LogsListViewController(), animated: true, completion: { [self] in
isLogsPresented = false
})
isLogsPresented = true
}
// MARK: - Private methods
private func clipButtonIfNeeded() {
let viewFrame = view.safeAreaLayoutGuide.layoutFrame
let buttonFrame = button.frame
var x = buttonFrame.minX
var y = buttonFrame.minY
if buttonFrame.maxX > viewFrame.maxX {
x = viewFrame.maxX - buttonFrame.width
y = buttonFrame.minY
} else if buttonFrame.minX < viewFrame.minX {
x = viewFrame.minX
y = buttonFrame.minY
}
if buttonFrame.maxY > viewFrame.maxY {
x = buttonFrame.minX
y = viewFrame.maxY - buttonFrame.height
} else if buttonFrame.minY < viewFrame.minY {
x = buttonFrame.minX
y = viewFrame.minY
}
button.frame = .init(x: x,
y: y,
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 openLoggingScreenAction() {
openLoggingScreen()
}
}

View File

@ -0,0 +1,106 @@
//
// 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, *)
public final class LoggingTogglingWindow: UIWindow {
let loggingController = LoggingTogglingViewController()
override public init(windowScene: UIWindowScene) {
super.init(windowScene: windowScene)
}
override public init(frame: CGRect) {
super.init(frame: frame)
rootViewController = loggingController
windowLevel = .statusBar
backgroundColor = .clear
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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 {
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
}
// MARK: - Public methdos
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 {
loggingController.openLoggingScreen()
}
}
private func openLoggingScreenOnTopViewController() {
guard let rootViewController = rootViewController else {
return
}
let topViewController = rootViewController.topVisibleViewController
topViewController.present(LogsListViewController(), animated: true, completion: { [weak self] in
self?.loggingController.set(isLogsPresented: false)
})
loggingController.set(isLogsPresented: true)
}
}

View File

@ -0,0 +1,67 @@
//
// 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 FileCreator {
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) -> Result<URL, Error> {
let result = getDocumentsDirectory()
guard case var .success(url) = result else {
return result
}
url.appendPathComponent(fullFileName)
do {
try data.write(to: url)
return .success(url)
} catch {
return .failure(error)
}
}
public func getDocumentsDirectory() -> Result<URL, Error> {
Result {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
}
}
}

View File

@ -0,0 +1,50 @@
//
// 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 -> Result<[OSLogEntryLog], Error> {
Result { try OSLogStore(scope: .currentProcessIdentifier) }
.flatMap { logStore in
Result {
try logStore
.getEntries()
.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
}
}
}

View File

@ -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 -> Result<[OSLogEntryLog], Error>
func filter(_ logs: [OSLogEntryLog], byText text: String) async -> [OSLogEntryLog]
}

View File

@ -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()
}

View File

@ -0,0 +1,142 @@
//
// 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
@available(iOS 15, *)
open class LogsStorageViewModel {
public enum LevelType: String, CaseIterable {
case all
case `default`
case info
case debug
case error
case fault
}
private let logsManipulator: LogsListManipulatorProtocol
private var allLogs: [OSLogEntryLog] = [] {
didSet {
filterLogs()
}
}
public var filteredLogs: [OSLogEntryLog] = [] {
didSet {
logsListView?.reloadTableView()
}
}
public var fileCreator: FileCreator?
weak public var logsListView: LogsListViewOutput?
public init(logsManipulator: LogsListManipulatorProtocol? = nil) {
self.logsManipulator = logsManipulator ?? DefaultLogsListManipulator()
}
@MainActor
open func loadLogs(preCompletion: VoidClosure? = nil, postCompletion: VoidClosure? = nil) async {
allLogs = []
logsListView?.setLoadingState()
preCompletion?()
if case let .success(logs) = await logsManipulator.fetchLogs() {
allLogs = logs
}
logsListView?.setNormalState()
postCompletion?()
}
open func filterLogs(filter: Closure<OSLogEntryLog, Bool>? = nil) {
filteredLogs = allLogs.filter { filter?($0) ?? true }
}
@MainActor
open func filterLogs(byText text: String) async {
guard !text.isEmpty else {
filteredLogs = allLogs
return
}
logsListView?.startSearch()
let localFilteredLogs = await logsManipulator.filter(allLogs, byText: text)
filteredLogs = localFilteredLogs
logsListView?.stopSearch()
}
open func getFileWithLogs() -> URL? {
guard let data = encodeLogs() else {
return nil
}
if case let .success(url) = fileCreator?.createFile(withData: data) {
return url
}
return nil
}
public 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)"
}
}

View File

@ -0,0 +1,20 @@
Pod::Spec.new do |s|
s.name = 'TILogging'
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' }
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'
s.swift_versions = ['5.3']
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

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMapUtils'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMoyaNetworking'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworking'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworkingCache'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIPagination'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUICore'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUtils'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITableKitUtils'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITransitions'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIElements'
s.version = '1.27.1'
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' }

View File

@ -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
}
}
}

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIKitCore'
s.version = '1.27.1'
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' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIYandexMapUtils'
s.version = '1.27.1'
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' }