天天看點

在iOS8建立一個互動性強的本地通知

通知(Notifications),是App用來和使用者交流的一種方式,特别是當App并沒有在前台運作的時候。通知,正如它的名稱所強調的,被用作向使用者‘通知’一個事件,或者僅僅向使用者提示一條重要資訊。總而言之,通知在提示類型的App當中非常有用,甚至在一些别的類型的App當中也是如此。比如,當使用者進入一個指定區域(這是iOS8的新特性),一個下載下傳任務完成,或者當朋友給你發送一條資訊的時候,一條通知就可以被顯示出來。無論如何,通知的目的就是獲得使用者的關注,然後他們就能處理通知了。

從程式設計的角度來說,通知有着一套相當标準的API,可以非常簡單地被實作。不需要太多的腦力,開發者可以根據文檔輕松的在App中加入通知功能。也就是說,詳細規定由系統轉發的通知的内容,在App啟動時處理通知,最後,從iOS8開始,處理任何由通知指定的動作(actions)。每一個App唯一改變的隻有業務邏輯而已。

在iOS8建立一個互動性強的本地通知

從iOS8開始,本質上來說有兩種通知:

  1. 本地通知(local notifications):由開發者定義,App觸發。觸發的時間是被事先安排好的。
  2. 遠端通知(remote notifications):這種情況下,通知可以被分成兩個類别:(a)推送通知(The push notifications),被伺服器初始化,然後通過APNS,最終到達使用者裝置。(b)靜默通知(The silent notifications),其實也是推送通知,但是他們并沒有被展示給使用者,而是立即被App處理以發起某項任務,最後當一切都完成時,一個本地通知被顯示以提示使用者。

除了以上的2種以外,iOS8引入了地點通知(location notifications)。它其實也是本地通知(local notifications),但是他們隻會在使用者一個特定的地理或者iBeacon區域時,才會被觸發。雖然我們看不到什麼細節,地點通知(location notifications)實作起來也很容易。

這個好消息昭示着一個重要的資訊:從iOS8開始,通知被加入了新的特性。簡單地說,從現在開始,當一個通知被展示時,開發者可以指定使用者可觸發的具體的動作(actions),而且甚至不用啟動App也可以處理這個通知。

随着新特性的引入,通知變得越來越引人注目。使用者被給予一個選擇清單,然後可以指令App立即執行特定的指令。使用者再也不用浪費時間在啟動App、處理通知上了。動作(Actions)使得通知和App越來越強大,當然也極大的提升了使用者體驗。

動作(Actions)可以被歸類到一個類目(categories)下。特别是當App安排了不少通知顯示的時候相當友善。用類目(categories),一個通知所有相關的動作(actions)都可以一次性的被捆綁和指定。反之,處理動作(actions)也非常簡單,隻需要實作其代理方法即可。每一個動作(actions)都有一個特殊的屬性标示符(identifier),被App用來辨識收到的動作(actions)然後适當地處理它。

如你所見,本篇文章的目的就是讓你了解以上所有細節。盡管動作(actions)和類目(categories)是新的技術,你最後會發現,他們其實實作起來并不困難。然而,在我們進入到下一個部分之前,我需要說明我們隻會就本地通知(local notifications)進行詳盡說明。我認為,如果想要在本篇文章中将通知的每個種類都詳盡說明,那麼這篇文章就會變得很泛泛,除此之外,我們也會将文章的重點放在對iOS8新引入的功能進行說明。

跟往常一樣,我強烈地推薦你去看看官方文檔。不僅僅是蘋果開發者中心,還去看看 WWDC 2014 #713 session video。将本篇文章和以上結合起來學習,你将會獲得所有通知新特性的知識。而且,我們在這裡并不會讨論遠端和地點通知,官方文檔是你學習他們的一個好地方。

關于本地通知

本地通知被安排在指定的日期和時間被App觸發。時刻記在心中,盡管在和使用者通信時通知十分有用,你也應該小心,過分的使用可能會導緻較差的使用者體驗。

有幾種方式來提示使用者一個通知,接下來會展示所有支援的通知類型。正如你已經了解的,你可以指定通知類型為他們中的幾個或者所有。

  1. Alert or Banner:通知可以用alert或者banner來顯示,這取決于使用者在設定中得選擇。他們都應當包含通知的消息(當然是可以本地化的)。
  2. 聲音(Sound):當一個通知被送達時,你可以‘告訴’iOS播放一段自定義或者系統預設的聲音。因為使用者不會一直看着裝置,被展示的通知有可能會被忽略,是以聲音顯得很有用。但是,對于不重要的通知,聲音不應該被使用。
  3. Badge:當通知到達時,一個badge數字會在App的圖示上顯示。當一個通知到達時,badge數字必增加1,當通知被處理後badge數字減1。當badge數字不為0或者為0,iOS會顯示或者隐藏badge。

在iOS7時,使用者隻能點選通知(或者在鎖屏時滑動)然後關聯的App會啟動,最後處理相關動作。現在開發者可以使用者提供具體的預先定義的動作了。相關聯的App可以在不被啟動的情況下,處理不關鍵或者重要的任務了,而且也可以根據使用者的選擇來執行不同的代碼。馬上我們就能通過這篇文章來學習如何實作它。

除了上面所說的,一個本地通知還可以包含附加的資料,此資料可以被App處理。此資料可以被包含在一個使用者資訊字典中,App可以通過通知來通路此資料,在啟動時或者未啟動時。

可以被安排的本地通知的數量并不是無限的,最多有64個本地通知可以被安排和展示。如果多餘這個數字,所有超過這個數字的通知都會被廢棄。盡管如此,無論通知是以什麼樣的形式被安排的,最早的那個會被最先展示。

接下來,讓我們看看我們今天學習會使用的到得示例App。

示例App概覽

通過開發一個示例App,我們會學習到所有通知的功能。實際上我們将會開發一款購物清單應用,使用者通過此應用發送的通知會得到他需要購買的物品清單。完成這個,我們需要下面兩個功能:

  1. 添加和删除一個物品
  2. 選擇一個日期和時間通知使用者。

如你所想,我們隻會實作本地通知。通過它,示範iOS8通知的新特性已經足夠了。

