Skip to content

Commit

Permalink
Introduce ViewControllerInstanceObserver (#19)
Browse files Browse the repository at this point in the history
* Revert using `after` in swizzler, as in this case we miss some time in viewDidAppear

* Introduce ViewControllerInstanceObserver

Separating view controllers by 2 types:
- ViewControllerObserver. This is a single observer per app. It consumes events from all view controllers
- ViewControllerInstanceObserver. Those are instance per view controller. They receive identifier in initializer and do not need view controller reference at all.

Instance observers:
- TTIObserver
- RenderingObserver
- LoggingObserver

Other observers:
- ViewControllerLeaksObserver
- LastOpenedScreenObserver

Renamed AppStateObserver to AppStateListener not to confuse with other observers.

Reorganized sources folder structure a bit.

* Fixing swiftlint issues

---------

Co-authored-by: Gleb Tarasov <[email protected]>
  • Loading branch information
pilot34 and Gleb Tarasov authored Oct 21, 2024
1 parent 82d30cc commit b43e2d9
Show file tree
Hide file tree
Showing 42 changed files with 504 additions and 475 deletions.
81 changes: 81 additions & 0 deletions PerformanceSuite/Sources/InstanceObservers/LoggingObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// LoggingObserver.swift
// PerformanceSuite
//
// Created by Gleb Tarasov on 23/09/2022.
//

import Foundation
import UIKit
import SwiftUI

/// Use this protocol for light-weight operations like logging only,
/// it is not intended to be used for some business-logic.
///
/// If you execute something heavy, offload it to some other background thread.
public protocol ViewControllerLoggingReceiver: ScreenMetricsReceiver {

/// Method is called during view controller's initialization
func onInit(screen: ScreenIdentifier)

/// Method is called during view controller's `viewDidLoad`
func onViewDidLoad(screen: ScreenIdentifier)

/// Method is called during view controller's `viewWillAppear`
func onViewWillAppear(screen: ScreenIdentifier)

/// Method is called during view controller's `viewDidAppear`
func onViewDidAppear(screen: ScreenIdentifier)

/// Method is called during view controller's `viewWillDisappear`
func onViewWillDisappear(screen: ScreenIdentifier)
}


/// Observer which forward all delegate methods to its receiver for logging purposes
final class LoggingObserver<V: ViewControllerLoggingReceiver>: ViewControllerInstanceObserver {

init(screen: V.ScreenIdentifier, receiver: V) {
self.screen = screen
self.receiver = receiver
}

private let screen: V.ScreenIdentifier
private let receiver: V

func beforeInit() {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onInit(screen: self.screen)
}
}

func beforeViewDidLoad() {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onViewDidLoad(screen: self.screen)
}
}

func afterViewWillAppear() {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onViewWillAppear(screen: self.screen)
}
}

func afterViewDidAppear() {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onViewDidAppear(screen: self.screen)
}
}

func beforeViewWillDisappear() {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onViewWillDisappear(screen: self.screen)
}
}

static var identifier: AnyObject {
return loggingObserverIdentifier
}
}

private let loggingObserverIdentifier = NSObject()
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import UIKit

