stanwood/Stanwood_Debugger_iOS

View on GitHub
Sources/Harlow/Modules/Bubble/Views/DebuggerUIButton.swift

Summary

Maintainability
A
3 hrs
Test Coverage
//
//  DebuggerUIButton.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
import Pulsator


class DebuggerUIButton: UIButton {
    
    private enum Positions {
        case topLeft, topRight, centerLeft, centerRight, bottomLeft, bottomRight
        
        static var buttonSize: CGSize {
            return CGSize(width: 65, height: 65)
        }
        
        static func position(for point: CGPoint) -> Positions {
            if Positions.centerLeft.rect.contains(point) {
                return .centerLeft
            } else if Positions.centerRight.rect.contains(point) {
                return .centerRight
            } else if Positions.topLeft.rect.contains(point) {
                return .topLeft
            } else if Positions.topRight.rect.contains(point) {
                return .topRight
            } else if Positions.bottomLeft.rect.contains(point) {
                return .bottomLeft
            } else if Positions.bottomRight.rect.contains(point) {
                return .bottomRight
            } else {
                return .centerLeft
            }
        }
        
        var rect: CGRect {
            let screenSize = UIScreen.main.bounds.size
            let size = CGSize(width: screenSize.width / 2, height: screenSize.height / 3)
            
            switch self {
            case .topLeft:
                let origin = CGPoint(x: 0, y: 0)
                let rect = CGRect(origin: origin, size: size)
                return rect
            case .topRight:
                let origin = CGPoint(x: size.width, y: 0)
                let rect = CGRect(origin: origin, size: size)
                return rect
            case .centerLeft:
                let origin = CGPoint(x: 0, y: size.height)
                let rect = CGRect(origin: origin, size: size)
                return rect
            case .centerRight:
                let origin = CGPoint(x: size.width, y: size.height)
                let rect = CGRect(origin: origin, size: size)
                return rect
            case .bottomLeft:
                let origin = CGPoint(x: 0, y: size.height * 2)
                let rect = CGRect(origin: origin, size: size)
                return rect
            case .bottomRight:
                let origin = CGPoint(x: size.width, y: size.height * 2)
                let rect = CGRect(origin: origin, size: size)
                return rect
            }
        }
        
        var origin: CGPoint {
            let screenSize = UIScreen.main.bounds.size
            let leftX: CGFloat = screenSize.width - (10 + Positions.buttonSize.width / 2)
            let rightX: CGFloat = 10 + Positions.buttonSize.width / 2
            let topY: CGFloat = 70 + Positions.buttonSize.height / 2
            let bottomY: CGFloat = screenSize.height - (60 + Positions.buttonSize.height / 2)
            
            switch self {
            case .topLeft:
                return CGPoint(x: rightX, y: topY)
            case .topRight:
                return CGPoint(x: leftX, y: topY)
            case .centerLeft:
                return CGPoint(x: rightX, y: screenSize.height / 2)
            case .centerRight:
                return CGPoint(x: leftX, y: screenSize.height / 2)
            case .bottomLeft:
                return CGPoint(x: rightX, y: bottomY)
            case .bottomRight:
                return CGPoint(x: leftX, y: bottomY)
            }
        }
    }
    
    private lazy var pulsator: Pulsator = Pulsator()
    
    var isPulseEnabled: Bool = false {
        didSet {
            switch isPulseEnabled {
            case true: pulsator.start()
            case false: pulsator.stop()
            }
        }
    }
    
    private let debugger: Debugging
    
    init(debugger: Debugging) {
        
        self.debugger = debugger
        
        super.init(frame: CGRect(origin: Positions.centerLeft.origin, size: Positions.buttonSize))
        center = Positions.centerLeft.origin
        
        layer.cornerRadius = Positions.buttonSize.width / 2
        
        let image = UIImage(named: "icon_virus", in: Bundle.debuggerBundle(), compatibleWith: nil)
        setImage(image, for: .normal)
        tintColor = .white
        setTitleColor(.white, for: .normal)
        backgroundColor = Harlow.Style.tintColor
        
        let pan = UIPanGestureRecognizer(target: self, action: #selector(panning(_:)))
        addGestureRecognizer(pan)
        
        NotificationCenter.default.addObservers(self, observers: Observer(selector: #selector(didAddDebuggerItem(_:)), name: Notification.Name.DebuggerDidAddDebuggerItem))
    }
    
    func preparePulse() {
        guard DebuggerSettings.isDebuggerBubblePulseAnimationEnabled else { return }
        pulsator.backgroundColor = Harlow.Style.tintColor.cgColor
        pulsator.radius = Positions.buttonSize.width * 0.875
        pulsator.numPulse = 3
        pulsator.animationDuration = 3
        pulsator.position = center
        
        superview?.layer.insertSublayer(pulsator, below: layer)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @objc func panning(_ pan: UIPanGestureRecognizer) {
        
        let translation = pan.translation(in: superview)
        pan.setTranslation(.zero, in: superview)
        
        var center = self.center
        center.x += translation.x
        center.y += translation.y
        self.center = center
        pulsator.position = center
        
        switch pan.state {
        case .ended, .cancelled:
            let position = Positions.position(for: center)
            pulsator.position = position.origin
            
            DispatchQueue.main.async {
                FeedbackGenerator.generate(style: .light)
            }
            
            UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.3, options: .curveEaseInOut, animations: {
                self.center = position.origin
                self.transform = .identity
            }, completion: { _ in
                
                UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options: .curveEaseOut, animations: {
                    self.pulsator.opacity = 1
                }, completion: nil)
            })
        case .began:
            
            UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
                self.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
                self.pulsator.opacity = 0
            }, completion: nil)
        default: break
        }
    }
    
    @objc private func didAddDebuggerItem(_ notification: Notification) {
        guard let addedItems = notification.object as? [AddedItem] else { return }
        
        addedItems.forEach({
            switch $0.type {
            case .analytics: animate(.analytics)
            case .networking: animate(.networking)
            case .error: animate(.error)
            case .logs: animate(.logs)
            case .crashes: animate(.crashes)
            }
        })
    }
    
    private func animate(_ icon: DebuggerIconLabel.DebuggerIcons) {
        
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
            [weak self]  in
            guard let `self` = self else { return }
            
            // Generating vibration feedback
            FeedbackGenerator.generate(style: .light)
            
            // Animating the icon
            guard !self.debugger.isDisplayed, DebuggerSettings.isDebuggerItemIconsAnimationEnabled else { return }
            let label = DebuggerIconLabel(icon: icon,frame: CGRect(x: 0, y: 0, width: 60, height: 60))
            label.center = CGPoint(x: self.center.x + 20, y: self.center.y)
            self.superview?.insertSubview(label, belowSubview: self)
            
            let point = CGPoint(x: self.center.x, y: self.center.y - 250)
            label.animate(to: point)
        }
    }
}