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

防御型代碼

接下來按照我說的去做以下步驟:中斷app并且使用菜單Simulator → Reset Contents and Settings重置模擬器。

(僅僅是把a(bǔ)pp刪掉是不行的,你需要整個(gè)重置模擬器)

然后重新運(yùn)行app,觀察app崩潰時(shí)的信息:

fatal error: Index out of range

編譯器指向的報(bào)錯(cuò)地點(diǎn)時(shí)在viewDidAppear()中的這一行:

let checklist = dataModel.lists[index]

這里發(fā)生了什么?顯然這時(shí)index的值不是-1,否則代碼不會(huì)執(zhí)行到if語句內(nèi)部。

事實(shí)上此時(shí)index是0,因?yàn)檫@是app剛啟動(dòng)的時(shí)候,UserDefault中不應(yīng)該有任何東西。app還沒有將任何東西寫進(jìn)“ChecklistIndex”鍵中。那么為什么是0呢?因?yàn)閁serDefault的integer(forKey)方法在找不到鍵對(duì)應(yīng)的值時(shí),默認(rèn)返回0,而此時(shí)0不是一個(gè)可用的行索引,因?yàn)榇藭r(shí)app中不存在任何具體的待辦事項(xiàng),所以lists數(shù)組是空的,所以索引為0的lists的對(duì)象也是不存在的。這就是app掛掉的原因。

幸運(yùn)的是UserDefault可以讓你設(shè)置這個(gè)默認(rèn)值,讓我們?cè)跀?shù)據(jù)模型中完成這件事。

打開DataModel.swift,添加以下方法:

func registerDefaults() {
        let dictionary: [String: Any] = ["ChecklistIndex": -1]
        UserDefaults.standard.register(defaults: dictionary)
    }

這里創(chuàng)建了一個(gè)新的字典實(shí)例,并且將-1添加到鍵值“ ChecklistIndex”中。

這里的方括號(hào)不是數(shù)組的意思,而是字典。數(shù)組和字典的區(qū)別是這樣的,字典看起來是這個(gè)樣子:

[ key1: value1, key2: value2, . . . ]

而數(shù)組看起來是這個(gè)樣子:

[ value1, value2, value3, . . . ]

這樣當(dāng)鍵中不存在值的時(shí)候,就會(huì)從你剛才建的字典當(dāng)中請(qǐng)求值了。

改變一下DataModel.swift中的init方法:

init() {
        loadChecklists()
        registerDefaults()
    }

再運(yùn)行一下app,現(xiàn)在它不會(huì)掛掉了。

為什么我們是在數(shù)據(jù)模型中做這個(gè)事呢?是這樣的,我并不想要在任何時(shí)刻都調(diào)用UserDefault。

事實(shí)上,我們要把全部UserDefaule相關(guān)的東西都移到DataModel中,還是打開DataModel.swift,添加以下代碼:

var indexOfSelectedChecklist: Int {
        get {
            return UserDefaults.standard.integer(forKey: "ChecklistIndex")
        }
        set {
            UserDefaults.standard.set(newValue, forKey: "ChecklistIndex")
        }
    }

這是你以前沒見過的一種東西。它看起來似乎是定義了一個(gè)新的整數(shù)型實(shí)例變量indexOfSelectedChecklist,但是這里的get{}和set{}是什么呢?

這叫做計(jì)算屬性。

這里沒有任何存儲(chǔ)空間分配給這些屬性(所以它們不算是真正的變量)

取而代之的是,當(dāng)app試圖從indexOfSelectedChecklist中讀取值時(shí),get花括號(hào)中的代碼會(huì)被執(zhí)行。而當(dāng)app試圖寫入一個(gè)新的值到indexOfSelectedChecklist中時(shí),set花括號(hào)中的代碼會(huì)被執(zhí)行。

從現(xiàn)在開始你可以通過簡(jiǎn)單的使用indexOfSelectedChecklist這個(gè)變量來自動(dòng)更新UserDefault,這非???,不是嗎?

你做了這個(gè)工作之后,其他的代碼就再也不用操心UserDefaults了。其他的對(duì)象僅僅是調(diào)用數(shù)據(jù)模型中的indexOfSelectedChecklist屬性就可以了。

隱藏執(zhí)行細(xì)節(jié)是面向?qū)ο缶幊痰囊粋€(gè)重要原則,這就是我們?yōu)槭裁匆@樣做的原因。

