Merge pull request #327 from TouchInstinct/feature/logging_api
Логирование
This commit is contained in:
commit
98ee540aac
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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...)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
Loading…
Reference in New Issue