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

上節(jié)課,我們已經(jīng)實(shí)現(xiàn)了一個(gè)本地通知。為什么我要求你們要先按Home鍵退回到主界面呢卿嘲?那是因?yàn)閕OS的消息通知,僅僅在app未使用時(shí)才會(huì)生效夫壁,如果你正在使用app拾枣,你當(dāng)然不需要關(guān)于這個(gè)app的提醒。

點(diǎn)擊Stop按鈕中斷app盒让,然后再次運(yùn)行app梅肤,這次不要按Home鍵退回主界面,再進(jìn)行一次漫長(zhǎng)的等待邑茄,看吧姨蝴,什么都不會(huì)發(fā)生,我只希望你不要等了太久肺缕。

消息通知的功能已經(jīng)實(shí)現(xiàn)了左医,但是它和用戶的待辦事項(xiàng)是相互獨(dú)立的,兩者之間還不存在關(guān)系同木,為了解決這個(gè)問(wèn)題浮梢,我們要以某種方式讓相關(guān)的事件注意到本地通知。怎么辦呢泉手?當(dāng)然是通過(guò)使用委托了黔寇。

在AppDelegate的class聲明的那一行上改動(dòng)一下:

class AppDelegate: UIResponder, UIApplicationDelegate,UNUserNotificationCenterDelegate {

這樣就讓AppDelegate成為了UNUserNotificationCenter的委托。

同時(shí)在AppDelegate.swift中添加以下方法:

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        print("Received local notification \(notification)")
    }

這個(gè)方法當(dāng)app仍在運(yùn)行時(shí)有本地通知發(fā)布時(shí)被調(diào)用斩萌。你不用在這里做任何操作缝裤,除了在調(diào)試區(qū)域打印一條消息。

當(dāng)app在前臺(tái)運(yùn)行時(shí)颊郎,它假定任何通知都會(huì)自己照顧自己憋飞。根據(jù)app的類型不同,通知也許會(huì)被展現(xiàn)給用戶姆吭,也許會(huì)自動(dòng)刷新界面榛做。

最后,告訴UNUserNotificationCenter現(xiàn)在AppDelegate是它的委托了。在application(didFinishLaunchingWithOptions)方法中添加一行代碼來(lái)完成這件事:

center.delegate = self

再次重新運(yùn)行app检眯,不要按Home鍵厘擂,等待10秒,10秒之后你會(huì)看到調(diào)試區(qū)域打印出了一條消息:

Received local notification <UNNotification: 0x60800003b160; date: 2017-07-23 13:38:50 +0000, request: <UNNotificationRequest: 0x6000004250c0; identifier: MyNotification, content: <UNNotificationContent: 0x600000107b30; title: Hello, subtitle: (null), body: I am a local notifcation, ...

好了锰瘸,你已經(jīng)確定它在工作了刽严,你需要從AppDelegate.swift中移除掉所有代碼,因?yàn)槟悴⒉恍枰看斡脩魡?dòng)app時(shí)都安排新的通知避凝。

從didFinishLaunchingWithOptions中把所有通知相關(guān)的代碼移除掉舞萄,僅保留以下兩行:

let center = UNUserNotificationCenter.current()
        center.delegate = self

你可以u(píng)serNotificationCenter(willPresent...)方法也留下,讓它繼續(xù)在調(diào)試區(qū)域打印消息管削。

擴(kuò)展數(shù)據(jù)模型

讓我們來(lái)考慮一下倒脓,app應(yīng)該如何處理這些消息。每條待辦事項(xiàng)都應(yīng)該有一個(gè)處理時(shí)間的字段(一個(gè)Date型對(duì)象含思,可以指定具體的日期和時(shí)間)并且需要有一個(gè)Bool型對(duì)象來(lái)判斷用戶是否想要對(duì)這一條信息進(jìn)行提示崎弃。

用戶也許不需要對(duì)每一條待辦事項(xiàng)都進(jìn)行提醒,所以你不能對(duì)所有的待辦事項(xiàng)都安排一條通知含潘。所以我們需要一個(gè)Bool型對(duì)象來(lái)進(jìn)行判斷吊履,名字就叫做shouldRemind好了。

你要在Add/Edit Item界面上增加關(guān)于它們的設(shè)置调鬓,完成后的樣子看起來(lái)會(huì)是這個(gè)樣子:

處理時(shí)間字段需要靠某種可以選擇時(shí)間的控制器實(shí)現(xiàn)艇炎。iOS自帶一個(gè)非常酷的日期選擇視圖腾窝,你可以直接把它添加到table view中缀踪。

首先,讓我們指出應(yīng)該在什么時(shí)間以什么方式來(lái)安排通知虹脯。我考慮的情況如下:

1驴娃、當(dāng)用戶添加一條新的待辦事項(xiàng)時(shí),如果同時(shí)設(shè)置shouldRemind標(biāo)示為true循集,則需要安排一條通知唇敞。

2、當(dāng)用戶對(duì)已存在的待辦事項(xiàng)的處理時(shí)間進(jìn)行編輯的時(shí)候咒彤,舊的通知安排需要被取消(如果之前有安排通知的話)疆柔,并且安排一條新的通知(如果用戶沒(méi)有取消shouldRemind設(shè)置的話)

3、當(dāng)用戶對(duì)shouldRemind狀態(tài)進(jìn)行true到false切換的時(shí)候镶柱,存在的通知需要被取消旷档,從false到true到時(shí)候需要安排一條通知。

4歇拆、當(dāng)用戶刪除一條待辦事項(xiàng)的時(shí)候鞋屈,需要取消通知(如果之前有的話)

5范咨、當(dāng)用戶刪除一個(gè)待辦分類的時(shí)候,其中所有已存在的通知都要被取消掉厂庇。

通過(guò)上面的分析渠啊,你要做的事情就一目了然了。

你同時(shí)需要注意一下权旷,不能為哪些處理時(shí)間已經(jīng)小于當(dāng)前時(shí)間的待辦事項(xiàng)安排通知昭抒。雖然iOS會(huì)自動(dòng)忽略這些通知,但是我們還是最好做到自己處理炼杖,養(yǎng)成考慮周全的習(xí)慣。

要把ChecklistItem對(duì)象和它們的通知聯(lián)系起來(lái)盗迟,就必須修改一下數(shù)據(jù)模型坤邪。

當(dāng)你安排一條本地通知的時(shí)候,你就創(chuàng)建了一個(gè)UNNotificationRequest對(duì)象罚缕。你可能會(huì)覺(jué)得既然如此艇纺,那么將UNNotificationRequest對(duì)象作為一個(gè)實(shí)例變量放入ChecklistItem中就好了,但是邮弹,這并不是正確的方法黔衡。