如果你后期決定將這些設(shè)置存儲(chǔ)到其他地方,比如iCloud或者數(shù)據(jù)庫(kù)里,你僅僅需要修改這一個(gè)地方就可以了。其他的代碼根本不用操心這些事,這是非常方便的。

使用新的計(jì)算屬性來更新一下AllListsViewController.swift:

override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        navigationController?.delegate = self
        
        let index = dataModel.indexOfSelectedChecklist   //修改這里
        if index != -1 {
            let checklist = dataModel.lists[index]
            performSegue(withIdentifier: "ShowChecklist", sender: checklist)
        }
    }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        UserDefaults.standard.set(indexPath.row, forKey: "ChecklistIndex")
//修改這里
        dataModel.indexOfSelectedChecklist = indexPath.row
        
        let checklist = dataModel.lists[indexPath.row]
        performSegue(withIdentifier: "ShowChecklist", sender: checklist)
    }
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        if viewController === self {
            //修改這里
            dataModel.indexOfSelectedChecklist = -1
        }
    }

現(xiàn)在代碼的可讀性也增加了不少。AllListsViewController不用再操心如何把值放入U(xiǎn)serDefault了,而僅僅是使用它。

再次運(yùn)行app,確保一切工作正常。

現(xiàn)在app可以記住你離開時(shí)的界面了,非常棒。但是新的功能帶來了一些小小的bug,過程是這樣的:

啟動(dòng)app,新增一個(gè)新的待辦分類,再添加一條新的待辦事項(xiàng)。然后在Xcode把a(bǔ)pp中斷掉。

因?yàn)槟銢]有實(shí)現(xiàn)退回到主界面,所以你新增的分類和待辦事項(xiàng)事項(xiàng)并沒有被保存到Checklists.plist中。

然而UserDefaults卻把離開是選擇的待辦事項(xiàng)保存下來了,這就是問題所在,UserDefault保存下來的索引現(xiàn)在不存在了。

再次運(yùn)行app,猜猜會(huì)怎么樣?app掛了,報(bào)錯(cuò)為:

fatal error: Index out of range

如果你不能重現(xiàn)這個(gè)錯(cuò)誤,那么就在indexOfSelectedChecklist中的set語句中加上一句,這樣就可以強(qiáng)迫UserDefaults在indexOfSelectedChecklist每次發(fā)生變化時(shí),進(jìn)行一次保存了:

set {
            UserDefaults.standard.set(newValue, forKey: "ChecklistIndex")
            UserDefaults.standard.synchronize()
        }

app掛掉的原因是UserDefaults和Checklists.plist中的內(nèi)容不同步。UserDefaults認(rèn)為app中應(yīng)該有一行被選中了,但是實(shí)際上這一行并不存在。所以你每次運(yùn)行app,都會(huì)掛掉。

正常情況下是不會(huì)發(fā)生這種事的,但是由于你是通過Xcode中的Stop按鈕直接中斷了app,導(dǎo)致plist文件沒有被保存。

用戶在實(shí)際在手機(jī)上使用app的過程中一般都會(huì)先按Home鍵將app轉(zhuǎn)到后臺(tái)。這樣Checklists.plist文件就有機(jī)會(huì)被保存下來,和UserDefaults保持同步。

然而,操作系統(tǒng)有時(shí)也會(huì)直接中斷掉app。

雖然在實(shí)際使用過程中這種可能性很小,你還是需要防范于未然。向這種類型的bug,你是無法收到反饋的,因?yàn)槟悴恢烙脩魰?huì)怎樣使用你的app。

向這種地方做些防御型代碼就顯得非常重要。你的代碼必須總是檢查這種情況,并且游刃有余的處理它們。

在我們這個(gè)情況中,你可以通過修改AllListsViewController中的viewDidAppear方法來修復(fù)這個(gè)問題。

將viewDidAppear方法中的if語句修改為下面這個(gè)樣子:

if index >= 0 && index < dataModel.lists.count {

你現(xiàn)在增加了一些判斷條件來取代僅僅是判斷index不等于-1?,F(xiàn)在是判斷index位于0和數(shù)據(jù)模型中的checklists數(shù)量之間的一個(gè)值。

這樣就避免了向dataModel.lists[index]請(qǐng)求某個(gè)index的對(duì)象時(shí),這個(gè)對(duì)象不存在的情況。

你之前可能沒見過&&操作符。這個(gè)操作符的意思是邏輯與。它經(jīng)常像下面這樣使用:

if something && somethingElse {
// do stuff
}

這個(gè)代碼讀作:如果something為真并且something else也為真,那么執(zhí)行if花括號(hào)內(nèi)的語句。

在viewDidAppear()中,你僅僅是當(dāng)index在0和checklists數(shù)量之間時(shí)才執(zhí)行轉(zhuǎn)場(chǎng)。

有個(gè)這個(gè)防御措施,你就可以保證app不會(huì)在checklists不存在時(shí)進(jìn)行轉(zhuǎn)場(chǎng)了,即使數(shù)據(jù)是不同步的。

??:即使app可以記得用戶離開時(shí)選擇了哪條待辦事項(xiàng),它還是有不足的地方,它不會(huì)記得用戶是否新增或者編輯了這行或者新增和編輯界面是否打開。
這種類型的界面在設(shè)計(jì)時(shí)就被認(rèn)為是臨時(shí)的。你打開這些界面做一些操作然后就關(guān)閉它們。如果app被轉(zhuǎn)到了后臺(tái),這些界面沒有被保存下來問題也不大。
至少對(duì)我們這個(gè)app是如此。假如你有一個(gè)app,在某個(gè)界面上的功能是一些非常復(fù)雜的操作,那么你也許需要當(dāng)app中斷時(shí),將這些操作保存下來,以免用戶重復(fù)操作。
在本節(jié)課程中你使用UserDefaults來記住那個(gè)界面被打開過,但是實(shí)際上iOS系統(tǒng)有一個(gè)專門的API用于這種功能的開發(fā),它們是State Preservation和Restoration。

首次運(yùn)行時(shí)的用戶體驗(yàn)

讓我們用UserDefaults做些其他事情。當(dāng)你第一次運(yùn)行app時(shí),如果app自動(dòng)為你創(chuàng)建好一條默認(rèn)的待辦事項(xiàng)分類是非常好的一個(gè)體驗(yàn),名字僅命名為“List”就可以了,并且切換到這條之后它可以立刻打開新增待辦事項(xiàng)界面,讓你添加待辦事項(xiàng)。

這也是標(biāo)準(zhǔn)的筆記本類型的app應(yīng)該具備的功能:打開app后自動(dòng)新建一個(gè)文件,可以讓你立刻開始輸入,并且你也可以回到導(dǎo)航層級(jí)的上一級(jí)查看所有筆記的列表。

為了達(dá)到這個(gè)目的,你需要使用UserDefaults來跟蹤用戶是否是首次運(yùn)行app。如果是,則新增一個(gè)Checklist對(duì)象。

你可以把這些邏輯全部放入DataModel中。

把新的設(shè)置放入registerDefaults()方法,是一個(gè)非常好的選擇。你可以新建一個(gè)名為“FirstTime”的鍵。

打開DataModel.swift,然后將registerDefaults()方法修改為下面這個(gè)樣子:

func registerDefaults() {
        let dictionary: [String: Any] = ["ChecklistIndex": -1,"FirstTime": true]
        UserDefaults.standard.register(defaults: dictionary)
    }

FirstTime被設(shè)置為布爾型,因?yàn)樗挥袃煞N可能,要么是要么否:是第一次運(yùn)行或者不是第一次運(yùn)行。

如果是app安裝后第一次運(yùn)行,那么“FirstTime”的值應(yīng)該是true。

還是在DataModel.swift中,添加一個(gè)新的方法handleFirstTime():

func handleFirstTime() {
        let userDefaults = UserDefaults.standard
        let firstTime = userDefaults.bool(forKey: "FirstTime")
        lists.append(checklist)
        indexOfSelectedChecklist = 0
        userDefaults.set(false, forKey: "FirstTime")
        userDefaults.synchronize()
    }

這里你用UserDefaults來檢查“FirstTime”鍵的值。如果“FirstTime”為true,那么這就是app第一次運(yùn)行。在這種情況下,你創(chuàng)建一個(gè)新的Checklist對(duì)象到數(shù)組中去。

你同時(shí)設(shè)置indexOfSelectedChecklist為0,用作第一個(gè)Checklist對(duì)象的索引,來保證app會(huì)自動(dòng)通過AllListsViewController的viewDidAppear()方法轉(zhuǎn)場(chǎng)到新的列表上。

