iOS Apprentice中文版-從0開始學(xué)iOS開發(fā)-第三十八課

我們先來講幾種循環(huán)數(shù)組的方法

首先是我們已經(jīng)用過很多次的for in,像下面這個樣子:

for category in categories { . . .

這一行的意思是把categories中的每一個對象依次放入名為category的臨時常量中。

然而,為了得到每個對象的index-path,而不是每個對象的名稱,你需要使用另一種方法:

for i in 0..<categories.count {
  let category = categories[i]
...
}

我們使用半開操作符0..<使臨時i依次從0到categories.count-1遞增。如果你需要數(shù)組中的索引而不是名稱,這是一種常見的方式。

還有一種方法是使用enumerated()方法,你會在下一個課程中見到它,現(xiàn)在我們先大概了解一下:

for (i,category) in categories.enumerated() {
...
}

回到我們的app,打開storyboard,拖拽一個新的Table View Controller到畫布中。在身份檢查器中設(shè)置Class為CategoryPickerViewController。

選擇table view cell,在屬性檢查器中設(shè)置Style為Basic,Identifier為Cell。

選定Category Cell,然后按住ctrl拖拽到這個新的table view controller上,然后在轉(zhuǎn)場類型中選擇Selection Segue下的Show。

將這個轉(zhuǎn)場的Identifier設(shè)置為PickCategory。

注意:如果你的界面中,右邊的table view controller頂部有一個返回Tag Location選項(xiàng),是沒問題的,不知道只作者截圖有問題,還是Xcode版本升級導(dǎo)致的。

storyboard部分就到此結(jié)束了,我們下面開始代碼部分。

打開LocationDetailsViewController.swift并且添加一個新的實(shí)例變量categoryName。你會用它來臨時存儲被選擇的分類名稱。

var categoryName = "No Category"

這個變量的初始值是 "No Category"。它同時也是分類列表中最上面的第一個選項(xiàng)。

修改一下viewDidLoad(),將categoryName的值放入標(biāo)簽中:

override func viewDidLoad() {
        super.viewDidLoad()
        descriptionTextView.text = ""
        categoryLable.text = categoryName   //修改這里
...

最后,添加轉(zhuǎn)場方法prepare(for:sender:) :

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "PickCategory" {
            let controller = segue.destination as! CategoryPickerViewController
            controller.selectedCategoryName = categoryName
        }
    }

這里只是簡單的設(shè)置了category picker的屬性selectedCategoryName。通過這一操作,現(xiàn)在app就有了一個分類。

運(yùn)行app,實(shí)際看看效果:

嗯,看起來效果不錯。你現(xiàn)在可以選擇一個分類了,但是你選擇某一行后不會自動關(guān)閉這個界面。當(dāng)你點(diǎn)擊返回按鈕后,你選擇的分類也不會顯示在界面上。

練習(xí):整個拼圖中缺少了哪一部分?

答案:CategoryPickerViewController目前沒有任何通訊方式向LocationDetailsViewController返回數(shù)據(jù),比如用戶選擇了一個新的分類。

此時你一定會恍然大悟,原來如此!你忘記給它一個委托協(xié)議了。這就是為什么它無法給其他視圖控制器傳遞消息。

確實(shí),一個委托協(xié)議是個不錯的方法,但是我想給你展示一個新的方法,這是storyboard的一個特色功能,可以達(dá)到和委托協(xié)議相同的效果,但是工作量要比創(chuàng)建一個委托協(xié)議小一些,它叫做:unwind segues(不知道怎么翻譯這個術(shù)語合適T T)。

如果你想知道storyboard中的的紅色“Exit”圖標(biāo)是什么,你現(xiàn)在有了你的答案,沒錯,它就是:unwind segues。

regular segue用于打開一個新的界面,而unwind segue用于關(guān)閉一個當(dāng)前激活的界面。聽起來很簡單。然而,創(chuàng)建unwind segue的方法不是非常直觀。

這個Exit圖標(biāo)似乎沒有任何作用,試試按住ctrl拖拽cell上去,它不會形成一個鏈接。

首先,你要添加一個特殊類型的動作方法。

打開LocationDetailsViewController.swift,添加下面的方法進(jìn)去:

@IBAction func categoryPickerDidPickCategory(_ segue: UIStoryboardSegue) {
        let controller = segue.source as! CategoryPickerViewController
        categoryName = controller.selectedCategoryName
        categoryLable.text = categoryName
    }

因?yàn)檫@個方法是以@IBAction前綴開頭的,所以它是個動作方法。但是它和一般的動作方法有什么區(qū)別呢?區(qū)別在于它的參數(shù),是一個UIStoryboardSegue對象。

