你會否好奇,為什麼Force Touch 在iPhone上面突然改名叫3D Touch?不用奇怪,你也不是第一個提出疑問的人。不久之前Craig Federighi(註:蘋果公司軟體工程高階副總裁,主管iOS軟體及Mac軟體),很顯然的也搞不清楚這個這個技術應有的名子,所以在產品發表會的簡報上面,介紹這個新技術的時候提到的3D Touch,但這名詞卻從此一鳴驚人。其實叫做Force Touch 本身也沒有什麼問題啦?只我們已經有太多星際大戰相關的笑話了不是嗎?
雖然在名詞上我們做了一些探討跟著墨,也知道Force Touch跟3D Touch在技術上的本質是相同的。但在實際的應用上3D Touch跟Force Touch 仍然是有區別的!很顯然的3D Touch 在偵測按壓力道的能力比起Force Touch更為敏銳。相較於Force Touch,3D Touch在分辨按壓力度的基礎上可以依照不同的按壓力道加以辨識並定義出更多的級別。
雖然這個觸控方式的革新對許多人來說看起來也許沒那麼重要,但它確實能它允許開發者在iPhone上做出許多更精確的測量跟應用。我們來看看這個叫做Gravity的App,他將iPhone轉換成了支援Force Touch的數位電子秤。雖然他目前在App Store上架的提交申請上,尚被Apple以不明的原因所拒絕。但就創意的角度來說它真的很完美,同時這App也向你展示了3D Touch 是如何運作的,心動了嗎?我們也來打造類似的App吧。
開始打造App
首先, 下載這個我已經做好的專案樣板。基本上,這只是一個給iPhone使用的空白SingleView樣板。在這個樣版裡面,我建立了(UILabel
和UIImage
) 並且已經在ViewController.swift
設定好了IBOutlets關聯。
我們的應用程式的設計相當簡單:由一個視圖控制器跟兩個標籤構成:一標籤個作為標題使用,另一個標籤將顯示在我們在iPhone上面的按壓力道的百分比。
我們開始coding吧!在iPhone 6s 和6s Plus的開發上,UITouch
物件有兩個新的稱之為:force
和maximumPossibleForce
的CGFloat
屬性。其中force
描述了一個觸摸行為的正常力度, 在正常的觸摸行為下平均值會是1.0。maximumPossibleForce
表示每個觸摸行為下面的可能出現的最大力度。
每當用戶觸摸螢幕上的任何物件,touchesBegan
的方法將會首先被呼叫、緊接著被呼叫的方法是touchesMoved
(如果使用者在屏幕上移動一個或多個手指,然後touchedCancelled
或touchesEnded
會依照實際的情況被呼叫。) 為了達成我們想要的目的,touchesMoved
是我們唯一需要的呼叫的方法(method)。
touchesMoved
方法內有兩個參數:touches
和event
。touches
是一個UITouch
物件的集合Set(不同對象的無序集合)。在實際的開發應用上,每一個touch『應該』只會有一個被觸摸的對象,但我們不能100%的確保這樣的事情會一直發生。(比方說我們在設計UI的時候不小心設定了一個觸控區域,但這觸控區域裡面不小心疊放了了兩個以上元件)。所以我們強烈的建議透過使用optional binding來檢查touches.first
是否為nil
來避免一些不必要的狀況。(註:touches.first
是在touches集合內的第一個被觸控的物件)。
所以我們在ViewController.swift
加入以下的方法:
override func touchesMoved(touches: Set, withEvent event: UIEvent?) {
if let touch = touches.first {
if #available(iOS 9.0, *) {
if traitCollection.forceTouchCapability == UIForceTouchCapability.Available {
// 3D Touch capable
}
}
}
}
在if
語句內,我們先檢查設備是否具備3D觸控能力。如果你正在做這個 project 是為了有趣好玩,這部分是可以選擇性的忽略,你可以不要添加這樣的判斷。如果你打算把應用程序發表到App Store並上架,我們認為這個檢查是必要的,因為像iPhone 6及更早以前的設備是不支援3D觸控的,我們可能會需要排除這些設備。
請注意,我還另外檢查設備上的作業系統是否運行iOS 9.0或以上的版本。我的做法,是透過在Swift 2.0中引入的新#available
語法。如果你想了解更多關於新的Swift 2.0 相關得訊息,我建議你閱讀這篇文章。同樣,這檢查也是選擇性的,如果你的最後發布的目標鎖定給iOS 9.0或以上的設備來使用,你可以選擇性的忽略它。
要獲得觸控壓力的百分比,我們可以很簡單的透過取得按壓時的最大力道來計算出最後的結果。(i.e. touch.maximumPossibleForce
)就是這次觸摸可能最大的按壓力道。最後更新標籤內的文字。
所以我們改寫之前的方法變成這樣:
override func touchesMoved(touches: Set, withEvent event: UIEvent?) {
if let touch = touches.first {
if #available(iOS 9.0, *) {
if traitCollection.forceTouchCapability == UIForceTouchCapability.Available {
// 3D Touch capable
let force = touch.force/touch.maximumPossibleForce
forceLabel.text = "\(force)% force"
}
}
}
}
如果你在iPhone 6s/6s Plus上執行這個App,他應該會顯示你按壓螢幕力道的百分比。然而我們這邊還想要做一些調整,你可能會想要把手機上面取得的公克數也顯示在iPhone上。根據 Ryan McLeod的文件,感測器最大的回傳重量是385克,超過的重量將會被忽略。所以,maximumPossibleForce
可以透過一些計算換算成385克(約3.8牛頓)。 通過簡單的計算,你可以以百分比的力量轉化為公克。所要做的就是回傳我們收到的力量並乘以385
,就會得到目前按壓力道的公克值。另外針對超過385克以上的按壓力道,我們最後會改變標籤的顯示,變成類似385克+
得顯示方式 。
現在,使用下面的代碼來更新我們先前已經寫好的方法:
override func touchesMoved(touches: Set, withEvent event: UIEvent?) {
if let touch = touches.first {
if #available(iOS 9.0, *) {
if traitCollection.forceTouchCapability == UIForceTouchCapability.Available {
if touch.force >= touch.maximumPossibleForce {
forceLabel.text = "385+ grams"
} else {
let force = touch.force/touch.maximumPossibleForce
let grams = force * 385
let roundGrams = Int(grams)
forceLabel.text = "\(roundGrams) grams"
}
}
}
}
}
Cool! 你已完成了一個非常簡單的數位電子秤App了。
現在,這個App在我們結束使用的還不會將重量歸零,我們希望在完成觸摸行為,當手指離開螢幕之後能將重量的標籤歸零。所以透過下面代碼來實現 touchesEnded
的方法來,使得標籤可以重置。
override func touchesEnded(touches: Set, withEvent event: UIEvent?) {
forceLabel.text = "0 gram"
}
主螢幕的Quick Actions
另一個關於3D Touch上很棒的應用是Quick Actions在主螢幕(home screen)上。 Quick actions提供一個快速且便捷的方式可以讓他們在可以快速的在App的功能裡面快速的跳轉。你只要在一個App Icon很用力按壓你就可以看到這個快結方式。由於發表了3D Touch的方式, Twitter 、 Instagram 和一些其他的Apple apps 也陸續的更新他們的軟體並使用這個新功能。.
讓我們為我們的scale app新增一個Quick Action吧。我們要新建的功能是讓使用者可以透過Quick Action來開啟我們的App,並且有著藍色的背景跟白色的內文背景。要新增Quick Actions,我們得先開啟專案內的info.plist
(點選Scale workspace
在project navigator,接著選擇Scale target
並且跳到Info分頁)。 在檔案中,添加UIApplicationShortcutItems
的陣列。每個陣列裡面的元素都是由Quick Action的屬性所構成:
UIApplicationShortcutItemType
(必填):這是Quick Action 的辨識字串。請注意,此字串必須是唯一的,且為本app專用的(不可以跟其他的App重複)。我們提供一個小建議:你可以使用bundle ID作為UIApplicationShortcutItemType
的前綴字串或使用其他如app的唯一識別字串。UIApplicationShortcutItemTitle
(必填):這是Quick Action的標題字串,並會顯示給使用者看到。舉例來說我們可以設定為“顯示拍攝最後一張照片”。UIApplicationShortcutItemSubtitle
(選填):這是Quick Action在Title下方的說明字串。舉例來說我們可以顯示“昨天最後拍攝的照片”。如果你想要新增icon到的Quick Action,你有兩種方法可以實現:可以使用Apple內建的系統icon或者使用自己的自定義的icon。UIApplicationShortcutItemIconType
(選填): 這是Quick Action的IconType字串,如果你想要你定義的Quick Action顯示的為Apple內建的系統icon,可以透過這個參數來指定你想顯示的系統Icon類型。UIApplicationShortcutItemIconFile
(選填): 這是Quick Action的IconFile字串,用來指定你想在Quick Action顯示的客製化Icon名稱,這個icon的尺寸為35×35。並且如果設定了本設定值,則前面提到的UIApplicationShortcutItemIconType將會被忽略而不產生作用。UIApplicationShortcutItemUserInfo
(optional): 一個 Dictionary 可以提供更多額外的資訊並傳送給Quick Action做更多的應用與處理。
在這個陣列裡面,我們定義了四個”OpenBlue” Quick Action的設定值。所以你的info.plist
看起來應該如下所示:
$(PRODUCT_BUNDLE_IDENTIFIER)
而不是com.appcoda.Scale
或是任何所使用的bundle ID。這是出自於安全考量。 如果因為任何的理由,我在General裡面更改了bundle ID,整個project將會受影響且Bundle ID 將會在所有的地方變更。此外,我將要手動的去更改每個將會變動的地方。在你的info.plist 檔案內,你可以看到很多Bundle Identifier key 的使用方法,而$(PRODUCT_BUNDLE_IDENTIFIER)
這個Bundle Identifier則描述了你專案內的Bundle ID。我們要做的最後一件事情是實現使用者透過Quick action來開啟這個App的一些處理。 最便捷的方式呼叫AppDelegate.swift
裡面的performActionForShortcutItem
方法來處理。
當被App被以Quick Action的方式啟動的時候,這個方法將會被呼叫。所以我們得添加下面的代碼來來處理quick actions方法:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
// Handle quick actions
completionHandler(handleQuickAction(shortcutItem))
}
我們的期望是透過程序的呼叫來取得一個布林值,而這個布林的值取決我們是否success / failure的使用quick action行為。另外,我們也建立了一個自己定義的handleQuickAction
的函數,來處理快捷方式。
有一個很棒的方式來處理Quick Action,是透過使用建立一個枚舉(enums)描述方式來處理UIApplicationShortcutItemType
的原始值。接著我們宣告來實現handleQuickAction
的方法來處理剛剛建立的enum
,並在App使用quick action來開啟的時候設定被背景顏色為藍色。
enum Shortcut: String {
case openBlue = "OpenBlue"
}
func handleQuickAction(shortcutItem: UIApplicationShortcutItem) -> Bool {
var quickActionHandled = false
let type = shortcutItem.type.componentsSeparatedByString(".").last!
if let shortcutType = Shortcut.init(rawValue: type) {
switch shortcutType {
case .openBlue:
self.window?.backgroundColor = UIColor(red: 151.0/255.0, green: 187.0/255.0, blue: 255.0/255.0, alpha: 1.0)
quickActionHandled = true
}
}
return quickActionHandled
}
這非常簡單不是嗎。如果你現在運行應用程序並使用Quick Action的方式啟動它,背景將會變成藍色的。
一件事要記住
但這裡有一件事情是你必須要記住。在App啟動順序方面,因為Quick Action的加入,這將使得以往我們所知的標準App啟動跟透過Quick Action啟動App變得有所差別。如你所知的,當一個App透過標準的方式來啟動(直接點選App Icon),willFinishLaunchingWithOptions
和didFinishLaunchingWithOptions
方法會被呼叫。但是如果一個App是透過Quick Actions來執行,他將會觸發performActionForShortcutItem
方法並執行。
你可以看到didFinishLaunchingWithOptions
方法的裡面,我們透過下面一行代碼來設置背景顏色為白色。當然,這是當這個App是通過點選App Icon正常啟動的狀況下。
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions:
[NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
self.window?.backgroundColor = UIColor.whiteColor()
return true
}
這就是問題所在: 當你通過quick action啟動應用程序, willFinish
, didFinish
會被呼叫,最後是performActionForShortcutItem
。這樣的做法其實有點笨,因為背景色被首先設置為白色,之後再變成藍色。雖然最後看起來的節果都唷樣,但這不是我們預期的,當用戶透過quick action來啟動個App時我們並不想設置背景顏色為白色。
為了解決這個問題,我們必須在實體化didFinishLaunchingWithOptions
方法中加入條件檢查:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions:
[NSObject: AnyObject]?) -> Bool {
print("didFinishLaunchingWithOptions called")
var isLaunchedFromQuickAction = false
// Check if it's launched from Quick Action
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem {
isLaunchedFromQuickAction = true
// Handle the sortcutItem
handleQuickAction(shortcutItem)
} else {
self.window?.backgroundColor = UIColor.whiteColor()
}
// Return false if the app was launched from a shortcut, so performAction... will not be called.
return !isLaunchedFromQuickAction
}
要確定應用程序是由一個Quick Action啟動,你可以檢查UIApplicationLaunchOptionsShortcutItemKey
這個啟動選項鍵。而UIApplicationShortcutItem
物件可作為啟動選項鍵的值。如果App是由一個Quick Action啟動,我們可以很簡單的呼叫handleQuickAction
並改變背景為藍色。
因為我們已經在didFinishLaunchingWithOptions
處理了quick action要做的所有工作,所以我們不想讓performActionForShortcutItem
再次呼叫handleQuickAction
函式。所以,最後我們返回值為false
,告訴系統不要再度的呼叫performActionForShortcutItem
方法。
好了就是這樣!現在,你可以再次運作並測試我們的應用程序了。Quick Action應該可以運作的很完美了。
結論
3D Touch是一個很棒的方式來提升你的App許多方便性跟便捷性,並且提高App的附加價值。然而你要知道,並非所有的設備都支持3D觸控,儘管這可能會在未來隨著世代的更替或新裝置的普及會逐漸改變,所以在撰寫相關的App的時候你得考慮這點。
在讀完這篇文章後,你應該能夠添加Quick Actions到你自己的iOS應用程序,並用來偵測使用者按壓的力度了。
作為參考,可以下載完整的Xcode項目這裡 。與往常一樣,可以的話針對這封教學或3D Touch請給我一些評論上或分享你的想法。
原文:3D Touch Introduction: Building a Digital Scale App and Quick Actions