取而代之的是,你要使用一個(gè)標(biāo)識(shí)符腌乡,每當(dāng)你創(chuàng)建一條本地通知時(shí)盟劫,你都給他一個(gè)標(biāo)識(shí)符,這個(gè)標(biāo)識(shí)符可以用一個(gè)字符串与纽。字符串中的內(nèi)容并不重要侣签,只要它不發(fā)生重復(fù)就行。

當(dāng)取消同時(shí)的時(shí)候急迂,你并不需要對(duì)UNNotificationRequest進(jìn)行操作影所,而是操作作為標(biāo)識(shí)符的字符串就可以了。所以正確的做法是將這個(gè)標(biāo)識(shí)符存放在ChecklistItem中僚碎。

即使用做通知標(biāo)識(shí)符的是一個(gè)字符串猴娩,但是實(shí)際上我們我們給它的值將是數(shù)字。你還需要將這些數(shù)字保存至Checklist.plist文件中勺阐。每次你安排或者取消一條通知時(shí)卷中,你就將數(shù)組轉(zhuǎn)換為字符串。這樣當(dāng)有一個(gè)ChecklistItem對(duì)象時(shí)渊抽,你就可以簡(jiǎn)單的找到對(duì)應(yīng)的通知仓坞,或者當(dāng)你有一個(gè)通知時(shí)就能簡(jiǎn)單的找到ChecklistItem對(duì)象。

創(chuàng)建一個(gè)數(shù)字序列ID腰吟,是非常普遍的一種行為无埃,就和關(guān)系型數(shù)據(jù)庫(kù)中的主鍵一樣徙瓶。

首先在ChecklistItem.swift中添加以下代碼:

var dueDate = Date()
var shouldRemind = false
var itemID: Int

我們將它取名為itemID,而不是簡(jiǎn)單的取名為id嫉称,那是因?yàn)閕d是OC中的一個(gè)特殊關(guān)鍵字侦镇,用id做變量名會(huì)使編譯器困惑。

其中的dueDate和shouldRemind都有初始值织阅,而itemID則沒(méi)有壳繁。這就是為什么你要指定itemID的類型的原因,而其他兩個(gè)不需要指定類型荔棉。因?yàn)閟wfit有類型推斷闹炉,記得嗎?

你還需要拓展一下init?(coder) 和 encode(with)润樱,這樣就可以在保存和讀取ChecklistItem對(duì)象時(shí)渣触,把它們也包含進(jìn)去了。

在init?(coder)中添加以下代碼:

dueDate = aDecoder.decodeObject(forKey: "DueDate") as! Date
        shouldRemind = aDecoder.decodeBool(forKey: "ShouldRemind")
        itemID = aDecoder.decodeInteger(forKey: "ItemID")

在encode(with)中添加以下代碼:

aCoder.encode(itemID, forKey: "ItemID")
        aCoder.encode(shouldRemind, forKey: "ShouldRemind")
        aCoder.encode(dueDate, forKey: "DueDate")

我們對(duì)dueDate使用了decodeObject(forKey)壹若,shouldRemain使用了decodeBool(forKey)嗅钻,而對(duì)itemID使用了decodeInteger(forKey),這是非常必要的店展,因?yàn)镹SCoder系統(tǒng)使用OC寫的养篓,這種語(yǔ)言對(duì)類型的要求非常嚴(yán)謹(jǐn)。

對(duì)OC而言Int赂蕴、Float柳弄、和Bool屬于原始類型。其他的東西比如String和Date屬于對(duì)象概说。這點(diǎn)和Swift不同语御,Swift對(duì)待所有東西都是按照對(duì)象處理。但是因?yàn)檫@里你要使用的是OC的框架席怪,所以你必須遵守OC的規(guī)則应闯。

非常棒,現(xiàn)在這些屬性也可以被存儲(chǔ)和讀取了挂捻。

現(xiàn)在Xcode中還存在一處報(bào)錯(cuò):init()需要itemID有一個(gè)值碉纺,因?yàn)槊啃陆ㄒ粋€(gè)對(duì)象都需要一個(gè)值。所以你需要在init()中給itemID分配一個(gè)值刻撒。

在init()中添加以下代碼:

override init() {
        itemID = DataModel.nextChecklistItemID()
        super.init()
    }

這個(gè)代碼的作用是無(wú)論app是否新創(chuàng)建一個(gè)ChecklistItem對(duì)象骨田,你都向DataModel請(qǐng)求一個(gè)新的ID。

我們現(xiàn)在就來(lái)添加這個(gè)新的方法声怔,這個(gè)方法和它的名字一樣每次都返回一個(gè)不同的ID态贤。

打開(kāi)DataModel.swift,添加這個(gè)新的方法:

class func nextChecklistItemID() -> Int {
        let userDefaults = UserDefaults.standard
        let itemID = userDefaults.integer(forKey: "ChecklistItemID")
        userDefaults.set(itemID + 1, forKey: "ChecklistItemID")
        userDefaults.synchronize()
        return itemID
    }

我們又見(jiàn)到了老朋友UserDefaults醋火。

這個(gè)方法從UserDefaults中得到目前的“ ChecklistItemID”的值悠汽,然后將它加1箱吕,然后將之前沒(méi)有加1的值返回給調(diào)用者。

同時(shí)它用userDefaults.synchronize()強(qiáng)制UserDefaults實(shí)時(shí)的將變化寫入磁盤柿冲,這樣就算app突然中斷了茬高,也不會(huì)丟失數(shù)據(jù),從而保證不會(huì)出現(xiàn)重復(fù)的值假抄。

在registerDefaults方法中為“ ChecklistItemID”的值添加初始值(注意一下怎栽,一定是要在FirstTime的后面添加):

 func registerDefaults() {
        let dictionary: [String: Any] = ["ChecklistIndex": -1,"FirstTime": true,"ChecklistItemID: 0"]
       ...

nextChecklistItemID第一次被調(diào)用后返回0,然后每次加1宿饱。就算你調(diào)用上億次都不會(huì)重復(fù)熏瞄。

類方法和實(shí)例方法(Class methods & instance methods)

如果你對(duì)下面的語(yǔ)句感到好奇,為什么是:

class func nextChecklistItemID()

而不是:

func nextChecklistItemID()

那么我很高興你如此細(xì)心谬以。

class關(guān)鍵字意味著你可以在不引用DataModel的前提下强饮,調(diào)用這個(gè)方法。

記住蛉签,你使用:

itemID = DataModel.nextChecklistItemID()

來(lái)調(diào)用類方法,而不是:

itemID = dataModel.nextChecklistItemID()

這是因?yàn)镃hecklistItem對(duì)象沒(méi)有一個(gè)用于引用DataModel的dataModel屬性沥寥。當(dāng)然碍舍,你可以給它一個(gè)這樣的引用,但是我決定使用類方法邑雅,這樣簡(jiǎn)單些片橡。