為了添加一個新物品,我們需要用到textfield控件。添加的物品會在一個tableview中展示出來,在tableview已經存在的物品中,可以通過在物品cell左滑動來删除物品。然後,date picker控件可以被用來設定通知展示的日期和時間。此date picker會在一個按鈕被點選了之後展示出來,當選擇好了日期之後,這個按鈕會被用來安排通知和重新顯示tableivew。我們會以動畫的形式來顯示和隐藏date picker,是以我們的App就會顯得更加吸引人啦,值得注意的是這裡也示範的如何在Swift中使用UIView來實作簡單地動畫效果。

對于我們将要安排的本地通知,我們會定義3種不同的動作(除了預設通知以外,所有本地通知都支援讓App啟動)。這些動作會給使用者如下的選擇(我寫下了動作标題和它将要做的事情):

  1. “好,買到了”(OK, got it):這個動作其實并不會做什麼,除了讓通知消失以外。在App中,沒有任何任務将會執行。
  2. “編輯清單”(Edit list):這個動作将會啟動App,然後textfield會獲得焦點,然後使用者可以直接寫下一個新的物品。
  3. “删除清單”(delete list):這個動作不會啟動App。現存的物品清單将會被删除,但是App并不會被啟動。下次使用者啟動App時,物品清單就會不見啦。

下面幾張圖示範了這個App的功能。正如我所說的,它很簡單,但是用來達到本篇文章的目的已經足夠啦:

在iOS8建立一個互動性強的本地通知
在iOS8建立一個互動性強的本地通知
在iOS8建立一個互動性強的本地通知
在iOS8建立一個互動性強的本地通知

最後再說幾句,我會在實作動作的時候具體講到他們。是以,目前将通知動作當做是根據通知使用者可觸發的方法就行了,所有與他們相關的内容你馬上就會看到了。

基本項目

我們的目标是了解所有和通知相關的新東西而不是從頭建立一個項目,你可以從這裡獲得基本項目。下載下傳它,解壓它,打開它,這個項目将會作為我們實作通知的模闆。

這個項目已經設計好了基本界面,在你閱讀下一章之前,請概覽一下此項目。打開Main.storyboard,看看那些subviews和已經連接配接好的IBoutle和IBAction。

除此之外,你會看到所有ViewController應該實作的協定也已經聲明好了。tableview的datasouce和delegate也已經寫好了,但是裡面并沒有邏輯相關的代碼。這僅僅是為了避免Xcode報錯而已。最後,在viewDidLoad方法中,你可以看到哪些對象被設定了代理。

這個基本項目很容易了解,是以你沒有必要花太多時間來看它。但是它還是值得你快速的閱覽一下的。

設計一個購物清單

首先,讓我們來實作這個購物清單。本部分我們的目标是通過textfield添加一個物品和在tableivew中展示所有的物品。當然,我們也會實作删除已經存在物品的功能。顯然,我們需要一個資料結構來儲存我們的資料,并且作為tableview的資料源。接下來,讓我們給ViewController類添加一個NSmutableArray屬性。確定你選擇的是ViewController.swift檔案。在此檔案中,找到IBOutlet屬性聲明然後添加下面的執行個體變量:

1

var

shoppintList: NSMutableArray!

好了,我們已經完成了第一步。注意到我們并不會在viewDidLoad方法中對它進行初始化,而是在我們會添加新的物品到它之中時初始化它。

現在儲存物品的資料結構已經聲明好了,接下來讓我們允許使用者通過textfied添加物品。實際上,我們希望新添加的物品在添加到了數組之後,tableivew馬上更新顯示它。要實作此功能,我們必須實作textFieldShouldReturn(textField:) 代理方法。正如你在基本項目中所見到的,UITextFieldDelegate協定已經被遵從了,ViewController也設定成為了txtAddItem textfield的代理類。

在這個代理方法中,我們希望做以下事情:

  • 如果shoppintList為nil,初始化它
  • 将textfield的txt添加為一個新的物品到數組
  • 讓tableview展示新的物品(我們稍後就會實作此功能)
  • 添加新物品後清空textfield的内容
  • 移除textfield的焦點,是以鍵盤就會隐藏了

上面的事情是不是看起來很多?其實不多,以可以通過下面的代碼片段來做這些事情:

1 2 3 4 5 6 7 8 9 10 11 12 13

func textFieldShouldReturn(textField: UITextField) -> Bool {

if

shoppingList == nil{

shoppingList = NSMutableArray()

}

shoppingList.addObject(textField.text)

tblShoppingList.reloadData()

txtAddItem.text = 

""

txtAddItem.resignFirstResponder()

return

true

}

上面的代碼所實作的功能已經非常清楚了,是以我認為不需要更多地讨論了。

接下來讓我們在tableview中展示shoppingList的内容。在ViewController類中,tableview的datasource和delegate方法我們已經加上去了,但是現在我們必須加上合适的代碼以讓它工作。讓我們從簡單的開始,tableview中得section和row的數量,每個cell的高度。你可以用下面的代碼整體替換項目中的活着隻是簡單的替換方法的内容。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

func numberOfSectionsInTableView(tableView: UITableView) -> Int {

return

1

}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

var

rows = 0

if

let list = shoppingList{

rows = list.count

}

return

rows

}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {

return

50.0

}

注意到我們對shoppingList數組進行了空指針判斷。

接下來,讓我們将數組中的每個元素指派給cell的label,然後他就會在tableview中展示出來了。在此之前,我需要強調一下,在Interface Builder中有一個cell原型,标示符(identifier)為idCellItem。首先,讓我們重用(dequeue)這個cell,然後将數組中每個元素指派給這個cell的label的text屬性。代碼如下:

1 2 3 4 5 6 7

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

var

cell = tableView.dequeueReusableCellWithIdentifier(

"idCellItem"

) as UITableViewCell

cell.textLabel?.text = shoppingList.objectAtIndex(indexPath.row) as NSString

return

cell

}

下面代碼的類型轉換部分:

1

cell.textLabel?.text = shoppingList.objectAtIndex(indexPath.row) as NSString

非常重要(as NSString部分),因為我們想讓編譯器清楚我們是想将一個string指派給label。

