要做一个全功能的绘图板,至少要支持以下这些功能:

  • 支持铅笔绘图(画点)

  • 支持画直线

  • 支持一些简单的图形(矩形、圆形等)

  • 做一个真正的橡皮擦

  • 能设置画笔的粗细

  • 能设置画笔的颜色

  • 能设置背景色或者背景图

  • 能支持撤消与重做

我们先做一些基础性的工作,比如创建工程。

工程搭建

先创建一个Single View Application 工程:

语言选择Swift:

为了最大程度的利用屏幕区域,我们完全隐藏掉状态栏,在Info.plist里修改或添加这两个参数:

然后进入到Main.storyboard,开始搭建我们的UI。

我 们给已存在的ViewController的View添加一个UIImageView的子视图,背景色设为Light Gray,然后添加4个约束,由于要做一个全屏的画板,必须要让Constraint to margins保持没有选中的状态,否则左右两边会留下苹果建议的空白区域,最后把User Interaction Enabled打开:

然后我们回到ViewController的View上:

  • 添加一个放工具栏的容器:UIView,为该View设置约束:

同样的不要选择Contraint to margins。

  • 在该View里添加一个UISegmentedControl,并给SegmentedControl设置6个选项,分别是:

  1. 铅笔

  2. 直尺

  3. 虚线

  4. 矩形

  5. 圆形

  6. 橡皮擦

  • 给这个SegmentedControl添加约束:

垂直居中,两边各留20,高度固定为28。

完整的UI及结构看起来像这样:

ImageView将会作为实际的绘制区域,顶部的SegmentedControl提供工具的选择。 到目前为止我们还没有写下一行代码,至此要开始编码了。

你可能会注意到Board有一部分被挡住了,这只是暂时的~

施工…

Board

我们创建一个Board类,继承自UIImageView,同时把这个类设置为Main.storyboard中ImageView的Class,这样当app启动的时候就会自动创建一个Board的实例了。

增加两个属性以及初始化方法:

var strokeWidth: CGFloat
var strokeColor: UIColor
override init() {
    self.strokeColor = UIColor.blackColor()
    self.strokeWidth = 1
    super.init()
}
required init(coder aDecoder: NSCoder) {
    self.strokeColor = UIColor.blackColor()
    self.strokeWidth = 1
    super.init(coder: aDecoder)
}

由于我们是依赖于touches方法来完成绘图过程,我们需要记录下每次touch的状态,比如began、moved、ended等,为此我们创建一个枚举,在touches方法中进行记录,并调用私有的绘图方法drawingImage:

enum DrawingState {
    case Began, Moved, Ended
}
class Board: UIImageView {
    private var drawingState: DrawingState!
    // 此处省略init方法与另外两个属性
    // MARK: - touches methods
    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        self.drawingState = .Began
        self.drawingImage()
    }
    override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
        self.drawingState = .Moved
        self.drawingImage()
    }
    override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
        self.drawingState = .Ended
        self.drawingImage()
    }
    // MARK: - drawing
    private func drawingImage() {
        // 暂时为空实现
    }
}

在我们实现drawingImage方法之前,我们先创建另外一个重要的组件:BaseBrush。

BaseBrush

顾名思义,BaseBrush将会作为一个绘图的基类而存在,我们会在它的基础上创建一系列的子类,以达到弹性的设计目的。为此,我们创建一个BaseBrush类,并实现一个PaintBrush接口:

import CoreGraphics
protocol PaintBrush {
    func supportedContinuousDrawing() -> Bool;
    func drawInContext(context: CGContextRef)
}
class BaseBrush : NSObject, PaintBrush {
    var beginPoint: CGPoint!
    var endPoint: CGPoint!
    var lastPoint: CGPoint?
    var strokeWidth: CGFloat!
    func supportedContinuousDrawing() -> Bool {
        return false
    }
    func drawInContext(context: CGContextRef) {
        assert(false, "must implements in subclass.")
    }
}

BaseBrush实现了PaintBrush接口,PaintBrush声明了两个方法:

  • supportedContinuousDrawing,表示是否是连续不断的绘图

  • drawInContext,基于Context的绘图方法,子类必须实现具体的绘图

只要是实现了PaintBrush接口的类,我们就当作是一个绘图工具(如铅笔、直尺等),而BaseBrush除了实现PaintBrush接口以外,我们还为它增加了四个便利属性:

  • beginPoint,开始点的位置

  • endPoint,结束点的位置

  • lastPoint,最后一个点的位置(也可以称作是上一个点的位置)

  • strokeWidth,画笔的宽度

这么一来,子类也可以很方便的获取到当前的状态,并作一些深度定制的绘图方法。

lastPoint 的意义:beginPoint和endPoint很好理解,beginPoint是手势刚识别时的点,只要手势不结束,那么beginPoint在手势识 别期间是不会变的;endPoint总是表示手势最后识别的点;除了铅笔以外,其他的图形用这两个属性就够了,但是用铅笔在移动的时候,不能每次从 beginPoint画到endPoint,如果是那样的话就是画直线了,而是应该从上一次画的位置(lastPoint)画到endPoint,这样才 是连贯的线。

回到Board