通常,如果動作方法有一個參數(shù)的話,它應(yīng)該是觸發(fā)這個動作的控件,比如按鈕和滑條。但是為了創(chuàng)建一個unwind segue,你需要將動作方法的參數(shù)寫為UIStoryboardSegue。

這個方法內(nèi)部代碼的意思非常明顯。你找到是那個視圖轉(zhuǎn)場到這個界面來的(就是源界面),在這里它就是CategoryPickerViewController,然后讀取它的selectedCategoryName屬性。它正好包含用戶選擇的分類名稱。

打開storyboard。按住ctrl拖拽cell到Exit按鈕上,這次應(yīng)該能夠創(chuàng)建鏈接了:

然后在彈出菜單的Selection Segue分節(jié)下選擇categoryPickerDidPickCategory,就是你剛才創(chuàng)建的用于unwind segue的動作方法的名字。

如果無法創(chuàng)建鏈接,請確定你選中的是cell,而不是Content View或者其中的Label。

運(yùn)行app,是不是非常簡單?好像也不是那么簡單,被選擇的分類被忽視掉了...

這是因?yàn)殡m然categoryPickerDidPickCategory()方法看到了selectedCategoryName屬性,但是這個屬性此時沒有寫入值。

你需要一個機(jī)制,當(dāng)unwind segue轉(zhuǎn)場被觸發(fā)時,你可以把用戶點(diǎn)擊的那一行的分類名稱寫入到selectedCategoryName屬性中。

我想這個機(jī)制應(yīng)該就是prepare(for:sender:),沒錯,這個方法對各種轉(zhuǎn)場都適用。

打開CategoryPickerViewController.swift,添加prepare(for:sender:)方法:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "PickedCategory" {
            let cell = sender as! UITableViewCell
            if let indexPath = tableView.indexPath(for: cell) {
                selectedCategoryName = categories[indexPath.row]
            }
        }
    }

這段代碼看起來就是把被選擇行的相應(yīng)地category name(分類名稱)放入selectedCategoryName屬性中。

這段代碼假設(shè)unwind segue轉(zhuǎn)場的名稱叫做“PickedCategory”,所以你還需要設(shè)置這個轉(zhuǎn)場的名稱。

不幸的事,unwind segue在storyboard中并不可見。沒有一個普通轉(zhuǎn)場那樣的大大的箭頭。你只能在略縮面板中選擇它:

看這里,看這里

選擇unwind segue,然后打開屬性檢查器,設(shè)置identifier為PickedCategory。

再次運(yùn)行app,現(xiàn)在category picker應(yīng)該可以正常工作了。只要你選擇一條分類,界面就會自動關(guān)閉,并且新的分類的名稱也可以顯示在返回的界面中。

unwind segue非常棒,并且比使用委托協(xié)議要簡單的多,特別是在我們設(shè)計(jì)的這個app中。

改進(jìn)用戶體驗(yàn)

雖然Tag Location界面已經(jīng)具備了很多功能,但是它還是可以再改進(jìn)一下。改進(jìn)一些小細(xì)節(jié),可以使你的app更加人性化,并且在競爭對手中脫穎而出。

我們先來看看Description text視圖的設(shè)計(jì):

在text view和cell的邊界之間有10點(diǎn)的距離,但是因?yàn)樗鼈z的背景都是白色的,這樣會使用戶無法分辨text view的起點(diǎn)位置。

有可能會導(dǎo)致用戶剛好點(diǎn)在邊邊上,而無法編輯文本,這是非常讓人討厭的。你以為你已經(jīng)點(diǎn)到了,但是其實(shí)沒有點(diǎn)到,并且沒有任何反饋提示,用戶有可能會以為這個app就是垃圾,怒刪之。

