BSGestureRecognizerForwarder
final class BSGestureRecognizerForwarder: NSObject {
weak var target: UIResponder?
var action: Selector?
init(target: UIResponder?, action: Selector?) {
self.target = target
self.action = action
}
@objc func swizzledAction(for sender: UIGestureRecognizer) {
guard let target = self.target, let action = self.action else {
return
}
if let gesture = sender as? UITapGestureRecognizer {
Logger.shared.track(UIControlEvent(date: .init(), object: self, action: action, params: [
"Action": action,
"Target": target,
"Event": "UITapGestureRecognizer",
"Gesture": gesture
]))
}
if target.responds(to: action), target.canPerformAction(action, withSender: nil) {
target.perform(action, with: nil)
}
}
}
struct UIResponderHooker {
static var touchesBeganIMP: IMP!
}
extension UIResponder {
private static let dispatchOnce: Void = {
if let method = class_getInstanceMethod(UIResponder.self, #selector(touchesBegan(_:with:))) {
UIResponderHooker.touchesBeganIMP = method_getImplementation(method)
}
switchSelector(#selector(UIResponder.touchesBegan(_:with:)), #selector(UIResponder.self_touchesBegan(_:with:)))
}()
@objc class func startupTracking() {
dispatchOnce
}
@objc func self_touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
typealias ClosureType = @convention(c) (UIResponder, Selector, Set<UITouch>, UIEvent?) -> Void
let touchesBegan: ClosureType = unsafeBitCast(UIResponderHooker.touchesBeganIMP, to: ClosureType.self)
touchesBegan(self, #selector(touchesBegan(_:with:)), touches, event)
guard !touches.isEmpty, let event = event else {
return
}
let className = NSStringFromClass(type(of: self))
let containsFilteredName: Bool = [
"AppDelegate", "UIApplication", "UIWindow", "UITransitionView",
"UIStackView", "UITableViewCellContentView", "UIDropShadowView"
].first(where: {
className.contains($0)
}) != nil
if className.hasPrefix("MobileDesign") ||
(!className.hasPrefix("_") && !containsFilteredName) {
Logger.shared.track(UIControlEvent(date: .init(), object: self, action: #selector(touchesBegan(_:with:)), params: [
"Touches": touches,
"Event": event
]))
}
}
}
extension UIResponder {
private enum ASKeys {
static var bsGestureActionsKey = "GestureActionsKey"
}
var bsGestureActions: [BSGestureRecognizerForwarder]? {
get {
objc_getAssociatedObject(self, &ASKeys.bsGestureActionsKey) as? [BSGestureRecognizerForwarder]
}
set {
objc_setAssociatedObject(self, &ASKeys.bsGestureActionsKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
}
extension UITapGestureRecognizer {
private static let dispatchOnce: Void = {
switchSelector(#selector(Self.init(target:action:)), #selector(Self.self_init(target:action:)))
switchSelector(#selector(Self.addTarget(_:action:)), #selector(Self.self_addTarget(_:action:)))
switchSelector(#selector(Self.removeTarget(_:action:)), #selector(Self.self_removeTarget(_:action:)))
}()
@objc class func startupTracking() {
guard self == UITapGestureRecognizer.self else {
return
}
dispatchOnce
}
@objc func self_init(target: Any?, action: Selector?) -> UIGestureRecognizer {
if let responder = target as? UIResponder {
if responder.bsGestureActions == nil {
responder.bsGestureActions = []
}
let forwarder = BSGestureRecognizerForwarder(target: responder, action: action)
responder.bsGestureActions?.append(forwarder)
return self_init(target: forwarder, action: #selector(forwarder.swizzledAction(for:)))
}
return self_init(target: target, action: action)
}
@objc func self_addTarget(_ target: Any, action: Selector) {
guard let responder = target as? UIResponder else {
return self_addTarget(target, action: action)
}
if responder.bsGestureActions == nil {
responder.bsGestureActions = []
}
let forwarder = BSGestureRecognizerForwarder(target: responder, action: action)
responder.bsGestureActions?.append(forwarder)
self_addTarget(forwarder, action: #selector(forwarder.swizzledAction(for:)))
}
@objc func self_removeTarget(_ target: Any?, action: Selector?) {
if let responder = target as? UIResponder,
let action = action,
let actions = responder.bsGestureActions {
for index in 0 ..< actions.count {
let ac = actions[index]
guard let bsTarget = ac.target, let bsAction = ac.action else {
continue
}
guard let responderClass = object_getClass(responder) else {
continue
}
let responderClassName = NSStringFromClass(responderClass)
guard let targetClass = object_getClass(bsTarget) else {
continue
}
let targetClassName = NSStringFromClass(targetClass)
if responderClassName == targetClassName, action.description == bsAction.description {
responder.bsGestureActions?.remove(at: index)
break
}
}
}
self_removeTarget(target, action: action)
}
}
extension UIControl {
private static let dispatchOnce: Void = {
switchSelector(#selector(UIControl.sendAction(_:to:for:)), #selector(UIControl.self_sendAction(_:to:for:)))
}()
@objc override class func startupTracking() {
dispatchOnce
}
@objc private func self_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
Logger.shared.track(UIControlEvent(date: .init(), object: self, action: action, params: [
"Action": action,
"Target": target ?? "empty-target",
"Event": event ?? "empty-event"
]))
self_sendAction(action, to: target, for: event)
}
}
extension UIViewController {
@objc override class func startupTracking() {
let selfViewWillAppearSelector = #selector(self_viewWillAppear(_:))
let orgViewWillAppearSelector = #selector(viewWillAppear(_:))
switchSelector(orgViewWillAppearSelector, selfViewWillAppearSelector)
let selfViewDidAppearSelector = #selector(self_viewDidAppear(_:))
let orgViewDidAppearSelector = #selector(viewDidAppear(_:))
switchSelector(orgViewDidAppearSelector, selfViewDidAppearSelector)
let selfViewWillLayoutSubviewsSelector = #selector(self_viewWillLayoutSubviews)
let orgViewWillLayoutSubviewsSelector = #selector(viewWillLayoutSubviews)
switchSelector(orgViewWillLayoutSubviewsSelector, selfViewWillLayoutSubviewsSelector)
let selfViewDidLayoutSubviewsSelector = #selector(self_viewDidLayoutSubviews)
let orgViewDidLayoutSubviewsSelector = #selector(viewDidLayoutSubviews)
switchSelector(orgViewDidLayoutSubviewsSelector, selfViewDidLayoutSubviewsSelector)
}
@objc private func self_viewWillAppear(_ animated: Bool) {
self_viewWillAppear(animated)
Logger.shared.track(UIViewControllerEvent(lifeCycle: .viewWillAppear, date: .init(), object: self, action: NSSelectorFromString(#function), params: [
"animated": animated
]))
}
@objc private func self_viewDidAppear(_ animated: Bool) {
self_viewDidAppear(animated)
Logger.shared.track(UIViewControllerEvent(lifeCycle: .viewDidAppear, date: .init(), object: self, action: NSSelectorFromString(#function), params: [
"animated": animated
]))
}
@objc private func self_viewWillLayoutSubviews() {
self_viewWillLayoutSubviews()
Logger.shared.track(UIViewControllerEvent(lifeCycle: .viewWillLayoutSubviews, date: .init(), object: self, action: NSSelectorFromString(#function), params: ""))
}
@objc private func self_viewDidLayoutSubviews() {
self_viewDidLayoutSubviews()
Logger.shared.track(UIViewControllerEvent(lifeCycle: .viewDidLayoutSubviews, date: .init(), object: self, action: NSSelectorFromString(#function), params: ""))
}
}
struct UIViewControllerEvent: TrackableEvent {
enum LifeCycle {
case viewDidLoad
case viewWillAppear, viewDidAppear
case viewWillLayoutSubviews, viewDidLayoutSubviews
}
var lifeCycle: LifeCycle
var date: Date
var object: CustomStringConvertible
var category: Logger.TrackingCategory {
switch lifeCycle {
case .viewWillAppear: return .display(subevent: .willAppear)
case .viewDidAppear: return .display(subevent: .didAppear)
case .viewWillLayoutSubviews: return .layout(subevent: .willLayoutSubviews)
case .viewDidLayoutSubviews: return .layout(subevent: .didLayoutSubviews)
default:
return .unspecified
}
}
var action: Selector
var params: CustomStringConvertible
}
struct UIControlEvent: TrackableEvent {
var date: Date
var object: CustomStringConvertible
var category: Logger.TrackingCategory {
guard let control = object as? UIControl else {
return .unspecified
}
switch control.allControlEvents {
case .touchUpInside:
return .touching(subevent: .upInside)
default:
return .touching(subevent: .any)
}
}
var action: Selector
var params: CustomStringConvertible
}
class AppDelegate: AppControllingDelegate {
private lazy var debugShortcutHandler = Bundle.entityBundleHasMultipleEnvironments ? DebugEnvironmentHandlingModel() : nil
lazy var deepLinkProvider = DeepLinkProvider()
lazy var appControllerRouter = AppControllerRouter(
featuresConfig: featuresConfig,
debugShortcutHandler: debugShortcutHandler,
deepLinkProvider: deepLinkProvider
)
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UIResponder.startupTracking()
UIControl.startupTracking()
UIViewController.startupTracking()
UITapGestureRecognizer.startupTracking()
URLSession.startTracking()
setup()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func makeAppController() -> AppControllerTemplate {
appControllerRouter.routingAppController
}
}
extension AppDelegate {
private func setup() {
let config = AppSetupConfiguration(
entityBundle: Bundle.firstEntityBundle,
featuresConfig: featuresConfig,
appInitializationStrategyFactory: appInitializationStrategyFactory,
deepLinkProvider: deepLinkProvider,
appLaunch: featuresConfig.preAppBundleInitConfig.appLaunch,
firebaseWrapper: firebaseWrapper,
debugShortcutHandler: debugShortcutHandler,
widgetRegistrationProvidings: widgetRegistrationProvidings,
keychainItemMigratorProvider: featuresConfig.preAppBundleInitConfig.keychainItemMigratorProvider
)
let steps = AppSetupStepsFactory.getDefaultSteps(config: config)
steps.executeAll()
appControllerRouter.route(to: .rasp)
}
}
extension AppDelegate: QuickActionShortCutsHandler {
public func resetQuickActionShortCut() {
if let quickActionShortcutStep = appControllerRouter.quickActionShortcutStep {
quickActionShortcutStep.execute()
}
}
}
extension URLSession {
static let sizzle: Void = {
let orgMethod = #selector(URLSession.shared.dataTask(with:completionHandler:) as ((URLRequest, @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask))
switchSelector(orgMethod, #selector(swizzle_dataTask(with:completionHandler:)))
}()
@objc class func startTracking() {
guard self == URLSession.self else {
return
}
sizzle
}
@objc private func swizzle_dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let requestDate = Date()
Logger.shared.track(NetworkEvent(object: self, date: requestDate, category: .network(subevent: .didSendRequest), action: NSSelectorFromString(#function), params: TrackableRequest(request: request)))
return swizzle_dataTask(with: request) { data, response, error in
if let response = response as? HTTPURLResponse {
let duration = fabs(requestDate.timeIntervalSinceNow)
let cate: Logger.TrackingCategory.Networking = error != nil
? .didReceiveResponseWithError
: .didReceiveResponse
Logger.shared.track(NetworkEvent(object: self, date: .init(), category: .network(subevent: cate), action: NSSelectorFromString(#function), params: TrackableResponse(duration: duration, data: data, response: response, error: error)))
}
completionHandler(data, response, error)
}
}
}
extension NSObject {
static func switchSelector(_ originalSelector: Selector, _ swizzledSelector: Selector) {
guard
let originalMethod = class_getInstanceMethod(self, originalSelector),
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) else {
return
}
let didAddMethod: Bool = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
struct TrackableRequest: Requestable, CustomStringConvertible {
let request: URLRequest
init(request: URLRequest) {
self.request = request
}
public var urlString: String {
request.url?.absoluteString ?? "empty-url"
}
public var headers: [String: String] {
request.allHTTPHeaderFields ?? [:]
}
public var methodString: String {
request.httpMethod ?? "empty-method"
}
var description: String {
"[Method: \(methodString), URL: \(urlString), Headers: \(headers)]"
}
}
struct TrackableResponse: Responsable, CustomStringConvertible {
var duration: Double
var data: Data?
var response: HTTPURLResponse
var error: Error?
public var urlString: String {
response.url?.absoluteString ?? "empty-url"
}
public var errorString: String? {
error?.localizedDescription
}
public var statusCode: Int {
response.statusCode
}
private let NF: NumberFormatter = {
let nf = NumberFormatter()
nf.maximumFractionDigits = 2
return nf
}()
var description: String {
let duration = NF.string(from: .init(value: duration)) ?? "NaN"
let base = "[Duration: \(duration)s, Status: \(statusCode), URL: \(urlString)"
if let error = errorString {
return base + ", Error: \(error)]"
}
if let data = data {
return base + ", Data: \(data.description)]"
}
return base
}
}
public struct NetworkEvent: TrackableEvent {
public var object: CustomStringConvertible
public var date: Date
public var category: Logger.TrackingCategory
public var action: Selector
public var params: CustomStringConvertible
}
let df = DateFormatter()
public struct Logger {
public static var shared = Logger()
private static let oslog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "no_bundle_id", category: "UITracking")
var savePath: String {
df.locale = Calendar.current.locale
df.dateFormat = "yyyy-MM-dd"
let name = "BS_Track_Log_" + df.string(from: .init()) + ".log"
return "\(NSHomeDirectory())/Documents/\(name)"
}
var saveURL: URL {
.init(fileURLWithPath: savePath)
}
lazy var logsString: String = {
if FileManager.default.fileExists(atPath: savePath),
let data = try? Data(contentsOf: saveURL),
let logs = String(data: data, encoding: .utf8) {
return logs
}
return ""
}()
public mutating func track(_ event: TrackableEvent) {
if !logsString.isEmpty {
logsString += "\n" + event.description
} else {
logsString = event.description
}
// write to system logs
var logType = OSLogType.info
if event.category == .network(subevent: .didReceiveResponseWithError) {
logType = .error
}
os_log(logType, log: Logger.oslog, "%{public}@", event.description)
#if DEBUG
print("BS_Track_Log path: \(savePath)")
print(logsString)
#endif
// write to sandbox logs
guard let data = logsString.data(using: .utf8) else {
return
}
do {
try data.write(to: saveURL, options: .atomic)
} catch {
print(error)
}
}
}
public extension Logger {
enum TrackingCategory {
case unspecified
case tracking(subevent: Tracking)
case touching(subevent: Touching)
case display(subevent: Displaying)
case layout(subevent: Layouting)
case network(subevent: Networking)
}
}
extension Logger.TrackingCategory: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.unspecified, .unspecified),
(.tracking(.start), .tracking(.start)),
(.tracking(.end), .tracking(.end)),
(.touching(.any), .touching(.any)),
(.display(.willAppear), .display(.willAppear)),
(.display(.didAppear), .display(.didAppear)),
(.layout(.willLayoutSubviews), .layout(.willLayoutSubviews)),
(.layout(.didLayoutSubviews), .layout(.didLayoutSubviews)),
(.network(.didSendRequest), .network(.didSendRequest)),
(.network(.didReceiveResponse), .network(.didReceiveResponse)),
(.network(.didReceiveResponseWithError), .network(.didReceiveResponseWithError)):
return true
default: return false
}
}
}
public extension Logger.TrackingCategory {
enum Tracking {
case start, end
}
enum Touching {
case any, upInside
}
enum Displaying {
case willAppear, didAppear
}
enum Layouting {
case willLayoutSubviews
case didLayoutSubviews
}
enum Networking {
case didSendRequest
case didReceiveResponse
case didReceiveResponseWithError
}
}
extension Logger.TrackingCategory: CustomStringConvertible {
public var description: String {
switch self {
case .unspecified: return "Unspecified"
case .tracking(let subevent):
switch subevent {
case .start: return "Tracking->Start"
case .end: return "Tracking->End"
}
case .touching(let subevent):
switch subevent {
case .any: return "Touching->Any"
case .upInside: return "Touching->UpInside"
}
case .layout(let subevent):
switch subevent {
case .willLayoutSubviews:
return "Layout->WillLayoutSubviews"
case .didLayoutSubviews:
return "Layout->DidLayoutSubviews"
}
case .display(let subevent):
switch subevent {
case .willAppear:
return "Display->WillAppear"
case .didAppear:
return "Display->DidAppear"
}
case .network(let subevent):
switch subevent {
case .didSendRequest:
return "Network->DidSendRequest"
case .didReceiveResponse:
return "Network->DidReceiveResponse"
case .didReceiveResponseWithError:
return "Network->DidReceiveResponseWithError"
}
}
}
}
public protocol Requestable {
var headers: [String: String] { get }
var urlString: String { get }
var methodString: String { get }
}
public protocol Responsable {
var urlString: String { get }
var errorString: String? { get }
var statusCode: Int { get }
}
public protocol TrackableEvent {
var category: Logger.TrackingCategory { get }
var object: CustomStringConvertible { get set }
var date: Date { get set }
var action: Selector { get set }
var params: CustomStringConvertible { get set }
}
extension TrackableEvent {
public var comment: String {
switch category {
case .unspecified: return "An undefined event was fired"
case .tracking(let subevent):
switch subevent {
case .start: return "A test case tracking is started"
case .end: return "A test case tracking is ended"
}
case .touching(let subevent):
switch subevent {
case .any: return "An any touch event was fired"
case .upInside: return "An user touch up inside event was fired"
}
case .layout(let subevent):
switch subevent {
case .willLayoutSubviews:
return "A page will layout subviews"
case .didLayoutSubviews:
return "A page finished layouting subviews"
}
case .display(let subevent):
switch subevent {
case .willAppear:
return "A page will be displayed on screen"
case .didAppear:
return "A page is displayed on screen"
}
case .network(let subevent):
switch subevent {
case .didSendRequest:
return "An HTTP request was sent"
case .didReceiveResponse:
return "An HTTP response is received"
case .didReceiveResponseWithError:
return "An HTTP response is received with error"
}
}
}
}
extension TrackableEvent {
public var description: String {
df.locale = Calendar.current.locale
df.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
let dateString = df.string(from: date)
return "[\(dateString)] [\(comment)] [\(category)] [\(object)->\(action)->\(params)]"
}
}
#!/bin/bash
components=(
ActiveInactiveLabel
)
num=0
echo "" > GDLRefs.txt
echo "Pulling git updates..."
git pull
for component in ${components[*]}
do
num=`expr $num + 1`
echo "Searching for $component: $num/${#components[*]}"
echo "$component: " >> GDLRefs.txt
refs=`find iOS -name *.swift -exec grep $component -rlnws {} \;`
refs=`echo $refs | tr -d '[ ]'`
refs=${refs//iOS/ iOS}
for ref in ${refs[*]}
do
echo $ref
echo $ref >> GDLRefs.txt
done
echo " " >> GDLRefs.txt
done
echo "Done of searchings."