概述
摘要:制作一款字母游戲同時學習閉包和布爾量。
概念:NSString,閉包,方法返回值,布爾量,NSRange
1.設置
2.從硬盤讀取:contentsOfFile
3.挑個詞,任何一個:UIAlertController
4.準備提交:lowercaseString和NSIndexPath
5.返回值:contains
6.或者其他什么?
7.總結
設置
項目1~4都特別簡單,因為我的目標是盡可能的多讓你了解Swift而不是嚇跑你,同時試著做點有用的東西。但現在你很有希望開始熟悉iOS開發的核心工具,是時候更換齒輪,來點更難的了。
這個項目中你將要學習怎么制作一個處理字母的單詞游戲,但是如同以往,我肯定會借機教你更多關于iOS開發的知識。雖然這次我們將要回到表視圖,但同時你也會學到如何從文件載入文本,如何在UIAlertController中獲得用戶的輸入,更深入地了解閉包的工作原理。
在Xcode中創建一個新的Master-Detail Application,命名為project5。選擇iPhone作為目標設備然后保存。現在打開IB中的Main.storyboard,刪除整個詳情視圖控制器——在最右邊。在文件導航器中右擊DetailViewController.swift,選擇刪除(Delete),然后選擇“Move to Trash”。
完成之后會出現很多錯誤,但都很好修復——我們只需刪除更多一點的Apple的模板即可!首先打開APPDelegate.swift,在開始的位置找到以下代碼:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
? ? ? ?// Override point for customization after application launch.
? ? ? ?let splitViewController = self.window!.rootViewController as! UISplitViewController
? ? ? ?let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
? ? ? ?navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
? ? ? ? splitViewController.delegate = self
? ? ? ? return true
}
刪除return true以外的所有內容,像這樣:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
? ? ? ? return ?true
}
在同個文件中,拉倒頁面底部找到一個特別長的方法:
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController:UIViewController, ontoPrimaryViewController primaryViewController:UIViewController) -> Bool {
然后刪除整個方法。
接下來,打開MasterViewController.swift,viewDidLoad()中刪除super.viewDidLoad()之外的所有內容,再刪除方法viewWillAppear()、insertNewObject()和prepareForSegue()的全部。最后,找到tableView(_:canEditRowAtIndexPath:)和tableView(_:commitEditingStyle:forRowAtIndexPath:)方法同樣也刪除掉。
最后的改動是刪除類頂部的一個屬性:
var detailViewController: DetailViewController? = nil
清理完成,但我們需要在IB上做點小改動來完成準備工作。跟項目1里面一樣,Apple的Master-Detail Application模板添加了一個分屏視圖控制器和連個導航控制器,同時還有表視圖控制器和帶詳情標簽(label)的普通視圖控制器——這對于我們要做的app來說,過于復雜了,讓我們削減一部分內容。
所以,在IB中打開Main.storyboard,選中并刪除分屏視圖控制器(最左邊的那個帶深灰色背景的),然后刪除底部的導航控制器和右邊的視圖控制器。現在,就剩下倆了:導航控制器和表視圖控制器。
Apple已經設置過了,所以分屏視圖控制器是這個項目的第一視圖控制器,即在應用運行時出現的第一頁面。我們剛刪了它,所以需要換個新的頂上去。選中留下的導航控制器,進入屬性觀察器(Alt+Cmd+4),在選項列表的大概中間位置勾選“Is Initial View Controller”。修改好了你就會在導航控制器的左邊看到一個指向它的箭頭。
這個項目被刪的只剩這么一點,你可能會想為什么我們不直接從零開始!但,剩下的量其實還是不少,而且刪代碼會有宣泄作用。更重要的是:現在你的項目已經變成了一個干凈的表視圖項目,可以開始自定義了——讓我們開始吧!
PS:從Single View application開始更快嗎?不好說,至少這讓你積累了一些清理Apple的模板的經驗。還有,刪東西很有趣!
從硬盤讀取:contentsOfFile
我們將要制作一款字母游戲,用戶要用字母組合拼出完整的單詞。我們會把一些可能的字母組合制成一個清單,然后放進一個獨立文件中。但我們怎么才能從文件中獲取文本呢?這對Swift的String數據類型來說就是小菜一碟。
先準備好app要用的文件,就是從hackingwithswift.com上下載。在Content文件夾中你會找到start.txt。把它拖到你的項目中,確保勾選“Copy items if needed”。
start.txt文件包含了超過12,000的我們可以用的8字母單詞,都是以一個單詞一行的形式保存的。我們需要把它轉換成一個我們可以玩的單詞數組。場景背后,這些換行被標記為“\n”這一特別的換行符號。所以,我們得把單詞清單載入到一個字符串,然后用“\n”來把它打斷成字符串數組。
首先,在類頂部定義一個新數組。頂部已經有一個在那兒了,所以把下面的代碼放到它下面的位置:
var allWords = [String] ()
同時你最好把Apple的數組類型也從[AnyObject]改成[String,因為我們只會把它用來存儲字符串。你還需要把表視圖的cellForRowAtIndexPath方法從:
let object = objects[indexPath.row] as NSDate
cell.textLabel!.text = object.description
改成:
let object = objects[indexPath.row]
cell.textLabel!.text = object
在項目1里面我們也做過這事兒,所以不是很難。這樣改是因為數組objects里面存放的肯定是字符串。
第二步,讀取我們的數組。分三個步驟:找到start.txt的存放路徑,載入文件內容,把內容分割裝進數組。
找文件路徑將會是以后常干的一件事,因為即便你知道文件名為“start.txt”,你也不確定它在文件系統的什么位置。所以我們用NSBundle的一個內建方法pathForResource()來找到它。它需要的參數是文件名和路徑擴展,然后返回一個String?——要么返回路徑要么返回nil。
把文件內容載入到字符串中也是需要你熟悉的內容,而且方法很簡單:當你創建一個String實例時,你可以讓它根據一個指定路徑的文件內容來創建自己。你也可以用參數告訴他文本使用的編碼,雖然這里我們不關心。
最后,我們需要根據“\n”把單個字符串分解成一個字符串數組。用字符串類的componentsSeparatedByString()方法就可以實現。告訴它分割符是什么,它就能返回一個數組。
開始碼代碼之前,有兩件事需要我們知道:方法pathForResource()和根據文件內容創建字符串返回的都是String?,也就是說我們需要用if/let語句來判斷和解包。
現在讓我們把下面的代碼放到viewDidLoad()里面super調用的后面:
if let startWordsPath = NSBundle.mainBundle().pathForResource("start", ofType: "txt") {
? ? ? ?if let startWords = try? String(contentsOfFile: startWordsPath, usedEncoding: nil) {
? ? ? ? ? ? ?allWords = startWords.componentsSeparatedByString("\n")
? ? ? ?}
} else {
? ? ? ?allWords = ["silkworm"]
}
如果你看的比較仔細,會發現這里有個新關鍵字:try?。之前你已經看過try!了,其實這里也可以用,因為我們是從自己app的目錄里載入文件,所以如果載入失敗則表明問題相當嚴重。但這樣寫我可以教你點新東西:try?表示“調用這些代碼,如果它出錯了就返回一個nil。”這表示你調用的代碼會一直起作用,但你需要小心翼翼地解包。
你會看到,代碼小心地檢查然后解包start文件的內容,然后把它轉換成數組。當轉換完成后,allWords會包含12,000+的字符串。
為了保證繼續之前一切正常,讓我們創建一個名叫startGame()的新方法。它會在我們每次為玩家生成一個新單詞之前被調用:
func startGame() {
? ? ? ? allWords = GKRandomSource.shareRandom().arrayByShufflingObjectsInArray(allWords) as! [String]
? ? ? ? title = allWords[0]
? ? ? ? objects.removeAll(keepCapacity: true)
? ? ? ? tableView.reloadData()
}
第一行打亂了數組中單詞的順序。我們在項目2里面用過,所以你得記住你需要引入GameplayKit架構來讓方法可以工作。
第二行,一旦隨機化完成,就把視圖控制器的標題設置成數組中的第一個單詞。這也就是需要用戶去找到的目標單詞。
第三行,把數組objects里面的所有值都刪除。這個數組是Xcode模板幫我創建的,我們要用它來存儲玩家的答案。我們現在不會把任何東西放進去,所以removeAll()不會做任何事。
第四行是最有趣的部分:它調用了tableView的reloadData()方法。這是定義在……等等!表視圖從哪兒來的?我們肯定沒有創建它。相反,它是為我們準備的因為——戲劇化的笑聲——MasterViewController并不是從UIViewController繼承的子類。
是的,我說過UIViewController用于app的所有屏,只是有時候并不是直接的使用。這里,MasterViewController繼承自UITableViewController,而UITableViewController繼承自UIViewController。這是個繼承鏈,每個部分都會增加它們自己的功能。
別害怕:大多數視圖控制器都是直接繼承自UIViewController或者先經過UITableViewController。只是iOS中表視圖無處不在,所以Apple就為開發者預燒了些額外行為。
那么什么是UITableViewController做而UIViewController不做的呢?對于初學者來說,就是張全屏表格。當視圖被呈現時,UITableViewController會自動刷新滾動條這樣用戶就會知道他們可以滾動屏幕;如果這時鍵盤也在屏幕上的話UITableViewController也會自動它的位置使內容不會被鍵盤遮擋。
無論如何,UITableViewController是MasterViewController的基礎,而這就是tableView的來源。調用reloadData()會促使表視圖檢查它有多少行同時全部重新載入。
我們的表視圖還沒任何行,所以reloadData()不會做任何事。但方法已經準備好,可以讓我們檢查有沒有正確載入了數據,所以,把它放在viewDidLoad()結束之前就可以了:
startGame()
挑個詞,任何一個:UIAlertController
這個游戲會鼓勵用戶輸入一個單詞,其構成的字母都來自于給定的8字母單詞。比如,如果這個單詞是“agencies”,用戶就可以輸入“cease”。我們會用UIAlertController來完成這件事,因為很完美,而且也給我個機會來介紹些新東西。我總是有其他的目的!
把這些代碼加入到viewDidLoad()里super調用后面:
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "promptForAnswer")
代碼使用系統提供的“add”來創建了一個新的UIBarButtonItem,點擊會調用方法promptForAnswer()。運行時方法會顯示一個帶有輸入框的UIAlertController,用戶點擊Submit后用戶提交的答案會被確認是否有效。
在我給你代碼之前,讓我解釋下你需要知道些什么。
我們將會使用一個比好,而且還會有點復雜。提醒一下,閉包是可以被當做變量來處理的代碼塊——我們可以把閉包傳送到某個地方,然后它被保存起來稍后執行。為此Swift復制了一份代碼并獲取了它涉及的對象,方便閉包晚點使用。
問題就在這里:如果閉包涉及的是視圖控制器會怎么樣呢?這樣將會出現一個超級循環:視圖控制器擁有一個對象,對象擁有閉包,閉包擁有視圖控制器,而什么都不會變。
我打算給你個形象點的例子,所以稍微忍耐下。想象你有兩個清潔機器人,一紅一藍。你告訴紅:“在藍停下來之前不要停止清理。”同時你告訴藍:“在紅停下來之前不要停止清理。”那么它們什么時候會停下來?永遠不會,因為誰都不會是第一個停下來的。
這就是我們要面對的一個很強的引用循環:對象A擁有對象B,對象B擁有一個引用對象A的閉包。而當閉包被創造出來時,它們會捕獲所需的一切,這樣對象B也就擁有了對象A。
強引用循環過去很難被發現,但Swift已經讓它們變得很簡單。實際上,即使你不確定循環的存在你也可以去使用它。
所以,振作起來:我們將要第一次見到真正的閉包。學句法會很痛苦。而且當你最后理解了之后,你就能應對網上那些燒腦案例。
準備好了嘛?下面就是promptForAnswer()方法:
func promptForAnswer() {
? ? ? ?let ac = UIAlertController(title: "Enter answer", message: nil, preferredStyle: .Alert)
? ? ? ?ac.addTextFieldWithConfigurationHandler(nil)
? ? ? ?let submitAction = UIAlertAction(title: "Submit", style: .Default) { [unowned self, ac] (action: UIAlertAction!) in
? ? ? ?let answer = ac.textFields![0]
? ? ? ?self.submitAnswer(answer.text!)
? ? ? ?}
? ? ? ?ac.addAction(submitAction)
? ? ? ?presentViewController(ac, animated: true, completion: nil)
}
這一個方法著實介紹了不少新東西,但我們先略過一些簡單的部分。
創建一個UIAlertController(項目2里做過了)
addTextFieldWithConfigurationHandler()只是在UIAlertController中加入了一個可編輯文本框。
addAction()用于向UIAlertController中添加UIAlertAction。項目2里也用過了。
presentViewController也是項目2里面用過的!
剩下的內容很狡猾:創建submitAction。短短幾行代碼有至少五個新內容要學,而且都很重要。先從最簡單的UITextField開始。
UILabel你是見過的:UIView的一個子類,在屏幕上用來顯示一段不可編輯的文本內容。UITextField也類似,只是它可以被編輯。我們用UIAlertController的addTextFieldWithConfigurationHandler()方法來添加單行文本輸入框,現在我們要讀取被輸入的值。
下一步是尾隨閉包句法。我懂,我懂:你還沒學過閉包呢,可現在卻要學尾隨閉包句法!好吧,它們是互相關聯的,而且尾隨閉包不是特別難,給個機會。
這里是項目2的部分代碼:
UIAlertAction(title: "Continue", style: .Default, handler: askQuestion)
這里的情況差不多:我們用UIAlertController和UIAlertAction來添加用戶可以按的按鈕。那個時候,我們用一個單獨的方法(askQuestion())來避免太早解釋閉包,但你你可以看到我把askQuestion()作為操作員參數傳入UIAlertAction。
閉包有點兒像無名的方法。就是指,我們傳遞進去執行的是一塊代碼,而不是方法名。所以從定義上來說,我們可以把這行代碼寫成如下形式:
UIAlertAction(title: "Continue", style: .Default, handler: { CLOSURE CODE HERE })
但有個很嚴重的問題:太丑了!如果你在閉包里面執行很多代碼,就會產生只有一行的方法使用了一個10行的參數。
所以,Swift給了個解決辦法,就是尾隨閉包句法。任何你要調用最后一個參數是閉包的方法時——這樣的方法有很多——你都可以直接去掉最后一個參數,然后用一組大括號把它傳進來。這是非強制而且自動的,會讓我們概念上的代碼變成下面的樣子:
UIAlertAction(title: "Continue", style: .Default) {
? ? ? ? ?CLOSURE CODE HERE
}
括號內的全都是閉包部分,就作為UIAlertAction的最后一個參數傳入。很方便!
接下來是“(action: UIAlertAction!) in”。如果你還記得,項目2中我們得調整askQuestion()方法這樣它才接受參數UIAlertAction告訴它哪個按鈕被觸碰了,像介個樣子:
func askQuestion(action: UIAlertAction!) {
我們沒的選。因為UIAlertAction的handler參數需要的是一個方法把它自己作為參數。這里發生的是:當它被觸碰時,我們給UIAlertAction一些代碼去執行,而它想知道這些代碼接受一個UIAlertAction類型的參數。
關鍵字in很關鍵:在它前面的都是描述這個閉包的;在它之后的全都是閉包的內容。所以(action: UIAlertAction!) in 表示它接受一個參數傳入,類型為UIAlertAction。
我用這種方式寫閉包是因為它跟項目2里面用過的很想。然而,Swift知道閉包得是什么樣,所以我們可以進行簡化:從這樣……
(action: UIAlertAction!) in
變成這樣:
action in
在當前項目中,我們可以更進一步的簡化:我們不會再閉包中引用action參數,也就是說,我們甚至不需要給它命名。Swift中,如果你沒有給一個參數命名,你可以直接使用下劃線,像這樣:
_ in
第四、第五個一起講:unowned和self.。
Swift會獲取閉包需要的任何常量和變量,基于閉包的環境內容。即,如果你在閉包外創建了一個整型,一個字符串,一個數組和另外一個類,然后在閉包內使用它們時,Swift就會獲取它們。
這很重要,因為閉包引用了變量,很有可能會改變它們的值。但我還沒說“獲取”的真正含義,而這時因為它會因為你使用的數據類型的不同而變化。幸好,Swift把它徹底隱藏了,這樣你就不需要擔心了……
除了一些強引用循環以外。這些是你需要考慮的。在這種情況中,對象甚至都無法毀滅。
Swift的辦法是讓你定義一些沒有被緊緊抓住的變量。兩步搞定,它太簡單了所以你會發現只要有機會你就會把它用到任何地方。
第一步,你必須告訴Swift哪些變量你不想被強引用。兩種方法:unowned或者weak。unowned近似于隱式解析可選,而weak類似于普通可選項:一個弱(weak)擁有(owned)引用的可能是nil,所以你需要解包;一個沒擁有(unowned)引用的是你確認過不會是nil所以不需要解包,然而如果你錯了你就會碰到問題。
代碼中我們使用的是:[unowned self, ac]。它聲明了self(指當前的視圖控制器)和ac(我們的UIAlertController)是被閉包作為沒擁有的引用獲取的,表示閉包可以使用它們,但不會創建一個強引用因為很清楚閉包并不擁有它們中的任何一個。
但這對于Swift來說還不夠。方法中我們還調用了視圖控制器的submitAnswer()方法。我們還沒有創建它呢,但你應該能知道它將會得到用戶輸入的答案并在游戲中判斷對錯。
submitAnswer()方法在閉包當前內容的外面,所以當你寫的時候,你可能沒注意到調用時隱含地需要閉包獲取self。意思是,如果沒有獲取視圖控制器,閉包無法調用submitAnswer()。
我們已經說過self不被閉包擁有,但Swift希望我們完全肯定我們知道自己在做什么:對當前視圖控制器的屬性或方法的每次調用都必須加上前綴“self.”,就像self.submitAnswer()。
項目1里我跟你說過兩種使用self的思路,還有“第一類人永遠不喜歡self.除非非要不可,因為當它被需要時它非常重要而且意義非凡,所以把它用在不必要的地方會讓人困惑。”
閉包對self的隱式獲取正是這樣的一個使用場所:在這里,Swift不會讓你省略掉它的。通過限制你在閉包中的self的用法,你可以簡單地確認你的代碼并沒有任何引用循環——不需要花多大的力氣就可以完成了。
準備提交:lowercaseString和NSIndexPath
你可以松一口氣了:我們已經搞定閉包部分了。我知道這不容易,但一旦你理解了基本閉包你就已經在你的Swift道路上邁出了一大步。
我們會做些比較簡單的編程工作,因為游戲就快完成了!
首先,再次編輯你的代碼,因為現在它要調用self.submitAnswer(),而我們還沒完成這個方法。所以,把下面的方法加入到類中:
func submitAnswer(answer: String) ?{
}
是的,它是空的——這足夠讓代碼變得清楚起來這樣我們就可以繼續了。
我們已經了解了閉包的結構了:尾隨閉包句法,不擁有的自己,傳入的一個參數,然后需要用self.來讓獲取變得明朗。我們還沒有聊過閉包的實際內容,因為不是很多。代碼如下:
let answer = ac.textFields![0]
self.submitAnswer(answer.text!)
第一行解包了文本框的數組,然后告訴Swift把它作為UITextField來處理。第二行將內容從文本框中取出然后傳遞到submitAnswer()方法中。【看下Xcode里這兒的代碼】
方法需要檢查玩家的單詞是不是由給出的字母組成的,單詞是否已經被用過(因為我們不想要重復的答案),還有單詞是不是實際存在的(否則用戶就可以輸入一些無意義的內容)。
如果三個檢查都通過,submitAnswer()需要把這個單詞添加進objects數組中,然后在表視圖中插入新的一行。我們可以用表視圖的reloadData()方法強行全部重新載入,但當我們只需要改變一行時這樣非常低效。
這里是我們第一次寫的submitAnswer()方法:
func submitAnswer(answer: String) {
? ? ? ? let lowerAnswer = answer.lowercaseString
? ? ? ? if wordIsPossible(lowerAnswer) {
? ? ? ? ? ? ? ? ?if wordIsOriginal(lowerAnswer) {
? ? ? ? ? ? ? ? ? ? ? ? ? ?if wordIsReal(lowerAnswer) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?objects.insert(answer, atIndex: 0)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? let indexPath = NSIndexPath(forRow: 0, inSection: 0)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
? ? ? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ?}
? ? ? ? ?}
}
先暫時忽略wordIsPossible(),wordIsOriginal() 和 wordIsReal()三個方法——我們先看剩下的代碼。
如果用戶輸入“cease”作為初始詞“agencies”的答案,很明顯是正確的,因為有一個“c”,兩個“e”,一個“a”和一個“s”。但是如果輸入的是“Cease”呢?現在它有了一個“C”,而“agencies”沒有。沒錯:字符串對大小寫敏感,即答案區分大小寫。
解決辦法相當簡單:所有的初始詞都是小寫的,所以我們只要在檢查玩家的答案時用lowercaseString屬性來小寫字符串就好啦。它被存放在常量lowerAnswer中因為我們還要多次使用。
接下來就是三個if語句,一個包含另一個,這叫嵌套語句。只有當三個語句都為真時代碼的主要部分才會被執行。
一旦我們知道單詞是對的,我們要做三件事:在objects[0]的位置上插入新單詞,這表示“把它加到數組的開始位置”,即最新的單詞要放在表視圖的頂部。
其他兩件事是相關的:我們向表視圖中插入新的一行。因為表視圖的數據都是來自于數組objects,所以這樣看上去有點奇怪。畢竟,我們剛把單詞放入objects數組中,所以為什么我們還得把其他東西插入到表視圖中呢?
答案是因為動畫。就像我說的,我們可以調用reloadData()方法讓表格重新載入全部的內容,但它意味著小改動的巨多額外任務,而且還引起了一次跳轉——單詞原來不在這兒,而現在它在了。
用戶可能難以發現這次跳轉,所以用insertRowsAtIndexPaths()讓我們告訴表視圖,新的一行已經被放在數組的指定位置,這樣它就可以帶著動畫效果更新每一小格的內容。加一行明顯比重新載入每一行要簡單太多!
還有兩個奇怪的地方需要解釋。首先,NSIndexPath是我們在項目1里草草看過的,因為他為表格里的每一項都準備了一個方框和一行空白。跟項目1一樣,這里我們也沒有用到方框,但行號等于我們在數組中加入元素的位置——這里是位置0。
第二,參數withRowAnimation讓你指定行載入時的動畫效果。不論何時你從表格中添加或刪除內容,值.Automatic表示“用標準系統準備的動畫效果來展示改變”,這里表示“從頂部滑入新的一行”。
你的代碼還不會編譯,因為我們還有三個未完成的方法。把這三個加到submitAnswer()方法下面讓一切重新啟動:
func wordIsPossible(word: String) -> Bool {
? ? ? ? return true
}
func wordIsOriginal(word: String) -> Bool {
? ? ? ? return true
}
func wordIsReal(word: String) -> Bool {
? ? ? ? return true
}
我們很快就會看看他到底做了些什么,然后用很多具體代碼補全。但現在,Cmd+R來試試你做出來的app,你應該可以點擊“+”來輸入單詞了。
返回值:contains
到目前為止,我們自定義的方法還沒返回過任何值。我們在項目4里用關鍵字return只是為了早點跳出decidePolicyForNavigationAction方法,但并沒有返回任何數據。所以,讓我們更進一步。
就像我說的,return用于跳出方法,無論何時。如果你就寫了個return,它就只是跳出方法而已。但如果你用return和一個值一起,它就會返回一個值給任何調用它的東西。
你得先告訴Swift你希望返回的是什么值,你才能發送一個值回來。Swift會自動檢查返回的是不是你指定的數據類型,所以這很重要。目前我們只是給三個方法各留了個殼。讓我們仔細看看其中的一個:
func wordIsOriginal(word: String) -> Bool {
return true
}
方法名為wordIsOriginal(),它只有一個字符串參數。在大括號之前有個新東西:-> Bool。這告訴Swift方法會返回一個布爾值,就是只能返回真或假的一個值。
方法只有一行代碼:return true。這就是return如何返回值的方法:我們從這個方法中返回真值,所以調用者可以在if語句中調用它來檢驗真假。
該方法可以擁有足夠多的代碼,包括其它需要的方法,來充分判斷單詞是否已經被用過。我們要讓它調用另外一個方法,就是用來檢查我們的objects數組是否已經包含這個被提供的單詞了。用下面的代碼替代現有的代碼:
return !objects.contains(word)
有兩個新內容。首先,contain()是一個檢查指定數組(參數1--objects)是否包含指定值(參數2--word)。包含則返回真。第二,“!”表示非運算,不是隱式解析可選值的解包。
用在變量常量之前為取非,所以,如果contains()返回真,!翻轉結果讓它為假;用在常量變量后面則為“解包隱式解析可選值”。
這樣做是因為我們的方法名為wordIsOriginal(),如果單詞之前未被用過,它應該返回真。如果我們使用的是return objects.contains(word),那么結果就會相反:如果單詞被用過則會返回真。所以我們使用了!來翻轉結果這樣當單詞是新的時就會返回真。
一個方法完成了,接下來是wordIsPossible(),它只有一個字符串參數,返回的是一個布爾量——真或假。這個方法較之前的更加復雜,但我已經讓算法盡可能簡單了。
我們怎么確定“cease”是從“agencies”中來,而且每個字母只用了一次?我用的方法是循環玩家的答案中的每一個字母,來看它是否在我們給出的初始詞中出現。如果有,我們就從初始詞中刪除這個字母,然后繼續循環。所以,如果我們想一個字母用兩次,那么第一次循環可以通過,但之后的循環就會終止。
你已經在項目4中碰到過rangeOfString()了,所以這應該會很簡單:
func wordIsPossible(word: String) -> Bool {
? ? ? ?var tempWord = title!.lowercaseString
? ? ? for letter in word.characters {
? ? ? ? ? ? ?if let pos = tempWord.rangeOfString(String(letter)) {
? ? ? ? ? ? ? ? ? ? tempWord.removeAtIndex(pos.startIndex)
? ? ? ? ? ? ?} else {
? ? ? ? ? ? ? ? ? ? return false
? ? ? ? ? ? ?}
? ? ? ?}
? ? ? ?return ?true
}
這里rangeOfString()方法的使用方法跟項目4里面稍有不同。記住,rangeOfString()返回的是一個關于對象發現的位置的可選項——也有可能為nil,所以我們使用了if/let。
用法還有些不同是因為我們用的是String(letter)而不是letter。因為我們的for循環用在字符串上,而它把字符串里的每個字母都取出來,并保存為一個新的字符串。rangeOfString()尋求的是字符串,而不是字符,所以我們需要用String(letter)來把字符變成字符串。
如果字母在字符串里被找到,我們就用removeAtIndex()來移除tempWord變量中的字母。這就是我們需要tempWord的全部理由:因為我們會一直從中移除字母,這樣我們就可以在下次循環時再檢查一次。
方法的最后是return true,因為只有當用戶單詞的每個字母都被在初始詞中只發現了一次時才會執行到。如果有字母沒找到,或者用了超過一次,方法就會執行到return false然后結束調用,這樣我們就可以確保單詞沒問題。
重要提示:我們已經告訴Swift方法返回的是一個布爾量,它會檢查代碼執行完之后的任何可能結果來確保返回值必須會布爾量。
輪到最后的方法了。用下面的代碼替換當前的wordIsReal()方法:
func wordIsReal(word: String) -> Bool {
? ? ? let checker = UITextChecker()
? ? ? let range = NSMakeRange(0, word.characters.count)
? ? ? let misspelledRange = checker.rangeOfMisspelledWordInString(word, range:range, startingAt: 0, wrap: false, language: "en")
? ? ? return misspelledRange.location == NSNotFound
}
這里有個新類,叫UITexChecker。這是iOS中用來檢查拼寫錯誤的類,是用來檢查單詞是否實際存在的絕佳方法。我們創建了一個該類的新實例,然后把它放進常量checker中。
這里還調用了個新的方法,叫NSMakeRange()。它用來創建字符排列,一個保存起始位置和長度的變量。我們想要檢查整個字符串,所以要從0開始,直到過完整個長度。
下一步,我們調用了UITextChecker實例的rangeOfMisspelledWordInString()方法。它有5個參數,但我們只關心第一、二個和第五個:第一個是目標單詞,第二個是掃描長度,最后一個是我們使用的語言。
參數三和四在這里沒啥用,但是為了完整我們說明下:參數3選擇了掃描的起始點,參數4讓我們設置如果從參數三的位置開始掃描沒有發現錯詞,是否要讓UITextChecker從排列的最開始開始。顯然這里沒啥用。
調用rangeOfMisspelledWordInString()返回的是結構體NSRange,告訴我們在哪兒發現了誤拼。但我們關心的是是否發現誤拼,如果沒有發現我們的NSRange就會得到一個特殊的位置:NSNotFound。普通的NSRange返回值告訴你誤拼的位置,但NSNotFound表示單詞拼寫正確——比如,這個單詞可以用。
這里的return語句的用法很新鮮:返回的是“==”運算的結果。這是很常用的編程方法,“==”返回的真或假取決于misspelledRange.location跟NSNotFound是否相等。這個結果的真假會作為方法的結果通過return返回。
我們可以用另一種方法實現,但不太常用:
if misspelledRange.location == NSNotFound {
return true
} else {
return false
}
項目差不多要完成了。運行測試下看看!
或者其他什么?
還有問題需要修復,但不是什么大問題。如果單詞是不曾出現過的,第一次輸入的真實單詞,我們就把它加進發現的單詞列表中。但如果單詞不真實呢?已經輸入過了、或者不存在的呢?這時我們拒絕了單詞但是沒有給用戶任何反饋。
所以最后一步是要給用戶一個失敗的反饋。這很無聊,因為就是要給submitAnswer()里的if語句加上else語句,每次都反饋一個信息給用戶。
方法調整后如下所示:
func submitAnswer(answer: String) {
? ? ? ?let lowerAnswer = answer.lowercaseString
? ? ? ?let errorTitle: String
? ? ? ?let errorMessage: String
? ? ? ?if wordIsPossible(lowerAnswer) {
? ? ? ? ? ? ? if wordIsOriginal(lowerAnswer) {
? ? ? ? ? ? ? ? ? ? ? ?if wordIsReal(lowerAnswer) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? objects.insert(answer, atIndex: 0)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? let indexPath = NSIndexPath(forRow: 0, inSection: 0)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? return
? ? ? ? ? ? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? errorTitle = "Word not recognised"
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? errorMessage = "You can't just make them up, you know!"
? ? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ?} else {
? ? ? ? ? ? ? ? ? ? ? ? errorTitle = "Word used already"
? ? ? ? ? ? ? ? ? ? ? ? errorMessage = "Be more original!"
? ? ? ? ? ? ? ? ?}
? ? ? ? ?} else {
? ? ? ? ? ? ? ? errorTitle = "Word not possible"
? ? ? ? ? ? ? ? errorMessage = "You can't spell that word from '\(title!.lowercaseString)'!"
? ? ? ? ?}
? ? ? ? ? let ac = UIAlertController(title: errorTitle, message: errorMessage, preferredStyle: .Alert)
? ? ? ? ? ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
? ? ? ? ? presentViewController(ac, animated: true, completion: nil)
}
如你所見,每個if都跟一個else配對,這樣用戶就能得到合適的反饋。所有的else都差不多一樣:給errorTitle和errorMessage設對用戶有用的值。最后一個是個有意思的例外,我們用字符串插值來顯示視圖標題的小寫格式。
如果用戶輸入了一個有效答案,return的調用促使Swift直接跳出該方法。這很有用,因為在方法的底部有個用errorTitle和errorMessage創建的UIAlertController,還添加了個nil操作員的OK按鈕,然后顯示警告。這樣只有當什么出錯時錯誤才會出現。
這個例子展示了關于Swift中常量很重要的一點:errorTitle和errorMessage都被定義為常量,也就是說它們的值一旦被設置就無法更改。我沒有給它們初始值,這沒關系——Swift允許你在它們被讀取之前完成就行,只要之后不去修改就行。
項目完成了!
總結
到這里為止你做了這么多,所以你的Swift學習也進展到了這里。還有我希望這個項目能告訴你,你可以用你的知識來做更進一步的東西。
在這個項目中,你學習到了更多關于UITableView的內容:如何重新讀取它們的數據和如何插入行。你還學會了如何向UIAlertController中添加文本輸入框,這樣你可以得到用戶輸入的內容。但你還學會了一些非常核心的內容:更多關于Swift的字符串,閉包,方法返回值,布爾量,NSRange等等。這些都是你在接下來的Swift生涯中將會反復用到的,也是在這個系列中不斷重復的。
你可能已經有怎么改進這個游戲的計劃了,如果還沒有下面有4個值得入手的想法:
1.不允許答案短于3個字母。最簡單的實現方法是使用wordIsReal()在單詞長度短于3時返回假。
2.把else語句全部改寫成一個方法showErrorMessage()。這個方法有兩個參數error message和title,并完成UIAlertController的任務。
3.不允許答案就是初始詞。現在的游戲還不能阻止用戶輸入初始詞。
4.修復start.txt的載入代碼。如果pathForResource()調用返回nil,我們載入一個只有一個單詞的數組:silkworm。但如果pathForResource()成功了,而用contentsOfFile創建NSString失敗了呢?這樣的話答案數組就是空的了!寫一個新的loadDefaultWords()方法,可以應對所有的錯誤。