聲明類方法使用關(guān)鍵字class func,這種類型的方法適用于整個(gè)類淮野。

到目前為止你使用的方法都是實(shí)例方法捧书,使用關(guān)鍵字func定義,只能用于類中一個(gè)特定的實(shí)例骤星。

以前我們沒(méi)有討論過(guò)類方法和實(shí)例方法的區(qū)別经瓷,在以后的課程中我們會(huì)逐漸深化這個(gè)話題句旱。就現(xiàn)在而言隘马,僅僅記住用class func聲明的方法可以允許你在任何對(duì)象上調(diào)用它,甚至在不引用這個(gè)對(duì)象的前提下庐舟。

我不得不做出一個(gè)權(quán)衡:給每個(gè)ChecklistItem對(duì)象一個(gè)到DataModel的引用是否值得队贱,或者簡(jiǎn)單的使用一個(gè)類方法就好了色冀?為了保持簡(jiǎn)單,我選擇后者柱嫌。如果你未來(lái)還在開(kāi)發(fā)app的話锋恬,那么你很可能遇到這種需要做出權(quán)衡的情況。

為了快速的測(cè)試分配的ID是否正常工作编丘,你可以把它放到ChecklistItem的標(biāo)簽中展示出來(lái)看与学,下面的代碼僅僅是用做測(cè)試彤悔,因?yàn)檫@些內(nèi)部的ID號(hào)沒(méi)有必要展示給用戶看。

打開(kāi)ChecklistViewController.swift癣防,改動(dòng)一下configureText(for:with:)方法:

func configureText(for cell: UITableViewCell,with item: ChecklistItem) {
        let lable = cell.viewWithTag(1000) as! UILabel
        //lable.text = item.text
        lable.text = "\(item.itemID):\(item.text)"
    }

把原來(lái)的那一行注釋掉蜗巧,不要?jiǎng)h掉,因?yàn)槟阋粫?huì)還要改回來(lái)蕾盯。

在重新運(yùn)行app之前幕屹,一定要重置模擬器,并且把Checklist.plist文件刪掉级遭,因?yàn)槲覀兊臄?shù)據(jù)模型已經(jīng)變了望拖,舊的文件結(jié)構(gòu)會(huì)導(dǎo)致app崩潰掉。

運(yùn)行app挫鸽,并且添加幾條待辦事項(xiàng)说敏,每一個(gè)都會(huì)得到一個(gè)唯一的ID,使用Home鍵回到iOS的主界面丢郊,然后中斷掉app盔沫,然后再次運(yùn)行app。然后在新增幾條待辦事項(xiàng)枫匾,你會(huì)看到它們的編號(hào)和之前的是連續(xù)的架诞。

效果圖

OK,ID們工作的很好「绍裕現(xiàn)在我們來(lái)添加“due date”和“should remind”到Add/Edit Item界面谴忧。

先不要把configureText(for:with:)改回去,我們還要繼續(xù)用它做測(cè)試角虫。

打開(kāi)ItemDetailViewController.swift沾谓,添加兩個(gè)outlet:

@IBOutlet weak var shouldRemindSwitch: UISwitch!
@IBOutlet weak var dueDateLable: UILabel!

打開(kāi)故事模版,選擇Item Detail View Controller中的table view(名字為Add Item的那個(gè))

為這個(gè)table新增一個(gè)分節(jié)戳鹅,這非常簡(jiǎn)單均驶,打開(kāi)屬性檢查器然后將Section字段設(shè)置為2就可以了。這樣會(huì)復(fù)制一個(gè)已存在的cell過(guò)去枫虏。

刪除這個(gè)新的cell中的Text Field辣恋。拖拽一個(gè)新的Table View Cell到這個(gè)新的cell的下面,這個(gè)這個(gè)新增的分節(jié)就有兩個(gè)cell了模软。

最終我們完成設(shè)計(jì)時(shí)伟骨,界面會(huì)是這個(gè)樣子:

拖拽一個(gè)Lable到第一個(gè)cell的左邊,輸入文本Remind Me燃异,設(shè)置字體為System 17携狭。

在拖拽一個(gè)Switch到這個(gè)cell的右邊。將這個(gè)Switch和shouldRemindSwitch連接起來(lái)回俐,然后在它的屬性檢查器中將Value設(shè)置為off逛腿,這樣它的初始狀態(tài)就是關(guān)閉的了稀并,開(kāi)關(guān)會(huì)由綠色變?yōu)榛疑?/p>

將這個(gè)Switch的頂部及右側(cè)固定起來(lái)(使用Pin菜單),這樣就保證了這個(gè)控件可以匹配所有的設(shè)備大小单默。

下面的一個(gè)cell應(yīng)該具備兩個(gè)標(biāo)簽:左邊的標(biāo)簽負(fù)責(zé)獲取并且顯示用戶選擇的時(shí)間碘举,右邊的負(fù)責(zé)選擇時(shí)間。實(shí)際上你不需要去拖拽兩個(gè)標(biāo)簽上去搁廓,僅僅是將這個(gè)cell的風(fēng)格修改為Right Detail引颈,然后將標(biāo)簽重命名為Due Date就可以了。

右邊的那個(gè)標(biāo)簽應(yīng)該和dueDateLabel outlet連接起來(lái)境蜕。(這個(gè)標(biāo)簽比較難以選中蝙场,你需要多點(diǎn)幾次試試)

你還需要將Remind Me標(biāo)簽以及Switch的位置移動(dòng)一下,讓它倆和下面的兩個(gè)標(biāo)簽保持左對(duì)齊粱年,你選擇下面的標(biāo)簽售滤,打開(kāi)尺寸檢查器,看看它們的x值是多少台诗,然后把Remind Me標(biāo)簽和Switch的x值設(shè)置為一致就可以了完箩,用不著拖來(lái)拖去的微操。

下面進(jìn)入代碼部分:

打開(kāi)ItemDetailViewController.swift拉队,添加一個(gè)新的實(shí)例變量dueDate:

var dueDate = Date()

對(duì)于每一個(gè)新的ChecklistItem弊知,due date都應(yīng)該默認(rèn)當(dāng)前時(shí)間。但是假如用戶選擇了時(shí)間氏仗,那么就要立刻把當(dāng)前時(shí)間替換掉吉捶。

這里還有一些其他選擇夺鲜,比如默認(rèn)時(shí)間設(shè)置為明天或者10分鐘以后皆尔,但是實(shí)際上,用戶基本上都會(huì)立即選擇時(shí)間币励,所以對(duì)于默認(rèn)時(shí)間不需要做太多考慮慷蠕。

改動(dòng)一下viewDidLoad():

