// // RectangleView.swift // WeScan // // Created by Boris Emorine on 2/8/18. // Copyright © 2018 WeTransfer. All rights reserved. // import UIKit import AVFoundation /// Simple enum to keep track of the position of the corners of a quadrilateral. enum CornerPosition { case topLeft case topRight case bottomRight case bottomLeft } /// The `QuadrilateralView` is a simple `UIView` subclass that can draw a quadrilateral, and optionally edit it. final class QuadrilateralView: UIView { private let quadLayer: CAShapeLayer = { let layer = CAShapeLayer() layer.strokeColor = UIColor.white.cgColor layer.lineWidth = 1.0 layer.opacity = 1.0 layer.isHidden = true return layer }() /// We want the corner views to be displayed under the outline of the quadrilateral. /// Because of that, we need the quadrilateral to be drawn on a UIView above them. private let quadView: UIView = { let view = UIView() view.backgroundColor = UIColor.clear view.translatesAutoresizingMaskIntoConstraints = false return view }() /// The quadrilateral drawn on the view. private(set) var quad: Quadrilateral? public var editable = false { didSet { editable == true ? showCornerViews() : hideCornerViews() quadLayer.fillColor = editable ? UIColor(white: 0.0, alpha: 0.6).cgColor : UIColor(white: 1.0, alpha: 0.5).cgColor guard let quad = quad else { return } drawQuad(quad, animated: false) layoutCornerViews(forQuad: quad) } } private var isHighlighted = false { didSet (oldValue) { guard oldValue != isHighlighted else { return } quadLayer.fillColor = isHighlighted ? UIColor.clear.cgColor : UIColor(white: 0.0, alpha: 0.6).cgColor isHighlighted ? bringSubviewToFront(quadView) : sendSubviewToBack(quadView) } } lazy private var topLeftCornerView: EditScanCornerView = { return EditScanCornerView(frame: CGRect(x: 0.0, y: 0.0, width: cornerViewSize, height: cornerViewSize), position: .topLeft) }() lazy private var topRightCornerView: EditScanCornerView = { return EditScanCornerView(frame: CGRect(x: 0.0, y: 0.0, width: cornerViewSize, height: cornerViewSize), position: .topRight) }() lazy private var bottomRightCornerView: EditScanCornerView = { return EditScanCornerView(frame: CGRect(x: 0.0, y: 0.0, width: cornerViewSize, height: cornerViewSize), position: .bottomRight) }() lazy private var bottomLeftCornerView: EditScanCornerView = { return EditScanCornerView(frame: CGRect(x: 0.0, y: 0.0, width: cornerViewSize, height: cornerViewSize), position: .bottomLeft) }() private let highlightedCornerViewSize: CGFloat = 75.0 private let cornerViewSize: CGFloat = 20.0 // MARK: - Life Cycle override init(frame: CGRect) { super.init(frame: frame) commonInit() } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func commonInit() { addSubview(quadView) setupCornerViews() setupConstraints() quadView.layer.addSublayer(quadLayer) } private func setupConstraints() { let quadViewConstraints = [ quadView.topAnchor.constraint(equalTo: topAnchor), quadView.leadingAnchor.constraint(equalTo: leadingAnchor), bottomAnchor.constraint(equalTo: quadView.bottomAnchor), trailingAnchor.constraint(equalTo: quadView.trailingAnchor) ] NSLayoutConstraint.activate(quadViewConstraints) } private func setupCornerViews() { addSubview(topLeftCornerView) addSubview(topRightCornerView) addSubview(bottomRightCornerView) addSubview(bottomLeftCornerView) } override public func layoutSubviews() { super.layoutSubviews() guard quadLayer.frame != bounds else { return } quadLayer.frame = bounds if let quad = quad { drawQuadrilateral(quad: quad, animated: false) } } // MARK: - Drawings /// Draws the passed in quadrilateral. /// /// - Parameters: /// - quad: The quadrilateral to draw on the view. It should be in the coordinates of the current `QuadrilateralView` instance. func drawQuadrilateral(quad: Quadrilateral, animated: Bool) { self.quad = quad drawQuad(quad, animated: animated) if editable { showCornerViews() layoutCornerViews(forQuad: quad) } } private func drawQuad(_ quad: Quadrilateral, animated: Bool) { var path = quad.path() if editable { path = path.reversing() let rectPath = UIBezierPath(rect: bounds) path.append(rectPath) } if animated == true { let pathAnimation = CABasicAnimation(keyPath: "path") pathAnimation.duration = 0.2 quadLayer.add(pathAnimation, forKey: "path") } quadLayer.path = path.cgPath quadLayer.isHidden = false } private func layoutCornerViews(forQuad quad: Quadrilateral) { topLeftCornerView.center = quad.topLeft topRightCornerView.center = quad.topRight bottomLeftCornerView.center = quad.bottomLeft bottomRightCornerView.center = quad.bottomRight } func removeQuadrilateral() { quadLayer.path = nil quadLayer.isHidden = true } // MARK: - Actions func moveCorner(cornerView: EditScanCornerView, atPoint point: CGPoint) { guard let quad = quad else { return } let validPoint = self.validPoint(point, forCornerViewOfSize: cornerView.bounds.size, inView: self) cornerView.center = validPoint let updatedQuad = update(quad, withPosition: validPoint, forCorner: cornerView.position) self.quad = updatedQuad drawQuad(updatedQuad, animated: false) } func highlightCornerAtPosition(position: CornerPosition, with image: UIImage) { guard editable else { return } isHighlighted = true let cornerView = cornerViewForCornerPosition(position: position) guard cornerView.isHighlighted == false else { cornerView.highlightWithImage(image) return } cornerView.frame = CGRect(x: cornerView.frame.origin.x - (highlightedCornerViewSize - cornerViewSize) / 2.0, y: cornerView.frame.origin.y - (highlightedCornerViewSize - cornerViewSize) / 2.0, width: highlightedCornerViewSize, height: highlightedCornerViewSize) cornerView.highlightWithImage(image) } func resetHighlightedCornerViews() { isHighlighted = false resetHighlightedCornerViews(cornerViews: [topLeftCornerView, topRightCornerView, bottomLeftCornerView, bottomRightCornerView]) } private func resetHighlightedCornerViews(cornerViews: [EditScanCornerView]) { cornerViews.forEach { (cornerView) in resetHightlightedCornerView(cornerView: cornerView) } } private func resetHightlightedCornerView(cornerView: EditScanCornerView) { cornerView.reset() cornerView.frame = CGRect(x: cornerView.frame.origin.x + (cornerView.frame.size.width - cornerViewSize) / 2.0, y: cornerView.frame.origin.y + (cornerView.frame.size.width - cornerViewSize) / 2.0, width: cornerViewSize, height: cornerViewSize) cornerView.setNeedsDisplay() } // MARK: Validation /// Ensures that the given point is valid - meaning that it is within the bounds of the passed in `UIView`. /// /// - Parameters: /// - point: The point that needs to be validated. /// - cornerViewSize: The size of the corner view representing the given point. /// - view: The view which should include the point. /// - Returns: A new point which is within the passed in view. private func validPoint(_ point: CGPoint, forCornerViewOfSize cornerViewSize: CGSize, inView view: UIView) -> CGPoint { var validPoint = point if point.x > view.bounds.width { validPoint.x = view.bounds.width } else if point.x < 0.0 { validPoint.x = 0.0 } if point.y > view.bounds.height { validPoint.y = view.bounds.height } else if point.y < 0.0 { validPoint.y = 0.0 } return validPoint } // MARK: - Convenience private func hideCornerViews() { topLeftCornerView.isHidden = true topRightCornerView.isHidden = true bottomRightCornerView.isHidden = true bottomLeftCornerView.isHidden = true } private func showCornerViews() { topLeftCornerView.isHidden = false topRightCornerView.isHidden = false bottomRightCornerView.isHidden = false bottomLeftCornerView.isHidden = false } private func update(_ quad: Quadrilateral, withPosition position: CGPoint, forCorner corner: CornerPosition) -> Quadrilateral { var quad = quad switch corner { case .topLeft: quad.topLeft = position case .topRight: quad.topRight = position case .bottomRight: quad.bottomRight = position case .bottomLeft: quad.bottomLeft = position } return quad } func cornerViewForCornerPosition(position: CornerPosition) -> EditScanCornerView { switch position { case .topLeft: return topLeftCornerView case .topRight: return topRightCornerView case .bottomLeft: return bottomLeftCornerView case .bottomRight: return bottomRightCornerView } } }