概述
摘要:從制作一個看圖app和了解關鍵概念開始swift編程。
概念:Constants and variables, method overrides, table views and image views, app bundles, NSFileManager, typecasting, arrays, loops, optionals, view controllers, storyboards, outlets, UIImage.
1. 設置
2.刪除基本代碼
3.用NSFileManager展示圖片
4.Interface Builder介紹
5.用UIImage載入圖像
6.最后的微調:hidesBarsOnTap
7.總結
設置
在這個項目中你會完成一個讓用戶可以滾屏查看圖像列表,然后選定一張仔細觀察的圖片瀏覽app。項目很簡單,是因為做的同時又很多其他東西需要你學,所以提醒自己要努力——學完的時間會有點漫長!
啟動Xcode,在歡迎界面中選擇“Create a new project”。選擇Master-Detail Application,然后點擊Next。Product Name輸入Project1,語言選擇Swift ,設備選擇Universal。
其中的Organization Indentifier這一項,要倒著填寫你的個人網站域名,比如com.hackingwithswift 。(本來網址是hackingwithswift.com,現在倒過來。)如果你想在iPhone或iPad上測試你的app,你需要填寫一些有效的地址,或者就填個com.example。
重要提示:有些Xcode的項目模板里有選項Use Core Data、Include Unit Tests和Include UI Tests。請確保在這個系列的所有項目中,這些選項都沒有被勾選。
現在再次點擊Next然后可以選擇項目保存的位置——桌面就行。選好保存位置,你就可以看到Xcode為你準備好的示例項目。首要任務是確定所有的設置都正確,這樣就可以保證項目按照預定的計劃進行。
當你運行一個app的時候,你要選擇它所要運行的iOS模擬器,又或者是你插到電腦上的實體設備。這些選項可以在Product>Destination 菜單下可以看到,還有其他如iPad2,iPad Air等等。
還有快捷鍵。在Xcode窗口的左上角有個play和stop按鈕,按鈕右邊有個Project1和device name。你可以點擊這個device name 來選擇不同的設備。
在本項目中,請選擇iPhone 5s,然后點擊play按鈕。這樣一來代碼會被編譯,也就是將你寫的代碼轉換成iPhone們可以理解的代碼,然后啟動模擬器,運行app。你會看到你跟app交互的時候,也就是點擊“+”列表中會添加新的日期時間;點擊“Edit”可以進入編輯模式,你可以更改或者刪除日期時間,按住時間向左拖動也可以刪除;點擊時間可以在新頁面中顯示時間。
你可以隨意開始或者終止你的項目,不論多少次。這里有些基本的忠告你得知道:
1.運行項目的快捷鍵是Cmd+R。
2.結束項目的快捷鍵是Cmd+.
3.如果你修改了項目,再按次Cmd+R。Xcode默認結束當前運行中的項目,然后另起一個。確定你已經勾選了“Do not show this message again”,這樣就不會再有煩人的提問了。
這個項目是關于讓用戶選擇一張圖去看,所以你得導入一些圖片。從GitHub下載相關的文件,從里面找到Project1 文件夾,在里面的Content文件夾里有你需要的圖片。
我想讓你直接把Content文件夾拖進你的Xcode項目中,就放在Info.plist下面。這時有個窗口會彈出來問你想怎么添加這些文件——確保“Copy items if needed”和“Create groups”已經勾選。
!:如果選擇了“Create folder references”,項目就無法工作。
點擊Finish后你就會在Xcode中看到一個黃色文件夾的圖標。如果圖標是藍色的,表示你沒有選擇“Create groups”,你就會悲劇了。
刪除基本代碼
Apple的示例包含了太多我們不需要的代碼,讓我們擦除掉。打開MasterViewController.swift進行編輯。大約在17行的位置你會看到override func viewDidLoad() {,然后幾行代碼之后會有個}在28行,對,一行只有一個}。如果你不確定自己選對了},只要確定它跟override的o垂直對其即可。
沒有行號?如果你的Xcode默認不顯示行號,我建議你打開它。進入Xcode的菜單,選擇Preferences,然后選擇Text Editing標簽然后勾選“Line numbers”。
當系統創建好視窗(screen),viewDidLoad()方法中的代碼塊就會被調用,你可以通過它來做一些初始配置。
這個方法(method)從func viewDidLoad() {開始,然后在不遠處的 } 結束。{ 和 } 用來標記一大塊代碼。而方法名下面一行的縮進,讓辨認代碼塊的起和止變得非常方便。說的夠多了,現在刪除這個方法中除了super.viewDidLoad()的所有代碼,因為這些對于本項目來說完全沒用。
Note:當我說刪除內容時,表示除了方法名和 { } 以外,其他都要刪除。所以,刪完的結果是:
、、、
override func viewDidLoad() {
super.viewDidLoad()
}
、、、
override func viewDidLoad() {
super.viewDidLoad()
}
這個方法現在啥都不干,等會兒咱們會添加一些內容進去。
接下來,找到insertNewObject()方法,然后刪除整個方法,包括insertNewobject(sender: AnyObject) { 和 } 。
swift中所有的方法都以func(func是function的縮寫)開頭,不論這個方法是用來完成什么任務的。只有一個小例外,不過在project24之前你都不會遇到,所以現在你可以簡單的認為函數和方法說的是一回事。
最后,刪除文件中的最后一個方法。它有個奇怪的名字,是Objective-C的遺留物。這個方法很長,雖然這里用不上它,但是你還是需要理解它表示什么意思。下面是方法描述:
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath)
讓我們來分解下:
override :表示該方法已經被定義過了,我們想用新的描述來重寫。如果不重寫,原先的定義的方法就會被執行,在這個實例中新方法什么也不會做。
func tableView :方法的名字是tableView,聽上去不是很有用。但Apple定義方法的方式是保證其中傳遞的信息——也就是參數——被命名的很有用。在這里,最先被傳遞的是被觸發的表視圖(table view)。表視圖是在樣例項目中包含所有日期的、可以滾動的東西,是iOS中一個核心組件。
tableView: UITableView :這就是剛才說的被傳遞到方法中的第一個參數——被代碼觸發的表視圖。它包含兩個信息:tableView 是方法中我們可以使用的表視圖的名字,UITableView是它的數據類型——用來描述它是什么的數據。每一個以“UI”開頭的都是Apple自帶的開發工具,所以UITableView是默認的Apple表視圖。
commitEditingStyle editingStyle: UITableViewCellEditingStyle :這一部分說明了方法最核心的內容——它打算做什么。通過它的名字,我們知道該方法需要一個表視圖,但commitEditingStyle部分才是實際的動作:代碼將會在用戶嘗試進入表視圖編輯模式時被觸發。也就是說,當用戶修改表格的內容時,這些代碼將會被調用。
還有:方法中有tableView: UITableView表示我們可以通過“tableView”這個參數引用表視圖,但這里參數是commitEditingStyle,聽上去就很傻很累贅。所以swift允許你給參數額外的名字:一個用在傳遞數據(這里是commitEditingStyle),另一個在方法內使用(editingStyle)。然后是冒號和數據類型UITableViewCellEditingStyle,意思是說,commitEditingStyle和editingStyle都是這個類型的數據。
forRowAtIndexPath indexPath: NSIndexPath:又是個不太清楚的參數名,但同時你也能看到他的用處:人們可以通過forRowAtIndexPath來調用這個方法,方法內部你可以直接使用indexPath。它的數據類型是NSIndexPath,包含了兩個信息:所在表的部位和表的行號。這里我們不需要部位,但需要行號——在示例中,每個被插入的時間都是一行。
我不會假裝Swift的方法長什么樣和怎么工作是很好理解的事情,但現在如果你不是很懂也不需要太擔心,因為幾小時的編程之后它們就會變成一種第二本能。至少你得知道當你在用它們的名字的時候,是在用什么方法還有什么參數。Parameters without names are just referenced as underscores:_.
于是,你剛刪除的方法名為tableView(_:commitEditingStyle:forRowAtIndexPath:)——很晦澀,我知道,所以人們才用最重要的部分來稱呼它,比如“在commitEditingStyle 方法中。”
最后一點要刪除的是很細微的東西:找到tableView(_:cellForRowAtIndexPath:)方法(接下來我會叫它cellForRowAtIndexPath)然后你會看到以下兩行代碼:
let object = objects[indexPath.row] as! NSDate
cell.textLabel!.text = object.description
我要你刪掉as! NSDate 和 .description,然后最后你的代碼是介個樣子:
let object = objects[indexPath.row]
cell.textLabel!.text = object
當你改好之后,Xcode就會開始提示你這里有問題。這完全正常——還有更多的地方需要改動。
現在,找到prepareForSegue() 方法然后你會看到另外一個你需要刪掉的as NSDate,刪掉之后這行代碼看起來是這樣:
let object = objects[indexPath.row]
如果現在你可以運行項目,你會看到這貨基本沒用,因為添加按鈕已經被刪掉了。但你現在無法運行這個程序,因為還有問題需要修復。
有問題?OK。如果你不確定什么代碼可以刪除,我寫了個特別的Xcode程序包含了本章中的所有改動。記住,現在的程序有問題所以跑不起來,但是下個章節就會修復。
用NSFileManager陳列圖片
使用的照片來自于國家海洋和氣象管理局(NOAA),這是一個美國政府機構,它網站上的內容我們都可以免費使用。一旦你的項目采用這些圖片,Xcode會自動把它們內建到程序中,這樣你就可以獲取它們。
在后臺中,一個iOS app實際就是一個包含了很多文件的目錄:二進制代碼(就是編譯完成后的代碼,可以運行),所有的app中用到的媒體資源,任何可視化排版(visual layout)文件,還有其他的比如元數據(metadata)和安全保障(security entitlements)。
這些app目錄被稱為bundles,而且他們有擴展文件(extension.app)。因為媒體文件在文件夾中很松散,所以我們可以讓系統告訴我們都有哪些文件在內,然后可以拉出我們想要的來。你可能已經注意到這些圖片名都是以“nssl”(National Severe Storms Laboratory)開頭的,所以任務很簡單:列出app目錄中所有的文件,然后找到以“nssl”開頭的那些。
就像我之前說的,方法viewDidLoad()從func viewDidLoad() { 開始到幾行后的 } 結束。我們要在其中添加更多代碼,就在super.viewDidLoad()下面:
let fm = NSFileManager.defaultManager()
let path = NSBundle.mainBundle().resourcePath!
let items = try! fm.contentsOfDirectoryAtPath(path)
for item in items {
? ? ? if item.hasPrefix("nssl") {
? ? ? ? ? object.append(item)
? ? ? }
}
我已經跟你說過任何以UI開頭的數據類型都是Apple的iOS自帶開發工具,但其實這只說對了一部分。UI表示用戶界面,所以這些類型主要是跟用戶可以接觸到的東西有關——UITableView是表格,UITextField是文本輸入框等等。但還有很多其它數據類型也是Apple提供的,比如這里的NSFileManager 和NSBundle。
長話短說,NS是Apple在97年買進的軟件NeXSTEP的縮寫。NeXSTEP開發的技術現在仍是iOS的核心部分。NSBundle 和NSFileManager是可以幫你完成偉大任務的數據類型,但他們沒有一個視覺化的組件(component)。附帶一提,這些NS數據類型同樣在OSX和iOS中存在,然而UI的數據類型只存在于iOS。
好了,現在讓我看看這些代碼做了些什么:
let fm = NSFileManager.defaultManager() 代碼定義了一個名為fm的常量然后將NSFileManager.defaultManager()返回的值賦予給它。這是個能讓我們操作文件系統的數據類型,在這里我們用它來尋找文件。
let path = NSBundle.mainBundle().resourcePath! ?這里定義了一個名為path的常量,其值被設置成我們的app目錄中的資源的路徑。所以,這行的意思是“告訴我在哪可以找到這些我添加進app中的圖像”。
let items = try! fm.contentsOfDirectoryAtPath(path) ?這里定義了第三個常量,名為items,被賦予path路徑指向的目錄下的內容。什么path路徑?就是前一行代碼返回的。如你所見,Apple的長方法名確實能讓它們的代碼具有自解釋性!
for item in items { ?這里開始了一個循環。循環是一堆被反復執行多次的代碼。在這里,每當我們在app 目錄(bundle)中找到一項時循環就被執行一次。注意這里還有個 { ,表示一個新代碼塊的開始,幾行后可以找到對應的 } 。
每次循環進行時,所有在大括號中的內容都會被執行一邊。我們可以將這行翻譯成“把這些小項當成一系列的文本字符串,然后拉出其中的每一個,然后給他們命名為item,然后執行后面的代碼塊。。。”我們使用文本字符串是因為contentsOfDirectoryAtPath()返回的是一個文件名的列表。
if item.hasPrefix("nssl") { 這是循環中的第一行。此時,我們會得到第一個需要處理的文件名,即item。我們用hasPrefix()方法來確定這是否為我們想要的那個:它需要一個參數(前綴),然后返回真或假。
“if”在這里表示這一行是一個條件語句:如果item有前綴“nssl”,那么……是的,另一個左大括號去標記另一個新的代碼塊。這次,代碼只會在hasPrefix()返回真時被執行。
objects.append(item) 這行代碼只有在hasPrefix()返回真的時候被執行,它把適配的文件名添加到列表objects的末尾。這就是在Xcode樣例項目中的,我們并沒有創建過它。
寥寥幾行代碼,卻有很多東西,總結下:
我們用let來定義常量。常量是我們要引用,但不會改變的值。比如,你的生日是個常量,但你的年齡不是——年齡是個變量,因為它總在變。
在其他開發者大量使用變量的時候,swift程序員更喜歡使用常量。這是因為當你真正開始碼代碼的時候,你會發現很多數據其實根本沒有變化過,所以你最好讓它成為常量。這讓系統可以做一些優化,也可以增加安全性,因為當你想要改變一個常量的值時,系統會出現錯誤提示。
Swift中的文本使用數據類型字符串(String)。Swift字符串非常強大——無論你使用何種語言,都沒有問題。
值的集合稱之為數組,一次僅限存儲一種數據類型。字符串數組寫作[String],只能存儲字符串。放進數字就會提示錯誤。有一種特殊的數據類型叫AnyObject,意思是說,你能想象的任何數據類型都可以被放在這個位置上。
關鍵字try!不太常用,這里表示“我知道調用這些代碼可能會失敗,但我確定它不會(失敗)。”如果失敗了,app就會崩潰。與此同時,如果代碼無法執行意味著app無法讀取它自己的數據,也就是說可能存在某些嚴重的錯誤,這就是為啥try!可以被用在這里。?
你可以用for someVar in someArray 來循環數組里的每一個元素。Swift逐個取出元素然后為他們各執行一次循環中的代碼。
如果你十分善于觀察,你可能注意到一個很小,很小的事情,這也是Swift種最復雜的部分。所以我會盡可能讓它簡單一點,然后慢慢展開:就是NSBundle.mainBundle().resourcePath!最后的感嘆號“!”。這不是印刷錯誤,如果你刪掉它,程序將停止執行,所以明擺著Xcode認為它很重要——而且它確實很重要。Swift有三種處理數據的方式:
? ? ? ? 1. 儲存著數據的變量或常量。比如foo: String 表示一個叫foo的字符串。
? ? ? ? 2. 可能儲存著數據的變量或常量,但我們不確定。這被稱為可選類型(optional type),看起來是這樣:foo: String? 你不能直接使用它,你必須先打開它。
? ? ? ? 3. 可能儲存著數據的變量或常量,我們實際確定它儲存著——至少一開始被設置好了的。它被稱為隱式解包可選類型,比如foo: String! ,你可以直接使用它(foo)。
當我跟人們解釋這些的時候,人們總是混淆可選和隱式解包可選,很大程度上是因為他們看上去很相似。隱式解包可選——就是“!”們——為兩個目的服務:讓碼代碼更簡單,也讓眾多的Apple API更好地兼容。
稍后我們會更深入地認識可選(optional),但現在重要的是NSBundle.mainBundle().resourcePath 是否會返回字符串。它返回的是可選字符串 String? 。在末尾加上“!”意味著我們強制將可選字符串解包,表示“我非常肯定返回的是字符串而不會是nil,所以請把它當做常規字符串給我。”
重要提醒:如果你解包的是值為nil的常量或變量,你的app將會崩潰。結果,有些人就稱“!”為崩潰操作符就因為它很容易出錯。try! 也是如此,很容易出錯。別擔心現在這些聽起來很難懂——接下來你會經常用到它們,然后會越來越有感覺。
在我們進入下一步之前,還有個小改動要做:現在你知道AnyObject,String 和數組是什么了,你應該可以看到MasterViewController的頂部有一行var objects = [AnyObject] (),它把數組objects定義為AnyObject類型。我們做了很多改動,所以現在我們可以肯定我們要增加的objects一定是字符串,所以,我們可以把定義語句做如下改動:
var objects = [String] ()
這下Swift就會確信我們在objects中存取的都是字符串了,這讓我們的代碼變得安全。現在你的項目已經可以正常運行了,運行之后你能看到一張都是你的圖片的名字的列表了。成功了!讓我們讓它變得更好玩兒……
Interface Builder 介紹
當你第一次運行模板app時你會看到,點擊一個日期就會有新的頁面自動跳出來顯示日期。這是由一個從左到右的圓滑動畫完成的,同時帶有一個Back鍵這樣你就可以回到先前的頁面。你可能也已經注意到你可以從左邊沿開始向右滑動來返回表視圖。
所有這些動作iOS的兩個重要的數據類型都已經為我們提供了:UISplitViewController和UINavigationController。憑這倆名字你可以猜到:
1.“UI”是指為iOS設計的交互組件。
2. “Controller”表明它提供了設計功能。
控制器是軟件開發三位一體的一部分:模型(Model),視圖(View),控制器(Controller)。理想情況下,你的app的每個部分都可以被分解成這三種類型:要么是模型(描述你正在操作中的數據的東西),視圖(app的用戶界面),或者是控制器(將數據在視圖和模型之間傳遞的代碼)。
現實中,事情幾乎不可能如此清晰,臃腫的控制器問題隨處可見:很多本該是模型和視圖的代碼最終進入到了控制器代碼區。雖然不理想,但你可以回頭重寫得更整潔。(然而你根本不會這么做!)
現在你可能正在想,你根本沒寫任何區分視圖控制器或導航控制器的代碼,你是對的。這是因為Apple提供了一個可以直觀修改app排版的工具——Interface Builder。在你的項目中選擇Main.storyboard,然后IB(Interface Builder)就出現了。
IB(也被認作storyboard editor) 是為了展示程序的視覺流程而設計的。你可能需要拉遠,盡管你的app很簡單,但用戶界面相當的復雜!同時按住Cmd和“-”一次來縮小一個視圖水平。現在你可以看到屏幕中有五個方塊:一個是分割視圖控制器(Split View Controller),兩個導航控制器(Navigation Controller),還有一個表視圖(Table View)和一個詳情頁("Detail view content goes here.")。
在方塊之間有從左到右的箭頭,顯示程序流程。這些叫做segues(切換標志)。中間像個啞鈴的叫關系切換,用于連接分割視圖控制器和兩個導航控制器,還有兩個導航控制器和它們右邊的方塊。
視圖控制器我們等會兒說,現在你需要知道這關系切換描述的是內嵌內容頁面。也就是說,頂部導航控制器和表視圖之間的關系是表視圖內嵌于導航控制器中。
分割視圖控制器和導航控制器之間的關系切換有點復雜,但也相當聰明。它內嵌了兩個導航控制器,但不能同時呈現。因為之前我們要求模擬器模擬的是iPhone5s。
如果你把模擬器改成iPad2再運行一次,你會看到新東西:豎直屏幕時,Master在頂部,你點擊圖片的名字時圖片詳情頁面會從左側滑入;而如果你按下鍵盤上的Cmd+→ ,你會同時看到表視圖和詳情視圖,因為屏幕自動分成了兩部分。
這就是Apple所說的“用戶界面自適應”,就是說你只要設計過你的app一次,不同設備上的iOS系統就會根據自己的設備信息來決定最好的呈現方式。現在我們已經看到同一信息的三種不同的呈現方式:iPhone,iPad豎屏和iPad橫屏,而代碼完全相同。雖然分割視圖控制器和兩個導航控制器略顯復雜,但結果是多設備的全適應。
再回到IB中,在頂部的表視圖和底部的導航控制器之間出現了第二種切換標志(segue)。這種切換標志看起來像兩個長方形中間被向左箭頭穿過,它叫“顯示詳情”切換。你已經看到它創建的動作了:從右側滑入的詳情視圖控制器。這是個自適應的切換標志,“iPhone中動畫從右側進入,在iPad中只是改變詳情視圖控制器。”
現在正好是個簡單接觸視圖控制器的好機會,因為它是iOS中最重要的組件之一。數據類型UIViewController代表app中所有的頁面,而且自帶異常豐富的內建功能。比如,它可以跟著設備旋轉,它可以在內存不足時響應,它可以隱藏其他視圖控制器等等。
視圖控制器也可以用一種聰明的方式來代表app中的部分頁面。比如,iPhone上的郵箱app有個視圖控制器顯示的是收件箱(inbox)中的信息,當你點擊其中一條信息時,會有一個新的視圖控制器來顯示信息詳情。
這樣的設置對iPhone來說簡直完美,特別是屏幕空間不足,一次只能做一件事的時候。但在iPad端,同樣的郵箱app就會同時顯示信息(屏幕左側)和詳情(屏幕右側),即同時顯示兩個視圖控制器。
屏幕背后,用的就是同一個分割視圖控制器,而iOS只是確保右排版(right layout)被自動使用。為代碼的反復利用歡呼!
在我們當前的app中,我們有五個視圖控制器:分割視圖控制器、兩個導航控制器、我們的主視圖控制器(即表視圖)和詳情視圖控制器。所有的數據類型都是UIViewController,但是利用繼承技術添加了他們各自的功能——每個定制視圖控制器逐字逐句地繼承了UIViewController的所有功能,然后加上他們自己的。
所以,分割視圖控制器負責根據設備選擇右側布局,導航控制器在頂部加上標題,主控制器上有表視圖和我們用NSFileManager寫的代碼,詳情控制器有顯示被選中日期的文本。你可以使用多層次繼承,比如數據類型D繼承自C,而C繼承自B,B繼承自A。聽上去有點復雜,但是這意味著你要盡可能提高代碼的利用率。
好,說夠了就該動動手了。我們需要修改詳情視圖控制器因為現在它的中間是顯示日期的文本。我們想顯示的是圖片而不是日期,所以我們得重做用戶界面。雙擊詳情視圖控制器以放大和選中,在中間你會看到內有“Detail view content goes here”的UILabel,用于顯示用戶無法修改的文本。選中并刪除它。
我們要用一個大大的UIImageView來替代這個UILabel,是的,就是一個用來顯示圖片的UI組件。iOS有一個頂呱呱的視圖類型集合,你可以從中拖拽任何你想要的到故事板(storyboard)中,所有這些都存儲在對象庫中。它就在Xcode的右下角,如果你看不到,可以使用快捷鍵Ctrl+Alt+Cmd+3。
有些用戶把對象庫設置成圖標視圖,也就是你會看到一個箭頭帶著一系列的黃色圓圈。這對初學者來說沒用,所以在對象庫的底部你會看到一個搜索框,點擊作伴的按鈕就會以列表的形式呈現對象庫。
對象庫包含所有可以使用的內建視圖類型,你會發現數量不少。晚點你愛看多少就看多少,但現在請使用搜索框:輸入image來找到圖像視圖組件(Image View component)。選中按住鼠標左鍵然后將它拖進詳情視圖控制器,然后松手。現在拖動它的邊沿讓它覆蓋整個視圖控制器——甚至包括名為Detail的灰色導航條。
現在的圖像視圖沒有內容,所以只有藍色背景和Image View的字眼。我們現在不會把任何內容賦予它——這是我們將要在程序運行中做的事。相對的,我們需要告訴圖像視圖如何根據我們的屏幕去調整自己的大小,無論iPhone還是iPad。
一開始可能會覺得奇怪,說到底你只是讓它充滿了視圖控制器,然后它倆一樣大了,不就這樣了?但,不確切。一開始,詳情視圖控制器是正方形的,但你看過正方形的iPhone嘛?如果你要支持各種設備,那又會怎樣呢?所以圖像視圖到底該如何響應?
iOS有自己的解決辦法,而且看上去像是一種魔法。它叫做自動排版(Auto Layout):它讓你定義一些關于視圖的規則,然后它能確保規則被很好的遵守。但它自己有兩條你必須要遵守的規則:
你的排版規則必須完整,也就是說,你不能只規定X方向上的要求,你也必須規定Y方向上的。X從屏幕左側開始,Y從屏幕頂部開始。
你的排版規則必須不會互相沖突。比如,你不能規定一個視圖離左邊界10點,離右邊界也是10點同時寬度還是1000點。而iPhone5s只寬320點。自動排版會嘗試通過打破規則來解決這些問題,但最終結果肯定不是你想要的。
你完全可以在IB中創建自動排版規則,也就是限制條件(constraints),而且它會在你沒有遵守規則時提示你,甚至自動修正你的錯誤。
我們將要創建4條限制條件:圖像視圖的上、下、左、右各一條,以此來無視詳情視圖控制器地進行填充。添加限制條件的方法很多,最簡單的是Pin按鈕,就在IB的底部右側。
右下角有4個按鈕:第一個是三個疊加長方形中間帶一個向下箭頭,第二個是上下倆長方形,第三個是兩根線夾一個正方形,第四個是兩根線夾一個三角形。全都沒有名字——誰說Apple總在用戶界面設計上表現的很出色?我們想要的是Pin按鈕,也就是第三個。你可以把鼠標放在上面看看工具提示,就是Pin。
選中圖像視圖,然后點擊Pin按鈕。這時會有個窗口彈出,名字是Add New Constraints。反選“Constrain to margins”,點擊上面的四條虛線,然后他們就會變成實線。
這樣就會創建規定圖像視圖的四邊跟詳情視圖四邊的距離限制條件了。當你把上面的虛線變成實線時,底部的按鈕就會變成“Add 4 Constraints”,也就是增加四條限制條件,現在點擊它就完成自動排版的設計任務了。
看起來你的排版沒啥變化,但確實有些細微的不同。首先,在詳情視圖控制器中的UIImageView周圍會有一圈藍色細線框,這是IB在提示圖像視圖的自動排版沒有問題。
接下來你會在文檔概要框中的image view下看到新出現的Constraints。如果你不太清楚,或者你的 文檔概要框被隱藏了,選中圖像視圖(image view)然后進入編輯菜單(Editor),選擇Reveal in Document Outline 來顯示文檔概要和高亮image view。4條限制條件全都隱藏在Constraints項目下,你可以點開來逐個察看。
限制條件完成后,還要在IB中做一件事,就是把我們新建的圖像視圖跟代碼連接起來。在界面中放置好圖像視圖顯然是不夠的——如果我們想在代碼中引用它,我們還得為它創建一個跟排版直接相關的屬性。
啥叫屬性?你已經見過常量(用let定義)和變量(用var定義),但這些都是臨時的所以只能用到方法結束的時候。屬性是指附屬于某個數據類型的常量或者變量。這里的圖像視圖就是我們為詳情視圖控制器創建的一個屬性,它跟詳情視圖控制器同時出現同時消失,它屬于詳情視圖控制器。
Xcode的樣板項目的詳情視圖控制器中有個UILabel,而且有個對應的屬性。我們會刪掉它然后再創建一個。
為了讓這過程變得簡單點兒,Xcode有個特別的布局叫輔助編輯器(Assistant Editor)。它會將Xcode的界面一分為二,上半部分是IB,下半部分是對應的代碼(也就是詳情視圖控制器的代碼)。
Xcode根據IB中選中的對象來決定顯示哪一部分的代碼,所以,確保選中圖像視圖,然后點擊菜單欄中的View>Assistant Editor>Show Assistant Editor,也可以通過快捷鍵Alt+Cmd+Return實現,又或者點擊輔助編輯器按鈕,也就是Xcode窗口右上角六個按鈕中的第二個,看起來像是倆重疊的圓圈。
右上角這六個按鈕分別是:標準編輯器(the standard editor),輔助編輯器(the assistant editor),版本編輯器(the version editor),然后是控制左窗格、下窗格和右窗格出現/隱藏的按鈕。在這六個按鈕下面的是很多探測器,最常用的是其中的第3、4、5個。
現在可以你看到上面的窗格是IB中的詳情視圖控制器,下面的窗格是我們還沒看過的DetailViewController.swift。這是管理詳情視圖控制器的代碼,在頂部你會看到以下代碼:
@IBOutlet weak var detailDescriptionLabel: UILabel!
這里有些新內容,讓我們分解下:
@IBOutlet:這個屬性用于告訴Xcode這行代碼跟IB之間有關聯。
weak:weak告訴iOS我們不會將這個對象放入內存中。這是因為這個對象已經被放到視圖中了,所以它屬于視圖。
var:定義一個新的變量或變量屬性。我們已經用過let來定義常量了。記住,一個常量的值只能被設置一次,而一個變量的值可以被改變。
detailDescriptionLabel:這是賦予使用的UILabel的名字。注意名字中大小寫的使用規則:變量和常量必須以小寫字母開頭,然后接下來的單詞的首字母則換成大寫的。比如myAwesomeVarible。這叫做駝峰拼寫法。
UILabel!:這申明了屬性的類型為UILabel,而且再次看到了隱式解析可選符號:!。這表示UILabel可能在那或者不在那,但我們可以確定它一定在那所以可以用符號“!”。
如果你還在理解隱式解析可選中掙扎,這行代碼可能會讓它簡單點兒。你看,當詳情視圖控制器被創建出來時,它的視圖還沒有載入——它只是一些運行于CPU中的代碼而已。
當所有基本要素都完成(比如給它分配了足夠多的內存),iOS繼續載入故事板中的排版布局,然后把所有的IBOutlet跟代碼進行關聯。
所以,當詳情控制器剛被創建時,UILabel還不存在因為還未被創建——但我們還需要給它留一些內存空間。此時,屬性為nil(空),即空內存。但當視圖載入和輸出口(outlet)連接完成,UILabel會指向一個真實的UILabel,而不是nil,這樣我們就可以使用它了。
簡單說,UILabel始于nil,然后在我們使用之前它被賦予一個值,這樣我們就可以確信想使用的時候它的值不會是nil——一個文本版的隱式解析可選。
(PS:如果你還是不懂隱式解析可選,完全OK——保持繼然后它們就會變得越來越明朗。)
返回項目中:我們沒有UILabel,所以你可以刪除整行代碼。別擔心:學@IBOutlet的知識并不是虛的,我們接下來要為圖像視圖創建一個新的輸入口(outlet)。在圖上操作就可以完成創建:按住Ctrl,鼠標左鍵點擊UImageView然后拖到代碼窗格中的@IBOutlet原來的位置上。如果你忘記掉了,它原來就在class DetailViewController: UIViewController {這行下面。
當你Ctrl拖拽時,會出現一條藍線。當你鼠標移動到代碼區時,會出現個提示“Insert Outlet or Outlet Collection(插入輸入口或輸入口集合)”,同時第二條水平藍線會出現在你將要插入outlet的位置。
松掉鼠標左鍵時會彈出一個窗口讓你填如一些信息。你需要做的是給創建的outlet一個名字。所以在名字欄輸入detailImageView然后點擊連接(connect)。完成后你會看到這行代碼:
@IBOutlet weak var detailImageView: UIImageView!
這跟剛才刪掉的代碼沒啥區別,但現在這是個圖像視圖而不是標簽。至關重要的是,如果你看這行代碼的最左邊,你會看到一個帶圓圈的灰色圓點。這是Xcode在告訴你這個outlet已經跟IB連接上了。如果沒有,你看到的會是一個不帶點的圓圈。
用UIImage載入圖像
為了改變布局,我們破壞了代碼——你會注意到代碼編輯器中出現了一條紅線,Xcode頂部還有個紅色的警告標志。這是因為DetailViewController.swift的其他部分指向的是UILabel,而我們剛把它給刪了。所以我們需要做點小改動讓它知道怎么去操作我們的新數據,然后修改下MasterViewController.swift來正確地傳遞新數據。
首先,在DetailViewController.swift中找到以下代碼:
var detailItem: AnyObject? {
? ? ? didSet {
? ? ? ? ? ?// Update the view.
? ? ? ? ? ?self.configureView()
? ? ? }
}
這里創建了一個名為detailItem的屬性,類型為AnyObject?——Swift這是在表明它可能是個什么類型的對象,或者什么都不是。但這個屬性有個彎兒,因為附帶了一個屬性觀察器,就是用didSet的形式。
每當屬性的值改變了之后,didSet屬性觀察器中的這塊代碼就會被執行。還有個對應的willSet屬性觀察器,是在屬性改變之前被執行的,沒didSet這么常用。
這里的didSet被用來調用self.configureView(),“調用我自己的configureView()方法。” self.其實并不必需,所以可以刪除。你可能對用“self.”來調用變量或方法的兩類程序員的思路感興趣。
第一類人從來不喜歡用self.除非逼不得已,因為用self.被使用的時候代表了十分重要的意義,所以用到其他地方會讓人困惑。另一類人則能用就用,甚至一些不必要的地方。公平起見,到處使用self.是個OC中的好習慣,而且習慣很難改。
在我們修復錯誤之前,我們再多介紹點知識。detailItem被定義為AnyObject?類型,挺蠢的。我們已經知道什么類型的數據將被主視圖控制器傳輸,因為我們操作的就是一組字符串。盡管是個可選類型,但detailItem只能是String。
所以,把AnyObject?改成String?,然后Cmd+B編譯你的項目。這會產生另一個錯誤,這沒關系,因為我們即將修復它!
錯誤出現在DetailViewController.swift中的configureView()方法內。現在的代碼是:
if let detail: AnyObject = self.detailItem {
? ? ? if let label = self.detailDescriptionLabel {
? ? ? ? ? ? label.text = detail.description
? ? ? }
}
detailItem屬性是個可選數據類型:原來是AnyObject? ,現在是String? 。這意味著要先解析它的值然后再使用它——我們需要確認其中是否有值,如果有,就取出來。最常見的可選類型取值方法是用“if let” 條件語句,也就是現在的代碼正在做的。比如:
if let foo = bar {
? ? ? doStuff(foo)
}
這些代碼的意思是“看下變量bar是否有值,如果有則把它的值賦予變量foo,然后調用doStuff()并把foo作為參數傳入。”此時,如果bar是String?類型,foo就會是String類型因為它不再有可選項了。如果bar沒有值,doStuff()永遠不會被調用。“if let”句法的好處是一次性完成檢查和解包的任務。
想到這里,讓我們再看次代碼:
if let detail: AnyObject = self.detailItem {
? ? ? ? if let label = self.detailDescriptionLabel {
? ? ? ? ? ? ? ? label.text = detail.description
? ? ? ? }
}
你會看到它首先檢查detailItem是否有值,如果有就將它解包,并值取出來放到一個名為detail的新常量中。接著檢查detailDescriptionLabel是否有值,如果有就取出其值,放到另一個名為label的新常量中。如果兩次解包都有值,那就把detail.description賦值給label的text屬性。因為日期本來就在,所以代碼會以文本的形式來顯示日期。這些代碼出錯是因為我們不再有label,所以將其改寫為:
if let detail = self.detailItem {
? ? ? ?if let imageView = self.detailImageView {
? ? ? ? ? ? ? ?imageView.image = UIImage(named: detail)
? ? ? ?}
}
讓我們看看都改了些什么:
我們不再需將detail定義為AnyObject,因為self.detailItem是String?類型而不是AnyObject?
我們將detailImageView解包為imageView,因為我們現在用的是圖像視圖而不是標簽。
我們現在設置的是圖像視圖的圖像,而不是標簽的文本。
新代碼引進了新數據類型,叫UIImage。它名兒里不帶View所以它不是個視圖——用戶看不到它。UIImage是一種用來載入圖像數據的數據類型,比如PNG或JPEG。
當你創建UIImage時,它有個named參數用于存放選定的圖片名。接著UIImage就會在app的目錄下尋找到這個圖片名,然后載入到app中。通過detail這個常量,也就是detailItem解包得到的,UIImage就會載入用戶選擇的圖片。
這是DetailViewController.swift中唯一的錯誤,但你可能比較好奇選中的圖片名是怎么從主視圖控制器傳入到詳情視圖控制器中的。想知道就看看MasterViewController.swift。
圖像名的傳遞是由MasterViewController.swift中的prepareForSegue()方法完成的。當跳轉將要發生時,該方法就會被調用,給你個機會給新的視圖控制器設定信息。這就是我們告訴詳情視圖控制器哪個文件被選中的地方,然后完成操作的代碼是下面的部分:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
? ? ? if segue.indentifier == "showDetail" ?{
? ? ? ? ? ? ?if let indexPath = self.tableView.indexPathForSelectedRow {
? ? ? ? ? ? ? ? ? let object = objects[indexPath.row]
? ? ? ? ? ? ? ? ? let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
? ? ? ? ? ? ? ? ? controller.detailItem = object
? ? ? ? ? ? ? ? ? controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
? ? ? ? ? ? ? ? ? controller.navigationItem.leftItemSupplementBackButton = true
? ? ? ? ? ? ? }
? ? ? ?}
}
代碼已完整,但只有三行是重要的:
let object = objects[indexPath.row]
let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
為了讓它好懂點,我們要改寫它。用下面的代碼來替代上面的三行:
let navigationController = segue.destinationViewController as! UINavigationController
let controller = navigationController.topViewController as! DetailViewController
controller.detailItem = objects[indexPath.row]
第一行尋找跳轉的目標視圖控制器,就是“現實詳情(show detail)”的跳轉目標。如果你還記得,目標是導航控制器,所以這行代碼的作用是讓目標控制器被當做導航控制器來處理。翻譯一下就是:“我知道你以為目標視圖控制器會是個常規的UIViewController,但信我:實際上那是個UINavigationController。”
當prepareForSegue()被調用時,新的視圖控制器在創建之后即將出現,改造新視圖得提供它需要的數據,所以確定新視圖控制器的類型很有必要。我們可以通過讀取segue.destinationViewController屬性來獲得新的視圖控制器,但問題是它可能是任何一種視圖控制器——Swift確實知道它是某種確定的UIViewController,但不知道具體是哪種。
有時這并不重要。如果你只是想展示一些東西而且不關心它的值,你不需要做任何的類型轉換,甚至不需要這些代碼——跳轉會自動發生。但prepareForSegue()很特別,可以讓我們在視圖控制器顯示之前做任何定制化操作,這里就是給詳情視圖控制器初始化一個屬性。
我們需要把destinationViewController的類型轉換為UINavigationController是因為第二行會發生的事:導航控制器有個名為topViewController的屬性指向現在正顯示的任何視圖控制器。對我們來說,就是我們的詳情視圖控制器,但iOS并不知道——它認為topViewController就是個常規的UIViewController。所以我們需要再次使用as! 改寫它:我們就是在告訴Swift這個對象的數據類型確實是DetailViewController。
最后,一旦我們發現了導航控制器(第一行),發現里面的詳情視圖控制器(第二行),我們就可以把詳情視圖控制器的屬性detailItem改成選中的圖片。把屬性detailItem設置成objects[indexPath.row]就是為了做這件事。
屬性objects,是我們從app目錄中載入的一個字符串數組。數組是一個接一個排列的一組值,你可以通過從零開始的數組腳標來讀取,objects中的第一個元素是objects[0],第二個是objects[1],第十個是objects[9],以此類推。
所以,什么是indexPath.row?indexPath是指表格中出現的幾行——比如,用戶點擊來觸發跳轉的那些。它的屬性row是指在表格中的位置,所以,objects[indexPath.row]意思是“獲取用戶點擊位置的數組objects中的元素”。
最后的改進:hidesBarsOnTap
現在,你有了個可以干活的app:你可以按Cmd+R來運行下,撥動列表中的圖片,然后點擊一個來看大圖。在完成項目之前,還有兩個小改動來讓它更閃亮。
首先,你可能已經注意到所有的圖像都是拉伸來填滿整個屏幕。這并不是個以外——這是UIImageView的默認設置。幾下就能改好:選中Main.storyboard,在詳情視圖控制器中選擇圖像視圖,然后選擇屬性觀察器(Attributes Inspector),就是Xcode界面右上角窗格六個觀察器中的第四個。
如果你懶得找,那就Cmd+Alt+4。拉伸是視圖模式的默認選項“尺寸適應Scale to Fit”,改成視圖適應(Aspect Fit)。
Aspect Fit讓你能看見整幅圖,Aspect Fill讓顯示中沒有空白部分——這意味著剪短圖片的一部分,不是橫向就是縱向。如果你使用Aspect Fill,圖像會充分地利用它的視圖區域,所以你得確保Clip Subviews已經勾選來避免太多的圖像。
第二個要做的改動是允許用戶使用全屏幕來看圖,就是隱藏掉導航欄。很簡單,UINavigationController有個屬性叫hidesBarsOnTap。當它被設置為真,用戶就可以觸碰當前視圖控制器的任何位置來隱藏的導航條,再觸碰下來重新顯示它。
警告:在iPhone上你得小心點設置它。如果讓它一直被處在開啟狀態,它會影響到表視圖的觸碰,特別是用戶想選擇東西的時候會引發大災難。所以在看詳情視圖控制器時需要激活它,在隱藏時使之無效。
你已經見過viewDidLoad()方法了,就是在視圖控制器的布局完成之后調用的。當視圖就要顯示時、已經被顯示時、顯示完成之前、顯示完成之后,還有好些其他方法被調用了。它們是:viewWillAppear(),viewsDidAppear(),viewWillDisappear()和viewDidDisappear()。我們要用viewWillAppear()和viewDidDisappear()來修改hidesBarsOnTap屬性讓它只有在詳情視圖控制器顯示的時候啟動。
打開DetailViewController.swift,然后直接在viewDidLoad()方法下面添加兩個新方法:
override func viewWillAppear(animated: Bool) {
? ? ? ? super.viewWillAppear(animated)
? ? ? ? navigationController?.hidesBarsOnTap = true
}
override func viewWillDisappear(animated: Bool) {
? ? ? ?super.viewWillDisappear(animated)
? ? ? ?navigationController?.hidesBarsOnTap = false
}
幾條重要的提示:
我們在每個方法都使用了override,因為它們已經在UIViewController中被定義過了,而且我們要用我們自己的。別擔心自己不知道什么要用什么時候不用,因為如果你不用它,Xcode會用出錯來提示你。
兩個方法都只有一個參數:動作是否被激活。這里我們并不十分關心這一點,所以這里我們會忽視它。
兩個方法都用了super前綴,意思是“告訴我的父數據類型這些方法被調用了。”在這里表示它把調用的方法繼承到UIViewController中,也就是它會自己完成自己的進程。
兩個視圖控制器都有一個可選屬性:navigationController,可以讓我們引用本身所在的導航控制器。可選是因為不是所有的視圖都在導航控制器中。在Swift中你可以在語句中使用問號來評價一個可選項,而語句只有當可選可以被解包時會被執行。所以,如果我們不在導航控制器中,hidesBarsOnTap就沒用。
如果你現在運行app,你可以輕觸圖片來看完整尺寸的圖片,它再也不會被拉伸。當你看圖時,你可以輕觸圖片來顯示或隱藏導航欄。搞定!
總結
項目很簡單,但你已經學到很多關于Swift,Xcode還有storyboards的東西。我知道這不簡單,但是相信我,你已經通過最難的部分了。
為了讓你知道你學到多遠了,這里是我們涉及到了的東西:常量和變量,方法重寫,表視圖和圖像視圖,app目錄,NSFileManager,類型轉換,數組,循環,可選項,視圖控制器,故事板,輸入口,自動布局,UIImage等。
很大的量,同時很殘酷的是你已經忘掉一半了。但是沒問題,因為我們總是通過重復來學習,所以如果你繼續跟著剩下的系列來學習,你會一次又一次的重復它們直到它們對你來說易如反掌。
如果你想花更多時間在這個app上,試著調查兩個視圖控制器的title屬性。這能讓你定制頂部導航條中的文字——讓它顯示選中圖像的名字是一件很簡單的事。