Swift 全功能的繪圖板開發

jopen 9年前發布 | 17K 次閱讀 Swift Apple Swift開發

原文 http://www.cocoachina.com/swift/20151125/14390.html

Swift 全功能的繪圖板開發 Swift 全功能的繪圖板開發 Swift 全功能的繪圖板開發

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

  • 支持鉛筆繪圖(畫點)

  • 支持畫直線

  • 支持一些簡單的圖形(矩形、圓形等)

  • 做一個真正的橡皮擦

  • 能設置畫筆的粗細

  • 能設置畫筆的顏色

  • 能設置背景色或者背景圖

  • 能支持撤消與重做

我們先做一些基礎性的工作,比如創建工程。

Swift 全功能的繪圖板開發

工程搭建

先創建一個Single View Application 工程:

Swift 全功能的繪圖板開發

語言選擇Swift:

Swift 全功能的繪圖板開發

為了最大程度的利用屏幕區域,我們完全隱藏掉狀態欄,在Info.plist里修改或添加這兩個參數: 

Swift 全功能的繪圖板開發

然后進入到Main.storyboard,開始搭建我們的UI。

我們給已存在的ViewController的View添加一個UIImageView的子視圖,背景色設為Light Gray,然后添加4個約束,由于要做一個全屏的畫板,必須要讓Constraint to margins保持沒有選中的狀態,否則左右兩邊會留下蘋果建議的空白區域,最后把User Interaction Enabled打開:

Swift 全功能的繪圖板開發

Swift 全功能的繪圖板開發

然后我們回到ViewController的View上:

  • 添加一個放工具欄的容器:UIView,為該View設置約束:

Swift 全功能的繪圖板開發

同樣的不要選擇Contraint to margins。

  • 在該View里添加一個UISegmentedControl,并給SegmentedControl設置6個選項,分別是:

  1. 鉛筆

  2. 直尺

  3. 虛線

  4. 矩形

  5. 圓形

  6. 橡皮擦

  • 給這個SegmentedControl添加約束:

Swift 全功能的繪圖板開發

垂直居中,兩邊各留20,高度固定為28。

完整的UI及結構看起來像這樣:

Swift 全功能的繪圖板開發

ImageView將會作為實際的繪制區域,頂部的SegmentedControl提供工具的選擇。 到目前為止我們還沒有寫下一行代碼,至此要開始編碼了。

Swift 全功能的繪圖板開發

你可能會注意到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中。

實時顯示當前的繪制狀態,并記錄繪制的最后一個點。

這些工作完成以后,我們就可以開始寫第一個工具了:鉛筆工具。

Swift 全功能的繪圖板開發

鉛筆工具

鉛筆工具應該支持連續不斷的繪圖(不斷的保存到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畫線。 

這樣一來,一個鉛筆工具就完成了,怎么樣,很簡單吧。

Swift 全功能的繪圖板開發

測試

到目前為止,我們的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]

編譯、運行,鉛筆工具可以完美運行~!

Swift 全功能的繪圖板開發

其他的工具

接下來我們把其他的繪圖工具也實現了。

其他的工具不像鉛筆工具,不需要支持連續不斷的繪圖,所以也就不用覆蓋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);

加入這一句代碼,一個真正的橡皮擦便實現了。

再次測試

現在我們的工程結構應該類似于這樣:

Swift 全功能的繪圖板開發

我們修改下ViewController中的brushes屬性的初始值:

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

編譯、運行: 

Swift 全功能的繪圖板開發

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

Swift 全功能的繪圖板開發

設計思路

在繼續完成剩下的功能之前,我想先對之前的代碼進行些說明。

為什么不用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(及其子類)也并不知道自己將會被用于哪,它們只需要實現自己的算法即可。類似于這樣的圖:

Swift 全功能的繪圖板開發

實際上這里包含了兩個設計模式。

策略設計模式

策略設計模式的UML圖:

Swift 全功能的繪圖板開發

策略設計模式在iOS中也應用廣泛,如AFNetworking的AFHTTPRequestSerializer和AFHTTPResponseSerializer的設計,通過在運行時動態的改變委托對象,變換行為,使程序模塊之間解耦、提高應變能力。

以我們的繪圖板為例,輸出不同的圖形就意味著不同的算法,用戶可根據不同的需求來選擇某一種算法,即BaseBrush及其子類做具體的封裝,這樣的好處是每一個子類只關心自己的算法,達到了高聚合的原則,高級模塊(Board)不用關心具體實現。

想象一下,如果是讓Board里自身來處理這些算法,那代碼中無疑會充斥很多與算法選擇相關的邏輯,而且每增加一個算法都需要重新修改Board類,這又與代碼應該對拓展開放、對修改關閉原則有了沖突,而且每個類也應該只有一個責任。

通過采用策略模式我們實現了一個好維護、易拓展的程序(媽媽再也不用擔心工具欄不夠用了^^)。

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

模板方法