我们实现了一个画笔的基类之后,就可以重新回到Board类了,毕竟我们之前的工作还没有做完,现在是时候完善Board类了。

我们用Board实际操纵BaseBrush,先为Board添加两个新的属性:

var brush: BaseBrush?
private var realImage: UIImage?

brush对应到具体的画笔类,realImage保存当前的图形,重新修改touches方法,以便增加对brush属性的处理,完整的touches方法实现如下:

// MARK: - touches methods
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
    if let brush = self.brush {
        brush.lastPoint = nil
        brush.beginPoint = touches.anyObject()!.locationInView(self)
        brush.endPoint = brush.beginPoint
        self.drawingState = .Began
        self.drawingImage()
    }
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
    if let brush = self.brush {
        brush.endPoint = touches.anyObject()!.locationInView(self)
        self.drawingState = .Moved
        self.drawingImage()
    }
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
    if let brush = self.brush {
        brush.endPoint = nil
    }
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
    if let brush = self.brush {
        brush.endPoint = touches.anyObject()!.locationInView(self)
        self.drawingState = .Ended
        self.drawingImage()
    }
}

我们需要防止brush为nil的情况,以及为brush设置好beginPoint和endPoint,之后我们就可以完善drawingImage方法了,实现如下:

private func drawingImage() {
    if let brush = self.brush {
        // 1.
        UIGraphicsBeginImageContext(self.bounds.size)
        // 2.
        let context = UIGraphicsGetCurrentContext()
        UIColor.clearColor().setFill()
        UIRectFill(self.bounds)
        CGContextSetLineCap(context, kCGLineCapRound)
        CGContextSetLineWidth(context, self.strokeWidth)
        CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor)
        // 3.
        if let realImage = self.realImage {
            realImage.drawInRect(self.bounds)
        }
        // 4.
        brush.strokeWidth = self.strokeWidth
        brush.drawInContext(context);
        CGContextStrokePath(context)
        // 5.
        let previewImage = UIGraphicsGetImageFromCurrentImageContext()
        if self.drawingState == .Ended || brush.supportedContinuousDrawing() {
            self.realImage = previewImage
        }
        UIGraphicsEndImageContext()
        // 6.
        self.image = previewImage;
        brush.lastPoint = brush.endPoint
    }
}

步骤解析:

开启一个新的ImageContext,为保存每次的绘图状态作准备。

初始化context,进行基本设置(画笔宽度、画笔颜色、画笔的圆润度等)。

把之前保存的图片绘制进context中。

设置brush的基本属性,以便子类更方便的绘图;调用具体的绘图方法,并最终添加到context中。

从当前的context中,得到Image,如果是ended状态或者需要支持连续不断的绘图,则将Image保存到realImage中。

实时显示当前的绘制状态,并记录绘制的最后一个点。

这些工作完成以后,我们就可以开始写第一个工具了:铅笔工具。

铅笔工具


笔工具应该支持连续不断的绘图(不断的保存到realImage中),这也是我们给PaintBrush接口增加
supportedContinuousDrawing方法的原因,考虑到用户的手指可能快速的移动,导致从一个点到另一个点有着跳跃性的动作,我们对铅
笔工具采用画直线的方式来实现。

首先创建一个类,名为PencilBrush,继承自BaseBrush类,实现如下:

class PencilBrush: BaseBrush {
    override func drawInContext(context: CGContextRef) {
        if let lastPoint = self.lastPoint {
            CGContextMoveToPoint(context, lastPoint.x, lastPoint.y)
            CGContextAddLineToPoint(context, endPoint.x, endPoint.y)
        } else {
            CGContextMoveToPoint(context, beginPoint.x, beginPoint.y)
            CGContextAddLineToPoint(context, endPoint.x, endPoint.y)
        }
    }
    override func supportedContinuousDrawing() -> Bool {
        return true
    }
}

如果lastPoint为nil,则基于beginPoint画线,反之则基于lastPoint画线。

这样一来,一个铅笔工具就完成了,怎么样,很简单吧。

测试

到目前为止,我们的ViewController还保持着默认的状态,是时候先为铅笔工具写一些测试代码了。

在ViewController添加board属性,并与Main.storyboard中的Board关联起来;创建一个brushes属性,并为之赋值为:

var brushes = [PencilBrush()]

在ViewController中添加switchBrush:方法,并把Main.storyboard中的SegmentedControl的ValueChanged连接到ViewController的switchBrush:方法上,实现如下:

@IBAction func switchBrush(sender: UISegmentedControl) {
    assert(sender.tag < self.brushes.count, "!!!")
    self.board.brush = self.brushes[sender.selectedSegmentIndex]
}

最后在viewDidLoad方法中做一个初始化:

self.board.brush = brushes[0]

编译、运行,铅笔工具可以完美运行~!

其他的工具

接下来我们把其他的绘图工具也实现了。

其他的工具不像铅笔工具,不需要支持连续不断的绘图,所以也就不用覆盖supportedContinuousDrawing方法了。

直尺

创建一个LineBrush类,实现如下:

class LineBrush: BaseBrush {
    override func drawInContext(context: CGContextRef) {
        CGContextMoveToPoint(context, beginPoint.x, beginPoint.y)
        CGContextAddLineToPoint(context, endPoint.x, endPoint.y)
    }
}