現在,我們終于可以添加一個新物品和展示所有的物品在tableview中啦。但其實還差一點點,這裡還差最後一個功能:删除以存在的物品。

這實作起來很簡單,讓我們定義一個超級簡單的新方法:

1 2 3 4 5

func removeItemAtIndex(index: Int) {

shoppingList.removeObjectAtIndex(index)

tblShoppingList.reloadData()

}

此方法隻接收一個參數,待删除物品的數組下标。我們使用這個參數,删除shoppingList數組中對應的物品,然後重新整理tableview的資料。

現在讓我們在左滑cell後顯示的删除按鈕上調用上面的方法。要實作此功能,我們需要實作tableView(tableView:commitEditingStyle:forRowAtIndexPath:) 代理。在此代理方法中我們調用上面的删除方法。下面是代碼:

1 2 3 4 5

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {

if

editingStyle == UITableViewCellEditingStyle.Delete {

removeItemAtIndex(indexPath.row)

}

}

這裡有兩件事情需要注意一下:1.這個if判斷語句是非常必要的,有了這個if判斷,删除方法就隻會在使用者點選删除按鈕後被觸發。2.代理方法中的indexPath.row的row就是我們想要删除的物品的數組下标。

你可能會想,為什麼我們要定義一個新的removeItemAtIndex(index:) 方法呢?畢竟我們隻需要2行代碼就能在代理方法中實作删除了。嗯,現在我不會回答此問題;去搜尋思考吧。

最後,我需要強調的是我們并沒有必要向這個簡單的App中加入編輯物品功能。畢竟這也沒什麼難度。我們現在做的已經夠了。

儲存和加載清單

盡管前面我們已經實作了這個App的基本功能,但是我們還需要加入另外兩個重要的功能來讓這個App正常的工作。我們需要将清單儲存到磁盤,然後在程式啟動時從磁盤讀取清單。

NSMutableArray提供了将資料寫入磁盤和從磁盤讀取資料的方法,我們可以友善使用他們。接下來我們将會定義兩個方法,來儲存和讀取資料。首先讓我們實作儲存方法,儲存到的檔案名字為shopping_list:

1 2 3 4 5 6

func saveShoppingList() {

let pathsArray = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, 

true

)

let documentsDirectory = pathsArray[0] as String

let savePath = documentsDirectory.stringByAppendingPathComponent(

"shopping_list"

)

shoppingList.writeToFile(savePath, atomically: 

true

)

}

方法的第一二行代碼,傳回了App的Ducument類目。然後我們建構儲存的檔案的路徑。最後,調用NSMutableArray方法writeToFile(_:atomically:)。這個方法将資料儲存到了磁盤。

實作了儲存方法後我們就能調用他了。如果你有對前面的App基本功能進行思考,應該能想到我們會在兩個地方調用到儲存方法:1.當一個新的物品被添加時,2.删除一個已經存在的物品時。

首先,在 textFieldShouldReturn(textField:) 代理方法中的return之前添加儲存方法:

1 2 3 4 5 6 7

func textFieldShouldReturn(textField: UITextField) -> Bool {

...

saveShoppingList()

return

true

}

非常好,現在我們可以将新添加的物品儲存到磁盤啦。接下來,讓我們到removeItemAtIndex(index:)方法中也加入儲存方法:

1 2 3 4 5

func removeItemAtIndex(index: Int) {

...

saveShoppingList()

}

接下來任何可能對我們的資料産生影響的操作,都會被儲存到磁盤啦。

現在我們可以實作讀取資料的方法啦。首先讓我們來看看方法定義:

1 2 3 4 5 6 7 8 9 10

func loadShoppingList() {

let pathsArray = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, 

true

)

let documentsDirectory = pathsArray[0] as String

let shoppingListPath = documentsDirectory.stringByAppendingPathComponent(

"shopping_list"

)

if

NSFileManager.defaultManager().fileExistsAtPath(shoppingListPath){

shoppingList = NSMutableArray(contentsOfFile: shoppingListPath)

tblShoppingList.reloadData()

}

}

這裡需要提醒一下,在讀取磁盤檔案之前我們總是會檢查一下這個檔案存不存在。在iOS當中,我們通過NSFileManager來檢查檔案存不存在。如果檔案不存在那麼當然什麼事情都不會發生啦。如果檔案存在的話,我們就會用檔案的内容來初始化shoppingList數組,然後讓tableview重新加載資料。

最後,我們當然得調用此方法。我們希望App在啟動完畢後馬上加載資料,在viewDidLoad方法中調用它就是一個很好的選擇。

1 2 3 4 5

override func viewDidLoad() {

...

loadShoppingList()

}

當我們不啟動App處理通知時,這兩個方法将會非常有用。

選擇一個提醒時間

現在textfield和tableview在我們的修改之後工作的非常好了。對物品清單的管理也幾乎要完成了。我們現在可以将重心移到date picker控件上來了。在這個部分,我們将會做一些非常簡單有趣的事情。我們會以動畫的形式展現date picker,然後我們就能為一個通知選擇一個日期和時間啦。

如果你去看看ViewController的viewDidLoad方法,你會發現一行隐藏date picker控件的代碼:

1

datePicker.hidden = 

true

接下來我們将會使Schedule Reminder按鈕像一個開關一樣工作:當點選它時,date picker将變得可見,tableview将會隐藏,再次點選它時它又會做完全相反的事情。

如我所說,tableview和date picker之間将會以動畫的形式切換,為了實作這個動畫切換,我竟會定義一個新的方法。在方法中,我會使用UIView的animateWithDuration(duration:animations:completionHandler:) 方法。這個方法幫助我們快速而友善的建立動畫,如果你曾經使用過它,你應該就會知道它的友善快捷之處。

我們先定義一個animateMyViews(viewToHide:viewToShow:)方法。從方法名就能看出,這個方法接收兩個參數,第一個是需要隐藏的view,第二個是将要顯示的view。請記住,在此方法中我們需要同時隐藏或者顯示tableview和date picker,是以我們需要在調用此方法是依次傳入合适的參數。

讓我們挪到代碼部分,首先我們來看看方法的實作,然後我們會詳盡的對方法進行讨論:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

