1. 程式人生 > >Swift 全功能的繪圖板開發

Swift 全功能的繪圖板開發

要做一個全功能的繪圖板,至少要支援以下這些功能:

  • 支援鉛筆繪圖(畫點)
  • 支援畫直線
  • 支援一些簡單的圖形(矩形、圓形等)
  • 做一個真正的橡皮擦
  • 能設定畫筆的粗細
  • 能設定畫筆的顏色
  • 能設定背景色或者背景圖
  • 能支援撤消與重做

我們先做一些基礎性的工作,比如建立工程。
這裡寫圖片描述

工程搭建

先建立一個Single View Application 工程:
這裡寫圖片描述
語言選擇Swift
這裡寫圖片描述
為了最大程度的利用螢幕區域,我們完全隱藏掉狀態列,在Info.plist裡修改或新增這兩個引數:
這裡寫圖片描述
然後進入到Main.storyboard,開始搭建我們的UI。
我們給已存在的ViewController

View新增一個UIImageView的子檢視,背景色設為Light Gray,然後新增4個約束,由於要做一個全屏的畫板,必須要讓Constraint to margins保持沒有選中的狀態,否則左右兩邊會留下蘋果建議的空白區域,最後把User Interaction Enabled開啟:
這裡寫圖片描述
這裡寫圖片描述
然後我們回到ViewControllerView上:

  • 新增一個放工具欄的容器: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.storyboardImageView的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的狀態,比如beganmovedended等,為此我們建立一個列舉,在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()
    }
}

我們需要防止brushnil的情況,以及為brush設定好beginPointendPoint,之後我們就可以完善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
    }
}

步驟解析:

  1. 開啟一個新的ImageContext,為儲存每次的繪圖狀態作準備。
  2. 初始化context,進行基本設定(畫筆寬度、畫筆顏色、畫筆的圓潤度等)。
  3. 把之前儲存的圖片繪製進context中。
  4. 設定brush的基本屬性,以便子類更方便的繪圖;呼叫具體的繪圖方法,並最終新增到context中。
  5. 從當前的context中,得到Image,如果是ended狀態或者需要支援連續不斷的繪圖,則將Image儲存到realImage中。
  6. 實時顯示當前的繪製狀態,並記錄繪製的最後一個點。

這些工作完成以後,我們就可以開始寫第一個工具了:鉛筆工具。
這裡寫圖片描述

鉛筆工具

鉛筆工具應該支援連續不斷的繪圖(不斷的儲存到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連線到ViewControllerswitchBrush:方法上,實現如下:

@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)
    }
}

這裡我們就用到了BaseBrushstrokeWidth屬性,因為我們想要建立一條動態的虛線。

矩形

建立一個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之間的關係

ViewControllerBoardBaseBrush這三者之間,雖然VC要知道另外兩個元件,但是僅限於選擇對應的工具給Board,Board本身並不知道當前的brush是哪個brush,也不需要知道其內部實現,只管呼叫對應的brush就行了;BaseBrush(及其子類)也並不知道自己將會被用於哪,它們只需要實現自己的演算法即可。類似於這樣的圖:
這裡寫圖片描述
實際上這裡包含了兩個設計模式。

策略設計模式

策略設計模式的UML圖:
這裡寫圖片描述
策略設計模式在iOS中也應用廣泛,如AFNetworkingAFHTTPRequestSerializerAFHTTPResponseSerializer的設計,通過在執行時動態的改變委託物件,變換行為,使程式模組之間解耦、提高應變能力。
以我們的繪圖板為例,輸出不同的圖形就意味著不同的演算法,使用者可根據不同的需求來選擇某一種演算法,即BaseBrush及其子類做具體的封裝,這樣的好處是每一個子類只關心自己的演算法,達到了高聚合的原則,高階模組(Board)不用關心具體實現。
想象一下,如果是讓Board裡自身來處理這些演算法,那程式碼中無疑會充斥很多與演算法選擇相關的邏輯,而且每增加一個演算法都需要重新修改Board類,這又與程式碼應該對拓展開放、對修改關閉原則有了衝突,而且每個類也應該只有一個責任。
通過採用策略模式我們實現了一個好維護、易拓展的程式(媽媽再也不用擔心工具欄不夠用了^^)。