虚线

创建一个DashLineBrush类,实现如下:

class DashLineBrush: BaseBrush {
    override func drawInContext(context: CGContextRef) {
        let lengths: [CGFloat] = [self.strokeWidth * 3, self.strokeWidth * 3]
        CGContextSetLineDash(context, 0, lengths, 2);
        CGContextMoveToPoint(context, beginPoint.x, beginPoint.y)
        CGContextAddLineToPoint(context, endPoint.x, endPoint.y)
    }
}

这里我们就用到了BaseBrush的strokeWidth属性,因为我们想要创建一条动态的虚线。

矩形

创建一个RectangleBrush类,实现如下:

class RectangleBrush: BaseBrush {
    override func drawInContext(context: CGContextRef) {
        CGContextAddRect(context, CGRect(origin: CGPoint(x: min(beginPoint.x, endPoint.x), y: min(beginPoint.y, endPoint.y)),
            size: CGSize(width: abs(endPoint.x - beginPoint.x), height: abs(endPoint.y - beginPoint.y))))
    }
}

我们用到了一些计算,因为我们希望矩形的区域不是由beginPoint定死的。

圆形

创建一个EllipseBrush类,实现如下:

class EllipseBrush: BaseBrush {
    override func drawInContext(context: CGContextRef) {
        CGContextAddEllipseInRect(context, CGRect(origin: CGPoint(x: min(beginPoint.x, endPoint.x), y: min(beginPoint.y, endPoint.y)),
            size: CGSize(width: abs(endPoint.x - beginPoint.x), height: abs(endPoint.y - beginPoint.y))))
    }
}

同样有一些计算,理由同上。

橡皮擦

从本文一开始就说过了,我们要做一个真正的橡皮擦,网上有很多的橡皮擦的实现其实就是把画笔颜色设置为背景色,但是如果背景色可以动态设置,甚至设置为一个渐变的图片时,这种方法就失效了,所以有些绘图app的背景色就是固定为白色的。

其实Apple的Quartz2D框架本身就是支持橡皮擦的,只用一个方法就可以完美实现。

让我们创建一个EraserBrush类,实现如下:

class EraserBrush: PencilBrush {
    override func drawInContext(context: CGContextRef) {
        CGContextSetBlendMode(context, kCGBlendModeClear);
        super.drawInContext(context)
    }
}

注意,与其他的工具不同,橡皮擦是继承自PencilBrush的,因为橡皮擦本身也是基于点的,而drawInContext里也只是加了一句:

CGContextSetBlendMode(context, kCGBlendModeClear);

加入这一句代码,一个真正的橡皮擦便实现了。

再次测试

现在我们的工程结构应该类似于这样:

我们修改下ViewController中的brushes属性的初始值:

var brushes = [PencilBrush(), LineBrush(), DashLineBrush(), RectangleBrush(), EllipseBrush(), EraserBrush()]

编译、运行:

除了橡皮擦擦除的范围太小以外,一切都很完美~!

设计思路

在继续完成剩下的功能之前,我想先对之前的代码进行些说明。

为什么不用drawRect方法

其 实我最开始也是使用drawRect方法来完成绘制,但是感觉限制很多,比如context无法保存,还是要每次重画(虽然可以保存到一个 BitMapContext里,但是这样与保存到image里有什么区别呢?);后来用CALayer保存每一条CGPath,但是这样仍然不能避免每次 重绘,因为需要考虑到橡皮擦和画笔属性之类的影响,这么一来还不如采用image的方式来保存最新绘图板。

既然定下了以image来保存绘图板,那么drawRect就不方便了,因为不能用UIGraphicsBeginImageContext方法来创建一个ImageContext。

ViewController与Board、BaseBrush之间的关系

在 ViewController、Board和BaseBrush这三者之间,虽然VC要知道另外两个组件,但是仅限于选择对应的工具给 Board,Board本身并不知道当前的brush是哪个brush,也不需要知道其内部实现,只管调用对应的brush就行 了;BaseBrush(及其子类)也并不知道自己将会被用于哪,它们只需要实现自己的算法即可。类似于这样的图:

实际上这里包含了两个设计模式。

策略设计模式

策略设计模式的UML图:

策略设计模式在iOS中也应用广泛,如AFNetworking的AFHTTPRequestSerializer和AFHTTPResponseSerializer的设计,通过在运行时动态的改变委托对象,变换行为,使程序模块之间解耦、提高应变能力。

以我们的绘图板为例,输出不同的图形就意味着不同的算法,用户可根据不同的需求来选择某一种算法,即BaseBrush及其子类做具体的封装,这样的好处是每一个子类只关心自己的算法,达到了高聚合的原则,高级模块(Board)不用关心具体实现。

想象一下,如果是让Board里自身来处理这些算法,那代码中无疑会充斥很多与算法选择相关的逻辑,而且每增加一个算法都需要重新修改Board类,这又与代码应该对拓展开放、对修改关闭原则有了冲突,而且每个类也应该只有一个责任。

通过采用策略模式我们实现了一个好维护、易拓展的程序(妈妈再也不用担心工具栏不够用了^^)。

策略模式的定义:定义一个算法群,把每一个算法分别封装起来,让它们之间可以互相替换,使算法的变化独立于使用它的用户之上。