func animateMyViews(viewToHide: UIView, viewToShow: UIView) {

let animationDuration = 0.35

UIView.animateWithDuration(animationDuration, animations: { () -> Void 

in

viewToHide.transform = CGAffineTransformScale(viewToHide.transform, 0.001, 0.001)

}) { (completion) -> Void 

in

viewToHide.hidden = 

true

viewToShow.hidden = 

false

viewToShow.transform = CGAffineTransformScale(viewToShow.transform, 0.001, 0.001)

UIView.animateWithDuration(animationDuration, animations: { () -> Void 

in

viewToShow.transform = CGAffineTransformIdentity

})

}

}

讓我們來看看這個方法都做了什麼:首先,我們以秒為機關定義了每個動畫的持續時間。注意到,這裡有兩個動畫将會先後順序執行。第一個動畫隐藏需要被隐藏的view,第二個動畫展示需要被展示的view。

首先,我們啟動第一個動畫,然後轉換viewToHide的transform屬性,這樣它的寬和高就會按比例縮小。這個view的實際frame并不會變化,但是這個動畫會産生一種拉遠消失的漂亮效果。當第一個動畫完成時,在它的動畫完成回調閉包(closure)裡我們首先設定2個view是否可見,第一個view被隐藏,第二個view變得可見了。然後我們對第二個view的transform屬性立即縮放。最後我們對第二個view的transform逐漸放大到正常值。第二個動畫的最終效果就是拉近。

注意到,如果你想改變動畫的持續時間,簡單的改變animationDuration值就行啦。上面的動畫持續時間是0.7秒鐘(0.35+0.35)。

你應該對這個動畫是如何實作的和最終的效果非常好奇吧?沒關系,馬上你就會知道了,現在我們還差一點點工作。我們現在需要實作唯一的一個IBAction方法。

這個方法将要做的事情非常簡單:首先檢查date picker最近的狀态,如果它是隐藏着的,我們會調用上面的動畫方法并以tableview為第一個參數,date picker為第二個參數。如果它沒有隐藏的話,我們就會給動畫方法傳入相反順序的參數。

1 2 3 4 5 6 7 8 9 10

@IBAction func scheduleReminder(sender: AnyObject) {

if

datePicker.hidden {

animateMyViews(tblShoppingList, viewToShow: datePicker)

}

else

{

animateMyViews(datePicker, viewToShow: tblShoppingList)

}

txtAddItem.enabled = !txtAddItem.enabled

}

上面的if-else語句已經非常清楚了。在這個方法的最後,我們使用一個布爾值标志,來允許或者不允許添加物品,當tableview顯示時允許,反之不允許。

現在,Schedule Reminder按鈕可以被用來切換顯示tableview和date picker啦。稍後,我們會在上面的IBAction方法中加入更多代碼,然後我們就能安排顯示本地通知啦。

現在你可以稍微把玩一下這個App了,在模拟器或者iPhone上面跑跑它吧。試試添加删除物品和動畫切換。如果你懶得跑它下面的圖檔也展示了這個App目前的效果:

在iOS8建立一個互動性強的本地通知

規定通知類型

到目前為止我們已經完成了一些有趣和cool的事情,我們的App也如我們所想的運作起來了。從此部分開始,我們将學習本地通知,并實踐每個新的功能。

在開始之前,先說明一下,在本部分和接下來的部分中我們都隻會在同一個一個方法中編寫代碼。盡管如此,我們将會一步步的實作它,這樣我們就能讨論每一個新功能了。

在本文章的開頭,我簡要的說明了一下本地通知。其中,我說明了通知的集中類型:在alert或者banner中顯示消息(稍後我會道速你如何在他們之中切換),聲音和badge。本部分,我們會規定通知的類型。總之,我們隻會實作在alert和banner中顯示消息,和播放一段聲音。我們不會實作badge數字,如果你想你可以自己實作它。實作它的方法在本文章的末尾有。

在開始下一個部分之前,有一個非常重要的地方我需要強調一下。所有有關一個App的通知的設定都會在使用者設定中展現出來。如果一個App使用了通知,那麼在第一次啟動程式時,App會詢問使用者是否允許App發送通知。不管使用者選擇了允許還是不允許,他都可以以後在使用者設定中改變他。是以,在程式啟動時看到了下圖中的alert時别驚訝,選允許,否則我們就什麼通知都看不懂了。

在iOS8建立一個互動性強的本地通知

接下來我們将建立一個叫setupNotificationSettings()的方法。除去定義方法部分,我們隻會在這裡編寫一行代碼。也許你會想,這很簡單,但是其實它做了一個非常重要的工作。通過這行代碼,我們告訴了App我們想要支援的通知類型。将此設定儲存進一個變量,以後我們會用到它。

下面是目前的代碼:

1 2 3 4 5

func setupNotificationSettings() {

// Specify the notification types.

var

notificationTypes: UIUserNotificationType = UIUserNotificationType.Alert | UIUserNotificationType.Sound

}

UIUserNotificationType是一個枚舉(enum)類型,它包含了通知所有可能的類型。如你所見,OR操作符(“|”)用來包括多種類型。在這裡你可以看到通知所有可能的類型。如果你想重溫一下Swift位操作符的話,這裡将會有你想要的。

在我們使用通知類型之前讓我們先看一點别的東西。

建立通知動作(Notification Actions)

前面我籠統的介紹了幾次通知動作,現在讓我們來詳細的了解一下他們。

一個動作就是一個UIMutableUserNotificationAction類的對象。UIMutableUserNotificationAction是iOS8新引入的類,有着許多有用的配置屬性:

  • 标示符(identifier):字元串,标示了一個對于整個App唯一的字元串。很明顯,你永遠不應該在同一個App中定義兩個同樣地标示符。通過此标示符,我們可以決定在使用者點選不同的通知時,調用哪個動作。
  • 标題(title):用來在展示給使用者的動作按鈕上。可以是簡單地或者本地化的字元串。為了讓使用者能馬上了解動作的含義,一定要仔細考慮這個标題的值,最好是1到2個字元。
  • (destructive):布爾值。當設定為true時,通知中相應地按鈕的背景色會變成紅色。這隻會在banner通知中出現。通常,當動作代表着删除、移除或者其他關鍵的動作是都會被标記為destructive以獲得使用者的注意。
  • authenticationRequired:布爾值。當設定為true時,使用者在點選動作之前必須确認自己的身份。當一個動作十分關鍵時這非常有用,因為為認證的操作有可能會破壞App的資料。
  • ActivationMode:枚舉。決定App在通知動作點選後是應該被啟動還是不被啟動。此枚舉有兩個值:(a)UIUserNotificationActivationModeForeground,(b)UIUserNotificationActivationModeBackground。在background中,App被給予了幾秒中來運作代碼。

