diff --git a/Projects/App/Sources/Application/AppDelegate.swift b/Projects/App/Sources/Application/AppDelegate.swift index e2b756e7a..2fbb1d412 100644 --- a/Projects/App/Sources/Application/AppDelegate.swift +++ b/Projects/App/Sources/Application/AppDelegate.swift @@ -103,3 +103,42 @@ private extension AppDelegate { ]) } } + +#if DEBUG + extension UIWindow { + override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + super.motionEnded(motion, with: event) + switch motion { + case .motionShake: + let topViewController: UIViewController? + if #available(iOS 15.0, *) { + topViewController = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .filter { $0.activationState == .foregroundActive } + .first? + .keyWindow? + .rootViewController + } else { + topViewController = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .filter { $0.activationState == .foregroundActive } + .first? + .windows + .first(where: \.isKeyWindow)? + .rootViewController + } + guard let topViewController else { break } + let logHistoryViewController = UINavigationController(rootViewController: LogHistoryViewController()) + if let nav = topViewController as? UINavigationController, + !(nav.visibleViewController is LogHistoryViewController) { + nav.visibleViewController?.present(logHistoryViewController, animated: true) + } else if !(topViewController is LogHistoryViewController) { + topViewController.present(logHistoryViewController, animated: true) + } + + default: + break + } + } + } +#endif diff --git a/Projects/Modules/LogManager/Sources/AnalyticsLogManager.swift b/Projects/Modules/LogManager/Sources/AnalyticsLogManager.swift index d2686403e..3a3be3361 100644 --- a/Projects/Modules/LogManager/Sources/AnalyticsLogManager.swift +++ b/Projects/Modules/LogManager/Sources/AnalyticsLogManager.swift @@ -115,6 +115,8 @@ public extension LogManager { ) { #if RELEASE Analytics.logEvent(log.name, parameters: log.params) + #elseif DEBUG + LogHistoryStorage.shared.appendHistory(log: log) #endif let message = """ \(log.name) logged diff --git a/Projects/Modules/LogManager/Sources/LogHistory/LogHistoryCell.swift b/Projects/Modules/LogManager/Sources/LogHistory/LogHistoryCell.swift new file mode 100644 index 000000000..f82f456e6 --- /dev/null +++ b/Projects/Modules/LogManager/Sources/LogHistory/LogHistoryCell.swift @@ -0,0 +1,68 @@ +#if DEBUG + import UIKit + + final class LogHistoryCell: UITableViewCell { + static let reuseIdentifier = String(describing: LogHistoryCell.self) + + private let logStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 8 + stackView.alignment = .leading + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private let logTitleLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 20) + label.numberOfLines = 0 + return label + }() + + private let logParametersLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 16) + label.textColor = .gray + label.numberOfLines = 0 + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + addView() + setLayout() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(log: any AnalyticsLogType) { + logTitleLabel.text = log.name + logParametersLabel.text = log.params + .filter { !($0.key == "date" || $0.key == "timestamp") } + .map { "- \($0) : \($1)" } + .joined(separator: "\n") + } + } + + private extension LogHistoryCell { + func addView() { + contentView.addSubview(logStackView) + logStackView.addArrangedSubview(logTitleLabel) + logStackView.addArrangedSubview(logParametersLabel) + } + + func setLayout() { + NSLayoutConstraint.activate([ + logStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + logStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + logStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + logStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) + ]) + } + } + +#endif diff --git a/Projects/Modules/LogManager/Sources/LogHistory/LogHistorySectionItem.swift b/Projects/Modules/LogManager/Sources/LogHistory/LogHistorySectionItem.swift new file mode 100644 index 000000000..9c36ef5a7 --- /dev/null +++ b/Projects/Modules/LogManager/Sources/LogHistory/LogHistorySectionItem.swift @@ -0,0 +1,17 @@ +#if DEBUG + import Foundation + + struct LogHistorySectionItem: Hashable, Equatable { + let index: Int + let log: any AnalyticsLogType + + func hash(into hasher: inout Hasher) { + hasher.combine(index) + hasher.combine(log.name) + } + + static func == (lhs: LogHistorySectionItem, rhs: LogHistorySectionItem) -> Bool { + lhs.index == rhs.index && lhs.log.name == rhs.log.name + } + } +#endif diff --git a/Projects/Modules/LogManager/Sources/LogHistory/LogHistoryStorage.swift b/Projects/Modules/LogManager/Sources/LogHistory/LogHistoryStorage.swift new file mode 100644 index 000000000..4889264a4 --- /dev/null +++ b/Projects/Modules/LogManager/Sources/LogHistory/LogHistoryStorage.swift @@ -0,0 +1,14 @@ +#if DEBUG + import Foundation + + final class LogHistoryStorage { + static let shared = LogHistoryStorage() + + private(set) var logHistory: [any AnalyticsLogType] = [] + + func appendHistory(log: any AnalyticsLogType) { + logHistory.append(log) + } + } + +#endif diff --git a/Projects/Modules/LogManager/Sources/LogHistory/LogHistoryViewController.swift b/Projects/Modules/LogManager/Sources/LogHistory/LogHistoryViewController.swift new file mode 100644 index 000000000..8e1b6dddf --- /dev/null +++ b/Projects/Modules/LogManager/Sources/LogHistory/LogHistoryViewController.swift @@ -0,0 +1,80 @@ +#if DEBUG + import UIKit + + public final class LogHistoryViewController: UIViewController { + private let logHistoryTableView: UITableView = { + let tableView = UITableView() + tableView.separatorInset = .init(top: 8, left: 0, bottom: 8, right: 0) + tableView.register(LogHistoryCell.self, forCellReuseIdentifier: LogHistoryCell.reuseIdentifier) + return tableView + }() + + private lazy var logHistoryTableViewDiffableDataSource = UITableViewDiffableDataSource< + Int, + LogHistorySectionItem + >( + tableView: logHistoryTableView + ) { tableView, indexPath, itemIdentifier in + guard let cell = tableView.dequeueReusableCell( + withIdentifier: LogHistoryCell.reuseIdentifier, + for: indexPath + ) as? LogHistoryCell + else { + return UITableViewCell() + } + cell.configure(log: itemIdentifier.log) + return cell + } + + public init() { + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + addView() + setLayout() + view.backgroundColor = .white + + let logs = LogHistoryStorage.shared.logHistory + .enumerated() + .map { LogHistorySectionItem(index: $0.offset, log: $0.element) } + + var snapshot = logHistoryTableViewDiffableDataSource.snapshot() + snapshot.appendSections([0]) + snapshot.appendItems(logs, toSection: 0) + + logHistoryTableViewDiffableDataSource.apply(snapshot, animatingDifferences: true) + + navigationItem.title = "애널리틱스 히스토리" + } + } + + private extension LogHistoryViewController { + func addView() { + view.addSubview(logHistoryTableView) + logHistoryTableView.translatesAutoresizingMaskIntoConstraints = false + } + + func setLayout() { + NSLayoutConstraint.activate([ + logHistoryTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + logHistoryTableView.leadingAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.leadingAnchor, + constant: 16 + ), + logHistoryTableView.trailingAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.trailingAnchor, + constant: -16 + ), + logHistoryTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16) + ]) + } + } + +#endif