最后,你將“FirstTime”設(shè)置為false,這樣再次運(yùn)行app時(shí),就不會(huì)在執(zhí)行這些邏輯了。

在DataModel的init方法中新增一個(gè)調(diào)用方法:

init() {
        loadChecklists()
        registerDefaults()
        handleFirstTime()
    }

重置模擬器,并且移除掉相關(guān)數(shù)據(jù),并且再次運(yùn)行app??纯词欠駮?huì)自動(dòng)新增一個(gè)待辦事項(xiàng)分類,并且自動(dòng)跳轉(zhuǎn)到新增待辦事項(xiàng)條目的界面。

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

這里還有我們需要添加的一些小特色,讓我們?cè)诎盐覀兊男pp打磨一下。畢竟,你是在做一個(gè)真正可以使用的app,如果你想要app有更高的排名,你就必須在這些細(xì)節(jié)上付出更多。

展示未完待辦事項(xiàng)的數(shù)量

在主界面,給每個(gè)待辦事項(xiàng)分類添加上展示還沒有完成的具體待辦的數(shù)量:

每個(gè)分類都可以展示其中未完的數(shù)量

首先,你需要一個(gè)方法來對(duì)這些待辦事項(xiàng)計(jì)數(shù)。

打開Checklist.swift,添加一個(gè)新的方法:

func countUnCheckedItems() -> Int {
        var count = 0
        for item in items where !item.checked {
            count += 1
        }
        return count
    }

使用這個(gè)方法,你就可以向任何Checklist對(duì)象請(qǐng)求其中還沒有被打上對(duì)勾符號(hào)的待辦事項(xiàng)的數(shù)量。這個(gè)方法返回一個(gè)整數(shù)型的值。

你使用了for循環(huán)歷遍items數(shù)組中的ChecklistItem對(duì)象。如果一個(gè)item對(duì)象的checked屬性被設(shè)置為false,你就將局部變量count加1。

記住,感嘆號(hào)放在前面的意思是取反。所以如果item.checked為true,那么取反后就是false,這條數(shù)據(jù)被我們過濾掉了。

??:如果感嘆號(hào)放在前面,那么它的意思就是邏輯非,就像這里這樣。當(dāng)一個(gè)感嘆號(hào)放在后面時(shí),它就是和可選型相關(guān)的操作。這是swift中一個(gè)符號(hào)多個(gè)意思的一個(gè)例子。當(dāng)有多義詞出現(xiàn)時(shí),正確的做法是通過上下文來理解它。

當(dāng)循環(huán)結(jié)束,你就已經(jīng)歷遍了所有對(duì)象了,然后你返回這些對(duì)象的總數(shù)給調(diào)用者。

練習(xí):如果你用let來代替var定義count的話,會(huì)發(fā)生什么呢?

答案:當(dāng)count是一個(gè)常量時(shí),Swift不會(huì)允許你修改這個(gè)值,所以count += 1這一行會(huì)報(bào)錯(cuò)。

順便說一下,你也可以把for語句修改為下面這個(gè)樣子:

for item in items {
  if !item.checked {
count += 1 
}
}

這樣看起來就熟悉多了,個(gè)人而言,我喜歡簡(jiǎn)短的for in where語句。

打開AllListsViewController.swift,對(duì)makeCell(for)方法做點(diǎn)小小的改動(dòng),將style: .default修改為style: .subtitle。

其他地方不動(dòng),除了使用.subtitle代替.default用作cell的風(fēng)格以外?!皊ubtitle”風(fēng)格會(huì)在主標(biāo)題下添加一個(gè)略小一點(diǎn)的次級(jí)標(biāo)題。你可以使用cell的detailTextLabel屬性來讀取這個(gè)次級(jí)標(biāo)題。

在tableView(cellForRowAt)中,在return cell前添加一行代碼:

cell.detailTextLabel!.text = "\(checklist.countUnCheckedItems()) Remaining"

你調(diào)用Checklist中的countUncheckedItems()方法,并且將count放入一個(gè)新的字符串,這個(gè)字符串就位于次級(jí)標(biāo)簽的文本屬性中。

和以前一樣(...)的作用是字符串插值。注意一下,你甚至可以在字符串插值中使用方法,多棒啊。

將文本放入cell的標(biāo)簽中,你使用了以下語句:

cell.textLabel!.text = someString
cell.detailTextLabel!.text = anotherString

這里的感嘆號(hào)是必須的,因?yàn)閠extLabel和detailTextLabel都是可選型。

textLabel屬性僅會(huì)出現(xiàn)在table view cell內(nèi)建的cell風(fēng)格中;對(duì)于自定義的cell設(shè)計(jì),它的值是nil。同樣的,并不是所有的cell風(fēng)格都包含detail label和detailTextLabel,在這種情況下,它們都是nil。

這里是使用了Subtitle風(fēng)格,是可以確保你同時(shí)擁有上面兩個(gè)label。因?yàn)閷?duì)于Subtitle來說它們永遠(yuǎn)不會(huì)為nil,所以你可以使用感嘆號(hào)來強(qiáng)制解包。解包后的可選型就變成了你實(shí)際需要的對(duì)象。

使用感嘆號(hào)的時(shí)候一定要小心,因?yàn)槿绻@個(gè)可選型為nil的話,你的app會(huì)立刻被掛掉。

你也可以把上面的語句寫成下面這個(gè)樣子:

if let label = cell.textLabel {
  label.text = someString
}
if let label = cell.detailTextLabel {
  label.text = anotherString
}

這樣就非常安全了,但是顯得繁瑣一些,在剛才的情況中使用感嘆號(hào)會(huì)很便利。

運(yùn)行app,現(xiàn)在每個(gè)分類都應(yīng)該可以展示其中剩余的待辦事項(xiàng)數(shù)量了。

每個(gè)cell都有了一個(gè)次級(jí)標(biāo)簽

還剩一個(gè)問題:這個(gè)剩余待辦事項(xiàng)的數(shù)量永遠(yuǎn)不會(huì)發(fā)生變化。如果你進(jìn)入一個(gè)目錄給幾條待辦事項(xiàng)事項(xiàng)打上對(duì)勾符號(hào),或者新增幾條待辦事項(xiàng),這個(gè)數(shù)字永遠(yuǎn)不會(huì)發(fā)生變化。這是因?yàn)槟銊?chuàng)建了這些cell以后從來沒有去更新它們。(試著自己解決一下這個(gè)問題呢)

練習(xí):試著列舉一下所有需要更新這個(gè)數(shù)字的情況。

答案:1、用戶在待辦事項(xiàng)上打上對(duì)勾,每當(dāng)一個(gè)打上一個(gè)對(duì)勾,這個(gè)數(shù)字就應(yīng)該減1,而每次取消掉一個(gè)對(duì)勾,這個(gè)數(shù)字就應(yīng)該加1。

2、用戶新增一條待辦事項(xiàng)時(shí),這個(gè)數(shù)字應(yīng)該加1。

3、用戶刪除掉某條待辦事項(xiàng)時(shí),這個(gè)數(shù)字應(yīng)該減1,注意一下,此時(shí)對(duì)勾符號(hào)的狀態(tài)沒有變化,但是實(shí)際上講,待辦事項(xiàng)就是少了一條。

所有這些變化都由ChecklistViewController負(fù)責(zé)控制,但是“剩余待辦”的標(biāo)簽卻是在AllListsViewController中。

所以你怎么樣使All Lists View Controller知道這些事情的發(fā)生呢?

如果你覺得,這非常簡(jiǎn)單,我應(yīng)該使用一個(gè)委托,那么你就逐漸開始入門了。你可以做一個(gè)新的ChecklistViewControllerDelegate協(xié)議,在以下事情發(fā)生時(shí)開始發(fā)送消息:

1、用戶出發(fā)對(duì)勾符號(hào)的開關(guān)

2、用戶新增待辦事項(xiàng)

3、用戶刪除待辦事項(xiàng)

但是這個(gè)委托,就是AllListsViewController應(yīng)該做出什么響應(yīng)呢??jī)H僅是在以上情況發(fā)生時(shí),更新一下cell的detailTextLabel的文本信息。

委托是非常不錯(cuò)的一個(gè)方法,但是在此處你不會(huì)這樣做,這里有一個(gè)更簡(jiǎn)單的方法,聰明的程序員都會(huì)選擇這個(gè)簡(jiǎn)單的途徑解決這個(gè)問題。

打開AllListsViewController.swift,并且添加viewWillAppear()方法做以下事情:

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        tableView.reloadData()
    }

