close

這一次我們來進行第五堂課的內容。

前面的第四堂課內容我並沒有發布文章來做補充說明,因為那全都是投影片的語法說明,這部分的內容大家自己看文檔或者去 iTube 抓投影片來看就可以了。

下面我同樣附上 Youtube 的連結,包含第四堂課與第五堂課。



首先,稍微提一下 Object - C 與 Swift 的兼容問題。

絕大部分之前用  Object - C 開發的 API 在現今 Swift 上面都依然可以使用,但少數幾個會有一點問題,下面會簡單提到。

Object - C 的 NSString 到 Swift 變成 String ,兩者是一樣的東西,可以互相橋接。

Object - C 的 NSArray 到 Swift 變成 Array<AnyObject> ,兩者是一樣的東西,可以互相橋接。

Object - C 的 NSDictionary 到 Swift 變成 Dictionary<NSObject, AnyObject> ,兩者是一樣的東西,可以互相橋接。

Object - C 的 NSNumber 到 Swift 變成 Int, Float, Double, Bool ,但這裡稍微有點不同。雖然在 Swift 中依然可以使用 NSNumber ,但是在 Object - C 中卻不能使用 Int 之類的類型宣告,它看不懂這種定義。

當我們需要將 String 轉換為 NSString 來使用時可以利用 "Cast" 來轉換,而且這是完全符合規則且確定的,使用 "as" 的時候,不需要在後面加上問號 "?" 。


接下來講師介紹 AnyObject 的另一個用處,Property List ,這不是一個類型,而是一種工具類。

下面我附上投影片中的定義解釋,這段話講得比較好。

It means an AnyObject which is known to be a collection of objects which are ONLY one of ...

NSString, NSArray, NSDictionary, NSNumber, NSData, NSDate

e.g. an NSDictionary whose keys were NSString and values were NSArray of NSDate is one 

意思是說,在表面上被使用者使用時,他只能被認知為 AnyObject 的類型,但是在後台運作時,它實際上是諸如 NSString, NSNumber, NSArray, NSDictionary, NSData, NSDate 六者之一,或是它們橋接到 Swift 中的版本。

那麼為什麼要使用 Property List 呢?

1. 為了做隱式的傳遞數據。很多時候我們不希望使用者知道後台傳遞的資料類型,他們只需要把資料傳送出去就可以了。

2. 另外一點就是 Property List 可以用於泛型的數據結構,可以表示成各種類型的資料,像是 Array, Dictionary...等等。


另外 Property List 在 iOS 中還有一個相當有用的用處,那就是 NSUserDefaults 。

NSUserDefaults 是一個相當微小的資料庫,他只存儲 Property List ,像圖片之類不再 Property List 的類型之內的大型數據就不能被存儲。

它的好處是,雖然它也是一種字典,但是一般字典在我們的 App 關閉之後,裡面的資料也就消失了,而 NSUserDefaults 內的資料會繼續保留下來。


如何使用 NSUserDefaults 呢?

我們可以使用一個類方法 (Type Method) , ".standardUserDefaults" ,例如投影片有個實例。

let defaults = NSUserDefaults. standardUserDefaults()

當你要讀取 NSUserDefaults 內的資料時,可以使用 ".objectForKey(String)" 。

let plist: AnyObject = defaults.objectForKey(String) 

當你要寫入資料進 NSUserDefaults 內時,可以使用 ".setObject(AnyObject, forKey: String) " 。

defaults.setObject(AnyObject, forKey: String)     // AnyObject must be a PropertyList 

另外我們不需要對讀取與寫入的動作進行存儲,系統會自動幫你執行。

但是這個自動存儲的動作只有在我們的 App 關閉時或者被切換到後台執行時才進行,在運作過程中是不會主動進行的。

因此,我們還需要一個方法可以讓我們主動將資料存儲進去,那就是 "synchronize " 。

if !defaults.synchronize() { /* failed! not much you can do about it */ }

(it’s not “free” to synchronize, but it’s not that expensive either) 


下面利用我們之前寫的 CalculatorBrain 的 Model 來加入一個 Property List 做示範。

先宣告一個類型為 AnyObject 的變量屬性 program ,然後在內部宣告一個計算屬性來將它宣告為一個 Property List 。

做法是宣告一個變量屬性 returnValue 為 String 類型的 Array 結構,並且回傳給 program 。