當我描述此App時,我說過我們将會建立3中不同的動作:

  1. 一個簡單的通知,點選後消失,不會做任何事情。
  2. 點選通知動作後添加一個物品。
  3. 點選通知動作後删除整個清單。

讓我們用代碼來實作每個動作。對于每個動作,我都會使用到上面描述的每個屬性。

1 2 3 4 5 6

var

justInformAction = UIMutableUserNotificationAction()

justInformAction.identifier = 

"justInform"

justInformAction.title = 

"OK, got it"

justInformAction.activationMode = UIUserNotificationActivationMode.Background

justInformAction.destructive = 

false

justInformAction.authenticationRequired = 

false

動作的标示符是“提示而已(justInform)”。動作隻會在backgroun運作,不會産生任何安全問題,是以我們設定了destructive和authenticationRequired為false。

下一個動作:

1 2 3 4 5 6

var

modifyListAction = UIMutableUserNotificationAction()

modifyListAction.identifier = 

"editList"

modifyListAction.title = 

"Edit list"

modifyListAction.activationMode = UIUserNotificationActivationMode.Foreground

modifyListAction.destructive = 

false

modifyListAction.authenticationRequired = 

true

很明顯,為了讓使用者能夠标記物品清單,我們需要App啟動。而且我們不希望使用者的物品清單被未驗明身份的人亂動,我們設定了authenticationRequired為true。

最後一個動作:

1 2 3 4 5 6

var

trashAction = UIMutableUserNotificationAction()

trashAction.identifier = 

"trashAction"

trashAction.title = 

"Delete list"

trashAction.activationMode = UIUserNotificationActivationMode.Background

trashAction.destructive = 

true

trashAction.authenticationRequired = 

true

通過這個動作,我們允許使用者在App沒有啟動的情況下删除整個物品清單。這個動作可能導緻使用者丢失所有資料,是以我們設定了destructive和authenticationRequired為true。

通過上面的代碼,你應該了解到了配置動作其實很簡單。

現在讓我們把以上三個動作配置代碼片段加入到setupNotificationSettings方法中吧!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

func setupNotificationSettings() {

...    

// Specify the notification actions.

var

justInformAction = UIMutableUserNotificationAction()

justInformAction.identifier = 

"justInform"

justInformAction.title = 

"OK, got it"

justInformAction.activationMode = UIUserNotificationActivationMode.Background

justInformAction.destructive = 

false

justInformAction.authenticationRequired = 

false

var

modifyListAction = UIMutableUserNotificationAction()

modifyListAction.identifier = 

"editList"

modifyListAction.title = 

"Edit list"

modifyListAction.activationMode = UIUserNotificationActivationMode.Foreground

modifyListAction.destructive = 

false

modifyListAction.authenticationRequired = 

true

var

trashAction = UIMutableUserNotificationAction()

trashAction.identifier = 

"trashAction"

trashAction.title = 

"Delete list"

trashAction.activationMode = UIUserNotificationActivationMode.Background

trashAction.destructive = 

true

trashAction.authenticationRequired = 

true

}

當一個通知的所有動作被配置好了之後,他們可以被包進一個類目(categories)裡。如果你的通知支援動作,那麼你就必須建立一個類目(categories)。通常情況下一個類目(category)配對一個通知,假設一個App中得所有通知都支援動作,那麼這個App也會有和通知一樣多的類目(categories)。

我們隻會在這個示例App中建立一個通知,是以這裡也隻會有一個類目(category)。從程式設計的角度來說,類目(category)就是一個UIMutableUserNotificationCategory類的對象,這也是iOS8新引入的類。這個類隻有一個屬性和一個方法。标示符屬性用來表示一個唯一的類目(category),方法用來将多個動作包含進來。

讓我們來了解一下這個方法,先看看這個方法的聲明(來自蘋果官方文檔):

1

func setActions(_ actions: [AnyObject]!, forContext context: UIUserNotificationActionContext)

第一個參數指明了需要包含進來的動作。是一個包含所有動作的數組,他們在數組中的順序也代表着他們将會在一個通知中調用的先後順序。

第二個參數非常重要。context形參是一個枚舉類型,描述了通知alert顯示時的上下文,有兩個值:

  1. UIUserNotificationActionContextDefault:在螢幕的中央展示一個完整的alert。(未鎖屏時)
  2. UIUserNotificationActionContextMinimal:展示一個banner alert。

在預設上下文(default context)中,類目最多接受4個動作,會以預先定義好的順序依次在螢幕中央顯示。在minimal上下文中,最多可以在banner alert中設定2個動作。注意在第二個情況中,你必須選擇一個較為重要的動作以顯示到banner通知裡。接下來我們會将這兩種情況都用代碼實作。

如我所說,上述方法的第一個參數必須為一個數組。是以在我們的配置通知方法中我們首先為兩個上下文建立兩個數組:

1 2 3 4 5 6 7

func setupNotificationSettings() {

...

let actionsArray = NSArray(objects: justInformAction, modifyListAction, trashAction)

let actionsArrayMinimal = NSArray(objects: trashAction, modifyListAction)

}

然後讓我們來建立一個新的類目(category)吧,首先我們設定它的标示符(identifier),然後将上面的2個數組分别設定:

1 2 3 4 5 6 7 8 9 10

func setupNotificationSettings() {

...

// Specify the category related to the above actions.

var

shoppingListReminderCategory = UIMutableUserNotificationCategory()

shoppingListReminderCategory.identifier = 

"shoppingListReminderCategory"

shoppingListReminderCategory.setActions(actionsArray, forContext: UIUserNotificationActionContext.Default)

shoppingListReminderCategory.setActions(actionsArrayMinimal, forContext: UIUserNotificationActionContext.Minimal)

}

