上節課我們已經解鎖了新技能,選擇圖標。
但是我們仍然有些細節有待改進,目前,在done方法中,你做了這件事:
let checklist = Checklist(name: textField.text!)
checklist.iconName = iconName
設置圖標的名稱可以認為是初始化Checklist的一部分,所以我們寫成下面這樣會更好一些:
let checklist = Checklist(name: textField.text!, iconName: iconName)
打開ListDetailViewController.swift,done方法中把新的這一行代碼替換進去。
僅僅是這樣,app肯定不會工作,你還需要給Checklist.swift新增一個具有兩個參數name和iconName的新的init方法
打開Checklist.swift,添加新的init方法:
init(name: String,iconName: String) {
self.name = name
self.iconName = iconName
super.init()
}
注意,是加一個新的,不要在老的上面改。Checklist現在有三個init方法了:
init(name):僅需要名稱時,使用這個方法。
init(name, iconName):同時需要名稱及圖標名稱時,用這個方法。
init?(coder):從plist文件中讀取對象時,用這個方法。
init(name)和init(name, iconName)幾乎是一樣的。除了參數有所不同。
所以我們可以改進一下這個地方,使用init(name)調用一個將圖標名稱默認為“No Icon”的init(name, iconName)方法。
將init(name)替換為下面的代碼:
convenience init(name: String) {
self.init(name: name, iconName: "No Icon")
}
這里用self.init(name, iconName)代替了super.init()。
因為它將一部分工作移交給了另一個init方法,所以此時init(name)方法被稱為便利初始化。
它和init(name, iconName)的功能一模一樣,只是節省了你需要在多處指定使用“No Icon”的工作。
init(name, iconName)成為了Checklist的指定初始化。它是創建一個新的Checklist對象的首要方法,而init(name)僅用于給比較懶的人,比如你和我。
運行app,確認一下一切工作正常。
練習:給ChecklistItem一個init(text)方法,或者一個init(text,checked)方法。
給app整個容
你會使用一些簡單的辦法來提高顏值,比如化個妝什么的,而不是手術級別的。導航控制器和table view默認的樣子已經比較好看了,雖然色調有點單一。本節課你將了解如何自定義這些UI元素的外觀。
雖然現在app外觀有點單調,但是你可以使用一些比較簡單的辦法讓它立馬個性起來,我們說的就是tint color。
tint color是UIKit用來表示某種東西可以交互的一個顏色系統,比如按鈕上的文字是淺藍色的,用戶無論用什么app,看到這種淺藍色,基本就會明白,這個按鈕可以點擊。
改變tint color是非常簡單的。
打開故事模版,找到文件指示器(File inspector),就是第一個子頁。
點擊Global Tint就可以打開顏色選擇器了,我們將顏色設置為Red:4,Green:169,Blue:235,這樣就得到了比較亮一些的藍色。
小貼士:如果顏色選擇器中僅顯示黑白灰三種顏色,那么你就點擊一個名字叫做Gray Scale Slider的下拉框,在下拉框中選擇為RGB Slider。
如果對勾符號不用黑色,而改用tint color那就更加完美了。
為了實現這個目的,在ChecklistViewController.swift中的configureCheckmark(for:with)方法中,添加一行代碼:
label.textColor = view.tintColor
運行app,是不是看起來感覺不一樣了?如果感覺一樣,請把這節課當作皇帝的新裝就好。
任何一個完整的app,都有會有自己的圖標。在本節課附加的資源中的Icon文件夾中你可以找到各種尺寸的圖標(附件不提供下載,請大家支持正版,或者自己去網上找些圖標素材練習),注意一下,圖標的顏色和我們剛才選擇的tint color是一致的。
把這些圖標添加到asset catalog(Assets.xcassets)。回憶一下,我們僅僅是把這些圖標拖入AppIcon中相應的位置就好了。
app,還有一個加載用的圖片和文件。在app還在讀取時,顯示一個靜態圖片可以造成app啟動很快的幻覺,這些都是騙人的把戲。(其實這里就是app打廣告的好地方,但是本書的作者字里行間都透露著對廣告的鄙視,所以。。。沒有然后)
Xcode的模版中包含一個叫做LaunchScreen.storyboard的文件,在運行的時候會先加載它。你可以把它做成和app差不多的樣子,但是我們還有一個更簡單的方法。
打開工程設置界面,在General子頁中,向下滾動,找到一個叫做App Icons and Launch Images的分節。
在Launch Screen File下拉框中,選擇Main.storyboard。
這樣app就會使用故事模版中的設計作為啟動時加載的文件。
啟動時,app會找到初始界面并且將它轉換為一個靜態圖片。對我們的app而言,就是All Lists View Controller。
從工程導航欄中刪除LaunchScreen.storyboard。
然后選擇菜單Product->Clean。或者在模擬器中刪除掉app,再重新運行一次,這樣就不會有任何殘留下的緩存了。(刪除的方法和真實手機一樣,用鼠標一直按住app的圖標,然后app圖標會開始晃動)
然后運行app,你就可以看到app一啟動,就顯示出了主界面,好像app立即啟動了一樣。
使用合適的啟動界面,可以使app顯得更加專業。
對于許多app而言,你都可以無腦的使用main storyboard來當啟動界面,此外,你要需要使啟動界面適合所有的設備,比如6s,7,plus等。
支持所有設備類型
我們的app應該在現有的所有iPhone型號上運行正常,從屏幕最小的iPhone SE到最大的iPhone 7 plus。table view controller在這方面非常靈活,它可以自動識別設備類型并且轉換尺寸,無論是變大還是變小都靈活自如。你可以自己在各種類型的模擬器上試試。
那么我們面臨的問題是什么呢?這里還是有很多東西需要微調的。
目前為止,我給你看的截圖都是基于iPhone SE的,并且我在自己的界面建造器中也是使用的iPhone SE尺寸進行設計。但是我們在大屏幕設備上運行的話會發生什么事呢?比如我們用iPhone 7 plus模擬器運行一下試試:
圖標不再完美的對齊cell的右側邊緣了。再試試輸入點文本上去:你會發現文本被截短了,因為text field變小了。為什么會發生這些情況呢?
當你在界面建造器中為你的app設計用戶界面的時候,它并不會自動匹配所有的設備類型,只是匹配你正在使用的模擬器型號。你需要幫助界面建造器,告訴它面對不同尺寸的屏幕時應該如何調整UI的大小及位置。這就我要介紹給你們的自動布局(Auto Layout)。
你想要的是,圖像永遠貼在右側邊緣,總是和詳細信息按鈕保持固定的距離。當屏幕大小發生變化時,圖像應該自動的調整自己的位置。
解決辦法就是給image view添加自動布局約束,告訴app屏幕邊緣和image view的關系。
選定Icon Image View。打開畫布底部的Pin Menu,然后按照一下步驟操作:
1、取消選擇Constrain to margins。
2、激活頂部和右側的連線(菜單上方有十字形的四條紅色虛線,激活后會變成實線)
3、勾選Width和Height復選框
4、For update Frames下拉框選擇為Items of New Constraints。
5、點擊Add 4 Constraints。
(有可能你會看不到For update Frames下拉框,這樣的話就忽略第四條)
現在image view看起來應該是這個樣子:
確認一下,代表約束的線條是藍色實心線條。如果它們是橙色或者紅色,那么你肯定是漏掉了上面的某個步驟。(重新添加一次約束,或者使用菜單Editor → Resolve Auto Layout Issues → Update Frames,看看能不能自動更新過來)
最重要的約束是右側那個,這一條告訴UIKit,image view的右手邊總是以固定距離貼著table view cell的右側邊緣。
換而言之,無論當前界面是寬還是窄,image view總是和詳細信息按鈕的相對位置不變。
剩下的三條約束,頂部、寬和高也都是必要的,因為所有的視圖都必須有足夠的約束指明它們的位置和大小。
如果你不指明約束,那么界面建造器會按照默認約束處理。但是哪怕你只是添加了一條約束,那么默認約束就失效了,你必須把所有的約束都補全。
確認約束是否生效,并不是非得在模擬器中運行app,那樣太消耗時間,你可以使用View as功能,這個功能在畫布的底部,我們上一個課程中也使用過它,它可以在界面建造器的內部切換iPhone的類型。如果你的約束添加的沒有問題,那么任何尺寸的iPhone上,它的位置都應該是固定的。
接下來,我們來讓text field在比較寬的屏幕中,自動延長。
選定Text Field,打開Pin menu激活四條紅線:
這個操作會把text field固定到table view cell的四個邊上。(紅線上下左右4個框里的數字是幾都沒關系,這些數字代表text field和cell四個邊的間距,重要的是4條紅線都需要被激活)
對Add/Edit Item界面的text field也做同樣的操作。
現在你輸入多長的文本都沒關系了,文字會自動向左滾動:
讓我們來輸入一條非常長的文本,這個文本傳遞到其他table view時會發生什么呢?
對All Lists界面完全沒問題:
使用“Subtitle”cell風格的table view,會自動隨著屏幕調整寬度。當文本過長時,它會自動縮短它們。
但是對于to-do items(待辦事項界面),看起來就不是那么漂亮了。在這個界面了,文本被過早的截短了。
因為這是一個自定義的cell設計,你要添加一些約束來避免這種事情發生。
打開故事模版,找到Checklist界面并且選定cell內的label。
首先使用Xcode菜單 Editor → Size to Fit Content,給label一個可以自由伸縮的尺寸。這樣做后也許會把label變得非常小,但是沒有關系。如果不這樣做的話,下面的步驟就會無法進行。(即使這一操作移動了label也沒關系)
你想要把label固定在視圖的右側邊緣,緊挨著詳細信息按鈕。我們來添加這個約束。
打開Pin菜單,取消選擇Constrain to margins。
激活右側的紅線,并且將其中的值修改為0,這樣它就緊貼著詳細信息按鈕了。
將Update Frames設置為Items of new Constraints(如果看不到這個選項就忽略它)。點擊Add 1 Constraint,結束。
好像有啥東西看起來有點不對。
記住,你總是要添加足夠的約束指定一個視圖的尺寸和位置。這里你僅僅添加了關于右側邊緣的約束,這是不夠的。
不要慌!當你添加約束時,漏點東西是很正常的。解決的辦法非常簡單,就是把漏掉的東西添加上就好了。
還是選中label,打開Align menu,就是Pin菜單左邊的那個。選中Vertically in Container。設置Update Frames為Items of New Constraints(看不到這個就忽略掉)
現在所有代表約束的線都應該是藍色了。label在X軸和Y軸上都有了有效的位置。
??:即使你沒有對label的尺寸指定任何的約束,約束也完美的生效了,這是為什么呢?
沒有指定尺寸的話,label會根據它的內容,文本和字體來計算它自己應該有多大。這叫做內容自適應尺寸(應該是這么個名詞吧!)
使用內容自適應調整的UI組件,比如label,不需要添加寬和高的相關約束,但是自適應調整生效的前提是之前你必須使用了菜單中的Size to Fit Content選項。
不幸的是,label現在雖然右對齊了,但是這并不是你想要的,它的左邊必須緊挨著cell的左邊。
最簡單的辦法就是給左邊同樣添加一條約束,使label緊挨著左側邊緣。
但是你不能使用Pin菜單來做這個事,因為這樣會使label和對勾符號連接起來,對勾符號的尺寸依賴于它是否被觸發顯示在屏幕上。你需要使用新的技術來完成這件事。
再次選中label,按住ctrl拖拽label到cell的內部任意一個位置上。放開鼠標,會彈出一個菜單。菜單上的選項依賴于你拖拽的方向,所以你看到的菜單也許和截圖中的有所不同。
在彈出菜單上選擇Leading Space to Container Margin,就可以完成這個約束的添加了。
這樣就新增了一條長長的藍色約束線,但是標簽的位置看起來還是有點問題。
選擇這條藍色的線,打開尺寸檢查器,將Constant設置為30。
現在看起來好多了:
現在label的兩側邊緣都固定好了,所以它會自動伸縮,保持和table view cell一致。
運行app,現在文本不會過早的被截短了。
特色功能:本地通知
我希望你還可以跟上我的思路。我們詳細的討論了關于視圖控制器(view controller)、導航控制器(navigation controller)、故事模版(storyboard)、轉場(segues)、表視圖(table view)、以及數據模型(data model)的相關內容。
如果你想要成為iOS app開發的大師的話,你必須精通這些課題,因為每一個app都用到了它們中間的一個或者幾個。
在本節課,你要了解一個擴展的課題:local notifications(本地通知),使用iOS 10中的User Notifications框架。
本地通知允許app在app沒有運行的時候,按照事先的安排,對用戶進行消息通知。
你要新增一個“due date(處理時間)”到ChecklistItem對象中,然后使用本地通知來告訴用戶某條待辦事項的戒指時間。
如果你對這個課題感興趣的話,那么,太好了。。。
這節課的內容是這個樣子的:
1、嘗試使用本地通知,觀察它是如何工作的。
2、允許用戶為待辦事項選擇一個處理時間。
3、創建一個時間選擇器。
4、對待辦事項使用本地通知,并且當用戶修改處理時間時,更新本地通知的時間。
在你了解如何把這些和app融為一體之前,我們先來安排本地通知計劃,看看會發生什么。
順便說一下,本地通知和推送消息是不一樣的(推送消息,也叫遠程通知)。推送消息允許你的app接收外部事件觸發的通知,比如你最喜歡的球隊奪冠了。
本地通知更像是一個鬧鐘:你設定一個時間,然后它就會發出蜂鳴聲。
app僅僅在用戶允許的情況下,可以進行本地通知,如果用戶不允許,那么通知永遠不會出現。你需要詢問用戶是否同意消息通知,我們就從這里開始入手。
打開AppDelegate.swift,導入一個新的框架:
import UserNotifications
這樣就告訴了Xcode,我們將要使用User Notifications框架。
在application(didFinishLaunchingWithOptions)方法中添加以下代碼,就添加在return true語句前面:
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert,.sound], completionHandler: {
granted,error in
if granted {
print("We have permission")
} else {
print("Permission deied")
}
})
回憶一下,application(didFinishLaunchingWithOptions)是在app啟動時被調用。它是app的準入點。是你在app啟動后要做某些操作時的最佳代碼位置。
因為你僅僅是使用一下本地通知,所以在這里用來請求用戶許可是最合適的。
你告訴了iOS這個app想要發送類型為“alert”的通知,并且附帶一個聲音效果。稍候,你會將這段代碼移入更加恰當的地方。
用點號開頭的東西
在我們的app中,見過了類似于.none, .checkmark, .subtitle以及現在的.alert和.sound。它們是枚舉符號。
一個枚舉enumeration,或者簡寫為enum,是一種數據類型,它由一系列符號和它們對應的值的列表組成。
例如UNAuthorizationOptions枚舉包含以下符號:
.badge
.sound
.alert
.carPlay
你可以把它們整合到一個數組中,來定義app發送給用戶的消息種類。這里你使用語句[.alert,.sound]組合了alert和sound。
在那里使用了枚舉是非常容易被識別的,因為它們名稱前面都有一個點。這是一種速寫的方法,它的完整寫法其實是:
UNAuthorizationOptions.alert,UNAuthorizationOptions.sound
幸運的是Swift是非常聰明的,它可以識別出.alert和.sound是來自UNAuthorizationOptions,這為你省了不少事。
運行app,這時你應該會得到一個許可請求的彈窗。
點擊Allow,那么下次app就不會再進行詢問了。iOS會記住第一次的結果。
如果你不小心點擊了Don't,也沒關系,你可以重置模擬器恢復這個彈窗,或者在app的setting中進行設置,就和其他app一樣。
中斷app運行,在didFinishLaunchingWithOptions方法中添加以下代碼:
let content = UNMutableNotificationContent()
content.title = "Hello"
content.body = "I am a local notifcation"
content.sound = UNNotificationSound.default()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
let request = UNNotificationRequest(identifier: "MyNotification", content: content, trigger: trigger)
center.add(request)
這樣就創建了一個新的本地通知。因為你寫了timeInterval: 10,所以這個通知會在app運行10秒后被觸發。
UNMutableNotificationContent描述了本地通知的內容。就是你在alert消息中設置的兩個文本信息,以及聲音。
最后,你將通知添加到UNUserNotificationCenter。這個對象是用于跟蹤所有本地通知并且在時間到了的時候觸發它們。
運行app,在app啟動后,按下Home間,回到手機的主界面(使用模擬器菜單Hardware->Home)
等待10秒,這也許會是你人生中比較漫長的10秒之一,然后你會看到一條彈出消息,當然還伴隨著一個提示音。
這就是本地通知,酷嗎?