Sources/Harlow/Controller/Debugger.swift
//
// Debugger.swift
//
// The MIT License (MIT)
//
// Copyright (c) 2018 Stanwood GmbH (www.stanwood.io)
//
// 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 Foundation
import UIKit
protocol Debugging: class {
var isEnabled: Bool { get set }
var isDisplayed: Bool { get set }
var isDebuggingDataPersistenceEneabled: Bool { get set }
}
/// Harlow acts as the framework controller, delegating logs
public class Harlow: Debugging {
/// Supported Services
public enum Service: CaseIterable {
case logs, networking, errors, crashes, analytics
}
struct Style {
private init () {}
static var tintColor: UIColor = UIColor(r: 210, g: 78, b: 79)
static let defaultColor: UIColor = UIColor(r: 51, g: 51, b: 51)
}
/// Debugger tintColor
public var tintColor: UIColor {
get { return Style.tintColor }
set { Style.tintColor = newValue; configureStyle() }
}
/// Error code exceptions
public var errorCodesExceptions: [Int] = [] {
didSet {
DebuggerNSError.errorCodesExceptions = errorCodesExceptions
}
}
/// Set to `true` to enable data persistence of services logs
public var isDebuggingDataPersistenceEneabled: Bool = false {
didSet {
appData.isDebuggingDataPersistenceEneabled = self.isDebuggingDataPersistenceEneabled
}
}
/// Enabled Services: `default = .all`
/// Note> You must call `isEnabled = true` to enable the debugger
public var enabledServices: [Service] = Service.allCases {
didSet {
self.isEnabled = isEnabled ? true : false
}
}
/// Enable Debugger View
public var isEnabled: Bool = false {
didSet {
DebuggerLogs.isEnabled = enabledServices.contains(.logs) ? isEnabled : false
DebuggerNetworking.isEnabled = enabledServices.contains(.networking) ? isEnabled : false
DebuggerNSError.isEnabled = enabledServices.contains(.errors) ? isEnabled : false
DebuggerCrash.isEnabled = enabledServices.contains(.crashes) ? isEnabled : false
configureDebuggerView()
}
}
/// Enable Shake to enable / disable
public var isShakeEnabled: Bool = true {
didSet {
if isShakeEnabled { UIApplication.swizzleEvents() }
else { UIApplication.unswizzleEvents() }
}
}
var isDisplayed: Bool = false
fileprivate var debuggerViewController: DebuggerViewController?
private let window: DebuggerWindow
private let coordinator: DebuggerCoordinator
private let actions: DebuggerActions
private let appData: DebuggerData
private let paramaters: DebuggerParamaters
private let debuggerNetworking: DebuggerNetworking
public init() {
window = DebuggerWindow(frame: UIScreen.main.bounds)
appData = DebuggerData()
paramaters = DebuggerParamaters(appData: appData)
actions = DebuggerActions(appData: appData)
debuggerNetworking = DebuggerNetworking()
coordinator = DebuggerCoordinator(window: window, actionable: actions, paramaterable: paramaters)
actions.coordinator = coordinator
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didDismissFullscreen), name: NSNotification.Name.DebuggerDidDismissFullscreen, object: nil)
configureStyle()
observeCrashes()
// Shake to enable
UIApplication.swizzleEvents()
NotificationCenter.default.addObserver(forName: NSNotification.Name.Shake, object: nil, queue: .main) { [weak self] (_) in
self?.isEnabled.toggle()
}
}
/**
Register custom `URLSessionConfiguration`
### Example Usage
```swift
let configuration = URLSessionConfiguration.waitsForConnectivity
debugger.regsiter(custom: configuration)
/// Use with URLSession || any networking libraries such as Alamofire and Moya
let session = URLSession(configuration: configuration)
```
- Parameters:
- configuration: URLSessionConfiguration
- SeeAlso: `DebuggerNetworking`
*/
public func regsiter(custom configuration: URLSessionConfiguration) {
DebuggerNetworking.register(custom: configuration)
}
@objc func applicationDidEnterBackground() {
appData.save()
}
private func configureStyle() {
window.tintColor = Style.tintColor
}
private func observeCrashes() {
DebuggerCrash.crashCompletion = {
[unowned self] in
self.appData.save()
}
}
private func configureDebuggerView() {
switch isEnabled {
case true:
if debuggerViewController == nil {
debuggerViewController = DebuggerWireframe.makeViewController()
DebuggerWireframe.prepare(debuggerViewController, with: actions, paramaters, self)
window.rootViewController = debuggerViewController
window.makeKeyAndVisible()
window.delegate = self
}
window.isHidden = false
window.isUserInteractionEnabled = true
if DebuggerCrash.didReceiveCrash {
DebuggerCrash.didReceiveCrash = false
NotificationCenter.default.post(name: NSNotification.Name.DebuggerDidAddDebuggerItem, object: [AddedItem(type: .crashes(item: nil), count: 1)])
}
case false:
window.isHidden = true
window.isUserInteractionEnabled = false
}
}
@objc private func didDismissFullscreen() {
isDisplayed = false
}
}
extension Harlow: DebuggerWindowDelegate {
func isPoint(inside point: CGPoint) -> Bool {
return debuggerViewController?.shouldHandle(point) ?? false
}
}
extension UIApplication {
private static var shakeEnded = true // Events are called twice, only respond to the ended state
fileprivate class func swizzleEvents() {
guard
let eventCall = class_getInstanceMethod(self, #selector(sendEvent(_:))),
let eventSwizzleCall = class_getInstanceMethod(self, #selector(eventHack(_:)))
else { return }
method_exchangeImplementations(eventCall, eventSwizzleCall)
}
fileprivate class func unswizzleEvents() {
guard
let eventCall = class_getInstanceMethod(self, #selector(sendEvent(_:))),
let eventSwizzleCall = class_getInstanceMethod(self, #selector(eventHack(_:)))
else { return }
method_exchangeImplementations(eventSwizzleCall, eventCall)
}
@objc private func eventHack(_ event: UIEvent) {
defer { eventHack(event) }
if event.type == .motion && event.subtype == .motionShake {
UIApplication.shakeEnded.toggle()
if UIApplication.shakeEnded {
NotificationCenter.default.post(name: Notification.Name.Shake, object: nil)
}
}
}
}