從iOS 8 開始Apple引入了擴展(Extension)用於加強系統應用服務和應用之間的交互。它的出現讓自定義鍵盤、系統分享集成等這些依靠系統服務的開發變成了可能。WWDC 2016上衆多更新也都是圍繞擴展這一主題來進行了的,例如開發的Siri、iMessage Apps其實都是依靠擴展來工做的。在最新的Xcode 8 beta中也增長了衆多的Extension 模板幫助開發者更快的實現不一樣類型的擴展。所以今天有必要介紹一下擴展相關的開發內容。html
iOS對於擴展的支持已經由最初的6類到了現在iOS10的19類(相信隨着iOS的發展擴展的覆蓋面也會愈來愈廣),固然不一樣類型的擴展其用途和用法均不盡相同,可是其工做原理和開發方式是相似的。下面列出擴展的幾個共同點:ios
擴展一般展示在系統UI或者其餘應用中,運行應該儘量的迅速而功能單一;swift
因爲目前iOS 10正式版還沒有發佈,官方文檔僅就目前9類擴展作了詳細指導說明,感興趣的話你們能夠前往查看。
官方對於應用擴展的生命週期描述以下圖:數組
一般用戶選擇了一個擴展的操做時宿主會向擴展發出一個請求來啓動此擴展,擴展的生命週期也由此開始(例如用戶在分享菜單中選擇了你的分享擴展),因爲擴展自己由控制器組成,所以此時就會調用相似於viewDidLoad之類的方法進行界面佈局和邏輯處理,執行完相應任務以後應該儘快將控制權交給宿主應用,擴展生命週期結束。緩存
儘管擴展和容器應用的生命週期之間沒有直接關係,可是擴展自己就是做爲容器應用的擴展而存在的,所以擴展和容器應用之間的交互又是不可避免的。一般擴展會經過自定義Scheme的形式來調用容器應用,而容器應用完成響應操做以後經過數據共享將數據共享給擴展來使用。微信
前面說過目前iOS支持19類擴展入口,如今就以Today擴展(也叫作Widget)爲例進行說明,在開始以前先對Today擴展有一個簡單的認識,下圖是微博、墨跡天氣、網易雲音樂的的Today擴展截圖,微博擴展能夠用來發送微博、查看更新,墨跡天氣則用來展現今日和明日的天氣,網易雲音樂則是推薦一些相關的歌單、專輯。網絡
咱們今天的例子將利用Today擴展實現一個簡單的「to do list」查看功能,在容器應用ToDoList中能夠增長和刪除待辦事項,而Today插件則展現最新的幾條待辦事項,若是沒有待辦事項則展現添加按鈕,點擊添加或列表則導航到ToDoList應用。應用的主界面和Today擴展最終截圖以下:session
在開發以前首先思考一下要實現一個這樣的ToDoList擴展須要注意哪些問題:app
NS_EXTENSION_UNAVAILABLE
,其實思考一下也是合理的,擴展中的UIApplication是宿主應用並不是容器應用,若是開發人員直接操做Today的宿主應用豈不危險?)?這幾個問題在下面的演示中將逐一解答,首先要簡單實現一個ToDoList應用,這裏就不得不考慮第一個問題,怎麼樣存儲數據才能保證後面的擴展開發可以正常訪問這些數據。事實上iOS 8 新增了App Groups
功能用於實現應用之間的數據共享問題(固然這個功能在OS X如今應該叫作macOS,早就出現了),在Xcode中開啓並設置App Groups,Xcode - Capabilities中找到App Groups打開並添加一個名爲「group.com.cmjstudio.todolist」組(注意組名稱必須以group
開頭,這一步操做至關於在iOS的開發證書中啓用App Groups服務並註冊分組,同時在Xcode - Build Settings - Code Signing Entitlements中配置對應的分組配置文件。從Xcode 8開始,證書配置將變得異常簡單,不用過多的登陸開發者帳號管理證書)。添加完分組以後將在項目中生成一個ToDoList.entitlements
文件(這其實就是一個xml配置文件,事實上往後若是添加其餘服務,其配置也會添加到這個文件中)。既然App Groups和開發證書相關,也就是說同一個開發證書下發布的應用只要配置了相同的組就能夠實現數據的共享。App Groups支持的經常使用數據共享包括NSUserDefaults、NSFileManager、NSFileCoordinator、NSFilePresenter、UIPasteboard、KeyChain、NSURLSession等,這裏不妨將數據存儲到NSUserDefaults中。
下面將快速建立一個簡單的ToDoList,使用UITableView進行展現,數據的操做邏輯放到TaskService.swift中:異步
import Foundation let TaskServiceDataKey = "TaskServiceData" public struct TaskService { public static let ToDoListGroupName = "group.com.cmjstudio.todolist" public static func addItem(title:String){ let userDefault = NSUserDefaults(suiteName: TaskService.ToDoListGroupName) var items = self.getItems() items.append(title) userDefault?.setObject(items, forKey: TaskServiceDataKey) userDefault?.synchronize() } public static func removeItem(title:String){ let items = self.getItems() let newItems = items.filter { (item) -> Bool in item != title } let userDefault = NSUserDefaults(suiteName: TaskService.ToDoListGroupName) userDefault?.setObject(newItems, forKey: TaskServiceDataKey) userDefault?.synchronize() } public static func getItems() -> [String]{ let userDefault = NSUserDefaults(suiteName: TaskService.ToDoListGroupName) var tasks = [String]() if let array = userDefault?.stringArrayForKey(TaskServiceDataKey) { tasks = array } return tasks } }
實現了ToDoList以後接下來就是進行擴展開發。首先在項目中添加一個名爲「ToDoListTodayExtension」的Today Extension類型的Target,並選擇激活這個Scheme以便後面測試。而後能夠看到在項目根目錄建立了一個「ToDoListTodayExtension」文件夾,它包含一個TodayViewController、MainInterface.storyboard和一個info.plist。在info.plist中定義了擴展入口點「com.apple.widget-extension」同時指定了MainInterface做爲展現入口,固然很容易就能夠猜到TodayViewController是MainInterface.storyboard中控制器對應的class。TodayViewController.swift是一個UIViewController控制器:
class TodayViewController: UIViewController, NCWidgetProviding { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view from its nib. } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } func widgetPerformUpdate(completionHandler: ((NCUpdateResult) -> Void)) { // Perform any setup necessary in order to update the view. // If an error is encountered, use NCUpdateResult.Failed // If there's no update required, use NCUpdateResult.NoData // If there's an update, use NCUpdateResult.NewData completionHandler(NCUpdateResult.newData) } }
能夠看出這個類還實現了NCWidgetProviding
協議,其中最重要的兩個方法就是用於自定義邊距的widgetMarginInsets
方法和更新插件的widgetPerformUpdate
方法。此時若是編譯運行(注意以前已經激活擴展的sheme,也就是從擴展運行)而且選擇宿主程序Today就會看到一個帶有「Hello World」字樣的擴展,這其實就是MainInterface的默認佈局(注意此時在Products中會生成一個ToDoListTodayExtension.appex就是對應的擴展包)。
接下來就能夠進行擴展的界面佈局了,你能夠選擇Storyboard或者code佈局,須要注意的是Today擴展的寬度永遠都會是屏幕寬度,佈局時不須要過多關心,而高度則須要經過調整TodayViewController的preferredContentSize來完成。
另外,這裏咱們須要思考一個問題:如何使用以前容器應用中編寫的TaskService.swift,由於它已經包含了數據的讀取方法,咱們沒有必要在擴展中再實現一遍相同的操做。根據前面文章中關於Swift的命名空間和做用域的介紹應該能夠想到將其提取到一個公共的命名空間中,而命名空間的實現一般是使用一個target實現的,這也正是官方推薦的作法。建立一個framework類型的Target而且將TaskSerivce.swift放到這個framework中,ToDoList和ToDoListTodayExtension均使用這個framework(在項目中增長一個名爲「ToDoListKit」的Cocoa Touch Framework類型的Target,同時注意將TaskService.swift和對應的類和方法聲明爲公共方法,在使用TaskService的中使用import ToDoListKit
導入這個Framework)。
在TodayViewController中增長UITableView和UIButton,當沒有數據時展現UIButton,點擊按鈕能夠經過extensionContext
跳轉到容器應用並增長新的代辦事項,前面提到過在擴展中是沒法直接利用UIApplication打開應用的由於擴展在宿主應用中運行,可是在控制器中增長了一個NSExtensionContext
類型的上下文來管理擴展操做,這樣也就解決了上面說到的第三個問題。擴展的高度則經過preferredContentSize
來進行設置,而後根據記錄數動態設置其高度,沒有數據則設置爲一行記錄的高度來展現添加按鈕。
import UIKit import NotificationCenter import ToDoListKit private let TodayViewControllerMaxCellCount = 3 private let TodayViewControllerCellHeight:CGFloat = 44.0 private let TodayViewControllerTableViewCellKey = "TodayViewControllerTableViewCell" class TodayViewController: UIViewController, NCWidgetProviding,UITableViewDataSource,UITableViewDelegate { override func viewDidLoad() { super.viewDidLoad() self.setup() self.loadData() } func widgetPerformUpdate(completionHandler: ((NCUpdateResult) -> Void)) { // Perform any setup necessary in order to update the view. // If an error is encountered, use NCUpdateResult.Failed // If there's no update required, use NCUpdateResult.NoData // If there's an update, use NCUpdateResult.NewData self.loadData() completionHandler(NCUpdateResult.NewData) } func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets { return UIEdgeInsetsZero } // MARK: - UITableView數據源和代理方法 func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.data.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier(TodayViewControllerTableViewCellKey) if cell == nil { cell = UITableViewCell(style: .Subtitle, reuseIdentifier: TodayViewControllerTableViewCellKey) cell.textLabel?.textColor = UIColor.whiteColor() cell.detailTextLabel?.textColor = UIColor.whiteColor() } let item = self.data[indexPath.row] cell.imageView?.image = UIImage(named: "calendar") cell.textLabel?.text = "Date & Time" cell.detailTextLabel?.text = item return cell } // MARK: - 事件響應 @IBAction func addButtonClick(sender: UIButton) { let url = NSURL(string: "todolist://add") self.extensionContext?.openURL(url!, completionHandler: nil) } // MARK: - 私有方法 private func setup(){ self.addButton.layer.cornerRadius = 3.0 self.tableView.rowHeight = TodayViewControllerCellHeight } private func loadData(){ self.data = [String]() let items = TaskService.getItems() // 控制最多顯示條數 for i in 0..<items.count { self.data.append(items[i]) if i >= TodayViewControllerMaxCellCount { break } } self.layoutUI() self.tableView.reloadData() } private func layoutUI(){ if self.data.count > 0 { self.addButton.hidden = true self.tableView.hidden = false self.preferredContentSize.height = CGFloat(self.data.count) * TodayViewControllerCellHeight } else { self.addButton.hidden = false self.tableView.hidden = true self.preferredContentSize.height = TodayViewControllerCellHeight } } // MARK: - 私有屬性 @IBOutlet weak var tableView: UITableView! @IBOutlet weak var addButton: UIButton! private var data:[String]! }
注意:官方已經明確指出Today擴展不支持UIScrollView滾動,建議顯示最新數據或者更多的數據經過分頁實現。
此外在擴展中使用了一個日曆圖標calendar
,而在容器應用ToDoList中這個圖片已經存在於Assets.xcassets
中,但在擴展中沒辦法直接訪問容器應用中的資源。一種解決方式是直接往擴展中添加一個calendar
圖標;另外一種就是直接選擇擴展這個Target—Build Phases—Copy Bundle Resources 而後添加容器中的資源。這麼作的好處是儘管實際運行中存在兩份資源,可是開發過程當中只須要維護一份。在ToDoListTodayExtension中咱們選擇第二種方式(固然若是你確實須要進行資源文件共享而不是使用兩份資源,你也能夠經過NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier(gropuName)
來讀取容器應用中的文件,但在這裏不太適合)。
固然接下來就是給ToDoListTodayExtension擴展配置App Groups,配置方法相似,惟一須要注意的是Group名稱必須和前面保持一致,設置爲「group.com.cmjstudio.todolist」。最後運行結果以下:
前面說過如今iOS支持的擴展類型愈來愈多,給開發者提供了更多的交互方式,除了Today擴展以外分享擴展應該是另外一個比較常見得擴展類型,好比經常使用的QQ、微信、微博等都實現了分享擴展。下面再以一個分享擴展爲例簡單介紹一下這種擴展的開發過程。
假設如今有一個圖片社區應用「MyPicture」,用戶能夠分享各類圖片和攝影做品,在系統相冊中用戶能夠選擇本身喜歡的圖片直接分享到「MyPicture」。關於應用和擴展的建立過程再也不贅述,假設已經建立完應用擴展「MyPictureShareExtension」。默認狀況下分享擴展編輯界面以下:
首先這個擴展的info.plist相比Today Extension多了一些配置選項,例如能夠編輯擴展名稱、語言等。這裏進行設置以下:
Bundle display name
名稱爲「MyPicture」。NSExtensionActivationRule
,增長最大支持分享圖片數NSExtensionActivationSupportsImageWithMaxCount
爲9,若是超過九張則不顯示分享按鈕,同時此項配置也確保在網頁分享、文件分享中再也不出現「MyPicture」擴展。更多配置參加Apple官方文檔 (SystemExtensionKeys)[https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/SystemExtensionKeys.html#//apple_ref/doc/uid/TP40014212-SW2],事實上激活規則還支持更爲複雜的斷言配置。
其次,Share Extension對應的控制器繼承於SLComposeServiceViewController
,其中最經常使用的方法和屬性以下:
charactersRemaining
。下圖是咱們即將實現的最終效果,點擊Category
能夠選擇圖片分類:
這裏重點關注圖片的發送過程,在Share Extension中是沒法直接獲取到圖片的(由於咱們分享的內容多是圖片,也多是網頁、視頻等,所以SLComposeServiceViewController
也不太可能會直接提供圖片訪問接口),全部的訪問數據包含進在extensionContext
的inputItems
中,這是一個NSInputItem
類型的數組。每一個NSInputItem
都包含一個attachments
集合,它的每一個元素都是NSItemProvider
類型,每一個NSItemProvider
就包含了對應的圖片、視頻、連接、文件等信息,經過它就能夠獲取到咱們須要的圖片資源。可是須要注意,經過NSItemProvider
進行資源獲取的過程較長,同時也會阻塞線程,若是直接在didSelectPost
方法中獲取圖片資源勢必形成用戶長時間等待,比較好的體驗是在presentationAnimationDidFinish
方法中就異步調用NSItemProvider
的loadItemForTypeIdentifier
方法進行圖片資源加載,並存儲到數組中以便在didSelectPost
方法中使用。
此外,爲了獲取更好的用戶體驗,圖片的上傳過程一樣須要放到後臺進行,首先想到的就是使用NSURLSession的後臺會話模式,值得一提的是在這個過程當中必須指定NSURLSessionConfiguration
的sharedContainerIdentifier
,由於上傳的過程當中首先會將資源緩存到本地,而擴展是沒辦法直接訪問宿主應用的緩存空間的,配置sharedContainerIdentifier
以便利經過App Group
使用容器應用的緩存空間。具體實現以下:
import UIKit import Social import MobileCoreServices import Alamofire private let ShareViewControllerContentTextMax = 200 private let ShareViewControllerDefaultCategoryTitle = "Category" class ShareViewController: SLComposeServiceViewController { override func viewDidLoad() { super.viewDidLoad() self.imageDatas = [NSData]() self.charactersRemaining = ShareViewControllerContentTextMax self.placeholder = "Please enter description" } // 顯示分享界面,在此時則異步加載圖片到self.images,避免在didSelectPost中再加載圖片影響體驗 override func presentationAnimationDidFinish() { // 用戶輸入項 guard let extensionItem = self.extensionContext?.inputItems.first else { return } guard let attachments = extensionItem.attachments as? [NSItemProvider] else { return } for attachment in attachments { let imageType = kUTTypeImage as String if attachment.hasItemConformingToTypeIdentifier(imageType) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { attachment.loadItemForTypeIdentifier(imageType, options: nil, completionHandler: { (coding, error) in if error == nil { guard let fileURL = coding as? NSURL else { return } guard let data = NSData(contentsOfURL: fileURL) else { return } self.imageDatas.append(data) // guard let image = UIImage(data: data) else { return } // self.images.append(image) } }) }) } } } // 內容驗證,輸入過程當中會不斷調用此方法 override func isContentValid() -> Bool { if let text = self.contentText { let len = text.characters.count if len > ShareViewControllerContentTextMax { return false } self.charactersRemaining = ShareViewControllerContentTextMax - len } return true } // 發送分享內容 override func didSelectPost() { // 上傳圖片和編輯內容、分類 self.upload() // 通知host app 操做完成 self.extensionContext!.completeRequestReturningItems([], completionHandler: nil) } // 自定義分享編輯界面sheet override func configurationItems() -> [AnyObject]! { return [self.categorySheetItem] } // MARK: - 私有方法 private func selectCategory(){ let temp = CategoryTableViewController(style: .Grouped) temp.selectedCategory = self.categorySheetItem.title temp.selectedCategoryHandler = { [weak self]category in guard let weakSelf = self else { return } weakSelf.categorySheetItem.title = category } self.pushConfigurationViewController(temp) } private func upload(){ let urlStr = "http://requestb.in/v34h3lv3" self.manager.upload(.POST,urlStr, multipartFormData: { (formData) -> Void in for data in self.imageDatas { formData.appendBodyPart(data: data, name: "image", mimeType: "image/jpeg") } // add parameter if self.contentText != nil { formData.appendBodyPart(data: self.contentText.dataUsingEncoding(NSUTF8StringEncoding)!, name: "content") } if self.categorySheetItem.title != ShareViewControllerDefaultCategoryTitle { formData.appendBodyPart(data: self.categorySheetItem.title.dataUsingEncoding(NSUTF8StringEncoding)!, name: "category") } }){ encodingResult in switch encodingResult { case Manager.MultipartFormDataEncodingResult.Success(_, _, _): debugPrint("request") case let Manager.MultipartFormDataEncodingResult.Failure(error): debugPrint(error) } } } // MARK: - 私有屬性 private lazy var categorySheetItem:SLComposeSheetConfigurationItem = { let temp = SLComposeSheetConfigurationItem() temp.title = ShareViewControllerDefaultCategoryTitle temp.tapHandler = self.selectCategory return temp }() // 自定義上傳配置,在後臺上傳避免阻塞UI,注意:因爲NSURLSession上傳過程當中須要先限緩存到本地可是擴展應用自己是沒辦法使用Host App緩存控件的,所以注意設置sharedContainerIdentifier,使用容器應用的空間 private lazy var manager:Alamofire.Manager = { let configName = "com.cmjstudio.mypicture.backgroundsession" let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configName) configuration.sharedContainerIdentifier = "group.com.cmjstudio.mypicture" // configuration.HTTPAdditionalHeaders = Alamofire.Manager.defaultHTTPHeaders let manager = Alamofire.Manager(configuration: configuration) manager.startRequestsImmediately = true manager.backgroundCompletionHandler = { debugPrint("completed.") } return manager }() private var imageDatas:[NSData]! }
注意:網絡操做部分這裏直接選擇
Alamofire
進行上傳,若是想本身實現圖片上傳,能夠查看iOS開發系列--網絡開發。另外,若是須要自定義分享編輯界面可讓ShareViewController
繼承自UIViewController
,具體細節參見Apple指導文檔
因爲使用了NSURLSession的後臺會話,當執行完相關操做後會調用容器應用的application(application, identifier, completionHandler)
方法,若有必要有些操做能夠在此方法中進行處理。
本文着重介紹了Today Extension和Share Extension兩種擴展,其實擴展是比較大的一塊內容,各種擴展實現方法也不盡相同,可是其生命週期、核心原理是相似的,本文也再也不一一探討。相信iOS 10中更加豐富的擴展類型也會讓應用之間的交互愈來愈豐富,有興趣的朋友也能夠訪問下載Xcode 8 beta版進行探索,有時間咱們也會寫一篇關於Intent Extension、Message Extensiond等新增擴展應用的文章。