不要把這個(gè)方法和viewDidAppear()弄混了。它們的不同之處在于其中的動(dòng)詞,will和did,viewWillAppear()在viewDidAppear()之前被調(diào)用,當(dāng)視圖即將可視化但是動(dòng)畫還沒有開始執(zhí)行的時(shí)候viewWillAppear()會(huì)被調(diào)用,而viewDidAppear()是在視圖在屏幕上已經(jīng)可視化以后的時(shí)候被調(diào)用。這里相差的時(shí)間可能就是半秒鐘左右。

iOS的API經(jīng)常這樣做:標(biāo)注有will的方法會(huì)在標(biāo)注為did的方法之前被調(diào)用。有時(shí)你需要提前做一些事情,而時(shí)候你需要稍候做一些事情,這里提供了兩個(gè)方法,就可以讓你靈活的選擇,那個(gè)是更合適的了。

??:API(ay-pee-eye)是Application Programming Interface(應(yīng)用程序接口)的簡(jiǎn)稱。當(dāng)有人提到iOS API時(shí),他的意思是iOS系統(tǒng)中全部的框架,對(duì)象,協(xié)議和方法。
iOS的API包羅萬象,比如UIKit,F(xiàn)oundation,Core Graphics等等。同樣的,如果有人提到Facebook API或者Google API的時(shí)候,它們的意思就是這些公司提供的你可以用來開發(fā)的對(duì)象。

這里,viewWillAppear()告訴table view更新它全部的內(nèi)容。這會(huì)使得tableView(cellForRowAt)在每一行可視化前被重新調(diào)用一次。

當(dāng)你點(diǎn)擊ChecklistViewController的導(dǎo)航欄上的back按鈕后,AllListsViewController界面會(huì)重新回到屏幕上,但是在這之前viewWillAppear()被調(diào)用了。因?yàn)閳?zhí)行了tableView.reloadData(),app會(huì)更新所有的cell,包括detailTextLabel。

重新加載所有cell看起來開銷有些大,但是在這種情況下,這是最簡(jiǎn)單的辦法。因?yàn)槲覀兊腶pp也不會(huì)包含太多的數(shù)據(jù),并且同一時(shí)間界面上大概也只有14行會(huì)被可視化,所以也不用太擔(dān)心內(nèi)存消耗。而這又為你節(jié)省了做一個(gè)委托的大量時(shí)間。

有時(shí)委托是最好的辦法,而有時(shí),你僅僅需要重新加載所有數(shù)據(jù)就可以了。

運(yùn)行app,看看是否生效。

練習(xí):當(dāng)某個(gè)分類中沒有剩余待辦事項(xiàng)時(shí),將標(biāo)簽更新為“All Done”

答案:改變一下tableView(cellForRowAt)中的語句:

let count = checklist.countUnCheckedItems()
        if count == 0 {
            cell.detailTextLabel!.text = "All Done"
        } else {
            cell.detailTextLabel!.text = "\(checklist.countUnCheckedItems()) Remaining"
        }

你將數(shù)量放入了一個(gè)局部變量,因?yàn)槟阋昧怂鼉纱巍S?jì)算一次并且將數(shù)量存入一個(gè)臨時(shí)變量比計(jì)算兩次要好的多。

練習(xí):當(dāng)某個(gè)分類中不存在待辦事項(xiàng)時(shí),將detailTextLable更新為“No Items”

答案:

let count = checklist.countUnCheckedItems()
        if checklist.items.count == 0 {
            cell.detailTextLabel!.text = "No Items"
        } else if count == 0 {
            cell.detailTextLabel!.text = "All Done"
        } else {
            cell.detailTextLabel!.text = "\(checklist.countUnCheckedItems()) Remaining"
        }

僅僅是判斷countUncheckedItems()是不夠多,因?yàn)楫?dāng)它返回0時(shí),你并不清楚是不存在待辦事項(xiàng)還是所有待辦事項(xiàng)都已經(jīng)被打了對(duì)勾符號(hào)了。你需要通過checklist.items.count去判斷checklist中的items的總數(shù),才能知道是否是不存在待辦事項(xiàng)。

效果圖

像這樣的小細(xì)節(jié),會(huì)使你的app有非常良好的用戶體驗(yàn)。問問你自己,你是覺得“0 Remaining”還是“All Done”更加貼心。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容