所以這里我們要改進(jìn)一下,不管用戶點(diǎn)擊到了這個cell的任何位置,text view都應(yīng)該被激活,即使用戶正好點(diǎn)到了邊邊上。

在LocationDetailsViewController.swift的// MARK: - UITableViewDelegate注釋后面添加下面的方法:

override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        if indexPath.section == 0 || indexPath.section == 1 {
            return indexPath
        } else {
            return nil
        }
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if indexPath.section == 0 && indexPath.row == 0 {
            descriptionTextView.becomeFirstResponder()
        }
    }

tableView(willSelectRowAt)方法限定了,僅前兩個分節(jié)(section)的cell可以被點(diǎn)擊。回憶一下,||操作符是或的意思,所以僅當(dāng)section為0或者1時,其中的cell可以被點(diǎn)擊,而其余的cell都是只讀的。

tableView(didSelectRowAt)方法用來處理實(shí)際被選擇的行。你不需要對Category或者Add Photo進(jìn)行響應(yīng),這些cell是鏈接到轉(zhuǎn)場的。

但是假如用戶點(diǎn)擊了第一個分節(jié)中的第一行,那么你立刻激活text view。&&操作符是與的意思,就是and。

運(yùn)行app,試試效果,看看點(diǎn)擊cell邊緣,而不是text view內(nèi)部,能否激活text view(如果模擬器中的小鍵盤沒有自動彈出的話,可以使用快捷鍵command + K)

任何你可以挽救用戶體驗(yàn)的工作都是非常值得的。

就text view而言,一旦你激活了小鍵盤,就無法在關(guān)閉它,要知道小鍵盤可是占據(jù)了一半的屏幕,這會讓用戶抓狂。

當(dāng)你點(diǎn)擊屏幕中其他位置時,讓小鍵盤自動關(guān)閉,是非常棒的一個功能,實(shí)現(xiàn)起來也不是特別麻煩。

在viewDidLoad()方法的最后面添加下面的語句:

let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(hideKeyboard))
        gestureRecognizer.cancelsTouchesInView = false
        tableView.addGestureRecognizer(gestureRecognizer)

gesture recognizer(手勢識別器)是非常便利的一個對象,它可以識別點(diǎn)擊和手指的移動。你只是簡單的創(chuàng)建一個gesture recognizer對象,當(dāng)特定的手勢被觀察到時,調(diào)用一個你指定的方法,并且把這個識別器添加到視圖中。

你使用了一個UITapGestureRecognizer,它可以識別簡單的點(diǎn)擊,還有一些其它的對象,可以識別掃動,按壓,合攏等。

注意一下#selector()關(guān)鍵字:

...target: self, action: #selector(hideKeyboard))...

每當(dāng)手勢發(fā)生時,通過#selector告訴UITapGestureRecognizer要被調(diào)用的方法。

這個模式叫做target-action(目標(biāo)-動作),并且你已經(jīng)使用過多次了,比如鏈接UIButton,UIButtonItems,以及其它控件的動作方法時,其實(shí)用的都是這個模式。

traget就是接受被發(fā)送消息的對象,通常就是self,action就是發(fā)送的消息。

這里你做的就是,當(dāng)在table view的其它地方出現(xiàn)點(diǎn)擊這個行為時,hideKeyboard消息就會被發(fā)送,所以你要執(zhí)行一個方法來響應(yīng)這個消息。

打開LocationDetailsViewController.swift添加hideKeyboard()方法。把它放在viewDidLoad()方法的下面,其實(shí)放在其它地方也可以:

@objc func hideKeyboard(_ gestureRecognizer: UIGestureRecognizer) {
        let point = gestureRecognizer.location(in: tableView)
        let indexPath = tableView.indexPathForRow(at: point)
        if indexPath != nil && indexPath!.section == 0 && indexPath!.row == 0 {
            return
        }
        descriptionTextView.resignFirstResponder()
    }