模板方法

在 传统的策略模式中,每一个算法类都独自完成整个算法过程,例如一个网络解析程序,可能有一个算法用于解析JSON,有另一个算法用于解析XML等(另外一 个例子是压缩程序,用ZIP或RAR算法),独自完成整个算法对灵活性更好,但免不了会有重复代码,在DrawingBoard里我们做一个折中,尽量保 证灵活性,又最大限度地避免重复代码。

我们将BaseBrush的角色提升为算法的基类,并提供一些便利的属性(如 beginPoint、endPoint、strokeWidth等),然后在Board的drawingImage方法里对BaseBrush的接口进 行调用,而BaseBrush不会知道自己的接口是如何联系起来的,虽然supportedContinuousDrawing(这是一个“钩子”)甚至 影响了算法的流程(铅笔需要实时绘图)。

我们用drawingImage搭建了一个算法的骨架,看起来像是模板方法的UML图:

图中右边的方框代表模板方法。

BaseBrush 通过提供抽象方法(drawInContext)、具体方法或钩子方法(supportedContinuousDrawing)来对应算法的每一个步 骤,让其子类可以重定义或实现这些步骤。同时,让模板方法(即dawingImage)定义一个算法的骨架,模板方法不仅可以调用在抽象类中实现的基本方 法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。

除了对算法的封装以外,模板方法还能防止“循环依赖”,即 高层组件依赖低层组件,反过来低层组件也依赖高层组件。想像一下,如果既让Board选择具体的算法子类,又让算法类直接调用drawingImage方 法(不提供钩子,直接把Board的事件下发下去),那到时候就热闹了,这些类串在一起难以理解,又不好维护。

模板方法的定义:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

其实模式都很简单,很多人在工作中会思考如何让自己的代码变得更好,“情不自禁”地就会慢慢实现这些原则,了解模式的设计意图,有助于在遇到需要折中的地方更加明白如何在设计上取舍。

以上就是我设计时的思路,说完了,接下来还要完成的工作有:

  • 提供对画笔颜色、粗细的设置

  • 背景设置

  • 全屏绘图(不能让Board一直显示不全)

先从画笔开始,Let’s go!

画笔设置

不管是画笔还是背景设置,我们都要有一个能提供设置的工具栏。

设置工具栏

所以我们往Board上再盖一个UIToolbar,与顶部的View类似:

  1. 拖一个UIToolbar到Board的父类上,与Board的视图层级平级。

  2. 设置UIToolbar的约束:左、右、下间距为0,高为44:

  3. 往UIToolbar上拖一个UIBarButtonItem,title就写:画笔设置。

  4. 在ViewController里增加一个paintingBrushSettings方法,并把UIBarButtonItem的action连接paintingBrushSettings方法上。

  5. 在ViewController里增加一个toolar属性,并把Xib中的UIToolbar连接到toolbar上。

UIToolbar配置好后,UI及视图层级如下:

RGBColorPicker

考虑到多个页面需要选取自定义的颜色,我们先创建一个工具类:RGBColorPicker,用于选择RGB颜色:

这个工具类很简单,没有采用Auto Layout进行布局,因为layoutSubviews方法已经能很好的满足我们的需求了。当用户拖动任何一个UISlider的时候,我们能实时的通过colorChangedBlock回调给外部。它能展现一个这样的视图:

不过虽然该工具类本身没有采用Auto Layout进行布局,但是它还是支持Auto Layout的,当它被添加到某个Auto Layout的视图中的时候,Auto Layout布局系统可以通过intrinsicContentSize知道该视图的尺寸信息。

最后它还有一个setCurrentColor方法从外部接收一个UIColor,可以用于初始化。

画笔设置的UI

我打算在用户点击画笔设置的时候,从底部弹出一个控制面板(就像系统的Control Center那样),所以我们还要有一个像这样的设置UI:


体的,创建一个PaintingBrushSettingsView类,同时创建一个PaintingBrushSettingsView.xib文件,
并把xib中view的Class设为PaintingBrushSettingsView,设置view的背景色为透明:

  1. 放置一个title为“画笔粗细”的UILabel,约束设为:宽度固定为68,高度固定为21,左和上边距为8。

  2. 放置一个title为“1”的UILabel,“1”与“画笔粗细”的垂直间距为10,宽度固定为10,高度固定为21,与superview的左边距为10。

  3. 放置一个UISlider,用于调节画笔的粗细,与“1”的水平间距为5,并与“1”垂直居中,高度固定为30,宽度暂时不设,在PaintingBrushSettingsView中添加strokeWidthSlider属性,与之连接起来。

  4. 放置一个title为“20”的UILabel,约束设为:宽度固定为20,高度固定为21,top与“1”相同,与superview的右间距为10。并把上一步中的UISlider的右间距设为与“20”相隔5。

  5. 放置一个title为“画笔颜色”的UILabel,宽、高、left与“画笔粗细”相同,与上面UISlider的垂直间距设为12。


  6. 置一个UIView至“画笔颜色”下方(上图中被选中的那个UIView),宽度固定为50,高度固定为30,left与“画笔颜色”相同,并且与“画笔
    颜色”的垂直间距为5,在PaintingBrushSettingsView中添加strokeColorPreview属性,与之连接起来。

  7. 放置一个UIView,把它的Class改为RGBColorPicker,约束设为:left与顶部的UISlider相同,底部与superview的间距为0,右间距为10,与上一步中的UIView的垂直间距为5。