在傳統的策略模式中,每一個算法類都獨自完成整個算法過程,例如一個網絡解析程序,可能有一個算法用于解析JSON,有另一個算法用于解析XML等(另外一個例子是壓縮程序,用ZIP或RAR算法),獨自完成整個算法對靈活性更好,但免不了會有重復代碼,在DrawingBoard里我們做一個折中,盡量保證靈活性,又最大限度地避免重復代碼。

我們將BaseBrush的角色提升為算法的基類,并提供一些便利的屬性(如beginPoint、endPoint、strokeWidth等),然后在Board的drawingImage方法里對BaseBrush的接口進行調用,而BaseBrush不會知道自己的接口是如何聯系起來的,雖然supportedContinuousDrawing(這是一個“鉤子”)甚至影響了算法的流程(鉛筆需要實時繪圖)。

我們用drawingImage搭建了一個算法的骨架,看起來像是模板方法的UML圖:

Swift 全功能的繪圖板開發

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

BaseBrush通過提供抽象方法(drawInContext)、具體方法或鉤子方法(supportedContinuousDrawing)來對應算法的每一個步驟,讓其子類可以重定義或實現這些步驟。同時,讓模板方法(即dawingImage)定義一個算法的骨架,模板方法不僅可以調用在抽象類中實現的基本方法,也可以調用在抽象類的子類中實現的基本方法,還可以調用其他對象中的方法。

除了對算法的封裝以外,模板方法還能防止“循環依賴”,即高層組件依賴低層組件,反過來低層組件也依賴高層組件。想像一下,如果既讓Board選擇具體的算法子類,又讓算法類直接調用drawingImage方法(不提供鉤子,直接把Board的事件下發下去),那到時候就熱鬧了,這些類串在一起難以理解,又不好維護。

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

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

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

  • 提供對畫筆顏色、粗細的設置

  • 背景設置

  • 全屏繪圖(不能讓Board一直顯示不全)

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

畫筆設置

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

設置工具欄

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

  1. 拖一個UIToolbar到Board的父類上,與Board的視圖層級平級。

  2. 設置UIToolbar的約束:左、右、下間距為0,高為44:

  3. Swift 全功能的繪圖板開發

  4. 往UIToolbar上拖一個UIBarButtonItem,title就寫:畫筆設置。

  5. 在ViewController里增加一個paintingBrushSettings方法,并把UIBarButtonItem的action連接paintingBrushSettings方法上。

  6. 在ViewController里增加一個toolar屬性,并把Xib中的UIToolbar連接到toolbar上。

UIToolbar配置好后,UI及視圖層級如下:

Swift 全功能的繪圖板開發

RGBColorPicker

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

Swift 全功能的繪圖板開發 Swift 全功能的繪圖板開發 Swift 全功能的繪圖板開發

這個工具類很簡單,沒有采用Auto Layout進行布局,因為layoutSubviews方法已經能很好的滿足我們的需求了。當用戶拖動任何一個UISlider的時候,我們能實時的通過colorChangedBlock回調給外部。它能展現一個這樣的視圖: 

Swift 全功能的繪圖板開發

不過雖然該工具類本身沒有采用Auto Layout進行布局,但是它還是支持Auto Layout的,當它被添加到某個Auto Layout的視圖中的時候,Auto Layout布局系統可以通過intrinsicContentSize知道該視圖的尺寸信息。

最后它還有一個setCurrentColor方法從外部接收一個UIColor,可以用于初始化。

畫筆設置的UI

我打算在用戶點擊畫筆設置的時候,從底部彈出一個控制面板(就像系統的Control Center那樣),所以我們還要有一個像這樣的設置UI:

Swift 全功能的繪圖板開發

具體的,創建一個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到了這一點,毛玻璃效果就算實現了~~

Swift 全功能的繪圖板開發

測試畫筆設置

我們在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
}

這么一來畫筆設置就做完了,代碼應該還是比較好理解,編譯、運行后,應該能看到:

Swift 全功能的繪圖板開發 Swift 全功能的繪圖板開發 Swift 全功能的繪圖板開發

完成度已經很高了^^!

Swift 全功能的繪圖板開發

背景設置

整體的框架基本上已經在之前的工作中搭好了,我們快速過掉這一節。

在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屬性上。

看上去像這樣:

Swift 全功能的繪圖板開發

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

編譯、運行,現在你可以用不同的背景色(或背景圖)了!

Swift 全功能的繪圖板開發 Swift 全功能的繪圖板開發 Swift 全功能的繪圖板開發

全屏繪圖

到目前為止,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()
    }
}

Swift 全功能的繪圖板開發

旅行到終點了~!

Swift 全功能的繪圖板開發

感謝一路的陪伴!

看了下,有些小長,文本+代碼有2w3+,全部代碼去除空行和空格有1w4+,直接貼代碼會簡單很多,但我始終覺得讓代碼完成功能并不是全部目的,代碼背后隱藏的問題定義、設計、構建更有意義,畢竟軟件開發完成“后”比完成“前”所花費的時間永遠更多(除非是一個只有10行代碼或者“一次性”的程序)。

希望與大家多多交流。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!