策略模式的定義:定義一個演算法群,把每一個演算法分別封裝起來,讓它們之間可以互相替換,使演算法的變化獨立於使用它的使用者之上。

模板方法

在傳統的策略模式中,每一個演算法類都獨自完成整個演算法過程,例如一個網路解析程式,可能有一個演算法用於解析JSON,有另一個演算法用於解析XML等(另外一個例子是壓縮程式,用ZIPRAR演算法),獨自完成整個演算法對靈活性更好,但免不了會有重複程式碼,在DrawingBoard裡我們做一個折中,儘量保證靈活性,又最大限度地避免重複程式碼。
我們將BaseBrush的角色提升為演算法的基類,並提供一些便利的屬性(如beginPointendPointstrokeWidth等),然後在BoarddrawingImage方法裡對BaseBrush的介面進行呼叫,而BaseBrush不會知道自己的介面是如何聯絡起來的,雖然supportedContinuousDrawing(這是一個“鉤子”)甚至影響了演算法的流程(鉛筆需要實時繪圖)。
我們用drawingImage搭建了一個演算法的骨架,看起來像是模板方法的UML圖:
這裡寫圖片描述

圖中右邊的方框代表模板方法。

BaseBrush通過提供抽象方法(drawInContext)、具體方法或鉤子方法(supportedContinuousDrawing)來對應演算法的每一個步驟,讓其子類可以重定義或實現這些步驟。同時,讓模板方法(即dawingImage)定義一個演算法的骨架,模板方法不僅可以呼叫在抽象類中實現的基本方法,也可以呼叫在抽象類的子類中實現的基本方法,還可以呼叫其他物件中的方法。
除了對演算法的封裝以外,模板方法還能防止“迴圈依賴”,即高層元件依賴低層元件,反過來低層元件也依賴高層元件。想像一下,如果既讓Board選擇具體的演算法子類,又讓演算法類直接呼叫drawingImage方法(不提供鉤子,直接把Board的事件下發下去),那到時候就熱鬧了,這些類串在一起難以理解,又不好維護。

模板方法的定義:在一個方法中定義一個演算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變演算法結構的情況下,重新定義演算法中的某些步驟。

其真實模式都很簡單,很多人在工作中會思考如何讓自己的程式碼變得更好,“情不自禁”地就會慢慢實現這些原則,瞭解模式的設計意圖,有助於在遇到需要折中的地方更加明白如何在設計上取捨。

以上就是我設計時的思路,說完了,接下來還要完成的工作有:

  • 提供對畫筆顏色、粗細的設定
  • 背景設定
  • 全屏繪圖(不能讓Board一直顯示不全)

先從畫筆開始,Let’s go!

畫筆設定

不管是畫筆還是背景設定,我們都要有一個能提供設定的工具欄。

設定工具欄

所以我們往Board上再蓋一個UIToolbar,與頂部的View類似:

  1. 拖一個UIToolbarBoard的父類上,與Board的檢視層級平級。
  2. 設定UIToolbar的約束:左、右、下間距為0,高為44:
    這裡寫圖片描述
  3. UIToolbar上拖一個UIBarButtonItemtitle就寫:畫筆設定。
  4. ViewController裡增加一個paintingBrushSettings方法,並把UIBarButtonItemaction連線paintingBrushSettings方法上。
  5. ViewController裡增加一個toolar屬性,並把Xib中的UIToolbar連線到toolbar上。

UIToolbar配置好後,UI及檢視層級如下:
這裡寫圖片描述

RGBColorPicker