PaintingBrushSettingsView类的完整代码如下:

class PaintingBrushSettingsView : UIView {
    var strokeWidthChangedBlock: ((strokeWidth: CGFloat) -> Void)?
    var strokeColorChangedBlock: ((strokeColor: UIColor) -> Void)?
    @IBOutlet private var strokeWidthSlider: UISlider!
    @IBOutlet private var strokeColorPreview: UIView!
    @IBOutlet private var colorPicker: RGBColorPicker!
    override func awakeFromNib() {
        super.awakeFromNib()
        self.strokeColorPreview.layer.borderColor = UIColor.blackColor().CGColor
        self.strokeColorPreview.layer.borderWidth = 1
        self.colorPicker.colorChangedBlock = {
            [unowned self] (color: UIColor) in
            self.strokeColorPreview.backgroundColor = color
            if let strokeColorChangedBlock = self.strokeColorChangedBlock {
                strokeColorChangedBlock(strokeColor: color)
            }
        }
        self.strokeWidthSlider.addTarget(self, action: "strokeWidthChanged:", forControlEvents: .ValueChanged)
    }
    func setBackgroundColor(color: UIColor) {
        self.strokeColorPreview.backgroundColor = color
        self.colorPicker.setCurrentColor(color)
    }
    func strokeWidthChanged(slider: UISlider) {
        if let strokeWidthChangedBlock = self.strokeWidthChangedBlock {
            strokeWidthChangedBlock(strokeWidth: CGFloat(slider.value))
        }
    }
}

strokeWidthChangedBlock和strokeColorChangedBlock两个Block用于给外部传递状态。setBackgroundColor用于初始化。

关于 Swift 1.2

在 Swift 1.2里,不能用 setBackgroundColor方法了,具体的,见Xcode 6.3的发布文档:Xcode 6.3 Release Notes,下面是用didSet代替原有的setBackgroundColor方法:

override var backgroundColor: UIColor? {
    didSet {
        self.strokeColorPreview.backgroundColor = self.backgroundColor
        self.colorPicker.setCurrentColor(self.backgroundColor!)
        super.backgroundColor = oldValue
    }
}

实现毛玻璃效果

在把PaintingBrushSettingsView显示出来之前,我们要先想一想以何种方式展现比较好,众所周知Control Center是有毛玻璃效果的,我们也想要这样的效果,而且不用自己实现。那如何产生效果? 答案是用UIToolbar就行了。

UIToolbar本身就是带有毛玻璃效果的,只要你不设置背景色,并且translucent属性为true,“恰好”我们页面底部就有一个UIToolbar,我们把它拉高就可以插入展现PaintingBrushSettingsView了。

只要get到了这一点,毛玻璃效果就算实现了~~

测试画笔设置

我们在ViewController新增加几个属性:

var toolbarEditingItems: [UIBarButtonItem]?
var currentSettingsView: UIView?
@IBOutlet var toolbarConstraintHeight: NSLayoutConstraint!

toolbarConstraintHeight 连接到Main.storyboard中对应的约束上就行了。toolbarEditingItems能让我们在UIToolbar上显示不同的 items,本来还需要一个toolbarItems属性的,因为UIViewController类本身就自带,我们便不用单独新增。 currentSettingsView是用来保存当前展示的哪个设置页面,考虑到我们后面会增加背景设置,这个属性还是有必要的。

我们先写一个往toolbar上添加约束的工具方法:

func addConstraintsToToolbarForSettingsView(view: UIView) {
    view.setTranslatesAutoresizingMaskIntoConstraints(false)
    self.toolbar.addSubview(view)
    self.toolbar.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[settingsView]-0-|",
        options: .DirectionLeadingToTrailing,
        metrics: nil,
        views: ["settingsView" : view]))
    self.toolbar.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[settingsView(==height)]",
        options: .DirectionLeadingToTrailing,
        metrics: ["height" : view.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height],
        views: ["settingsView" : view]))
}

这个工具方法会把传入进来的view添加到toolbar上,同时添加相应的约束。注意高度的约束,我是通过 systemLayoutSizeFittingSize方法计算出设置视图最佳的高度,这是为了达到更好的拓展性(背景设置与画笔设置所需要的高度很可 能会不同)。

然后再增加一个setupBrushSettingsView方法:

func setupBrushSettingsView() {
    let brushSettingsView = UINib(nibName: "PaintingBrushSettingsView", bundle: nil).instantiateWithOwner(nil, options: nil).first as PaintingBrushSettingsView
    self.addConstraintsToToolbarForSettingsView(brushSettingsView)
    brushSettingsView.hidden = true
    brushSettingsView.tag = 1
    brushSettingsView.setBackgroundColor(self.board.strokeColor)
    brushSettingsView.strokeWidthChangedBlock = {
        [unowned self] (strokeWidth: CGFloat) -> Void in
        self.board.strokeWidth = strokeWidth
    }
    brushSettingsView.strokeColorChangedBlock = {
        [unowned self] (strokeColor: UIColor) -> Void in
        self.board.strokeColor = strokeColor
    }
}

