[iOS] How to detect view appear in window (Get impression) - RunLoop

Updated
Jul 6, 2020 12:37 PM
Created
Mar 28, 2020 10:40 AM
Tags
SwiftCocoaMemo
Keywords

CFRunLoopObserverCreateWithHandler

import Foundation

import AppLogger

/**
 - Current limitations
   - no supports tracking visibility with overlay
 */
public final class ImpressionTrackingNode<Content: ASDisplayNode>: WrapperNode<Content> {
  
  public enum State {
    case inView
    case outView
  }
  
  public override var supportsLayerBacking: Bool {
    false
  }
  
  private var token: MainRoopActivityObserver.Subscription?
  
  public private(set) var state: State = .outView
  
  private var _stateUpdated: (State) -> Void = { _ in }
  private var _onAppear: () -> Void = { }
  private var _onDisappear: () -> Void = { }

  
  public override func didLoad() {
    token = MainRoopActivityObserver.addObserver(acitivity: [.beforeWaiting, .exit]) { [weak self] in
      guard let self = self else { return }
      self.updateState()
    }
  }
        
  deinit {
    token.map {
      MainRoopActivityObserver.remove($0)
    }
  }
  
  @discardableResult
  public func onStateUpdated(_ callback: @escaping (State) -> Void) -> Self {
    _stateUpdated = callback
    return self
  }
  
  @discardableResult
  public func onAppear(_ callback: @escaping () -> Void) -> Self {
    _onAppear = callback
    return self
  }
  
  @discardableResult
  public func onDisappear(_ callback: @escaping () -> Void) -> Self {
    _onDisappear = callback
    return self
  }
  
  @inline(__always)
  private func updateState() {
    
    let current = state

    defer {
      if current != state {
        _stateUpdated(state)
        switch state {
        case .inView:
          _onAppear()
        case .outView:
          _onDisappear()
        }
      }
    }
    
    guard let window = self.view.window else {
      state = .outView
      return
    }
               
    let rect = view.convert(view.bounds, to: window)
        
    if window.frame.intersects(rect) {
      state = .inView
    } else {
      state = .outView
    }
      
  }
}

enum MainRoopActivityObserver {
  
  struct Subscription {
    let observer: CFRunLoopObserver?
  }
      
  static func addObserver(acitivity: CFRunLoopActivity, callback: @escaping () -> Void) -> Subscription {
    
    let o = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, acitivity.rawValue, true, 0, { observer, activity in
      callback()
    });
    
    CFRunLoopAddObserver(CFRunLoopGetMain(), o, CFRunLoopMode.defaultMode);
    
    return .init(observer: o)
  }
  
  static func remove(_ subscription: Subscription) {
    subscription.observer.map {
      CFRunLoopRemoveObserver(CFRunLoopGetMain(), $0, CFRunLoopMode.defaultMode);
    }
  }
  
}