讓不懂程式設計的人愛上iPhone開發(2018秋iOS12+Swift4.2+Xcode10版)-07
歡迎回來繼續我們的學習。
現在我們的to-do 清單上還有不少事情要做,接下來先處理一個比較簡單的:產生一個隨機數,然後讓它顯示在螢幕上。
在遊戲開發的時候,我們經常會用到隨機數。當然,大家最感興趣的隨機數可能就是體彩雙色球了,可惜的是彩票裡面的所謂隨機數未必隨機啊,主任才是決定一切的力量~
自買彩票十幾年以來,哥中的最大獎是210元。
當時就開始各種幻想,以為千萬頭獎近在咫尺,從此走上人生巔峰,可誰知道從此好運遠離,連中個5元都得一等再等。

當然,除了彩票,包括支付寶的錦鯉,還有王校長的抽獎,也是需要隨機數的幫助。


算了還是別做夢了,來說說遊戲裡面的隨機數吧。
在此之前提一句,如果哪位哥中了頭獎,別忘了留個聯絡方式,土豪我們做個朋友吧!
實際上,我們不可能讓當前的電子計算機真正產生完全隨機不可預測的數字,而使用所謂的偽隨機數(pseudo-random)生成器來產生貌似是隨機的效果。真隨機數或許只有在量子計算機時代,使用量子真隨機數發生器才能實現。