我们在这个方法里实例化了一个PaintingBrushSettingsView,并添加到toolbar上,增加相应的约束,以及一些初始化设置和两个Block回调的处理。

然后修改viewDidLoad方法,增加以下行为:

//---
self.toolbarEditingItems = [
    UIBarButtonItem(barButtonSystemItem:.FlexibleSpace, target: nil, action: nil),
    UIBarButtonItem(title: "完成", style:.Plain, target: self, action: "endSetting")
]
self.toolbarItems = self.toolbar.items
self.setupBrushSettingsView()
//---

在paintingBrushSettings方法里响应点击:

@IBAction func paintingBrushSettings() {
    self.currentSettingsView = self.toolbar.viewWithTag(1)
    self.currentSettingsView?.hidden = false
    self.updateToolbarForSettingsView()
}
func updateToolbarForSettingsView() {
    self.toolbarConstraintHeight.constant = self.currentSettingsView!.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + 44
    self.toolbar.setItems(self.toolbarEditingItems, animated: true)
    UIView.beginAnimations(nil, context: nil)
    self.toolbar.layoutIfNeeded()
    UIView.commitAnimations()
    self.toolbar.bringSubviewToFront(self.currentSettingsView!)
}

updateToolbarForSettingsView也是一个工具方法,用于更新toolbar的高度。

由于我们采用了Auto Layout进行布局,动画要通过调用layoutIfNeeded方法来实现。

响应点击“完成”按钮的endSetting方法:

@IBAction func endSetting() {
    self.toolbarConstraintHeight.constant = 44
    self.toolbar.setItems(self.toolbarItems, animated: true)
    UIView.beginAnimations(nil, context: nil)
    self.toolbar.layoutIfNeeded()
    UIView.commitAnimations()
    self.currentSettingsView?.hidden = true
}

这么一来画笔设置就做完了,代码应该还是比较好理解,编译、运行后,应该能看到:

完成度已经很高了^^!

背景设置

整体的框架基本上已经在之前的工作中搭好了,我们快速过掉这一节。

在 Main.storyboard中增加了一个title为“背景设置”的UIBarButtonItem,并将action连接到 ViewController的backgroundSettings方法上,你可以选择在插入“背景设置”之前,先插入一个FlexibleSpace 的UIBarButtonItem。

创建BackgroundSettingsVC类,继承自UIViewController,这与画 笔设置继承于UIView不同,我们希望背景设置可以在用户的相册中选择照片,而使用UIImagePickerController的前提是要实现 UIImagePickerControllerDelegate、UINavigationControllerDelegate两个接口,如果让 UIView来实现这两个接口会很奇怪。

创建一个BackgroundSettingsVC.xib文件:

放置一个title为“从相册中选择背景图”的UIButton,约束为:左、上边距为8,宽度固定为135,高度固定为30。
放置一个RGBColorPicker,约束为:左、右边距为8,与UIButton的垂直间距为20,底部与superview齐平。
把UIButton的Touch Up Inside事件连接到BackgroundSettingsVC的pickImage方法上;RGBColorPicker连接到BackgroundSettingsVC的colorPicker属性上。

看上去像这样:

BackgroundSettingsVC类的完整代码:

class BackgroundSettingsVC : UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    var backgroundImageChangedBlock: ((backgroundImage: UIImage) -> Void)?
    var backgroundColorChangedBlock: ((backgroundColor: UIColor) -> Void)?
    @IBOutlet private var colorPicker: RGBColorPicker!
    lazy private var pickerController: UIImagePickerController = {
        [unowned self] in
        let pickerController = UIImagePickerController()
        pickerController.delegate = self
        return pickerController
    }()
    override func awakeFromNib() {
        super.awakeFromNib()
        self.colorPicker.colorChangedBlock = {
            [unowned self] (color: UIColor) in
            if let backgroundColorChangedBlock = self.backgroundColorChangedBlock {
                backgroundColorChangedBlock(backgroundColor: color)
            }
        }
    }
    func setBackgroundColor(color: UIColor) {
        self.colorPicker.setCurrentColor(color)
    }
    @IBAction func pickImage() {
        self.presentViewController(self.pickerController, animated: true, completion: nil)
    }
    // MARK: UIImagePickerControllerDelegate Methods
    func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
        let image = info[UIImagePickerControllerOriginalImage] as UIImage
        if let backgroundImageChangedBlock = self.backgroundImageChangedBlock {
            backgroundImageChangedBlock(backgroundImage: image)
        }
        self.dismissViewControllerAnimated(true, completion: nil)
    }
    // MARK: UINavigationControllerDelegate Methods
    func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
        UIApplication.sharedApplication().setStatusBarHidden(true, withAnimation: .None)
    }
}

同样用两个Block进行回调;setBackgroundColor公共方法用于设置内部的RGBColorPicker的初始颜色状态;在UINavigationControllerDelegate里隐藏系统默认显示的状态栏。

回到ViewController,我们对背景设置进行测试。

像setupBrushSettingsView方法一样,我们增加一个setupBackgroundSettingsView方法:

