299 lines
11 KiB
Swift
299 lines
11 KiB
Swift
//
|
|
// 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()
|
|
|
|
// 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: "identifier")
|
|
refreshControl.addTarget(self, action: #selector(reloadLogs), for: .valueChanged)
|
|
shareButton.addTarget(self, action: #selector(shareLogs), for: .touchUpInside)
|
|
}
|
|
|
|
open override func configureAppearance() {
|
|
super.configureAppearance()
|
|
|
|
configureSegmentView()
|
|
configureReloadButton()
|
|
configureSearchView()
|
|
|
|
view.backgroundColor = .systemBackground
|
|
tableView.backgroundColor = .systemBackground
|
|
}
|
|
|
|
// MARK: - LogsListViewOutput
|
|
|
|
open func reloadTableView() {
|
|
applySnapshot()
|
|
}
|
|
|
|
open func setLoadingState() {
|
|
view.subviews.forEach { $0.isUserInteractionEnabled = false }
|
|
startLoadingAnimation()
|
|
}
|
|
|
|
open func setNormalState() {
|
|
view.subviews.forEach { $0.isUserInteractionEnabled = true }
|
|
stopLoadingAnimation()
|
|
}
|
|
|
|
open func startSearch() {
|
|
[shareButton, segmentView, tableView]
|
|
.forEach { $0.isUserInteractionEnabled = false }
|
|
startLoadingAnimation()
|
|
}
|
|
|
|
open func stopSearch() {
|
|
[shareButton, segmentView, tableView]
|
|
.forEach { $0.isUserInteractionEnabled = true }
|
|
stopLoadingAnimation()
|
|
}
|
|
|
|
// MARK: - UISearchBarDelegate + UITextFieldDelegate
|
|
|
|
open func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
|
guard !searchText.isEmpty else {
|
|
filterLogsByText()
|
|
return
|
|
}
|
|
|
|
timer?.invalidate()
|
|
timer = Timer.scheduledTimer(timeInterval: 3,
|
|
target: self,
|
|
selector: #selector(filterLogsByText),
|
|
userInfo: nil,
|
|
repeats: false)
|
|
}
|
|
|
|
open func searchBarResultsListButtonClicked(_ searchBar: UISearchBar) {
|
|
searchView.resignFirstResponder()
|
|
}
|
|
|
|
open func textFieldDidEndEditing(_ textField: UITextField) {
|
|
viewModel.fileCreator = .init(fileName: textField.text ?? "", fileExtension: "log")
|
|
}
|
|
|
|
// MARK: - Open methods
|
|
|
|
open func createDataSource() -> DataSource {
|
|
let cellProvider: DataSource.CellProvider = { collectionView, indexPath, itemIdentifier in
|
|
let cell = collectionView.dequeueReusableCell(withIdentifier: "identifier",
|
|
for: indexPath) as? LogEntryTableViewCell
|
|
|
|
cell?.configure(with: itemIdentifier)
|
|
|
|
return cell
|
|
}
|
|
|
|
return .init(tableView: tableView, cellProvider: cellProvider)
|
|
}
|
|
|
|
open func applySnapshot() {
|
|
var snapshot = Snapshot()
|
|
|
|
snapshot.appendSections([TableSection.main.rawValue])
|
|
snapshot.appendItems(viewModel.filteredLogs, toSection: TableSection.main.rawValue)
|
|
|
|
dataSource.apply(snapshot, animatingDifferences: true)
|
|
}
|
|
|
|
open func startLoadingAnimation() {
|
|
activityView.isHidden = false
|
|
activityView.startAnimating()
|
|
}
|
|
|
|
open func stopLoadingAnimation() {
|
|
activityView.isHidden = true
|
|
activityView.stopAnimating()
|
|
}
|
|
|
|
// MARK: - Private methods
|
|
|
|
private func configureSegmentView() {
|
|
for (index, segment) in LogsStorageViewModel.LevelType.allCases.enumerated() {
|
|
let action = UIAction(title: segment.rawValue, handler: viewModel.actionHandler(for:))
|
|
segmentView.insertSegment(action: action, at: index, animated: false)
|
|
}
|
|
|
|
segmentView.selectedSegmentIndex = 0
|
|
}
|
|
|
|
private func configureReloadButton() {
|
|
shareButton.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
|
|
}
|
|
|
|
private func configureSearchView() {
|
|
searchView.placeholder = "Date, subcategory, message text, etc."
|
|
searchView.layer.backgroundColor = UIColor.systemBackground.cgColor
|
|
searchView.searchBarStyle = .minimal
|
|
}
|
|
|
|
private func startSharingFlow() {
|
|
let alert = AlertFactory().alert(title: "Enter file name", actions: [
|
|
.init(title: "Cancel", style: .cancel, action: nil),
|
|
.init(title: "Share", style: .default, action: { [weak self] in
|
|
self?.presentShareScreen()
|
|
})
|
|
])
|
|
|
|
alert.present(on: self, alertViewFactory: { configuration in
|
|
let alertController = UIAlertController(title: "", message: "", preferredStyle: .alert)
|
|
.configured(with: configuration)
|
|
|
|
alertController.addTextField(configurationHandler: { textField in
|
|
textField.delegate = self
|
|
})
|
|
|
|
return alertController
|
|
})
|
|
}
|
|
|
|
private func presentShareScreen() {
|
|
guard let file = viewModel.getFileWithLogs() else {
|
|
AlertFactory().retryAlert(title: "Can't create a file with name: {\(viewModel.fileCreator?.fullFileName ?? "")}",
|
|
retryAction: { [weak self] in
|
|
self?.startSharingFlow()
|
|
}).present(on: self)
|
|
return
|
|
}
|
|
|
|
let activityViewController = UIActivityViewController(activityItems: [file],
|
|
applicationActivities: nil)
|
|
present(activityViewController, animated: true, completion: nil)
|
|
}
|
|
|
|
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 ?? "")
|
|
}
|
|
}
|
|
}
|