override func viewDidLoad() {
        super.viewDidLoad()
        
        if let item = itemToEdit {
            title = "Edit Item"
            textField.text =  item.text
            doneBarButton.isEnabled = true
            shouldRemindSwitch.isOn = item.shouldRemind  //新增這一行
            dueDate = item.dueDate   //新增這一行
        }
        updateDueDateLabel()   //新增這一行
    }

對(duì)于已經(jīng)存在的ChecklistItem對(duì)象,你設(shè)置switch的狀態(tài)需要使用這個(gè)對(duì)象的shouldRemind屬性食呻,如果是新增的流炕,那么初始狀態(tài)默認(rèn)為off,我們?cè)诠适履0嬷凶隽嗽O(shè)置仅胞。

你同時(shí)還從ChecklistItem中獲取了due date每辟。

這個(gè)updateDueDateLabel()是個(gè)新的方法,我們現(xiàn)在把它添加上:

func updateDueDateLabel() {
        let formtter = DateFormatter()
        formtter.dateStyle = .medium
        formtter.timeStyle = .short
        dueDateLable.text = formtter.string(from: dueDate)
    }

你使用DateFormatter來(lái)將日期轉(zhuǎn)換為文本干旧。

它的工作原理非常明顯:你給它的date部分設(shè)置了一個(gè)風(fēng)格渠欺,time部分設(shè)置了另外一個(gè)風(fēng)格,并且從中獲得格式化好的Date對(duì)象椎眯。

你可以試試其他類型的風(fēng)格挠将,但是由于label的尺寸有點(diǎn)小胳岂,所以也看不出什么效果。

DateFormatter最酷的地方是它返回的是當(dāng)?shù)貢r(shí)間舔稀,不管你在地球上的那個(gè)地方乳丰,DateFormatter都是返回你所在地的當(dāng)?shù)貢r(shí)間。

最后一件事情就是修改done方法:

@IBAction func done() {
        if let item = itemToEdit {
            item.text = textField.text!
            
            item.shouldRemind = shouldRemindSwitch.isOn //新增這一行
            item.dueDate = dueDate  //新增這一行
            
            delegate?.itemDetailViewController(self, didFinishEditing: item)
        } else {
        let item = ChecklistItem()
        item.text = textField.text!
        item.checked = false
        
            item.shouldRemind = shouldRemindSwitch.isOn //新增這一行
            item.dueDate = dueDate  //新增這一行
            
        delegate?.itemDetailViewController(self, didFinishAdding: item)
        }
    }

當(dāng)用戶點(diǎn)擊done按鈕的時(shí)候你將switch和due實(shí)例變量的值返回給ChecklistItem對(duì)象内贮。

運(yùn)行app产园,改變開(kāi)關(guān)的狀態(tài)。app在中斷后也會(huì)記得開(kāi)關(guān)的最終狀態(tài)(記得先退回主界面再中斷app)

due date還沒(méi)有生效贺归,想要讓它工作淆两,你必須先創(chuàng)建一個(gè)時(shí)間選擇器。

??:你也許想知道為什么你對(duì)dueDate使用了一個(gè)實(shí)例變量拂酣,而shouldRemind沒(méi)有秋冰。
因?yàn)椴⒉恍枰@樣做,你可以輕易的從switch控件中得到它的狀態(tài)值婶熬,通過(guò)isON屬性剑勾,這個(gè)屬性返回值也是true和false。
然而赵颅,從dueDateLabel中將時(shí)間讀取出來(lái)就沒(méi)那么容易了虽另,因?yàn)檫@個(gè)label存儲(chǔ)的文本是String型的,不是Date饺谬。所以我們用了一個(gè)實(shí)例變量來(lái)跟蹤日期的值捂刺。

時(shí)間選擇器

時(shí)間選擇器(date picker)對(duì)我們而言并不是什么新的視圖控制器。我們要實(shí)現(xiàn)的效果是募寨,點(diǎn)擊Due Date這一行自動(dòng)在table view中插入一個(gè)UIDatePicker組件族展,日歷型的app通常就具備這一功能。

時(shí)間選擇器

打開(kāi)ItemDetailViewController.swift拔鹰,添加一個(gè)新的實(shí)例變量來(lái)跟蹤時(shí)間選擇器是否可見(jiàn):

var datePickerVisible = false

并且添加showDatePicker()方法:

func showDatePicker() {
        datePickerVisible = true
        let indexPathDatePicker = IndexPath(row: 2, section: 1)
        tableView.insertRows(at: [indexPathDatePicker], with: .fade)
    }

這里將剛添加的實(shí)例變量設(shè)置為true仪缸,并且告訴table view插入一個(gè)新行到Due Date這一行下面。這個(gè)新插入到行將用來(lái)容納UIDatePicker列肢。

問(wèn)題是:用于date picker這一行的cell從哪來(lái)恰画?你不能像靜態(tài)cell那樣直接把它放入table view。因?yàn)檫@樣就會(huì)使它總是可見(jiàn)瓷马。而你僅僅想要用戶點(diǎn)擊Due Date這一行后它才顯示拴还。

Xcode有一個(gè)非常酷的功能可以使你添加附加視圖到場(chǎng)景中欧聘,而并不立即顯示它們片林。這是我們解決這個(gè)問(wèn)題的不二之選。

打開(kāi)故事模版找到Add Item界面。拖拽一個(gè)table view cell拇厢,不要把它拖拽到視圖控制器里面爱谁,而是拖拽到頂部的dock里,見(jiàn)下圖:

拖拽完畢后孝偎,故事模版看起來(lái)會(huì)是這個(gè)樣子:

這個(gè)新的table view cell對(duì)象屬于這個(gè)場(chǎng)景访敌,但是它還不是這個(gè)場(chǎng)景的table view的一部分。

這個(gè)cell也有點(diǎn)小衣盾,不足以容納一個(gè)date picker寺旺,所以首先我們來(lái)把它弄大點(diǎn)。

選擇這個(gè)table view cell打開(kāi)尺寸檢查器势决,設(shè)置Height為217阻塑,date picker的高是216,所以我們要設(shè)置高一個(gè)點(diǎn)位果复,在頂部留一點(diǎn)空隙陈莽,否則會(huì)非常難看。

然后打開(kāi)屬性檢查器虽抄,設(shè)置Selection為None走搁,這樣就使cell在你點(diǎn)擊它的時(shí)候不會(huì)變灰。

然后拖拽一個(gè)date picker到這個(gè)cell中迈窟,它應(yīng)該剛好可以容納進(jìn)去私植。

使用Pin菜單將date picker的四條邊都固定好。注意不要勾選Constrain to margins復(fù)選框车酣。

當(dāng)你完成后曲稼,新的cell看起來(lái)應(yīng)該是這個(gè)樣子的:

那么你如何將這個(gè)cell放入table view中呢?首先湖员,做兩個(gè)個(gè)新的outlets并且把它們分別和date picker與cell連接起來(lái)贫悄,這樣你就可以在代碼中引用這兩個(gè)視圖了。

打開(kāi)ItemDetailViewController.swift破衔,添加以下代碼:

@IBOutlet weak var datePickerCell: UITableViewCell!
@IBOutlet weak var datePicker: UIDatePicker!

回到故事模版清女,注意一下頂部的dock欄钱烟,上面有一個(gè)黃色圓圈的圖標(biāo)晰筛,它就代表這個(gè)視圖控制器。

按住ctrl從這個(gè)黃色圓圈圖標(biāo)拉線到灰色的那個(gè)代表table view cell的圖標(biāo)拴袭,然后選擇datePickerCell outlet:

然后還是按住ctrl從這個(gè)黃色圓圈圖標(biāo)到Date Picker上读第,之后選擇datePicker就完成了date picker的連接。

非常棒拥刻,現(xiàn)在你完成了cell和date picker的連接怜瞒,你可以通過(guò)寫點(diǎn)代碼,把它們添加到table view上了。

通常你會(huì)執(zhí)行tableView(cellForRowAt)方法吴汪,但是記住惠窄,這是用于靜態(tài)cell的情況。像我們這種情況下漾橙,不存在數(shù)據(jù)源杆融,所以也就不存在cellForRowAt。

如果你觀察下ItemDetailViewController.swift霜运,你不會(huì)看到有這個(gè)方法存在脾歇。通過(guò)一些列手段,你可以為靜態(tài)的table view重寫數(shù)據(jù)源淘捡,并且提供你自己寫的方法藕各。

我們這樣在ItemDetailViewController.swift中添加cellForRowAt:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if indexPath.section == 1 && indexPath.row == 2 {
            return datePickerCell
        } else {
            return super.tableView(tableView, cellForRowAt: indexPath)
        }
    }

注意:你不能對(duì)它進(jìn)行太多的操作,當(dāng)它由一個(gè)靜態(tài)table view使用時(shí)焦除,因?yàn)樗苍S會(huì)影響這些靜態(tài)cell的內(nèi)部工作方式激况。但是如果你足夠小心的話,你可以避免它膘魄。

這個(gè)if語(yǔ)句檢查cellForRowAt是否被date picker的indexPath調(diào)用誉碴。如果是,它返回你剛設(shè)計(jì)的datePickerCell瓣距。這樣操作是安全的黔帕,因?yàn)檫@個(gè)table view對(duì)row 2,section 1毫不知情蹈丸,所以你不會(huì)影響到已存在的靜態(tài)cell成黄。

對(duì)于其他任何不是date picker cell的行,這個(gè)方法會(huì)調(diào)用super.tableView(tableView, cellForRowAt: indexPath)逻杖,通過(guò)這種手段來(lái)保證其他的靜態(tài)cell正常工作奋岁。

你還需要重寫tableView(numberOfRowsInSection):

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if section == 1 && datePickerVisible {
            return 3
        } else {
            return super.tableView(tableView, numberOfRowsInSection: section)
        }
    }

如果date picker可見(jiàn),那么section 1就有三行荸百,如果不可見(jiàn)闻伶,則僅返回原始的數(shù)據(jù)源。

同樣的够话,我們來(lái)重寫tableView(heightForRowAt)方法:

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if indexPath.section == 1 && indexPath.row == 2 {
            return 217
        } else {
            return super.tableView(tableView, heightForRowAt: indexPath)
        }
    }

到目前為止你的table view中的cell都是同樣的高度蓝翰,都是44,但是改變它并不難女嘲,你可以通過(guò)“heightForRowAt”來(lái)控制每個(gè)cell的高度畜份。

如果是date picker所屬的cell的話,我們?cè)O(shè)置它的高為217欣尼。

date picker僅在用戶點(diǎn)擊due date這一行的cell時(shí)才顯示爆雹,我們來(lái)添加相關(guān)的代碼:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        textField.resignFirstResponder()
        
        if indexPath.section == 1 && indexPath.row == 1 {
            showDatePicker()
        }
    }

當(dāng)due date這一行被點(diǎn)擊后調(diào)用showDatePicker(),如果此時(shí)界面上有虛擬小鍵盤的話,也會(huì)被自動(dòng)隱藏掉。

此時(shí)钙态,你已經(jīng)完成了大部分工作慧起,但是due date這一行現(xiàn)在實(shí)際上并不能被點(diǎn)擊,這是因?yàn)镮temDetailViewController.swift中已經(jīng)存在了一個(gè)“willSelectRowAt”方法册倒,它總是返回nil完慧,所以點(diǎn)擊會(huì)被忽視掉。

我們來(lái)改動(dòng)一下tableView(willSelectRowAt):

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

現(xiàn)在due date這一行會(huì)被選中了剩失,而其他行不會(huì)屈尼。

運(yùn)行app,試試效果拴孤。添加一個(gè)新的待辦事項(xiàng)脾歧,并且點(diǎn)擊due date這一行。

不出意外的話你會(huì)發(fā)現(xiàn)app掛了演熟,如果沒(méi)有掛的話鞭执,那就真的很意外了,通過(guò)一些調(diào)查我發(fā)現(xiàn)芒粹,當(dāng)你為靜態(tài)table view重寫了數(shù)據(jù)源后兄纺,你還需要提供委托方法:tableView(indentationLevelForRowAt)

這不是你經(jīng)常使用的一個(gè)方法,但是因?yàn)槟銊?dòng)了用于靜態(tài)table view的數(shù)據(jù)源化漆,所以你必須重寫它估脆。我早就告訴過(guò)你(其實(shí)并沒(méi)有)。

添加tableView(indentationLevelForRowAt)方法:

override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
        var newIndexPath = indexPath
        if indexPath.section == 1 && indexPath.row == 2 {
            newIndexPath = IndexPath(row: 0,section: indexPath.section)
        }
        return super.tableView(tableView, indentationLevelForRowAt: newIndexPath)
    }

app會(huì)因?yàn)檫@個(gè)方法掛掉的原因是標(biāo)準(zhǔn)的數(shù)據(jù)源對(duì)section 1座云,row2的cell(就是date picker所屬的cell)毫不知情疙赠,甚至不知道它的存在,因?yàn)檫@個(gè)cell在設(shè)計(jì)時(shí)并不屬于這個(gè)table view朦拖。

所以在插入date picker所屬的cell后圃阳,數(shù)據(jù)源表示我沒(méi)見(jiàn)過(guò)它,所以app就躺槍了璧帝。為了克服這個(gè)問(wèn)題捍岳,你需要在date picker顯示時(shí)欺騙數(shù)據(jù)源,使它確信這一行真的存在睬隶。這就是indentationLevelForRowAt這個(gè)方法的作用锣夹。

