// // QuadrantControl.swift // VerticalSlider // // Created by Claude RICAUD on 23/09/2020. // import UIKit @IBDesignable class QuadrantControl: UIControl { var actionToExecute: Int = 0 // 1 to 4 for buttons, 5 and 6 for up and down ; 0 if no action /// These values can be set in our storyboard // Overall size is defined when creating @IBInspectable public var quadrantBackColor: UIColor = .green // .blue @IBInspectable public var linesColor: UIColor = UIColor.black @IBInspectable public var quadrantHiliteColor: UIColor = UIColor.red // .red // Quadrant when hilited @IBInspectable public var centerColor: UIColor = UIColor.lightGray // .red // Center up and down @IBInspectable public var linesThickness: CGFloat = 2 @IBInspectable public var centerControlSize: CGSize = CGSize(width: 40, height: 40) @IBInspectable public var nwButtonLabel : String = "NW" // NorthWest @IBInspectable public var neButtonLabel : String = "NE" // NorthEast @IBInspectable public var seButtonLabel : String = "SE" // SouthEast @IBInspectable public var swButtonLabel : String = "SW" // SouthWest private var circleOuterRadius : CGFloat { return bounds.size.width / 2.0 } private var circleInnerFrame : CGRect { bounds.insetBy(dx: linesThickness, dy: linesThickness) } private var circleInnerRadius : CGFloat { return circleOuterRadius - linesThickness } private var circleCenter : CGPoint { return CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) } private var top : CGPoint { CGPoint(x: circleCenter.x, y: linesThickness) } private var right : CGPoint { CGPoint(x: circleInnerFrame.maxX, y: circleCenter.y) } private var bottom : CGPoint { CGPoint(x: circleCenter.x, y: circleInnerFrame.maxY) } private var left : CGPoint { CGPoint(x: circleInnerFrame.minX, y: circleCenter.y) } private var innerTop : CGPoint { CGPoint(x: circleCenter.x, y: linesThickness) } private var innerRight : CGPoint { CGPoint(x: circleInnerFrame.maxX, y: circleCenter.y) } private var innerBottom : CGPoint { CGPoint(x: circleCenter.x, y: circleInnerFrame.maxY) } private var innerLeft : CGPoint { CGPoint(x: circleInnerFrame.minX, y: circleCenter.y) } private var topCenterControl : CGFloat { circleCenter.y - centerControlSize.height/2 } private var bottomCenterControl : CGFloat { circleCenter.y + centerControlSize.height/2 } private var centerControlRect : CGRect { let controlHalfWidth = centerControlSize.width / 2.0 return CGRect(x: circleCenter.x-controlHalfWidth, y: topCenterControl, width: centerControlSize.width, height: centerControlSize.height) } // Those rect should be computed according to label effective width var nwButtonRect : CGRect { return CGRect(x: circleCenter.x/2.0 - 5 * CGFloat(nwButtonLabel.count), y: topCenterControl - 12, width: 10*CGFloat(nwButtonLabel.count), height: 20) } var neButtonRect : CGRect { return CGRect(x: 3*circleCenter.x/2.0 - 5 * CGFloat(nwButtonLabel.count), y: topCenterControl - 12, width: 10*CGFloat(nwButtonLabel.count), height: 20) } var seButtonRect : CGRect { return CGRect(x: 3*circleCenter.x/2.0 - 5 * CGFloat(nwButtonLabel.count), y: bottomCenterControl, width: 10*CGFloat(nwButtonLabel.count), height: 20) } var swButtonRect : CGRect { return CGRect(x: circleCenter.x/2.0 - 5 * CGFloat(nwButtonLabel.count), y: bottomCenterControl, width: 10*CGFloat(nwButtonLabel.count), height: 20) } enum Quadrant { case nw, ne, se, sw, all var action: Int { switch self { case .nw: return 1 case .ne: return 2 case .se: return 3 case .sw: return 4 default: return 0 } } } enum Direction { case up, down, both var action: Int { switch self { case .up: return 5 case .down: return 6 default: return 0 } } } private var touchedQuadrant : Quadrant = .all private var updatedTouchedQuadrant : Quadrant = .all private var hilitedQuadrant: Bool = false private var touchedArrow : Direction = .both private var hilitedArrow: Bool = false // MARK: - Init override init(frame: CGRect) { super.init(frame: frame) contentMode = .redraw self.backgroundColor = .clear } required init?(coder: NSCoder) { super.init(coder: coder) self.isUserInteractionEnabled = true contentMode = .redraw self.backgroundColor = .clear } // MARK: - Draw // --------------------- drawQuadrants ---------------------------------------------------- // Description: Draw circle with 4 quadrants and separation lines, or a single quadrant, which may be hilited // Parameters // aQuadrant: Int? = nil to fill a selected quadrant or all // Comments : // ------------------------------------------------------------------------------------------------- func drawQuadrants(aQuadrant: Quadrant = .all) { let color : UIColor = hilitedQuadrant ? quadrantHiliteColor : quadrantBackColor let circlePath = UIBezierPath(ovalIn: circleInnerFrame) color.setFill() // Let us create the 4 quarter circles paths let nwQuadrantPath = UIBezierPath() // From top to center to left nwQuadrantPath.move(to: CGPoint(x: circleCenter.x, y: top.y)) nwQuadrantPath.addLine(to: CGPoint(x: circleCenter.x, y: circleCenter.y)) nwQuadrantPath.addLine(to: CGPoint(x: left.x, y: circleCenter.y)) let nwArcPath = UIBezierPath(arcCenter: circleCenter, radius: circleInnerRadius, startAngle: -.pi, endAngle: -.pi/2, clockwise: true) nwQuadrantPath.append(nwArcPath) nwQuadrantPath.close() let neQuadrantPath = UIBezierPath() // From top to center to right neQuadrantPath.move(to: CGPoint(x: circleCenter.x, y: top.y)) neQuadrantPath.addLine(to: CGPoint(x: circleCenter.x, y: circleCenter.y)) neQuadrantPath.addLine(to: CGPoint(x: right.x, y: circleCenter.y)) let neArcPath = UIBezierPath(arcCenter: circleCenter, radius: circleInnerRadius, startAngle: -.pi/2, endAngle: 0, clockwise: true) neQuadrantPath.append(neArcPath) neQuadrantPath.close() let seQuadrantPath = UIBezierPath() // From bottom to center to right seQuadrantPath.move(to: CGPoint(x: circleCenter.x, y: bottom.y)) seQuadrantPath.addLine(to: CGPoint(x: circleCenter.x, y: circleCenter.y)) seQuadrantPath.addLine(to: CGPoint(x: right.x, y: circleCenter.y)) let seArcPath = UIBezierPath(arcCenter: circleCenter, radius: circleInnerRadius, startAngle: 0, endAngle: .pi/2, clockwise: true) seQuadrantPath.append(seArcPath) seQuadrantPath.close() let swQuadrantPath = UIBezierPath() // From bottom to center to left swQuadrantPath.move(to: CGPoint(x: circleCenter.x, y: bottom.y)) swQuadrantPath.addLine(to: CGPoint(x: circleCenter.x, y: circleCenter.y)) swQuadrantPath.addLine(to: CGPoint(x: left.x, y: circleCenter.y)) let swArcPath = UIBezierPath(arcCenter: circleCenter, radius: circleInnerRadius, startAngle: .pi/2, endAngle: .pi, clockwise: true) swQuadrantPath.append(swArcPath) swQuadrantPath.close() switch aQuadrant { case .all : // Draw the full circle circlePath.fill() case .nw : nwQuadrantPath.fill() quadrantBackColor.setFill() neQuadrantPath.fill() seQuadrantPath.fill() swQuadrantPath.fill() case .ne : neQuadrantPath.fill() quadrantBackColor.setFill() nwQuadrantPath.fill() seQuadrantPath.fill() swQuadrantPath.fill() case .se : seQuadrantPath.fill() quadrantBackColor.setFill() nwQuadrantPath.fill() neQuadrantPath.fill() swQuadrantPath.fill() case .sw : swQuadrantPath.fill() quadrantBackColor.setFill() nwQuadrantPath.fill() neQuadrantPath.fill() seQuadrantPath.fill() } circlePath.lineWidth = linesThickness linesColor.setStroke() // tout Green au départ // Draw the 4 Quadrants // We draw from center to allow one day with different line colors circlePath.move(to: circleCenter) circlePath.addLine(to: innerTop) circlePath.move(to: circleCenter) circlePath.addLine(to: innerRight) circlePath.move(to: circleCenter) circlePath.addLine(to: innerBottom) circlePath.move(to: circleCenter) circlePath.addLine(to: innerLeft) circlePath.stroke() nwButtonLabel.draw(in: nwButtonRect, withAttributes: nil) neButtonLabel.draw(in: neButtonRect, withAttributes: nil) seButtonLabel.draw(in: seButtonRect, withAttributes: nil) swButtonLabel.draw(in: swButtonRect, withAttributes: nil) } // --------------------- drawCenterControl ---------------------------------------------------- // Description: Draw inner control (up and down) // Parameters // with color: UIColor Background color // Comments : // ------------------------------------------------------------------------------------------------- func drawCenterControl(/* with color: UIColor, */arrow: Direction = .both, cornerRadius: CGFloat = 4.0) { let centerPath = UIBezierPath(roundedRect: centerControlRect, cornerRadius: cornerRadius) centerColor.setFill() // lightGray centerPath.fill() centerPath.stroke() let color : UIColor = hilitedArrow ? quadrantHiliteColor : quadrantBackColor color.setFill() // may be hilited // "🔺" let upArrowPath = UIBezierPath() upArrowPath.move(to: CGPoint(x: circleCenter.x-8, y: circleCenter.y-2)) upArrowPath.addLine(to: CGPoint(x: circleCenter.x+8, y: circleCenter.y-2)) upArrowPath.addLine(to: CGPoint(x: circleCenter.x, y: circleCenter.y-14)) upArrowPath.close() upArrowPath.lineWidth = 2.0 upArrowPath.stroke() // upArrowPath.fill() // "🔻" let downArrowPath = UIBezierPath() downArrowPath.move(to: CGPoint(x: circleCenter.x-8, y: circleCenter.y+2)) downArrowPath.addLine(to: CGPoint(x: circleCenter.x+8, y: circleCenter.y+2)) downArrowPath.addLine(to: CGPoint(x: circleCenter.x, y: circleCenter.y+14)) downArrowPath.close() downArrowPath.lineWidth = 2.0 downArrowPath.stroke() switch arrow { case .both : // not hilited, any upArrowPath.fill() downArrowPath.fill() case .up : upArrowPath.fill() // hilited quadrantBackColor.setFill() downArrowPath.fill() case .down : downArrowPath.fill() // hilited quadrantBackColor.setFill() upArrowPath.fill() } } // --------------------- draw ---------------------------------------------------- // Description: Draws all controls with their frames and label and background color // Parameters // rect: CGRect // Comments : // Only override draw() if you perform custom drawing. // An empty implementation adversely affects performance during animation. // ------------------------------------------------------------------------------------------------- override func draw(_ rect: CGRect) { // Drawing code drawQuadrants(/* with: quadrantBackColor,*/ aQuadrant: touchedQuadrant) drawCenterControl(/* with: centerColor,*/ arrow: touchedArrow, cornerRadius: 4.0) } // MARK: - Control actions // --------------------- distance ---------------------------------------------------- // Description: distance between 2 points // Parameters // a: CGPoint // b: CGPoint? // Comments : // ------------------------------------------------------------------------------------------------- func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat { let xDist = a.x - b.x let yDist = a.y - b.y return CGFloat(sqrt(xDist * xDist + yDist * yDist)) } // --------------------- touchLocToQuadrant ------------------------------------------- // Description: Convert a touch location (expressed in control coordinates) to a quadrant ref // Parameters // touchLoc: CGPoint in control coordinates // Comments : // ------------------------------------------------------------------------------------------------- func touchLocToQuadrant(_ touchLoc: CGPoint) -> Quadrant { var touched: Quadrant let touchFromCenter = CGPoint(x: touchLoc.x - circleCenter.x, y: touchLoc.y - circleCenter.y) switch (touchFromCenter.x, touchFromCenter.y) { case (...0, ...0): touched = .nw case (0..., ...0): touched = .ne case (0..., 0...): touched = .se case (...0, 0...): touched = .sw case (_, _): touched = .all } return touched } // --------------------- beginTracking ---------------------------------------------------- // Description: We touch a button // Parameters // touch: UITouch // with event: UIEvent? // Comments : // ------------------------------------------------------------------------------------------------- override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { let touchLoc = touch.location(in: self) if centerControlRect.contains(touchLoc) { hilitedArrow = true if touchLoc.y <= circleCenter.y { touchedArrow = .up } else { touchedArrow = .down } self.setNeedsDisplay() return true } if circleInnerFrame.contains(touchLoc) { let d = distance(circleCenter, touchLoc) if d >= circleOuterRadius { print("Out of control") } else { touchedQuadrant = touchLocToQuadrant(touchLoc) hilitedQuadrant = touchedQuadrant != .all self.setNeedsDisplay() } } return true } override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { let touchLoc = touch.location(in: self) // Test for the arrows first if hilitedArrow && !centerControlRect.contains(touchLoc) { touchedArrow = .both hilitedArrow = false self.setNeedsDisplay() return false } if circleInnerFrame.contains(touchLoc) { let d = distance(circleCenter, touchLoc) if d >= circleOuterRadius { print("Out of control") touchedQuadrant = .all hilitedQuadrant = false self.setNeedsDisplay() return false } else if hilitedQuadrant { updatedTouchedQuadrant = touchLocToQuadrant(touchLoc) if updatedTouchedQuadrant != touchedQuadrant { touchedQuadrant = .all hilitedQuadrant = false self.setNeedsDisplay() return false } self.setNeedsDisplay() return true } } else { touchedQuadrant = .all hilitedQuadrant = false self.setNeedsDisplay() return false } return true } override func endTracking(_ touch: UITouch?, with event: UIEvent?) { if hilitedQuadrant { actionToExecute = touchedQuadrant.action } else { if hilitedArrow { actionToExecute = touchedArrow.action } else { actionToExecute = 0 } } touchedQuadrant = .all hilitedQuadrant = false touchedArrow = .both hilitedArrow = false self.setNeedsDisplay() } }