Swift 全功能的繪圖板開發
要做一個全功能的繪圖板,至少要支援以下這些功能:
- 支援鉛筆繪圖(畫點)
- 支援畫直線
- 支援一些簡單的圖形(矩形、圓形等)
- 做一個真正的橡皮擦
- 能設定畫筆的粗細
- 能設定畫筆的顏色
- 能設定背景色或者背景圖
- 能支援撤消與重做
- …
我們先做一些基礎性的工作,比如建立工程。
工程搭建
先建立一個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個選項,分別是:- 鉛筆
- 直尺
- 虛線
- 矩形
- 圓形
- 橡皮擦
- 給這個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類似:
- 拖一個
UIToolbar
到Board
的父類上,與Board
的檢視層級平級。 - 設定
UIToolbar
的約束:左、右、下間距為0,高為44:
- 往
UIToolbar
上拖一個UIBarButtonItem
,title
就寫:畫筆設定。 - 在
ViewController
裡增加一個paintingBrushSettings
方法,並把UIBarButtonItem
的action
連線paintingBrushSettings
方法上。 - 在
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的背景色為透明:
- 放置一個title為“畫筆粗細”的
UILabel
,約束設為:寬度固定為68,高度固定為21,左和上邊距為8。 - 放置一個title為“1”的
UILabel
,“1”與“畫筆粗細”的垂直間距為10,寬度固定為10,高度固定為21,與superview
的左邊距為10。 - 放置一個
UISlider
,用於調節畫筆的粗細,與“1”的水平間距為5,並與“1”垂直居中,高度固定為30,寬度暫時不設,在PaintingBrushSettingsView
中新增strokeWidthSlider
屬性,與之連線起來。 - 放置一個title為“20”的
UILabel
,約束設為:寬度固定為20,高度固定為21,top與“1”相同,與superview
的右間距為10。並把上一步中的UISlider
的右間距設為與“20”相隔5。 - 放置一個title為“畫筆顏色”的
UILabel
,寬、高、left與“畫筆粗細”相同,與上面UISlider
的垂直間距設為12。 - 放置一個
UIView
至“畫筆顏色”下方(上圖中被選中的那個UIView),寬度固定為50,高度固定為30,left與“畫筆顏色”相同,並且與“畫筆顏色”的垂直間距為5,在PaintingBrushSettingsView
中新增strokeColorPreview
屬性,與之連線起來。 - 放置一個
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, didFinishPic