運(yùn)行app,這一次點(diǎn)擊due date后理疙,可以正常顯示出date picker了晕城。

當(dāng)你選擇date picker中的時(shí)間的時(shí)候泞坦,選擇的結(jié)果應(yīng)該反饋在Due Date這一行中窖贤,但是現(xiàn)在并沒(méi)有起到這個(gè)效果。

我們需要監(jiān)聽(tīng)date picker的值的改變事件。無(wú)論何時(shí)赃梧,當(dāng)date picker上的滾輪被轉(zhuǎn)動(dòng)時(shí)都必須觸發(fā)這一事件滤蝠。為了實(shí)現(xiàn)這個(gè)需求,你需要添加一個(gè)新的方法授嘀。

打開(kāi)ItemDetailViewController.swift物咳,添加這個(gè)方法:

@IBAction func dateChanged(_ datePicker: UIDatePicker) {
        dueDate = datePicker.date
        updateDueDateLabel()
    }

這非常簡(jiǎn)單。它使用date picker的時(shí)間來(lái)更新dueDate蹄皱,然后更新Due Date這一行的標(biāo)簽览闰。

打開(kāi)故事模版,按住ctrl拖拽Date Picker到視圖控制器巷折,并且選擇dateChanged動(dòng)作方法⊙辜現(xiàn)在所有的連接都完成了。

你一定要確認(rèn)這個(gè)動(dòng)作方法連接的是date picker的Value Changed事件锻拘∮涂裕可以通過(guò)查看鏈接檢查器來(lái)確認(rèn)。

運(yùn)行app署拟,試試效果婉宰。當(dāng)你轉(zhuǎn)動(dòng)date picker上的滾輪時(shí),Due Date中的標(biāo)簽也會(huì)隨著變化推穷。

然而心包,當(dāng)你編輯一條已存在的待辦事項(xiàng)的時(shí)候,data picker總是顯示當(dāng)前時(shí)間馒铃。

在showDatePicker()方法的底部添加一行:

datePicker.setDate(dueDate, animated: false)

這樣就給了UIDatePicker組件一個(gè)合適的時(shí)間谴咸。

確認(rèn)一下它是否按照我們的意圖工作,編輯一條已存在的待辦事項(xiàng)骗露,最好用已經(jīng)設(shè)置過(guò)due date的岭佳,確認(rèn)一下date picker上的時(shí)間和due date標(biāo)簽上的時(shí)間一致。

當(dāng)date picker可見(jiàn)的時(shí)候如果Due Date上的標(biāo)簽?zāi)軌蚋吡溜@示萧锉,那么久太棒了珊随。你可以使用tint color來(lái)實(shí)現(xiàn)這一目的(這也是日歷型app常見(jiàn)的功能)

再改一次showDatePicker:

func showDatePicker() {
        datePickerVisible = true
        let indexPathDateRow = IndexPath(row: 1, section: 1)
        let indexPathDatePicker = IndexPath(row: 2, section: 1)
        
        if let dateCell = tableView.cellForRow(at: indexPathDateRow) {
            dateCell.detailTextLabel!.textColor = dateCell.detailTextLabel!.tintColor
        }
        
        tableView.beginUpdates()
        tableView.insertRows(at: [indexPathDatePicker], with: .fade)
        tableView.reloadRows(at: [indexPathDateRow], with: .none)
        tableView.endUpdates()
        
        datePicker.setDate(dueDate, animated: false)
    }

這樣就將detailTextLabel的顏色設(shè)置為了tint color。它同時(shí)也告訴table view需要重新加載Due Date這一行柿隙。但是cell之間的間隔線沒(méi)有被更新叶洞。

因?yàn)槟阍谕粫r(shí)間對(duì)這個(gè)table view進(jìn)行了兩種操作,插入一個(gè)新行并且重新加載另一個(gè)禀崖,你需要把它們放到叫做beginUpdates()和 endUpdates()的東西之間衩辟,這樣就可以同時(shí)更新所有東西了。

運(yùn)行app波附,現(xiàn)在日期是淺藍(lán)色了艺晴。

當(dāng)用戶再次點(diǎn)擊Due Date這一行時(shí)扮超,date picker應(yīng)該自動(dòng)消失掉星持。如果你現(xiàn)在這樣做的話app就會(huì)掛掉,這樣肯定不會(huì)為你在app store中帶來(lái)太多好評(píng)。

添加一個(gè)新的方法:

func hideDatePicker() {
        if datePickerVisible {
            datePickerVisible = false
            
            let indexPathDateRow = IndexPath(row: 1, section: 1)
            let indexPathDatePicker = IndexPath(row: 2, section: 1)
            
            if let cell = tableView.cellForRow(at: indexPathDateRow) {
                cell.detailTextLabel!.textColor = UIColor(white: 0, alpha: 0.5)
            }
            
            tableView.beginUpdates()
            tableView.reloadRows(at: [indexPathDateRow], with: .none)
            tableView.deleteRows(at: [indexPathDatePicker], with: .fade)
            tableView.endUpdates()
        }
    }

這個(gè)方法的作用和showDatePicker()却音。它從table view中刪除了date picker cell并且將date label的顏色恢復(fù)為灰色康栈。

改變一下tableView(didSelectRowAt)來(lái)觸發(fā)顯示和隱藏狀態(tài):

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        textField.resignFirstResponder()
        
        if indexPath.section == 1 && indexPath.row == 1 {
            if !datePickerVisible {
                showDatePicker()
            } else {
                hideDatePicker()
            }
        }
    }

還存在一種情況煤裙,需要我們把date picker隱藏起來(lái):當(dāng)用戶點(diǎn)擊text field的時(shí)候安拟。

如果虛擬鍵盤和時(shí)間選擇器重疊在一起的話,會(huì)非常難看抖锥,所以你最好還是把時(shí)間選擇器隱藏起來(lái)亿眠。這個(gè)視圖控制器已經(jīng)是text field的委托了,我們處理起來(lái)會(huì)非常簡(jiǎn)單磅废。

添加textFieldDidBeginEditing()方法:

func textFieldDidEndEditing(_ textField: UITextField) {
        hideDatePicker()
    }

這樣就非常完美了缕探。

運(yùn)行app并且確認(rèn)是否一切工作正常。

安排本地通知

經(jīng)過(guò)這么漫長(zhǎng)的插曲还蹲,希望大家不要忘了爹耗,我們最終的目的是安排本地通知。

面向?qū)ο缶幊痰囊粋€(gè)原則是谜喊,對(duì)象可以盡可能的利用自己潭兽。因此,讓ChecklistItem對(duì)象來(lái)安排它自己的通知斗遏。

打開(kāi)ChecklistItem.swift:

func scheduleNotification() {
        if shouldRemind && dueDate > Date() {
            print("We should schedule a notification")
        }
    }

這里我們對(duì)比了due date和當(dāng)前時(shí)間山卦。你可以通過(guò)使用Date對(duì)象來(lái)獲得當(dāng)前時(shí)間。

語(yǔ)句dueDate > Date() 比較兩個(gè)時(shí)間后返回true和false诵次。

如果返回false的話账蓉,則print不會(huì)執(zhí)行。

注意一下這個(gè)“&&”符號(hào)逾一,表示“與”铸本,只有當(dāng)Remind Me被設(shè)置為on,且due date大于Date()時(shí)遵堵,print才被執(zhí)行箱玷。

當(dāng)用戶新增或者編輯完一條待辦事項(xiàng)后,點(diǎn)擊Done按鈕時(shí)陌宿,你調(diào)用這個(gè)方法锡足。

打開(kāi)ItemDetailViewController.swift,在didFinishEditing和didFinishaAdding前面添加一行:

item.scheduleNotification()

運(yùn)行app,試試效果壳坪。添加一條新的待辦事項(xiàng)舶得,將開(kāi)關(guān)狀態(tài)設(shè)置為on,不要改變due date爽蝴。然后點(diǎn)擊Done沐批。

這時(shí)在調(diào)試區(qū)域應(yīng)該沒(méi)有打印出消息纫骑,因?yàn)閐ue date小于當(dāng)前時(shí)間(當(dāng)你點(diǎn)擊Done按鈕的時(shí)候已經(jīng)有幾秒過(guò)去了)

再添加一條待辦事項(xiàng),將switch設(shè)置為on珠插,并且選一個(gè)幾分鐘后的due date惧磺。

然后點(diǎn)擊Done按鈕颖对,這時(shí)調(diào)試區(qū)域應(yīng)該打印出一條消息“We should schedule a notification”

現(xiàn)在你可以確認(rèn)這個(gè)方法確實(shí)被調(diào)用了捻撑,我們來(lái)實(shí)際的把本地消息添加進(jìn)去。首先考慮新增待辦事項(xiàng)的情況缤底。

打開(kāi)ChecklistItem.swift,將scheduleNotification()修改為:

    func scheduleNotification() {
        if shouldRemind && dueDate > Date() {
            //1
            let content = UNMutableNotificationContent()
            content.title = "Reminder"
            content.body = text
            content.sound = UNNotificationSound.default()
            //2
            let calender = Calendar(identifier: .gregorian)
            let components = calender.dateComponents([.month,.day,.hour,.minute], from: dueDate)
            //3
            let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
            //4
            let request = UNNotificationRequest(identifier: "\(itemID)", content: content, trigger: trigger)
            //5
            let center = UNUserNotificationCenter.current()
            center.add(request)
            
            print("We should schedule a notification")
        }
    }

你在第一次調(diào)試本地通知的時(shí)候應(yīng)該見(jiàn)過(guò)這些代碼顾患,但是這里有些不同。

1个唧、將item的文本放入通知中

2江解、從dueDate中提取月、日徙歼、小時(shí)和分鐘犁河。我們不關(guān)心年和秒。

3魄梯、之前你用UNTimeIntervalNotificationTrigger來(lái)測(cè)試本地消息桨螺,但是現(xiàn)在這里,你使用它來(lái)展示詳細(xì)的時(shí)間酿秸。

4灭翔、創(chuàng)建UNNotificationRequest對(duì)象。這里比較重要的是辣苏,我們把待辦事項(xiàng)的ID轉(zhuǎn)換為String型肝箱,并且使用它來(lái)確定通知。假如你之后需要取消這條消息的話稀蟋,就可以用這個(gè)標(biāo)示找到它煌张。

5、添加新的通知到UNUserNotificationCenter退客。

唯一的問(wèn)題就是唱矛,Xcode給出了一大堆報(bào)錯(cuò)。

出什么事了呢井辜?ChecklistItem還沒(méi)有導(dǎo)入本地消息的框架绎谦,現(xiàn)在它只有NSObject、NSCoder和Foundation框架粥脚。

導(dǎo)入框架非常簡(jiǎn)單:

import UserNotifications

這樣就可以了窃肠。

這里還有另外一個(gè)小問(wèn)題。如果你重置過(guò)模擬器刷允,那么此時(shí)app就不再被允許發(fā)送本地通知冤留。

你不能假定app總是被允許發(fā)送通知消息的碧囊。最初你測(cè)試的時(shí)候,是將請(qǐng)求許可的代碼放入了AppDelegate中纤怒,但是現(xiàn)在不行了糯而,也不推薦這樣做。

因?yàn)槟惚救丝隙ㄓ憛捘切?qiáng)制的消息泊窘,這種app一點(diǎn)都不受歡迎熄驼,我們讓自己的app變得美好一些。

打開(kāi)ItemDetailViewController.swift,添加以下方法進(jìn)去:

@IBAction func shouldRemindToggled(_ switchControl: UISwitch) {
        textField.resignFirstResponder()
        
        if switchControl.isOn {
            let center = UNUserNotificationCenter.current()
            center.requestAuthorization(options: [.alert,.sound], completionHandler: {
                granted ,error in /*do nothing*/
            })
        }
    }

當(dāng)switch設(shè)置為on時(shí)烘豹,會(huì)自動(dòng)提示用戶允許通知消息瓜贾,一旦用戶給予了許可 ,app不會(huì)再次請(qǐng)求許可了就携悯。

同時(shí)記得添加import UserNotifications祭芦。導(dǎo)入U(xiǎn)serNotifications。

運(yùn)行app憔鬼,新增一個(gè)待辦事項(xiàng)龟劲,設(shè)置due date到幾分鐘后,點(diǎn)擊Done按鈕轴或,并且會(huì)到iOS主界面昌跌。

你就可以看到本地通知已經(jīng)生效了:

現(xiàn)在新增部分已經(jīng)實(shí)現(xiàn)了,還剩下幾個(gè)情況侮叮,1避矢、用戶編輯待辦事項(xiàng)時(shí),2囊榜、用戶刪除待辦事項(xiàng)時(shí)审胸。

我們先來(lái)做編輯部分,當(dāng)用戶編輯待辦事項(xiàng)時(shí)卸勺,會(huì)發(fā)生以下情況:

1砂沛、Remind Me曾經(jīng)是off,現(xiàn)在被設(shè)置為on曙求。你需要安排一條通知

2碍庵、Remind Me曾經(jīng)時(shí)on,現(xiàn)在被設(shè)置為off悟狱,你要取消掉已存在的通知

3静浴、Remind Me保持為on,但是due date改變了挤渐,你需要取消舊的通知苹享,安排新的通知。