考慮到多個頁面需要選取自定義的顏色,我們先建立一個工具類:RGBColorPicker,用於選擇RGB顏色:

class RGBColorPicker: UIView {

    var colorChangedBlock: ((color: UIColor) -> Void)?

    private var sliders = [UISlider]()
    private var labels = [UILabel]()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.initial()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        self.initial()
    }

    private func initial() {
        self.backgroundColor = UIColor.clearColor()
        let trackColors = [UIColor.redColor(), UIColor.greenColor(), UIColor.blueColor()]

        for index in 1...3 {
            let slider = UISlider()
            slider.minimumValue = 0
            slider.value = 0
            slider.maximumValue = 255
            slider.minimumTrackTintColor = trackColors[index - 1]
            slider.addTarget(self, action: "colorChanged:", forControlEvents: .ValueChanged)
            self.addSubview(slider)
            self.sliders.append(slider)

            let label = UILabel()
            label.text = "0"
            self.addSubview(label)
            self.labels.append(label)
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        let sliderHeight = CGFloat(31)
        let labelWidth = CGFloat(29)
        let yHeight = self.bounds.size.height / CGFloat(sliders.count)

        for index in 0..<self.sliders.count {
            let slider = self.sliders[index]
            slider.frame = CGRect(x: 0, y: CGFloat(index) * yHeight, width: self.bounds.size.width - labelWidth - 5.0, height: sliderHeight)

            let label = self.labels[index]
            label.frame = CGRect(x: CGRectGetMaxX(slider.frame) + 5, y: slider.frame.origin.y, width: labelWidth, height: sliderHeight)
        }
    }

    override func intrinsicContentSize() -> CGSize {
        return CGSize(width: UIViewNoIntrinsicMetric, height: 107)
    }

    @IBAction private func colorChanged(slider: UISlider) {
        let color = UIColor(
            red: CGFloat(self.sliders[0].value / 255.0),
            green: CGFloat(self.sliders[1].value / 255.0),
            blue: CGFloat(self.sliders[2].value / 255.0),
            alpha: 1)

        let label = self.labels[find(self.sliders, slider)!]
        label.text = NSString(format: "%.0f", slider.value)

        if let colorChangedBlock = self.colorChangedBlock {
            colorChangedBlock(color: color)
        }
    }

    func setCurrentColor(color: UIColor) {
        var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0
        color.getRed(&red, green: &green, blue: &blue, alpha: nil)
        let colors = [red, green, blue]

        for index in 0..<self.sliders.count {
            let slider = self.sliders[index]
            slider.value = Float(colors[index]) * 255

            let label = self.labels[index]
            label.text = NSString(format: "%.0f", slider.value)
        }
    }
}

這個工具類很簡單,沒有采用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))
        }
    }
}

strokeWidthChangedBlockstrokeColorChangedBlock兩個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連線到ViewControllerbackgroundSettings方法上,你可以選擇在插入“背景設定”之前,先插入一個FlexibleSpaceUIBarButtonItem
建立BackgroundSettingsVC類,繼承自UIViewController,這與畫筆設定繼承於UIView不同,我們希望背景設定可以在使用者的相簿中選擇照片,而使用UIImagePickerController的前提是要實現UIImagePickerControllerDelegateUINavigationControllerDelegate兩個介面,如果讓UIView來實現這兩個介面會很奇怪。
建立一個BackgroundSettingsVC.xib檔案:

  1. 放置一個title為“從相簿中選擇背景圖”的UIButton,約束為:左、上邊距為8,寬度固定為135,高度固定為30。
  2. 放置一個RGBColorPicker,約束為:左、右邊距為8,與UIButton的垂直間距為20,底部與superview齊平。
  3. 把UIButton的Touch Up Inside事件連線到BackgroundSettingsVCpickImage方法上;RGBColorPicker連線到BackgroundSettingsVCcolorPicker屬性上。

看上去像這樣:
這裡寫圖片描述

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, didFinishPic