IOS drawing is the collection point, the Bezier curve gets the shape, and the drawing context renders it

Asana/Drawsana graphics library, well designed

He could draw lines, text, eraser, pentagons, rectangles, arrows, angles,

It supports multiple operations, such as undoing the previous step, restoring the previous step, and panning selected rendered graphics

His implementation made heavy use of protocols

Design: Focus on data structures

It can be divided into three levels: behavior processing (collection point passing), graph drawing, and rendering view (collection point at the beginning and rendering rendering at the end).

Graphic drawing

Shape determines viewable (renderable) and clickable

ShapeSelectable protocol, added shape region and affine transform.

The most basic shape,Shape

There is a distinction of ID, type,

Render, can it respond to click events,

Change drawing Settings (color of drawing, fill color of drawing, width of drawing)


public protocol Shape: AnyObject, Codable {
  var id: String { get set }

  static var type: String { get }

  func render(in context: CGContext)

  func hitTest(point: CGPoint) -> Bool

  func apply(userSettings: UserSettings)
}

Copy the code
Generic shape protocolShapeSelectable

Add functions for the following three protocols

The ShapeWithBoundingRect protocol inherits from Shape and adds a region

public protocol ShapeWithBoundingRect: Shape {
  var boundingRect: CGRect { get }
}
Copy the code

ShapeWithTransform protocol inherits Shape and adds affine transform


public protocol ShapeWithTransform: Shape {
  var transform: ShapeTransform { get set }
}

Copy the code

The final shape of the general protocol isShapeSelectable.

He inherits from the above two agreements


public protocol ShapeSelectable: ShapeWithBoundingRect, ShapeWithTransform {
}
Copy the code
For specific shapes, take angular shapes for exampleAngleShape

The Angle shape requires three points

class AngleShape: ShapeSelectable{ public var a: CGPoint = .zero public var b: CGPoint = .zero public var c: CGPoint = .zero // ... // Implement general shape information, id, type, line width, etc.}Copy the code

There’s another protocol, ShapeWithThreePoints

The protocol takes three points and calculates the rectangular area determined by three points

extension AngleShape: ShapeWithThreePoints{}


public protocol ShapeWithThreePoints {
  var a: CGPoint { get set }
  var b: CGPoint { get set }
  var c: CGPoint { get set }
  