而這個變量屬性 returnValue 會先將 updateStack 內的資料線掃過一遍,將每一個資料的字串類型 (String) 都插入 returnValue 這個 Array 之內。

也就是說 program 會得到一個實際為 Array<String> 類型,但表現為 AnyObject 類型的 updateStack ,這種表示方式就是 Property List 。

但是這樣寫起來過於冗長,為了讓程式碼更加簡潔,我們可以將其縮減為一行,方法是利用 "map" 。

map 文檔內容如下所示:

Declaration : @warn_unused_result @rethrows func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

Description : Return an Array containing the results of mapping transform over self.

也就是說,map 可以將 updateStack 內的每一個資料都映射出去給一組新的 Array 。


接著將 set 的內容補上之後可得到下面完整的 Property List 宣告程式碼。


下面接著進行新的內容,Views 。

它是 iOS 中 UIView 類的一個子類,表示螢幕上的一個長方形區域,定義了一個座標系空間來繪圖和處理觸摸的操控。

Views 是一個多層次結構,所以 Views 可以存在於另一個 Views 之中。

每一個 View 都只能有一個父類 (Super View) ,但卻可以有很多子類 (Sub View) 。

或許各位會看到宣告一個 super view 時的類型宣告是 UIView? ,一個 Optional。這是因為 Swift 允許擁有不在當前畫面中的 VIew 存在,這些不在畫面中的 View 都會回傳 NIL 。

var superview : UIView?

而一個子類 sub view 的宣告可以是 Array 的類型,如同前面所說,一個 View 可以有很多 sub view 。

var subview : [UIView]

再利用 Xcode 來編寫程式的時候,我們很幸運的不需要自己寫程式再創造 subview ,直接用 storyboard 的功能拖曳出來就可以了。

不過這並不代表無法自己編寫 view ,下面有兩種基本的方法可以幫助我們建立和移除 view 的結構。

1. addSubview(aView: UIView) // sent to aView’s (soon to be) superview 

2. removeFromSuperview() // this is sent to the view you want to remove (not its superview) 

前者可以向即將成為 super view 的對象發送訊息,後者則可以將自己從 super view 中移除。


再來,我們要開始了解繪圖與觸摸處理的內容和座標系統的資料結構。

有一個觀念我們必須在一開始先知道,那就是所有座標空間的單位類型都是 CGFloat

有需要的話,我們可以利用初始化方法將 CGFloat 轉換為 Double 或者 Float 。相反地, Double 和 Float 也可以用初始化方法將其轉換為 CGFloat 。

另外有種結構 (Struct) 叫做 CGPoint ,它由兩個 Float 類型的值組成,分別是 x 和 y ,用來定義座標中的一個點。

var point = CGPoint(x: 37.0, y: 55.2) 

point.y -= 30

point.x += 20.0

另一個結構叫做 CGSize ,它在座標中定義一個長與一個寬。

var size = CGSize(width: 100.0, height: 50.0) 

size.width += 42.5

size.height += 75 

最後一個結構體叫做 CGRect ,它是由 CGPoint 的 point 和 CGSize 的 size 所組成的結構。由 point 決定矩形的起始點,再由 size 的長與寬建立舉行的長寬。

struct CGRect { 

       var origin: CGPoint

       var size: CGSize   }

CGRect 結構中有許多有用的屬性,如下所示。

var minX: CGFloat    //  left edge

var midY: CGFloat    //  midpoint vertically

intersects(CGRect) -> Bool    //  does this CGRect intersect this other one? 

intersect(CGRect)    //  clip the CGRect to the intersection to the other one

contains(CGPoint)->Bool    //  does this CGRect contain this CGPoint?

還有一個觀念相當基本,那就是所用的單位是點,而不是像素 (pixel) 。

高分辨率的螢幕中,一個點中會包含許多像素。這時候有個屬性相當有用,contentScaleFactor ,它會返回給你每個點佔多少像素的值。


當我們要進行繪圖時,還有個相當重要的屬性,叫做 bounds,它是 CGRect 類型。,他定義了在視圖中的繪製區域。

而這裡有一個比較容易模糊的觀念,那就是繪製是基於自身的座標體系。

另外有兩個屬性很容易跟 bounds 混肴,那就是 center 和 frame 。這兩個屬性所參照的是父視圖的座標體系,它們的用途是幫助這個圖形在父視圖中定位。