??:在Objective-C中,選擇器(selector)是一種引用Objective-C方法名稱的類型。 在Swift中,Objective-C選擇器由Selector結(jié)構(gòu)表示,可以使用#selector表達(dá)式來構(gòu)造。 同時需要向Objective-C傳遞方法名稱。
這就是在hideKeyboard()方法名稱前加上@objc前綴的作用。
本書寫作的時候Swift版本還是3,不需要添加這個前綴,但是Swift4中必須要這樣做,我在后面翻譯的時候,也會不斷的將Swift4的新特性加進(jìn)去。
話說Swift的本意是要擺脫Objective-C,但是畢竟iOS框架與Objective-C已經(jīng)相愛相殺20余年了,所以你懂的。。

無論何時,用戶點(diǎn)擊table view內(nèi)任何地方時,手勢識別器就會調(diào)用這個方法。方便的是,它也傳遞了一個引用作為參數(shù)給自己,它可以讓你向手勢識別器詢問點(diǎn)擊的發(fā)生位置。

gestureRecognizer.location(in: tableView)方法返回一個CGPoint數(shù)。CGPoint是你在UIKit中隨處可見的一種結(jié)構(gòu)。它包含兩個字段,x和y,用于描述界面上的位置。

使用這個CGPoint數(shù),你就可以向table view詢問目前是位置上是哪個index-path。這非常重要,因?yàn)楫?dāng)用戶點(diǎn)擊text view內(nèi)部時,你不能把鍵盤隱藏掉。而如果點(diǎn)擊的是其它地方,你則需要隱藏這個鍵盤。

練習(xí):這里的if語句你熟悉嗎?能不能試著解釋一下呢?

答案:用戶可能會在table view內(nèi)部輕擊,而不在單元格內(nèi),例如在兩個部分之間的某個位置或部分text view上。 在這種情況下,indexPath將是nil,所以此時IndexPath是個可選型,需要使用if let或者感嘆號來對它進(jìn)行解包。

只有當(dāng)index-path不是section為0和row為0時,你才隱藏小鍵盤。

??:如果一個可選型有可能為nil的話,你不能對其強(qiáng)制解包,除非你愿意承擔(dān)app崩潰掉的風(fēng)險。所以上面方法中的indexPath!.section和indexPath!.row看起來很危險,但是這里是沒問題的,因?yàn)榍懊嬗幸粋€短路語句,就是indexPath != nil,如果indexPath為nil則整個if條件為假,其中的語句不會執(zhí)行。

另外一個可選的寫法是:

if indexPath == nil ||
          !(indexPath!.section == 0 && indexPath!.row == 0) {
  descriptionTextView.resignFirstResponder()
}

能看明白嗎?這個if語句和之前的意思完全相反,但目的是相同的,這個if語句使用了||或操作符,意思是indexPath為nil或者index-path不是section為0和row為0時,隱藏小鍵盤(感嘆號出現(xiàn)在表達(dá)式前面時,是非的意思)。

熟練的使用各種邏輯操作符,是你編程生涯中很重要的一個環(huán)節(jié),幸好它不是很難。

當(dāng)然你也可以用if let安全的解包,像下面這個樣子:

if let indexPath = indexPath, indexPath.section != 0 &&
                              indexPath.row != 0 {
return
}
descriptionTextView.resignFirstResponder()

我只是給你簡單的展示了一下if語句的多樣性。你可以選擇一個你喜歡的一直用下去就好了。

運(yùn)行app,點(diǎn)擊text view會自動彈出一個小鍵盤,如果沒有就用快捷鍵command + K。

我們還可以實(shí)現(xiàn),當(dāng)用戶進(jìn)行滾動操作的時候,也自動隱藏小鍵盤。

打開storyboard,選擇Tag Location界面中的table view。在屬性檢查器中找到Keyboard,選擇其中的Dismiss on drag選項(xiàng),這樣就可以實(shí)現(xiàn)了,簡單吧。

如果設(shè)置沒有生效的話,就在真實(shí)設(shè)備上試試,模擬器中的虛擬鍵盤有時候不是很聰明。

同時試試Dismiss interactively選項(xiàng),看看那個你更加喜歡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內(nèi)容