然後…這樣就行啦,為一個通知相關的動作建立一個類目就這樣完成了。

注冊通知設定

通過上面的3個部分,我們已經将本地通知的所有新功能已經實作了。現在我們需要将這些設定注冊到使用者設定中。為了完成這個目标,我們将會用到UIUserNotificationSettings類(iOS8新引入),然後在下面的init方法中,我們會指定通知類型和類目(category)。

1

convenience init(forTypes allowedUserNotificationTypes: UIUserNotificationType, categories actionSettings: NSSet?)

第一個參數是我們為通知設定的類型,第二個方法是一個集合(NSSet),在這個集合中必須包含一個App所有通知支援的類目。在本例中,我們隻有一個類目,但是我們還是需要使用集合來傳遞它。

下面是代碼實作:

1 2 3 4 5 6

func setupNotificationSettings() {

...

let categoriesForSettings = NSSet(objects: shoppingListReminderCategory)

}

現在,我們就可以建立一個UIUserNotificationSettings對象了,然後傳入相應的參數。

1 2 3 4 5 6

func setupNotificationSettings() {

...

let newNotificationSettings = UIUserNotificationSettings(forTypes: notificationTypes, categories: categoriesForSettings)

}

最後,讓我們将它注冊一下吧!

1 2 3 4

func setupNotificationSettings() {

...

UIApplication.sharedApplication().registerUserNotificationSettings(newNotificationSettings)

}

第一次啟動App時上述代碼就會執行,它會在使用者設定中建立一條我們的App記錄。

最後,在我展現一個完整的setupNotificationSettings(),還有一點需要注意。這個方法會在viewDidLoad方法中被調用,這意味着每當App被啟動的時候它都會執行一次。很顯然一遍又一遍的設定同樣地值是在做無用功,這樣如果我們将上面的方法用一個if判斷執行一下的話就好了。在這個判斷中,我們檢查通知的類型是否已經被設定了,如果沒有if塊中的代碼就會被執行。

1 2 3 4 5 6 7

func setupNotificationSettings() {

let notificationSettings: UIUserNotificationSettings! = UIApplication.sharedApplication().currentUserNotificationSettings()

if

(notificationSettings.types == UIUserNotificationType.None){

...

}

}

首先,我們通過UIApplication的類方法currentUserNotificationSettings()來擷取通知的類型。通過這個方法傳回的UIUserNotificationSettings類的對象,我們可以檢查它的types枚舉屬性。請記住這個屬性為枚舉類型。如果它的值為None,那麼通知類型就還沒有被注冊,然後我們就運作上面的方法來注冊通知類型,否則什麼也不做。

通過上面的代碼我們避免了重複注冊通知類型。但是如果你想修改通知類型、添加動作或者類目的話,你可以将if開始和結束行注釋掉,然後運作一次App。新的設定會被添加,你就能測試一下他們了。然後移除注釋,避免重複注冊。

好了,設定通知的工作已經完成了。下面你可以看到setupNotificationSettings()方法的完整版本:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

func setupNotificationSettings() {

let notificationSettings: UIUserNotificationSettings! = UIApplication.sharedApplication().currentUserNotificationSettings()

if

(notificationSettings.types == UIUserNotificationType.None){

// Specify the notification types.

var

notificationTypes: UIUserNotificationType = UIUserNotificationType.Alert | UIUserNotificationType.Sound

// Specify the notification actions.

var

justInformAction = UIMutableUserNotificationAction()

justInformAction.identifier = 

"justInform"

justInformAction.title = 

"OK, got it"

justInformAction.activationMode = UIUserNotificationActivationMode.Background

justInformAction.destructive = 

false

justInformAction.authenticationRequired = 

false

var

modifyListAction = UIMutableUserNotificationAction()

modifyListAction.identifier = 

"editList"

modifyListAction.title = 

"Edit list"

modifyListAction.activationMode = UIUserNotificationActivationMode.Foreground

modifyListAction.destructive = 

false

modifyListAction.authenticationRequired = 

true

var

trashAction = UIMutableUserNotificationAction()

trashAction.identifier = 

"trashAction"

trashAction.title = 

"Delete list"

trashAction.activationMode = UIUserNotificationActivationMode.Background

trashAction.destructive = 

true

trashAction.authenticationRequired = 

true

let actionsArray = NSArray(objects: justInformAction, modifyListAction, trashAction)

let actionsArrayMinimal = NSArray(objects: trashAction, modifyListAction)

// Specify the category related to the above actions.

var

shoppingListReminderCategory = UIMutableUserNotificationCategory()

shoppingListReminderCategory.identifier = 

"shoppingListReminderCategory"

shoppingListReminderCategory.setActions(actionsArray, forContext: UIUserNotificationActionContext.Default)

shoppingListReminderCategory.setActions(actionsArrayMinimal, forContext: UIUserNotificationActionContext.Minimal)

let categoriesForSettings = NSSet(objects: shoppingListReminderCategory)

// Register the notification settings.

let newNotificationSettings = UIUserNotificationSettings(forTypes: notificationTypes, categories: categoriesForSettings)

UIApplication.sharedApplication().registerUserNotificationSettings(newNotificationSettings)

}

}

别忘了在viewDidLoad方法中調用它:

1 2 3 4 5

override func viewDidLoad() {

...

setupNotificationSettings()

}

安排本地通知

如果你在iOS之前的版本中使用過本地通知的話,你一定知道安排一個通知是很簡單地事情。在iOS8,安排一個通知并沒有什麼變化。事實上,所有的基本設定都是一模一樣的。唯一的新東西就是必須給一個通知設定一個類目,這樣通知就能知道當使用者點選的時候該啟動哪些動作了。

你可能已經猜到了,我們會定義一個新的方法來配置和安排一個本地通知。在我們實作這個方法之前,我們先看看一個本地通知中得重要屬性:

  • fireDate:一個通知應當被顯示的日期和時間。NSDate對象。
  • alertBody:通知的内容。應當盡量的簡潔明了,這樣使用者才能馬上了解它。
  • alertAction:在預設情況下,點選一個banner通知會導緻App啟動。在以alert形式顯示的通知中,會建立一個和這個動作對應的按鈕。在此屬性中,你必須指定這個按鈕的标題。比如,在這個示例App中,我們将View List設定為它的标題或者alert的動作。