1. var center: CGPoint // the center of a UIView in its superview’s coordinate system
2. var frame: CGRect // the rect containing a UIView in its superview’s coordinate system 


的確有些時候我們會想要自己建立 NIView ,例如我們想要繪製資料庫沒有的圖案或是想要自己設定觸控動作的細節時。

這時候的做法就是自己創造一個 UIView subclass ,然後調用 drawRect 並且修改。

override func drawRect(regionThatNeedsToBeDrawn: CGRect)

You can draw outside the regionThatNeedsToBeDrawn, but it’s never required 

The regionThatNeedsToBeDrawn is purely an optimisation 


下面還有一個重點是講師特別提出來的,我直接貼上原文。

NEVER call drawRect!! EVER! Or else! 

Instead, if you view needs to be redrawn, let the system know that by calling ... 

setNeedsDisplay() setNeedsDisplayInRect(regionThatNeedsToBeRedrawn: CGRect) 

iOS will then call your drawRect at an appropriate time 


接下來介紹我們用來做圖的工具, UIBezierPath ,下面我直接採用投影片上的敘述說明,反正我不會講得更好。

UIBezierPath automatically draws in the “current” context (drawRect sets this up for you) Methods for adding to the UIBezierPath (lineto, arcs, etc.) and setting linewidth, etc. Methods to stroke or fill the UIBezierPath 

下面舉個例子來說明如何用它來繪圖。

第一步,先將 UIBezierPath 這個類調用出來。

let path = UIBezierPath()

第二步,先設置一個起始點,然後調用實例方法將一條線拉至另一個點,接著再以同樣的方法拉另一條線至另一個點。

path.moveToPoint(CGPoint(80, 50)) // assume screen is 160x250 

path.addLineToPoint(CGPoint(140, 150)) 

path.addLineToPoint(CGPoint(10, 150))

第三步,將圖形封閉起來,程式會自動將最後一個點與起始點拉一條線相連,這樣就可以形成一個封閉的三角形。

path.closePath()

但事實上,到這個階段並不會有圖形產生,因為我們剛剛所繪製的線是一種敘述,可以想像為一條透明的線。

所以我們必須將這個圖形進行填充。

第四步,宣告填充顏色、外框顏色、具體線寬,然後填充和描邊。

UIColor.greenColor().setFill() // note this is a method in UIColor, not UIBezierPath 

UIColor.redColor().setStroke() // note this is a method in UIColor, not UIBezierPath 

path.linewidth = 3.0 // note this is a property in UIBezierPath, not UIColor 

path.fill()

path.stroke()

這邊要注意一下,顏色的宣告並不是用 UIBezierPath 來實現,而是用 UIColor 來實現。

至此,我們可以得到一個綠色填充的三角形,圖形如下所示。


順帶提一下 UIColor ,這也是一個相當強大的對象工具。

我們可以用它來給 UIView 的背景設置顏色。

var backgroundColor: UIColor

可以給顏色設置透明度。

let transparentYellow = UIColor.yellowColor().colorWithAlphaComponent(0.5) 

Alpha is between 0.0 (fully transparent) and 1.0 (fully opaque) 

當然也可以重疊兩張帶有透明屬性的圖案在一個位置上,但是這個動作因為需要資源太大,所以被設定為必須另外宣告。

var opaque = false 

如果整個視圖上的圖案都需要相同的透明度設置的話,有個簡單的方法就是直接在背景做宣告。

var alpha: CGFloat 


再補充一點,當我們想讓其中一個 View 暫時不被看到的時候,可以使用隱藏屬性,這時候它不僅不會被繪製,也不會被任何觸控動作觸發。

var hidden: Bool


再來講講文檔的繪製。

一般來說,我們會直接從 Storyboard 中拉出 UILabel 來編輯文字,但不排除有時候可能會需要直接在 drawRect 上編寫,於是我們可以調用 NSAttributedString 這個對象來實現。

let text = NSAttributedString(“hello”)

text.drawAtPoint(aCGPoint)

let textSize: CGSize = text.size // how much space the string will take up

與 String 類型的屬性不同,我們無法利用 var 宣告來在以後我們需要的時候改變它的內容,但這裡我們可以藉由調用 NSMutableAttributedString() 這個類方法來得到一個可變的屬性。

let mutableText = NSMutableAttributedString(“some string”)