感興趣的童鞋可以看這裡:
ofollow,noindex"> https://www. wired.com/story/quantum -mechanics-could-solve-cryptographys-random-number-problem/在產生隨機數之前,首先我們需要定義一個變數來儲存該隨機數。
在Xcode中開啟ViewController.swift,在currentValue這個變數的定義語句之後新增一行程式碼:
@IBOutlet weak var targetLabel: UILabel!
如果我們沒有告訴編譯器targetValue是怎樣的一種變數,那麼它就不知道該為targetValue分配多少儲存空間,更不會知道我們是否正確的使用了該變數。
此外,Swift中的變數必須有一個數值,因此我們賦予其一個初始值0.當然,0這個數值在實際的遊戲中並不會用到,我們會使用所產生的隨機數來替代它。
還記得之前提過的變數型別嗎?這裡我們所定義的targetValue就是一個例項變數。之所以將其定義為一個例項變數,是因為我們需要在多處用到它,比如在viewDidLoad()中,同時在使用者觸碰按鈕之前記住該隨機數,以及在showAlert()方法將數值和使用者所選擇的數值進行比較預算。
接下來我們就需要生成這個隨機數了。
在之前版本的Swift中,我們需要使用外部的函式如arc4random_uniform()才能生成隨機數。在最新的Swift4.2中,Swift的資料型別Int天生就支援隨機數生成,當然,之前的方法依然適用。
在我們這個遊戲裡,最適合生成隨機數的地方當然就是遊戲開始處。
在Xcode中開啟ViewController.swift,然後在viewDidLoad()方法中新增下面的這行程式碼:
targetValue = Int.random( in: 1...100)
此時viewDidLoad()方法的完整程式碼如下:
override func viewDidLoad() { super.viewDidLoad() currentValue = lroundf(slider.value) targetValue = Int.random( in: 1...100) }
這裡我們呼叫了Int資料型別的內建函式random()來產生一個隨機整數,並將targetValue的最終數值範圍限定在1到100之間。這裡的三個點表示我們希望隨機數包含100,如果我們不希望隨機數包含100,那麼就需要使用1..<100,就會得到1到99之間的隨機數。
希望你別被上面的數學運算嚇倒。
雖然很多人說只有學好數學才能成為一個好的程式猿,但實際上絕大多數應用中所用到的數學都不會比上面的算術複雜。當然,在遊戲和科學計算中用到的數學要稍微複雜一些。不過如果你的目的並不是成為一個演算法高手或者數學家,所以也不必太擔心。
接下來,讓我們更改showAlert()的方法如下:
@IBAction func showAlert(){ //1.設定message訊息的內容 let message = "滑動條的當前數值是: \(currentValue)" + "\n目標數值是: \(targetValue)" let alert = UIAlertController(title:"把油桶踢過來", message:message, preferredStyle: .alert) let action = UIAlertAction(title:"2019年要跟著學幾個拿手菜",style: .default, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) }
後面就不重複提醒大家了,數字編號為1的註釋行下面的程式碼就是唯一修改的地方。
在上面的程式碼中,我們向字串中添加了儲存在targetValue中的隨機數,這個大家應該不再陌生了。\(targetValue)將被真實的隨機數替代。
\n這個符號之前沒接觸過。實際上它在字串中出現的時候,就意味著我們要換行了。
好了,點選Run運行遊戲,試試看有什麼變化。

注意:之前我們曾經使用+這個符號來將兩個數字相加(和大家在小學數學裡面學到的那樣),但這裡我們則使用+來將文字粘合到一起形成一個長的字串。
在程式語言中,相同的符號有時代表著不同的作用,而具體的含義由上下文決定(跟人類語言一樣)。
新增新的遊戲回合
如果你多試幾次,會發現對話方塊所提示的隨機數目標數值幾乎就沒改變過。這樣的話這遊戲也就沒啥意思了。
還好意思說自己是隨機數,一次都不變。這就好比雙色球和大樂透的每期中獎號碼都是重複的,還有誰願意買?(我!!!)
這是因為我們在viewDidLoad()方法裡面生成了隨機數,而之後就沒有再次生成。而這個方法只會在應用第一次開啟建立檢視控制器的時候才會呼叫,之後就沒它的事了。
我們的to-do清單上寫的是,”在遊戲的每個回合開始的時候生成一個隨機數“。好吧,這裡的回合指的是什麼呢?
當遊戲開始的時候,玩家的初始分數是0,遊戲回合數是1。我們把滑動條設定在中間的位置(50),然後計算一個隨機數。接著我們等玩家觸碰按鈕。當玩家觸碰按鈕之後,一輪遊戲就結束了。
這個時候我們會計算這一輪的得分,然後把它新增到總得分裡面去。接著我們讓遊戲回合數加1,然後開啟新的一輪遊戲。我們再次把滑動條重置到中間的位置,然後計算一個新的隨機數。就這樣,只要你願意,直到地老天荒,直到宇宙的終結(實際上直到你的手機沒電為止~)。
每當你發現自己在想,”在新一輪的遊戲開始後我們要做這個,做那個。。。“,這個時候就該建立一個新的方法了。這個方法會建立自己的獨有功能。
記住這一點,讓我們繼續前進吧 。
回到Xcode,切換到ViewController.swift,新增一個新的方法。
具體放的位置不是很重要,只要它是在class ViewController的花括號裡面,這樣編譯器就知道它屬於ViewController物件了。
func startNewRound() { targetValue = Int.random( in: 1...100) currentValue = 50 slider.value = Float(currentValue) }
這裡和我們之前所做的事情沒有太大區別,只不過我們把開始新一輪遊戲的邏輯機制放到了一個獨立的方法startNewRound()裡面去。這樣做的好處是,我們可以在後面重復使用這部分程式碼的邏輯。
然後更改viewDidLoad()方法的程式碼如下:
override func viewDidLoad() { super.viewDidLoad() startNewRound() }
注意我們已經把viewDidLoad()方法中原有的相關程式碼刪除,並使用對startNewRound()方法的呼叫來代替。
玩家觸碰按鈕顯示提示對話方塊後,可以在showAlert()方法裡面呼叫這個方法來開啟新的一輪遊戲。
讓我們更改showAlert()方法裡面的程式碼:
@IBAction func showAlert(){ let mesage = "滑動條的當前數值是:\(currentValue)" + "\n目標數值是: \(targetValue)" let alert = UIAlertController(title:"Hello Messi", message:mesage, preferredStyle: .alert) let action = UIAlertAction(title:"ok",style: .default,handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) //2.開始新的回合 startNewRound() }
這裡新增的唯一一行新程式碼就是註釋編號為2下面的startNewRound()
在此之前,我們所接觸到的檢視控制器裡面的方法都是在某個事件發生的時候呼叫:比如當應用載入檢視控制器的時候呼叫viewDidLoad()方法,當玩家觸碰按鈕的時候呼叫showAlert()方法,當玩家拖動滑動條的時候呼叫sliderMoved()方法。這就是我們之前談到的事件驅動模型。
以上的方法呼叫都是自動的,而這裡開啟新的一輪遊戲則是手動呼叫方法的例子。我們從物件中的某個方法給相同物件的另一個物件傳送了一條訊息,聽起來似乎有點拗口。
祕密:在iOS開發裡面,傳送訊息和呼叫方法其實是一個意思~
比如在這裡,檢視控制器給自己傳送一條startNewRound()的訊息以開啟新的一輪遊戲。
然後iPhone就會切換到新的方法,並從頭到尾執行裡面的語句。當方法裡面的語句執行完畢後,就會返回之前的方法,繼續其中的程式碼。可能是第一次呼叫時的viewDidLoad()方法,也可能是此後每一個回合的showAlert()方法。
有時你可能看到類似下面的方法呼叫方式:
self.startNewRound()
這個是之前不帶self.的方法呼叫,其作用相同。還記得剛才我提到檢視控制器給自己傳送訊息嗎?其實這就是self的意思。
在Swift中,當我們需要對某個物件呼叫方法時,通常這樣來寫:
receiver.methodName(parameters)
這裡的receiver就是我們要傳送訊息的接收物件。如果我們要給自己傳送訊息,receiver就是self。不過因為給self傳送訊息是十分常見的事情,所以大部分時候這個關鍵詞可以省略掉。
不過實際上這並非我們首次呼叫方法。之前的addAction()方法是UIAlertController,而present()方法則是所有檢視控制器都會有的,包括你自己的檢視控制器。
當我們使用Swift程式設計的時候,大部分時候都是讓物件呼叫方法,因為這就是我們應用中物件的通訊方式。
這裡要說明一點,把開啟新一輪遊戲的邏輯機制放到一個獨立的方法裡面是很好的編碼習慣。如果我們不這樣做,你就會看到下面的程式碼:
override func viewDidLoad() { super.viewDidLoad() targetValue = 1 + Int((arc4random()_uniform(100)) currentValue = 50 slider.value = Float(currentValue) } @IBAction func showAlert() { . . . presentViewController(alert,animated:true,completion:nil) targetValue = 1 + Int(arc4random_uniform(100)) currentValue = 50 slider.value = Float(currentValue) }
這樣做有什麼不好呢?首先,我們在兩個不同的地方重新寫了相同的程式碼。好吧,這裡只是3行程式碼,如果是300行怎麼辦?如果我們打算更改這部分邏輯又該怎麼辦?如果我們要在10個不同的地方用這部分程式碼怎麼辦?
還有,你剛開始寫這段程式碼的時候非常清楚它是幹嗎用的。但1周以後呢?1個月以後呢?半年以後呢?你還記得這樣的程式碼究竟是幹嘛的嗎?假如幾個月後你發現要修改其中的一兩行,但又忘了其它地方的類似程式碼,你就完蛋了。程式碼重複是產品中出現bug的主要來源之一。
只要你的程式碼需要在兩個不同的地方完成相同的事情,你就需要考慮建立一個新的方法。相信我,別偷懶。偷懶一時爽,程式碼死光光~
還有,方法的名稱也有助於我們瞭解它的確切功能。如果你沒看過前面的教程,我直接給你下面的三行程式碼:
targetValue = Int.random( in: 1...100) currentValue = 50 slider.value = Float(currentValue)
你能知道這幾行程式碼究竟是幹嗎用的嗎?當然,你可能瞭解這裡用隨機數建立了一個數值,然後設定了一個初始值,再把初始值賦予了滑動條。
僅此而已,你能知道這其實是在開啟新一輪的遊戲嗎?不管你能不能,我反正是不知道。有些程式猿會加上一些註釋程式碼,但最好的方法還是學學哥:
startNewRound()
清清楚楚明明白白真真切切,比註釋行還要清晰明瞭。雖然註釋行也很有用,但讓程式碼具有自解釋性更重要。
而且在後面你可以隨時檢視和修改startNewRound()方法,更不用說在Xcode裡面搜尋它也變得簡單了。
點選Run運行遊戲,確保每次觸碰都會產生新的1到100之間的隨機數。
你或許注意到了,每個新的回合開啟時,滑動條都會重置到中間的位置。這是因為在startNewRound方法中我們重置currentValue的數值到50,然後讓滑動條的結點位置更改為這一點。
這個和我們最初的做法不同,之前我們是讀取滑動條的位置,然後把它的數值賦予currentValue。哥這樣做的目的是讓每一輪新的遊戲都從相同的位置開始,感覺要靠譜點。
小練習:
如果你比較無聊,可以嘗試下修改程式碼,讓滑動條不必每次開始的時候都放在中間的位置。
型別轉換
對了,你可能對以下程式碼中Float(…)和Int(…)的作用有點迷糊:
targetValue = Int.random( in: 1...100) slider.value = Float(currentValue)
Swift是所謂的強型別語言,也就是說它對可以放進盒子裡的形狀非常挑剔(也就是說Think Different行不通?!)。比如,一個變數是Int型別的,如果你把Float型別的數值交給它儲存,它會直接拒絕的,反過來也一樣。
我們從UISlider所獲得的數值正好是Float型別的,也就是說帶小數點,之前我們曾經見識過。不過currentValue是Int型別的,因此如果你直接這樣寫程式碼是不行的:
slider.value = currentValue
編譯器會直接報錯。有些程式語言可能會自動幫你把Int型別轉換成Float型別,但Swift是不會樂意幫你做這種事情的~
當我們使用Float(currentValue)的時候,編譯器會把currentValue中儲存的整數值轉換成一個新的Float浮點數,從而可以賜給UISlider。
Swift雖然是一門比較新的語言,但是相比大多數的流行程式語言來說,對於變數型別的使用要求非常嚴格。這一點對於程式設計入門的新手來說會造成混亂和困惑,當然更加不幸的時Swift的錯誤提示資訊還不能準確的指出程式碼錯在哪裡。
作為入門新手只需要記住,當你看到類似這樣的錯誤資訊時,cannot assign value of type 'something' to type 'something else'
,就意味著你在資料的變數型別上使用出錯了。而解決方案只能是強制的把某個型別轉換成另一個。
繼續前進!
把我們的目標數值放到標籤裡
好吧,我們已經知道如何生成隨機數,然後把它儲存在一個名為targetValue的變數裡。現在我們需要把這個數值放到螢幕上,不然玩家就是瞎貓去抓活老鼠,要多難有多難。
之前我們設計storyboard介面的時候,已經添加了一個用來放置目標值的標籤。現在要做的事情就是讀取targetValue變數的數值,然後把它放到標籤裡。為了做到這一點,我們需要完成兩件事情:
1.建立一個到標籤物件的outlet,這樣我們才好向它傳送訊息
2.讓標籤顯示新的文字內容
其實之前對滑動條已經做過類似的事情了。還記得我們之前曾新增過@IBOutlet這個變數嗎?通過這種方式,我們可以從檢視控制器的任何一個地方引用該滑動條。通過這個outlet變數,我們可以向滑動條請求獲取它的數值,具體來說就是通過slider.value。對於這個標籤我們將用到相同的方式來完成。
在Xcode中切換到ViewController.swift,然後在其它outlet變數定義的下面新增下面的這行程式碼:
@IBOutlet weak var targetLabel: UILabel!
在Main.storyboard中,點選選擇代表目標值的標籤
在Xcode右側切換到Connections inspector,然後從New Referencing Outlet拖一條線到View Controller,如下圖所示:

從彈出選單中選擇targetLabel,這樣就建立了從介面視覺元素到檢視控制器中相關變數的關聯。
當然,這只是建立連線的方式之一,你還可以用之前提到的其它方式來建立連線。
好了,又到了程式碼時間。讓我們在Xcode中切換回程式碼介面,開啟ViewController.swift
在startNewRound()方法的下面新增一個新的方法:
func updateLabels(){ targetLabel.text = String(targetValue) }
我們把這個邏輯放到一個單獨的方法裡,這是因為在其它地方我們還會用到。
方法的名稱已經很清晰的說明了它的作用:使用它來更新標籤的文字內容。
當然,現在我們只是用它來更新單個標籤的文字內容,後面還會新增程式碼來更新其它標籤的文字內容(總得分,遊戲回合數)。
updateLabels這個方法裡面的程式碼現在應該可以看懂了,我們把一個字串的內容賦予目標標籤的文字屬性。
不過你可能要問,為什麼我們不能更簡單一點,用下面的程式碼呢:
targetLabel.text = targetValue;
原因很簡單,我們不能把整數型別的數值直接賦予字串型別的變數。方井蓋沒法放進圓孔。
targetLabel這個outlet變數代表對UILabel物件的引用。UILabel標籤物件有自己的text屬性,它是一個字串物件。我們只能把字串型別的資料放到text裡面。
但是剛才的程式碼試圖把targetValue,一個Int型別的整數直接放到字串型別的變數裡。在Swift中這樣的事情顯然不可能發生。
所以我們需要先把整數型別的變數轉換為一個字串,這就是String(targetValue)的作用。。其實之前我們也做過類似的事情,現在你應該知道為什麼要這麼樣做了吧。
但是用下面的方式是可行的:
targetLabel.text = “\(targetValue)”
需要注意的是,updateLabels()是個常規的方法,它沒有作為一個動作方法關聯到任何UI介面控制元件上,因此除非我們手動呼叫它,它就難有出頭之日。要區分某個方法是否是動作方法很簡單,只需要看前面是否有@IBAction即可。
呼叫updateLabels()方法比較合適的地方是在每次呼叫startNewRound()方法後,因為在那裡我們將要獲取新的目標值。我們無需在兩個不同的地方呼叫updateLabels(),只需將其包含在startNewRound()方法中即可。
因此讓我們修改startNewRound()的方法如下:
func startNewRound() { targetValue = 1 + Int (arc4random_uniform(100)) currentValue = 50 slider.value = Float(currentValue) //4.更新目標數值的標籤文字內容 updateLabels() }
註釋編號為4的註釋行下面的程式碼s就是新增的程式碼。
Xcode的自動完成功能會幫你不小的幫,作為一個懶人,你只需要敲下方法名稱的頭幾個字母,比如upd,Xcode會自動幫你完成其它的。你只需要回車確認建議即可:

點選Xcode上的Run按鈕來運行遊戲,現在我們可以在螢幕上看到隨機生成的目標值了。
在結束之前,我們再來點理論知識惡補一下吧。
科普:動作方法vs一般方法
好吧,我們現在已經用到了兩種方法,分別是和介面中控制元件關聯起來的動作方法,以及有著獨立意識的一般方法。那麼它們之間有什麼區別呢?
答案是:沒什麼區別。
唯一不同的就是@IBAction這個標示。當我們在方法前面加了這個東西后,Interface Builder就會知道我們可以把它和介面中的按鈕,滑動條或者其它控制元件關聯起來。
至於那些沒有@IBAction標識的方法就不能和介面中的控制元件建立關聯了。
// 基本動作方法
@IBAction func showAlert() // 動作方法,不過添加了到觸發這個動作的物件的引用 @IBAction func sliderMoved(_ slider: UISlider) @IBAction func buttonTapped(_ button: UIButton) // 不能和Interface Builder中的控制元件建立關聯 func updateLabels()
好了,今天的學習到此結束,內容相比之前有點多,先休息下,上福利了。

遙想公瑾當年。。。

本人聯絡方式:
微信:iseedo
郵件: eseedo@gmail.com
QQ討論群: 375143733
如有疑問,請先發送郵件到我的郵箱: eseedo@gmail.com
我會在收到郵件後儘早答覆。
也可以加微信,但可能不是很合適的答疑途徑。
另外,為了節省大家的寶貴時間,提高溝通效率,請在提問的時候儘量附上 專案原始碼以及以下資訊 :
1.開發環境(系統版本,Xcode和iOS版本)
2.問題描述及重現(想實現什麼效果,結果是怎樣的,具體涉及到什麼操作)
3.為解決問題所做的努力(做了哪些嘗試,分別是怎樣的結果)