final class RenderingObserver<R: RenderingMetricsReceiver>: ViewControllerObserver, FramesMeterReceiver {
final class RenderingObserver<R: RenderingMetricsReceiver>: ViewControllerInstanceObserver, FramesMeterReceiver {

init(
screen: R.ScreenIdentifier,
Expand All @@ -26,28 +26,26 @@ final class RenderingObserver<R: RenderingMetricsReceiver>: ViewControllerObserv

private var metrics = RenderingMetrics.zero

func beforeInit(viewController: UIViewController) {}
func beforeInit() {}

func beforeViewDidLoad(viewController: UIViewController) {}
func beforeViewDidLoad() {}

func afterViewDidAppear(viewController: UIViewController) {
func afterViewDidAppear() {
PerformanceMonitoring.queue.async {
self.metrics = RenderingMetrics.zero
self.framesMeter.subscribe(receiver: self)
}
}

func afterViewWillAppear(viewController: UIViewController) {}
func afterViewWillAppear() {}

func beforeViewWillDisappear(viewController: UIViewController) {
func beforeViewWillDisappear() {
PerformanceMonitoring.queue.async {
self.framesMeter.unsubscribe(receiver: self)
self.reportMetricsIfNeeded()
}
}

func beforeViewDidDisappear(viewController: UIViewController) {}

static var identifier: AnyObject {
return renderingObserverIdentifier
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@
import UIKit

/// Observer that calculates `TTIMetrics` during view controller lifetime.
final class TTIObserver<T: TTIMetricsReceiver>: ViewControllerObserver, ScreenIsReadyProvider {
final class TTIObserver<T: TTIMetricsReceiver>: ViewControllerInstanceObserver, ScreenIsReadyProvider {

init(screen: T.ScreenIdentifier,
metricsReceiver: T,
timeProvider: TimeProvider = defaultTimeProvider,
appStateObserver: AppStateObserver = DefaultAppStateObserver()
appStateListener: AppStateListener = DefaultAppStateListener()
) {
self.screen = screen
self.metricsReceiver = metricsReceiver
self.timeProvider = timeProvider
self.appStateObserver = appStateObserver
self.appStateListener = appStateListener
}

private let screen: T.ScreenIdentifier
private let metricsReceiver: T
private let timeProvider: TimeProvider
private let appStateObserver: AppStateObserver
private let appStateListener: AppStateListener


private var screenCreatedTime: DispatchTime?
Expand All @@ -37,7 +37,7 @@ final class TTIObserver<T: TTIMetricsReceiver>: ViewControllerObserver, ScreenIs

private var customCreationTime: DispatchTime?

func beforeInit(viewController: UIViewController) {
func beforeInit() {
let now = timeProvider.now()
let action = {
self.sameRunLoopAsTheInit = true
Expand All @@ -60,7 +60,7 @@ final class TTIObserver<T: TTIMetricsReceiver>: ViewControllerObserver, ScreenIs
}
}

func beforeViewDidLoad(viewController: UIViewController) {
func beforeViewDidLoad() {
// if there is time passed between `init` and `viewDidLoad`, it means view controller was created earlier, but displayed only recently,
// in this case we don't consider `init` time, but start measuring in `viewDidLoad`.
// Ideally we should start before `loadView`, but it is impossible to swizzle `loadView` because usually nobody calls `super.loadView`
Expand All @@ -80,7 +80,7 @@ final class TTIObserver<T: TTIMetricsReceiver>: ViewControllerObserver, ScreenIs
}
}

func afterViewWillAppear(viewController: UIViewController) {
func afterViewWillAppear() {
let now = timeProvider.now()
let action = {
if self.viewWillAppearTime != nil && self.ttiCalculated == false {
Expand Down Expand Up @@ -112,7 +112,7 @@ final class TTIObserver<T: TTIMetricsReceiver>: ViewControllerObserver, ScreenIs
}
}

func afterViewDidAppear(viewController: UIViewController) {
func afterViewDidAppear() {
let now = timeProvider.now()
let action = {
if self.shouldReportTTI && self.viewDidAppearTime == nil {
Expand All @@ -129,7 +129,7 @@ final class TTIObserver<T: TTIMetricsReceiver>: ViewControllerObserver, ScreenIs
}
}

func beforeViewWillDisappear(viewController: UIViewController) {
func beforeViewWillDisappear() {
// if screenIsReady wasn't called until now, we consider that screen was ready in `viewDidAppear`.
let action = {
if self.shouldReportTTI && self.screenIsReadyTime == nil {
Expand All @@ -146,8 +146,6 @@ final class TTIObserver<T: TTIMetricsReceiver>: ViewControllerObserver, ScreenIs
}
}

func beforeViewDidDisappear(viewController: UIViewController) {}

static var identifier: AnyObject {
return TTIObserverHelper.identifier
}
Expand Down Expand Up @@ -201,7 +199,7 @@ final class TTIObserver<T: TTIMetricsReceiver>: ViewControllerObserver, ScreenIs
}

private var shouldReportTTI: Bool {
return !ttiCalculated && !appStateObserver.wasInBackground && !ignoreThisScreen
return !ttiCalculated && !appStateListener.wasInBackground && !ignoreThisScreen
}
}

Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,92 +1,26 @@
//
// LoggingObserver.swift
// PerformanceSuite
// LastOpenedScreenObserver.swift
// Pods
//
// Created by Gleb Tarasov on 23/09/2022.
// Created by Gleb Tarasov on 19/10/2024.
//

import Foundation
import UIKit
import SwiftUI

/// Use this protocol for light-weight operations like logging only,
/// it is not intended to be used for some business-logic.
///
/// If you execute something heavy, offload it to some other background thread.
public protocol ViewControllerLoggingReceiver: ScreenMetricsReceiver {
final class LastOpenedScreenObserver: ViewControllerObserver {
func beforeInit(viewController: UIViewController) {}

/// Method is called during view controller's initialization
func onInit(screen: ScreenIdentifier)
func beforeViewDidLoad(viewController: UIViewController) {}

/// Method is called during view controller's `viewDidLoad`
func onViewDidLoad(screen: ScreenIdentifier)

/// Method is called during view controller's `viewWillAppear`
func onViewWillAppear(screen: ScreenIdentifier)

/// Method is called during view controller's `viewDidAppear`
func onViewDidAppear(screen: ScreenIdentifier)

/// Method is called during view controller's `viewWillDisappear`
func onViewWillDisappear(screen: ScreenIdentifier)

/// Method is called during view controller's `viewDidDisappear`
func onViewDidDisappear(screen: ScreenIdentifier)
}


/// Observer which forward all delegate methods to its receiver for logging purposes
final class LoggingObserver<V: ViewControllerLoggingReceiver>: ViewControllerObserver {

init(screen: V.ScreenIdentifier, receiver: V) {
self.screen = screen
self.receiver = receiver
}

private let screen: V.ScreenIdentifier
private let receiver: V


func beforeInit(viewController: UIViewController) {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onInit(screen: self.screen)
}
}

func beforeViewDidLoad(viewController: UIViewController) {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onViewDidLoad(screen: self.screen)
}
}

func afterViewWillAppear(viewController: UIViewController) {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onViewWillAppear(screen: self.screen)
}
}
func afterViewWillAppear(viewController: UIViewController) {}

func afterViewDidAppear(viewController: UIViewController) {
rememberOpenedScreenIfNeeded(viewController)
PerformanceMonitoring.consumerQueue.async {
self.receiver.onViewDidAppear(screen: self.screen)
}
}

func beforeViewWillDisappear(viewController: UIViewController) {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onViewWillDisappear(screen: self.screen)
}
}
func beforeViewWillDisappear(viewController: UIViewController) {}

func beforeViewDidDisappear(viewController: UIViewController) {
PerformanceMonitoring.consumerQueue.async {
self.receiver.onViewDidDisappear(screen: self.screen)
}
}

static var identifier: AnyObject {
return loggingObserverIdentifier
}
func beforeViewDidDisappear(viewController: UIViewController) {}

// MARK: - Top screen detection

Expand Down Expand Up @@ -194,8 +128,6 @@ final class LoggingObserver<V: ViewControllerLoggingReceiver>: ViewControllerObs

}

private let loggingObserverIdentifier = NSObject()

// We cannot check `viewController is UIHostingController` because of generics,
// so we use helper protocol here
protocol HostingControllerIdentifier { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ final class ViewControllerLeaksObserver: ViewControllerObserver {
}
}

static let identifier: AnyObject = NSObject()

// MARK: - Helpers

private func selfAndAllChildren(viewController: UIViewController) -> [UIViewController] {
Expand Down
Loading

0 comments on commit b43e2d9

Please sign in to comment.