而既然它的名字叫做 NSAttributedString ,那麼當然是代表說它可以用來設置內部屬性了。

func setAttributes(attributes: Dictionary, range: NSRange)

func addAttributes(attributes: Dictionary, range: NSRange)

NSForegroundColorAttributeName : UIColor

NSStrokeWidthAttributeName : CGFloat

NSFontAttributeName : UIFont


既然講到了文字,那麼字體當然是無法跳過的重點。

在這個講求視覺享受的時代,一個好的 App 肯定不能有不堪入目的字體設計。

那麼如何知道什麼樣的字體比較適合,比較好看呢?

恩,電腦會幫你選的,相信它的眼光肯定比你好。

class func preferredFontForTextStyle(UIFontTextStyle) -> UIFont

如果你比較喜歡自己來選擇,也是可以啦,例如下面就告訴我們三種不同風格的字體表達方式。

UIFontTextStyle.Headline 

UIFontTextStyle.Body

UIFontTextStyle.Footnote

如果你不想讓電腦幫你選擇適合字體,也懶得自己選,就用系統默認設置吧。

class func systemFontOfSize(pointSize: CGFloat) -> UIFont 

class func boldSystemFontOfSize(pointSize: CGFloat) -> UIFont


那麼如何顯示圖像呢?

通常我們真的就是直接用 storyboard 的功能來實現,抓取圖片、貼上圖片、修改圖片等等,不過下面還是列上幾個利用 UIImage 的方法來進行設置的程式碼。

let image: UIImage = ...

image.drawAtPoint(aCGPoint) // the upper left corner of the image put at aCGPoint 

image.drawInRect(aCGRect) // scales the image to fit aCGRect 

image.drawAsPatternInRect(aCGRect) // tiles the image into aCGRect


我知道單純用文字敘述的話,跟我一樣笨的人應該都是有看沒有懂。

幸運的是講師也知道,所以下面會有一個 Demo 來實際示範。

跟前面一樣,我這裡就不重複 Tool 的操作了,大家自己看影片來照做就可以了,我這邊只針對程式碼的部分做介紹。


下面是 FaceView 的初始畫面,我們即將用這個實現一個笑臉的動作。

大家可以看到下面的註解中有一行是 drawRect 的調用,因為需要消耗大量的資源,所以基本上在我們不打算用手動輸入程式碼來繪圖的時候都是默認註解的。

現在我們就是打算自己編寫,所以把註解解開來調用它吧。


首先,我們在 drawRect 內先宣告一個 facePath 的常量屬性,藉以設置中心、半徑、開始角度與結束角度和順逆時針方向的屬性。

let facePath = UIBezierPath(arcCenter: faceCenter, radius: faceRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)

由於我們打算畫一張笑臉,所以會是一個完整的圓形,角度從0度至360度,順逆時針無所謂。

另外由於父視圖會因為螢幕旋轉等原因導致長寬尺寸改變,所以我們不設置一個固定值,分別設置兩個變量屬性, faceCenter 與 faceRadius 。

var faceCenter: CGPoint {

        return convertPoint(center, fromView: superview) }

var faceRadius: CGFloat {

        return min(bounds.size.height, bounds.size.width) / 2 }

可以看到 faceCenter 我們宣告為 CGPoint 類型,並且利用計算屬性回傳一個 convertPoint 實例方法,這個方法能夠讓我們回傳父視圖的中心點。

參考其文檔內容如下。

Declaration : func convertPoint(point: CGPoint, fromView view: UIView?) -> CGPoint

Description : Converts a point from the coordinate system of a given view to that of the receiver.

A point specified in the local coordinate system (bounds) of view.

Parameters :

point : A point specified in the local coordinate system (bounds) of view.

view : The view with point in its coordinate system. If view is nil, this method instead converts from window base coordinates. Otherwise, both view and the receiver must belong to the same UIWindow object.

Returns : The point converted to the local coordinate system (bounds) of the receiver.

這裡我曾經好奇為什麼直接用存儲屬性來宣告,問了其他人後得到答案是,convertPoint 內的兩個屬性必須先初始化之後才能用存儲屬性來宣告。

而這邊的兩個屬性都是由父視圖引入,根據 Views 的原理,不再當前視圖的都等同於 nil ,自然無法使用存儲屬性宣告。

而 faceRadius 同樣以計算屬性宣告為邊界長與寬中的最小值在除與二得到半徑。