  var strokeWidth: CGFloat { get set }
}
Copy the code

ShapeWithThreePoints three point shape protocol, unified processing of three point shape area;

The shapes of this library, mostly ShapeWithTwoPoints, the 2-point shape protocol, unify the 2-point shape area,

Ellipse, star, rectangle and line segment are all 2-point shapes

Behavior processing, this library is tool

The tool Tool is a further encapsulation of the shape

A generic template for the tool currently in use

DrawingTool, a common template for the tool, contains the following information

Public protocol DrawingTool: AnyObject {// ongoing var isProgressive: Bool {get} String {get} // User finger click func handleTap(context: ToolOperationContext, point: Func handleDragStart(context: ToolOperationContext, point: Func handleDragContinue(context: ToolOperationContext, point:) func handleDragContinue(context: ToolOperationContext, point:) Func handleDragEnd(Context: ToolOperationContext, point: ToolOperationContext) Func handleDragCancel(context: ToolOperationContext, point: Func apply(Context: ToolOperationContext, userSettings: userSettings)}Copy the code
Function templates for the tool currently in use

Using the function template of tool,

I have my two-point tool, I have my three-point tool, I have my pen,

There are selection tools…

The example here is 2 points in the shape of a tool, DrawingToolForShapeWithTwoPoints

He’s got a property inside, shapeInProgress, the shape he’s drawing,

The shape is determined by two points

pen class DrawingToolForShapeWithTwoPoints: DrawingTool { public typealias ShapeType = Shape & ShapeWithTwoPoints open var name: String {fatalError("Override me")} // shapeInProgress creates public var shapeInProgress with makeShape() method below: ShapeType? open func makeShape() -> ShapeType { fatalError("Override me") } // ... DrawingTool {DrawingTool}Copy the code
The specific tool currently in use

Because the previous protocol was well defined,

A lot of generic code in protocols and superclasses,

So the specific drawing tool, the implementation is relatively simple

/ / line drawing tool public class LineTool: DrawingToolForShapeWithTwoPoints {public override var name: String {return "Line"} public override func makeShape() -> ShapeType {return LineShape()}} arrow tool public class ArrowTool: DrawingToolForShapeWithTwoPoints { public override var name: String { return "Arrow" } public override func makeShape() -> ShapeType { let shape = LineShape() shape.arrowStyle = The standard return shape}} / / rectangle tool public class RectTool: DrawingToolForShapeWithTwoPoints {public override var name: String { return "Rectangle" } public override func makeShape() -> ShapeType { return RectShape() } } // ...Copy the code

Rendered view

First collect points through custom gestures

Overwrite the touch method to collect the point

Class ImmediatePanGestureRecognizer: UIGestureRecognizer {/ / touch override func touchesBegan (_ touches: Set<UITouch>, with event: UIEvent) { // ... } override func touchesMoved(_ touches: Set< uittouch >, with event: UIEvent) { } // end touches override func touches (_ touches: Set< uittouch >, with event: UIEvent) {// end touches override func touches: Set< uittouch >, with event: UIEvent... }}Copy the code
Final render render

I need two UIImages for drawing,

Draw a stroke, render the UIImage that you drew earlier, render the stroke that you’re drawing, and you get the view that you drew, which is the second UIImage

There are three UIImages, one final version persistentBuffer,

Render the previously drawn UIImage, transientBuffer

Get the map view, that is, a second UIImage, transientBufferWithShapeInProgress

public class DrawsanaView: UIView {

     private var persistentBuffer: UIImage?
     private var transientBuffer: UIImage?
     private var transientBufferWithShapeInProgress: UIImage?

Copy the code

The corresponding processing code,

Here is a line drawing/translation operations map anonymous functions, updateUncommittedShapeBuffers,

let updateUncommittedShapeBuffers: () -> Void = { self.transientBufferWithShapeInProgress = DrawsanaUtilities.renderImage(size: Self.drawing.size) {// Render the previously drawn UIImage self.transientBuffer? .draw(at:.zero) // Render the drawing self.tool? .renderShapeInProgress(transientContext: $0)} / / get the map view transientBufferWithShapeInProgress / / get the map view, Present self. DrawingContentView. Layer. Contents = self. TransientBufferWithShapeInProgress? .cgimage // if self.tool? .isProgressive == true { self.transientBuffer = self.transientBufferWithShapeInProgress } }Copy the code

implementation

  • POP, using the protocol, can avoid a lot of duplicate code

  • It’s easier to understand if you write process-oriented. The logic is all in one lump. Because the same code, copy everywhere, difficult to maintain, prone to error

For example, Angle drawing

Angular shape, three points

public class AngleShape: ShapeWithThreePoints, ShapeWithStrokeState, ShapeSelectable{
  public static let type: String = "Angle"
  
  public var id: String = UUID().uuidString
  public var a: CGPoint = .zero
  public var b: CGPoint = .zero
  public var c: CGPoint = .zero
  
}

Copy the code

The method of drawing lines for angles

Public func render(in context: CGContext) {// Start rendering transform.begin(context: SetLineCap (capStyle) context.setlineJoin (joinStyle) context.setlineWidth (strokeWidth) context.setStrokeColor(strokeColor.cgColor) if let dashPhase = dashPhase, let dashLengths = dashLengths { context.setLineDash(phase: dashPhase, lengths: Dash.move (to: a) context.addline (to: lengths)} else {context.setlinedash (phase: 0, lengths: [])} Move (to: b) context.addline (to: c) context.strokePath() // Draw the Angle arc in the middle, and the Angle text renderInfo(in: Transform.end (context: context)}Copy the code

Call below, draw the middle Angle arc, and Angle text

private func renderInfo(in context: CGContext) {// Calculate the starting Angle, If a == c {return} let center = b var startAngle = atan2(a.y-b.y, a.x-b.x) var endAngle = atan2(c.y-b.y, a.x-b.x) c.x - b.x) if 0 < endAngle - startAngle && endAngle - startAngle < CGFloat.pi { // swap startAngle & endAngle startAngle = startAngle + endAngle endAngle = startangle-endangle startAngle = startangle-endangle} SetLineWidth (strokeWidth / 2) context.addArc(Center: center, radius: 24, startAngle: startAngle, endAngle: endAngle, clockwise: True) context.strokePath() context.setlineWidth (strokeWidth) startAngle, endAngle: endAngle) }Copy the code

Call, draw Angle size text

private func renderDegreesInfo(in context: CGContext, startAngle: CGFloat, endAngle: CGFloat) {// Get the Angle size, rich text let radius: CGFloat = 44 let fontSize: CGFloat = 14 let font = uifont.systemfont (ofSize: fontSize) let string = NSAttributedString(string: "\(degreesBetweenThreePoints(pointA: a, pointB: b, pointC: ° c)), "the attributes: [NSAttributedString. Key. The font, font, NSAttributedString. Key. ForegroundColor: StrokeColor]) let normalEnd = startAngle < endAngle? endAngle + 2 * CGFloat.pi : endAngle let centerAngle = startAngle + (normalEnd - startAngle) / 2 let arcCenterX = b.x + cos(centerAngle) * radius - FontSize / 2 let arcCenterY = b.y + sin(centerAngle) * radius - fontSize / 2 draw string. Draw (at: CGPoint(x: arcCenterX, y: arcCenterY)) }Copy the code

We can figure out what the Angle is by looking at three points

private func degreesBetweenThreePoints(pointA: CGPoint, pointB: CGPoint, pointC: // let a = pow((pointb.x-pointa.x), 2) + pow((pointb.y-pointa.y), X) + pow((pointb.y-pointc.y), 2) // let c = pow((pointc.x-pointa.x), 2) // Let c = pow((pointc.x-pointa.x), 3) // 2) + pow((pointC.y - pointA.y), 2) if a == 0 || b == 0 { return 0 } return Int(acos((a + b - c) / sqrt(4 * a * b) ) * 180 / CGFloat.pi) }Copy the code

Operation undo, reversible implementation

Draw a view DrawsanaView with an operationStack inside

public class DrawsanaView: UIView {

  public lazy var operationStack: DrawingOperationStack = {
      return DrawingOperationStack(drawing: drawing)
    }()

}
Copy the code

Implementation of DrawingOperationStack

This class, which has an array of two operations,

Used as a stack, last in, first out,

General modification, are changed behind

There are three methods,

Undo and undo, easy to understand

For the remaining add operations, func apply(operation)

One stroke at a time, add it to undoStack and wait for the user to do it

public class DrawingOperationStack { public private(set) var undoStack = [DrawingOperation]() var redoStack = [DrawingOperation]() public func apply(operation: DrawingOperation) {shouldAdd(to: self) else { return } undoStack.append(operation) redoStack = [] operation.apply(drawing: drawing) delegate? .drawingOperationStackDidApply(self, operation: } @objc public func undo() {guard let operation = undoStack. Last else {return} operation.revert(drawing: drawing) redoStack.append(operation) undoStack.removeLast() delegate? .drawingOperationStackDidUndo(self, operation: } @objc public func redo() {guard let operation = redoStack. Last else {return} operation.apply(drawing: drawing) undoStack.append(operation) redoStack.removeLast() delegate? .drawingOperationStackDidRedo(self, operation: operation) } }Copy the code

Add the logic of the operation

For each tool, touch completion/use completion adds an action

public func handleDragEnd(context: ToolOperationContext, point: CGPoint){ // ... / / add the operating context. OperationStack. Apply (operation: AddShapeOperation (shape: shape)) / /... }Copy the code
This library, it’s quite distinctive. Function is powerful, it is necessary to consider all aspects, around to go around