牢記以上幾點,現在放我們定義這個方法配置這個通知。不用說先讓我們建立一個UILocalNotification對象:

1 2 3 4 5 6 7

func scheduleLocalNotification() {

var

localNotification = UILocalNotification()

localNotification.fireDate = datePicker.date

localNotification.alertBody = 

"Hey, you must go shopping, remember?"

localNotification.alertAction = 

"View List"

}

還有一點,我們必須指定使用者點選通知後對應的類目動作。回憶一下,我們前面已經定義了一個類目和類目标示符,我們在這裡就能使用到這個标示符了:

1 2 3 4 5

func scheduleLocalNotification() {

...

localNotification.category = 

"shoppingListReminderCategory"

}

這很簡單。最後,我們需要使用UIApplication的scheduleLocalNotification(_:) 方法來真正的安排一個通知,不然這個通知永遠都不會“通知”到你啦。

1 2 3 4 5

func scheduleLocalNotification() {

...

UIApplication.sharedApplication().scheduleLocalNotification(localNotification)

}

接下來,讓我們調用這個方法吧。讓我們在scheduleReminder(sender:) 按鈕方法中加入新的代碼,在顯示tableview之前我們調用上面的代碼。注意,這裡有一點需要避免:如果我們在已經安排了一個本地通知以後在重新安排一個,之前的通知仍然會有效。如果我們忽略了這一點,我們可能會建立許多個不需要的通知。為了避免這樣,我們簡單的在date picker顯示的時候移除所有已經安排的通知,這樣就沒事啦。下面是IBAction方法代碼:

1 2 3 4 5 6 7 8 9 10 11 12 13 14

@IBAction func scheduleReminder(sender: AnyObject) {

if

datePicker.hidden {

animateMyViews(tblShoppingList, viewToShow: datePicker)

UIApplication.sharedApplication().cancelAllLocalNotifications()

}

else

{

animateMyViews(datePicker, viewToShow: tblShoppingList)

scheduleLocalNotification()

}

txtAddItem.enabled = !txtAddItem.enabled

}

這就是啦。在配置了類型、動作和其他的細節之後,安排他是一件很簡單的事情。有了上面的代碼,現在通知已經可以如我們期望的工作了。

修複安排通知的時間問題

目前為止我們都做的很好,每個功能都工作的很完美。但是,當你運作這個程式的時候你可能會發現一個關于通知送達時間的問題盡管它看起來是正常的,我發現它存在一點問題。是以,在我們測試通知之前,讓我先深入的講一下這個問題。也許在你運作這個App的時候并不會察覺這個問題,但是在實時性強的App中,通知推送的時間非常重要,而且不準确的時間可能會造成非常嚴重的問題。

那麼這個問題是什麼呢?嗯,接下來讓我以一個例子來說明它:如果你在10:23:14的時候安排了一個通知在14:00被推送(不用管日期,假設是同一天),這個通知其實不會在14:00:00的時候被推送。而是在14:00:14的時候。我認為對于通知來說這是一個非常嚴重的問題。為什麼會這樣呢?那是因為在date picker中,我們可以設定時間,但是卻不能設定到秒。然後系統就會将我們設定時的時間的秒指派給通知的推送時間,而不是使用0。

那麼,我們該怎樣修複這個BUG呢?很簡單,通過代碼來修複。如果你以前沒有接觸過日期和時間的話也不用擔心。,這将是一個簡單而有趣的任務。

一個日期對象(NSDate對象)可以分為幾個部分,叫做date components。這些component是NSDateComponents類的屬性,可以可以同時讀和寫,是以當我們從date picker選擇的日期得到了這些components之後我們就可以修改其中的seconds屬性了。很明顯,通過這些components我們可以重新得到一個NSDate對象,有了這個對象之後,我們就可以友善的将其設定為通知的推送日期了。

正如你在下面的實作中所看到的,将一個日期對象轉換成date components依賴于NSCalendar類。這個類提供讓我們完成這些工作的方法。

1 2 3 4 5 6 7 8 9

func fixNotificationDate(dateToFix: NSDate) -> NSDate {

var

dateComponets: NSDateComponents = NSCalendar.currentCalendar().components(NSCalendarUnit.DayCalendarUnit | NSCalendarUnit.MonthCalendarUnit | NSCalendarUnit.YearCalendarUnit | NSCalendarUnit.HourCalendarUnit | NSCalendarUnit.MinuteCalendarUnit, fromDate: dateToFix)

dateComponets.second = 0

var

fixedDate: NSDate! = NSCalendar.currentCalendar().dateFromComponents(dateComponets)

return

fixedDate

}

詳細讨論NSCalendar并不是我們的目的。但是這也将是一個非常有趣的話題,是以你可以通過這裡來了解更多有關NSCalendar的東西。

現在是時候選擇一個合适的地點來調用這個方法了,在設定通知推送時間的時候調用這個方法将是一個很好地選擇。傳回scheduleLocalNotification()方法,将date picker選擇的日期作為參數傳入此方法,然後将此方法傳回的日期設定為通知的fireDate。下面是更新後的scheduleLocalNotification()方法:

1 2 3 4 5 6 7 8 9

func scheduleLocalNotification() {

var

localNotification = UILocalNotification()

localNotification.fireDate = fixNotificationDate(datePicker.date)

localNotification.alertBody = 

"Hey, you must go shopping, remember?"

localNotification.alertAction = 

"View List"

localNotification.category = 

"shoppingListReminderCategory"

UIApplication.sharedApplication().scheduleLocalNotification(localNotification)

}

現在,我們的通知終于可以如願以償的按時被推送了!

處理通知動作

現在關于通知,我們隻差最後一個部分了,那就是處理使用者點選通知相關按鈕時候的各種動作。和往常一樣,這裡有幾個主要的委托方法我們需要實作。