畢竟我們無法控制使用者要如何擺弄螢幕,乾脆就以螢幕的狀態為變量做宣告,這裡用 min 實例方法,他可以回傳兩個值中的最小值。

參考其文檔如下。

Declaration : @warn_unused_result func min<T : Comparable>(x: T, _ y: T) -> T

Description : Returns the lesser of x and y.


前面在示範繪圖方法時就已經說過,要讓圖案可以順利繪製出來,除了中心點與尺寸外,還需要宣告外框的線條參數與填充顏色。

而在上面的程式碼中,除了宣告顏色和線寬之外,還利用屬性觀測器 (Property Inspector) 來使 View 可以隨著設定的改變立即變化。

var lineWidth: CGFloat = 3 {

        didSet {

            setNeedsDisplay() } }

var color: UIColor = UIColor.blueColor() {

        didSet {

            setNeedsDisplay() } }

屬性觀測器的實際使用方法就是利用 didSet {} 來實現。

其參考文檔如下。

Declaration : func setNeedsDisplay()

Description : Marks the receiver’s entire bounds rectangle as needing to be redrawn.

You can use this method or the setNeedsDisplayInRect: to notify the system that your view’s contents need to be redrawn. This method makes a note of the request and returns immediately. 

The view is not actually redrawn until the next drawing cycle, at which point all invalidated views are updated.

也就是說,當屬性觀測器 didSet 發現 newValue 改變後,會執行 setNeedsDisplay() 這個實例方法的動作,重新繪製圖案。

要注意的是,上面兩個宣告只是宣告 lineWidth 和 color 兩個變量屬性,真正將其宣告為圖案的屬性是在下面的程式碼。

facePath.lineWidth = lineWidth

color.set()

facePath.stroke()

上面的 set() 是 UIColor 的實例方法,可以將宣告的顏色進行填充動作。

其參考文檔如下。

Declaration : func set()

Description : Sets the color of subsequent stroke and fill operations to the color that the receiver represents.

If you subclass UIColor, you must implement this method in your subclass. Your custom implementation should modify both the stroke and fill color in the current graphics context by setting them both to the color represented by the receiver.

stroke() 則是 UIBezierPath 的實例方法,可以進行線條繪製的動作。

其參考文檔如下。

Declaration : func stroke()

Description : Draws a line along the receiver’s path using the current drawing properties.

The drawn line is centered on the path with its sides parallel to the path segment. This method applies the current drawing properties to the rendered path.

This method automatically saves the current graphics state prior to drawing and restores that state when it is done, so you do not have to save the graphics state yourself.


到這裡已經可以初步執行程式進行繪圖了。

不過這裡依然有兩個問題,第一個就是圖案太貼邊界,看著不好看。第二就是當我旋轉螢幕時,原本的正圓形果然被拉成橢圓形了。

不過這兩個問題好解決。

1. 邊界的問題只需要將 faceRadius 的宣告多藉由另一個變量屬性 scale 的宣告就可以改變半徑與邊界長度的比例了。

2. 變形的問題只需要在 faceView 的屬性設置那邊將 mode 改為 redraw 。


接下來我們將笑臉的兩個眼睛和嘴巴的圖案也進行編寫。

這個部分應該算是很好理解的,我就不多說了。

不過有一個新的實例方法要幫大家介紹一下。

addCurveToPoint ,這是用來繪製弧形的方法,藉由設定起始點、結束點與中點就能夠以弧線將三個點連起來。

其參考文檔如下敘述。

Declaration : func addCurveToPoint(endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)

Description : Appends a cubic Bézier curve to the receiver’s path.

The end point of the curve.

Parameters : 

endPoint : The end point of the curve.

controlPoint1 : The first control point to use when computing the curve.

controlPoint2 : The second control point to use when computing the curve.


最後在進行繪製動作將眼睛與嘴巴繪製進剛剛的圓臉中。

bezierPathForEye(.left).stroke()

        bezierPathForEye(.right).stroke()

        let smiliness = 0.85

        let smilePath = bezierPathForSmile(smiliness)

        smilePath.stroke()


至此,我們完成了初步的笑臉圖案繪製動作。

這堂課也到此結束,以上。

By 宅宅阿軒

arrow
arrow
    文章標籤
    Swift iOS8
    全站熱搜

    Cloud 發表在 痞客邦 留言(0) 人氣()