4浴麻、沒(méi)有任何改變得问,你不需要做任何事囤攀。

5、Remind Me保持為off宫纬,也不用做任何事焚挠。

當(dāng)然,上面所有情況中漓骚,都必須due date大于當(dāng)前時(shí)間才安排通知消息蝌衔。

好長(zhǎng)的一個(gè)列表啊。在編程前认境,把所有的可能性列出來(lái)胚委,是一個(gè)非常好的習(xí)慣挟鸠。

看起來(lái)你要寫非常多的代碼了叉信,但是實(shí)際上非常簡(jiǎn)單。

首先艘希,你觀察這里是否已經(jīng)存在一條消息硼身。如果有,你簡(jiǎn)單的把它取消掉覆享。然后判斷是否需要安排一條新的佳遂。

這樣就可以處理上面的所有情況了,甚至有時(shí)候僅僅把已經(jīng)存在的通知保留下來(lái)就可以了撒顿。算法有點(diǎn)粗糙丑罪,但是很有效。

打開(kāi)ChecklistItem.swift,添加以下方法:

func removeNotification() {
        let center = UNUserNotificationCenter.current()
        center.removePendingNotificationRequests(withIdentifiers: ["\(itemID)"])
    }

這個(gè)方法的作用是移除已存在的某條待辦事項(xiàng)的通知安排凤壁,注意一下removePendingNotificationRequests()要求一個(gè)數(shù)組作為標(biāo)示吩屹,所以你把(itemID)放入一對(duì)方括號(hào)中。

在scheduleNotification()的頂部調(diào)用這個(gè)方法:

func scheduleNotification() {
        
        removeNotification()

...

運(yùn)行app拧抖,添加一個(gè)待辦事項(xiàng)煤搜,并且將due date設(shè)置到兩分鐘后。一條新的通知就被安排上了唧席。會(huì)到主界面等待它的出現(xiàn)擦盾。

編輯待辦事項(xiàng)并且改變due date,到三分鐘或者4分鐘后淌哟,這樣舊的消息就被取消了迹卢,然后根據(jù)新的時(shí)間安排了一條新的消息。

添加一條新的待辦事項(xiàng)徒仓,然后把switch設(shè)置為off腐碱,舊的消息會(huì)被取消,并且不會(huì)安排新的消息蓬衡。

再次編輯上面哪條待辦事項(xiàng)喻杈,改變一下時(shí)間彤枢,不要?jiǎng)悠渌模€是不會(huì)被安排消息筒饰。

我們還有最后一種情況要處理缴啡,就是刪除待辦事項(xiàng),有兩種情況需要考慮:

1瓷们、用戶通過(guò)滑動(dòng)的方式刪除某一條待辦事項(xiàng)

2业栅、用戶刪除了整個(gè)待辦事項(xiàng)分類的目錄

當(dāng)刪除發(fā)生時(shí),有一個(gè)方法會(huì)被告知這件事谬晕。你可以簡(jiǎn)單的執(zhí)行這個(gè)方法碘裕,然后看看有沒(méi)有安排消息通知,有的話就取消掉攒钳。

打開(kāi)ChecklistItem.swift帮孔,添加以下方法:

deinit {
        removeNotification()
    }

所有的工作都做完了。這個(gè)特殊的deinit方法會(huì)在刪除某一條待辦事項(xiàng)以及刪除整個(gè)目錄的時(shí)候被調(diào)用不撑。

運(yùn)行app文兢,測(cè)試一下各種情況。如果一切正常的話焕檬,就把代碼里的print語(yǔ)句都刪掉姆坚。雖然不刪也沒(méi)什么關(guān)系,用戶是看不到它們的实愚,但是我們所做的一切都是為了代碼的簡(jiǎn)潔兼呵。

同時(shí)也把item ID從ChecklistViewController的label中移除,這僅僅是為了測(cè)試使用的腊敲。

好累啊

我們從設(shè)計(jì)草圖開(kāi)始击喂,一直到完整的完成了一個(gè)app。我們接觸了許多高級(jí)的課題兔仰,希望你能跟的上思路茫负,明白我們是在做什么。你堅(jiān)持到了現(xiàn)在乎赴,我非常為你感到驕傲忍法。

如果你對(duì)其中的一些細(xì)節(jié)迷惑不解,那是正常的榕吼,沒(méi)有關(guān)系饿序。睡一覺(jué),然后重新在看一遍羹蚣。編程需要你去思考原探,但是并不需要你通宵達(dá)旦的和它在一起。不要害怕重頭再來(lái)一遍。要記住咽弦,溫故而知新徒蟆。

本課程聚焦于UIKit,以及其中的重要控件和模式型型。在下一節(jié)課段审,我們會(huì)先花點(diǎn)時(shí)間將將swift語(yǔ)言。當(dāng)然闹蒜,你還會(huì)再和我一起做一個(gè)更酷的app寺枉。

最終我們的故事模版是這個(gè)樣子的:

看起來(lái)很壯觀吧。

你得到了應(yīng)得的回報(bào)绷落,當(dāng)你準(zhǔn)備好開(kāi)始下一課前姥闪,好好休息一下吧,我也休息一下砌烁,有兩段結(jié)束語(yǔ)不翻譯了筐喳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市往弓,隨后出現(xiàn)的幾起案子疏唾,更是在濱河造成了極大的恐慌蓄氧,老刑警劉巖函似,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異喉童,居然都是意外死亡撇寞,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門堂氯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蔑担,“玉大人,你說(shuō)我怎么就攤上這事咽白∑∥眨” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵晶框,是天一觀的道長(zhǎng)排抬。 經(jīng)常有香客問(wèn)我,道長(zhǎng)授段,這世上最難降的妖魔是什么蹲蒲? 我笑而不...
    開(kāi)封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮侵贵,結(jié)果婚禮上届搁,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好卡睦,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布宴胧。 她就那樣靜靜地躺著,像睡著了一般表锻。 火紅的嫁衣襯著肌膚如雪牺汤。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天浩嫌,我揣著相機(jī)與錄音檐迟,去河邊找鬼。 笑死码耐,一個(gè)胖子當(dāng)著我的面吹牛追迟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播骚腥,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼敦间,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了束铭?” 一聲冷哼從身側(cè)響起廓块,我...
    開(kāi)封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎契沫,沒(méi)想到半個(gè)月后带猴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡懈万,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年拴清,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片会通。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡口予,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涕侈,到底是詐尸還是另有隱情沪停,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布裳涛,位于F島的核電站木张,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏调违。R本人自食惡果不足惜窟哺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望技肩。 院中可真熱鬧且轨,春花似錦浮声、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至至朗,卻和暖如春屉符,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锹引。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工矗钟, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嫌变。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓吨艇,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親腾啥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子东涡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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