func setupBackgroundSettingsView() {
    let backgroundSettingsVC = UINib(nibName: "BackgroundSettingsVC", bundle: nil).instantiateWithOwner(nil, options: nil).first as BackgroundSettingsVC
    self.addConstraintsToToolbarForSettingsView(backgroundSettingsVC.view)
    backgroundSettingsVC.view.hidden = true
    backgroundSettingsVC.view.tag = 2
    backgroundSettingsVC.setBackgroundColor(self.board.backgroundColor!)
    self.addChildViewController(backgroundSettingsVC)
    backgroundSettingsVC.backgroundImageChangedBlock = {
        [unowned self] (backgroundImage: UIImage) in
        self.board.backgroundColor = UIColor(patternImage: backgroundImage)
    }
    backgroundSettingsVC.backgroundColorChangedBlock = {
        [unowned self] (backgroundColor: UIColor) in
        self.board.backgroundColor = backgroundColor
    }
}

修改viewDidLoad方法:

self.toolbarEditingItems = [
    UIBarButtonItem(barButtonSystemItem:.FlexibleSpace, target: nil, action: nil),
    UIBarButtonItem(title: "完成", style:.Plain, target: self, action: "endSetting")
]
self.toolbarItems = self.toolbar.items
self.setupBrushSettingsView()
self.setupBackgroundSettingsView() // Added~!!!

实现backgroundSettings方法:

@IBAction func backgroundSettings() {
    self.currentSettingsView = self.toolbar.viewWithTag(2)
    self.currentSettingsView?.hidden = false
    self.updateToolbarForSettingsView()
}

编译、运行,现在你可以用不同的背景色(或背景图)了!

全屏绘图
到目前为止,Board一直显示不全(事实上,我很早就实现了全屏绘图,但是优先级一直被我排在最后),现在是时候来解决它了。

解决思路是这样的:当用户开始绘图的时候,我们把顶部和底部两个View隐藏;当用户结束绘图的时候,再让两个View显示。

为了获取用户的绘图状态,我们需要在Board里加个“钩子”:

// 增加一个Block回调
var drawingStateChangedBlock: ((state: DrawingState) -> ())?
private func drawingImage() {
    if let brush = self.brush {
        // hook
        if let drawingStateChangedBlock = self.drawingStateChangedBlock {
            drawingStateChangedBlock(state: self.drawingState)
        }
        UIGraphicsBeginImageContext(self.bounds.size)
        // ...

这样一来用户绘图的状态就在ViewController掌握中了。

ViewController想要控制两个View的话,还需要增加几个属性:

@IBOutlet var topView: UIView!
@IBOutlet var topViewConstraintY: NSLayoutConstraint!
@IBOutlet var toolbarConstraintBottom: NSLayoutConstraint!

然后在viewDidLoad方法里增加对“钩子”的处理:

self.board.drawingStateChangedBlock = {(state: DrawingState) -> () in
    if state != .Moved {
        UIView.beginAnimations(nil, context: nil)
        if state == .Began {
            self.topViewConstraintY.constant = -self.topView.frame.size.height
            self.toolbarConstraintBottom.constant = -self.toolbar.frame.size.height
            self.topView.layoutIfNeeded()
            self.toolbar.layoutIfNeeded()
        } else if state == .Ended {
            UIView.setAnimationDelay(1.0)
            self.topViewConstraintY.constant = 0
            self.toolbarConstraintBottom.constant = 0
            self.topView.layoutIfNeeded()
            self.toolbar.layoutIfNeeded()
        }
        UIView.commitAnimations()
    }
}

只有当状态为开始或结束的时候我们才需要更新UI状态,而且我们在结束的事件里延迟了1秒钟,这样用户可以暂时预览下全图。

依靠Auto Layout布局系统以及我们在钩子里对高度的处理,用户在设置页面绘图时也能完美运行。

保存到图库

最后一个功能:保存到图库!

在toolbar上插入一个title为“保存到图库”的UIBarButtonItem,还是可以先插入一个FlexibleSpace的UIBarButtonItem,然后把action连接到ViewController的saveToAlbumy方法上:

@IBAction func saveToAlbum() {
    UIImageWriteToSavedPhotosAlbum(self.board.takeImage(), self, "image:didFinishSavingWithError:contextInfo:", nil)
}

我为Board添加一个新的公共方法:takeImage:

func takeImage() -> UIImage {
    UIGraphicsBeginImageContext(self.bounds.size)
    self.backgroundColor?.setFill()
    UIRectFill(self.bounds)
    self.image?.drawInRect(self.bounds)
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image
}

然后是一个方法指针的回调:

func image(image: UIImage, didFinishSavingWithError error: NSError?, contextInfo:UnsafePointer) {
    if let err = error {
        UIAlertView(title: "错误", message: err.localizedDescription, delegate: nil, cancelButtonTitle: "确定").show()
    } else {
        UIAlertView(title: "提示", message: "保存成功", delegate: nil, cancelButtonTitle: "确定").show()
    }
}

旅行到终点了~!

感谢一路的陪伴!

看 了下,有些小长,文本+代码有2w3+,全部代码去除空行和空格有1w4+,直接贴代码会简单很多,但我始终觉得让代码完成功能并不是全部目的,代码背后 隐藏的问题定义、设计、构建更有意义,毕竟软件开发完成“后”比完成“前”所花费的时间永远更多(除非是一个只有10行代码或者“一次性”的程序)。

希望与大家多多交流。

Swift 全功能的绘图板开发的更多相关文章

  1. 你知道为什么Xcode6中Swift没有智能提示和自己主动补全功能吗 ?

    你知道为什么Xcode6中Swift没有智能提示和自己主动补全功能吗 ? 长沙戴维营教育将为你解开这个巨大的谜团大BUG! http://www.ubuntucollege.cn/course/29/ ...

  2. 【Qt编程】基于Qt的词典开发系列<十四>自动补全功能

    最近写了一个查单词的类似有道词典的软件,里面就有一个自动补全功能(即当你输入一个字母时,就会出现几个候选项).这个自动补全功能十分常见,百度搜索关键词时就会出现.不过它们这些补全功能都是与你输入的进行 ...

  3. 解决VS Code开发Python3语言自动补全功能不带括号的问题

    Visual Studio Code(以下简称VS Code)用来开发Python3,还是很便利的,本身这个IDE就是轻量级的,才几十兆大小,通过安装插件的方式支持各种语言的开发.界面也美美哒,可以在 ...

  4. Xcode6中Swift没有智能提示和自动补全功能

    今天在学习Swift的过程中,编写代码,发现没有智能提示和自动补全功能,一阵不适应,在网上溜达了下,找到了解决办法,测试可行 中文系统新建工程,copyright里有日期,2014年,“年”..然后删 ...

  5. APICloud首款全功能集成开发工具重磅发布,彰显云端一体理念

    近日,APICloud重磅推出首款云端一体的全功能集成开发工具--APICloud Studio 2.为了更深入了解这款开发工具的特性及优势,APICloud CTO 邹达针对几个核心问题做出了解答. ...

  6. 解决VS Code开发Python3语言自动补全功能

    1.打开设置界面 2)使用快捷键组合[Ctrl+Shift+p] . 输入setting,选中首选项的user setting模式设置界面 在打开的User Settings文件界面,搜索:pytho ...

  7. Entity Framework7 入门之全功能.NET版本下使用EF7(含源码)另附数据迁移常见错误处理

    Entity Framework7 入门之全功能.NET(Console, WinForms, WPF等)使用EF7 昨天,我们介绍了EF的新特性和开发计划,如果你还不了解,请移步 Entity Fr ...

  8. OpenResty 是一个全功能的 Web 应用服务器

    OpenResty (也称为 ngx_openresty)是一个全功能的 Web 应用服务器.它打包了标准的 Nginx 核心,很多的常用的第三方模块,以及它们的大多数依赖项. 通过众多进行良好设计的 ...

  9. 全球首个全流程跨平台界面开发套件,PowerUI分析

    一.       首个全流程跨平台界面开发套件,PowerUI正式发布 UIPower在DirectUI的基础上,自主研发全球首个全流程跨平台界面开发套件PowerUI(PUI)正式发布,PowerU ...

随机推荐

  1. How to build UDK2015?

    UDK2015 is a stable release of portions of the EDKII project. 本文记录在Win7下用VS2012 编译UDK2015的过程. Step1, ...

  2. 『转载』Debussy快速上手(Verdi相似)

    『转载』Debussy快速上手(Verdi相似) Debussy 是NOVAS Software, Inc(思源科技)发展的HDL Debug & Analysis tool,这套软体主要不是 ...

  3. [转] iOS使用NSMutableAttributedString 实现富文本(不同颜色字体、下划线等)

    转自: 在iOS开发中,常常会有一段文字显示不同的颜色和字体,或者给某几个文字加删除线或下划线的需求.之前在网上找了一些资料,有的是重绘UILabel的textLayer,有的是用html5实现的,都 ...

  4. Javascript进阶篇——(DOM—节点---属性、访问节点)—笔记整理

    节点属性在文档对象模型 (DOM) 中,每个节点都是一个对象.DOM 节点有三个重要的属性 : 1. nodeName : 节点的名称 2. nodeValue :节点的值 3. nodeType : ...

  5. arraylist与List<>

    arraylist 不用规定 类型 list<>用规定类型 推荐list 这个编译检查类型  出错率低 代码质量高

  6. Linux下oracle 11g安装

    服务器环境要求     硬盘 20G以上 ,必须高于1G的物理内存,交换空间一般为内存的2倍,例如:1G的内存可以设置swap 分区为3G大小   在Root用户下执行以下步骤: 修改用户的SHELL ...

  7. C#限制转换后的double小数点留1位

    glb_timer1WaitSeconds += 0.1; string s = string.Format("{0:f1}", glb_timer1WaitSeconds);

  8. PHP原始的数据库操作

    <?php    //这是一个工具类;作用是完成对数据库的操作;    class SqlHelper{        public $conn;        public $dbname=& ...

  9. UVA 12169 Disgruntled Judge

    我该怎么说这道题呢...说简单其实也简单,就枚举模拟,开始卡了好久,今天看到这题没a又写了遍,看似会超时的代码交上去a了,果然实践是检验真理的唯一标准... #include <iostream ...

  10. HDU 5417 Victor and Machine

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5417 Problem Description Victor has a machine. When t ...