在我們實作和處理動作之前,讓我給你介紹幾個代理方法,通過他們你可以友善的開發你的App。注意,在這個示例程式中,我們并不會真正的使用到他們,我們隻會通過他們列印一些消息。現在,請打開AppDelegate.swift檔案。

第一個代理方法是關于通知設定的。這個代理方法在程式啟動時被調用(不管是正常啟動還是通過一個本地通知),包含了所有App通知的設定選項。接下來你可以看到它的定義。我們所做的,僅僅是列印通知類型:

1 2 3 4

func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {

println(notificationSettings.types.rawValue)

}

通過上述的方法,你可以得到所有UIUserNotificationSettings支援的類型。當你需要檢查你的App所支援的通知和動作的類型時,這個方法非常有用。别忘了,使用者可以通過使用者設定來改變通知類型,是以我們不能保證,初始的通知類型一直都有效。

當你安排了一個通知之後,無論你的App是否在運作,這個通知都将被推送。通常情況下,開發者設定通知如何在App沒有運作或者被挂起的時候被推送,所有的代碼實作也聚焦在這兩個方面。但是,我們也應該處理當App在運作時通知如何被處理。感謝蘋果,iOS SDK讓這變得非常簡單,有一個代理方法正可以處理這種情況:

1 2 3 4 5

func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {

// Do something serious in a real app.

println(

"Received Local Notification:"

)

println(notification.alertBody)

}

當然在某些情況下在App運作時你并不需要處理通知。但是在另外一個情況下,上面的代理方法是處理通知動作的地方。

現在讓我們來看看當使用者點選了一個通知動作按鈕後将會調用的代理方法。更具我們給動作設定的标示符(identifier),我們決定那個動作被調用,然後App就會執行對應的代碼了。我們會有三種動作:

  1. 簡單地讓通知消失(标示符:justInform)。
  2. 添加一個新的物品(标示符:editList)。
  3. 删除整個物品清單(标示符:trashAction)。

從上面我們看出,我們不需要對第一個動作做任何事情。但是我們需要處理另外兩種動作。我們将根據identifier的值給每一種情況發送一個NSNotification,在ViewController類中,我們監視這些NSNotification,然後我們處理他們。

讓我們從新的代理方法開始:

1 2 3 4 5 6 7 8 9 10 11

func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) {

if

identifier == 

"editList"

{

NSNotificationCenter.defaultCenter().postNotificationName(

"modifyListNotification"

, object: nil)

}

else

if

identifier == 

"trashAction"

{

NSNotificationCenter.defaultCenter().postNotificationName(

"deleteListNotification"

, object: nil)

}

completionHandler()

}

在上述幾種情況中我們根據動作的的标示符,發送不同名稱的NSNotification對象。注意到,我們在方法的結束調用了completionHandler()方法,根據規定我們必須調用它,這樣系統才能知道我們已經處理完了通知動作。在處理本地通知時,這個代理方法非常重要,在這裡你通過使用者的點選執行相應地代碼。

接下來,讓我們打開ViewController.swift檔案。首先,讓我們監視我們之前發送的NSNotification。在viewDidLoad中加入下面的代碼:

1 2 3 4 5 6

override func viewDidLoad() {

...

NSNotificationCenter.defaultCenter().addObserver(self, selector: 

"handleModifyListNotification"

, name: 

"modifyListNotification"

, object: nil)

NSNotificationCenter.defaultCenter().addObserver(self, selector: 

"handleDeleteListNotification"

, name: 

"deleteListNotification"

, object: nil)

}

modifyListNotification()和deleteListNotification()方法都是我們自定義的方法,我們接下來會實作他們。

首先我們實作第一個方法。因為App是通過使用者點選了編輯物品動作啟動的,是以我們需要将textfield控件設定為第一響應。完成此任務,隻需加入一行代碼:

1 2 3

func handleModifyListNotification() {

txtAddItem.becomeFirstResponder()

}

通過這行代碼,鍵盤會自動展現然後使用者就能立即添加一個新的物品了。

通過删除清單按鈕,我們希望從物品對象數組移除所有的物品。接下來,我們首先移除shoppingList數組中得所有對象,然後将它(空數組)儲存到磁盤。最後我們重新加載tableview的資料,這樣的話,當使用者啟動App時就什麼物品都看不到了。

1 2 3 4 5

func handleDeleteListNotification() {

shoppingList.removeAllObjects()

saveShoppingList()

tblShoppingList.reloadData()

}

有了上面的代碼實作,我們這個示例程式現在終于完成啦!

啟動示例App

是時候測試一下我們的App了,首先在模拟器或者真機中啟動它。添加一些物品,然後安排一個本地通知。為了避免等待太久時間,安排他在1到2分鐘之後被推送,然後退出App。下面的圖檔模拟了上述的幾個操作,和通知在不同的情況被推送的樣子。

添加一個新的物品到清單:

在iOS8建立一個互動性強的本地通知

安排一個本地通知:

在iOS8建立一個互動性強的本地通知

banner形式展現通知,檢視動作(minimal context):

在iOS8建立一個互動性強的本地通知

alert形式展現通知,所有動作(default context):

在iOS8建立一個互動性強的本地通知

在通知中心展示通知(minimal context):

在iOS8建立一個互動性強的本地通知

為了在banner和alert之間切換,在你的裝置中打開設定App。找到Shopping Alert選項,點選進入。

在iOS8建立一個互動性強的本地通知

在下圖示記的地方,根據你希望通知展示的樣子選擇alert或者banner。

在iOS8建立一個互動性強的本地通知

退出設定App,安排一個新的本地通知,這樣你就能你選擇的結果了。

總結

在iOS8當中,通知看起來更好用了,使用者現在甚至可以直接處理通知而不用啟動App。在本篇文章中我們提到了幾個新的概念,有新也有舊。重要的是,現在設定通知的類型、動作和類目都十分簡單友善,如果你的App需要通知,那就使用他們吧!正如我在介紹中所說,還有另外幾種通知,如遠端和地點。盡管我們沒有去實作他們,在知道了本地通知是如何工作了之後,你也會知道如何實作其他通知的大緻路徑了。以後你隻需要去搜尋他們的實作具體細節。好了,上面所有就是我敬獻給你的,靜下來好好思考一下。玩得開心!