一. 環(huán)境
xcode:8.3.3
模擬器:iPhone 7
系統(tǒng):iOS 10
二. 新建項目
xcode -> File -> new -> Target -> iMessage Extension
三. 處理警告
新創(chuàng)建的項目,還啥也沒做就一堆警告,尷尬。一共三個
1. 警告一
objc[55816]: Class PLBuildVersion is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks/PhotoLibraryServices.framework/PhotoLibraryServices (0x124fcb6f0) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks/AssetsLibraryServices.framework/AssetsLibraryServices (0x124b59cc0). One of the two will be used. Which one is undefined.
這個告警的字面意思就是說PLBuildVersion這個類在AssetsLibraryServices.framework和PhotoLibraryServices.framework中都定義了。一般好像不會有問題,但根據(jù)stackoverflow上的描述,有人崩在這里了。so,警告是無法解決了,但別人提出了一個防止崩潰的方法,如下圖:

其實不用加這么多,重點是添加權(quán)限描述。
2. 警告二
2017-08-03 18:34:47.005 iMessageExtension[55816:2564570] Failed to inherit CoreMedia permissions from 55814: (null)
問題原因:I have no idear
解決方法:I have no idear
3. 警告三
2017-08-03 18:34:47.096382+0800 iMessageExtension[55816:2564531] [App] if we're in the real pre-commit handler we can't actually add any new fences due to CA restriction
2017-08-03 18:34:47.102420+0800 iMessageExtension[55816:2564531] [App] if we're in the real pre-commit handler we can't actually add any new fences due to CA restriction
無法解決問題的解決方法:如下圖,將NSExtensionPrincipalClass添加上info.plist->NSExtension中就好了。此時NSExtension中就同時指定了NSExtensionPrincipalClass和NSExtensionMainStoryboard。對于我的環(huán)境而言,警告的確是沒有了。這里有一個問題,那就是圖中標(biāo)識的1和2不能共存,共存之后,UI界面無法改變~!所以需要刪除第2項。尷尬,iMessage感覺好雞肋。

新建的項目,什么也沒干就冒出來三個警告,還都無能為力~!WTF
四. 修改配置項,使用純代碼開發(fā)
創(chuàng)建完extension后,默認(rèn)使用sb進行UI開發(fā),通過修改info.plist來修改,如下圖。


iMessage extension中沒有appdelegate,但有一個類似的:MSMessagesAppViewController。我們新建一個BGMessagesViewController繼承這個類。
在解決警告時我提到過一個問題:添加NSExtensionPrincipalClass同時刪除NSExtensionMainStoryboard時,會存在問題。
問題描述:如果創(chuàng)建Extension時,選擇的語言是swfit,那么這樣做會崩潰。
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** setObjectForKey: object cannot be nil (key: C97B1A7E-F753-45D5-8F54-CF7DA2D23FF5)'
*** First throw call stack:
(
0 CoreFoundation 0x000000010c924b0b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x0000000108a40141 objc_exception_throw + 48
2 CoreFoundation 0x000000010c83f682 -[__NSDictionaryM setObject:forKey:] + 1042
3 Foundation 0x0000000108749d5e -[_NSExtensionContextVendor _setPrincipalObject:forUUID:] + 106
4 Foundation 0x00000001087492ff __105-[_NSExtensionContextVendor _beginRequestWithExtensionItems:listenerEndpoint:withContextUUID:completion:]_block_invoke + 804
5 libdispatch.dylib 0x000000010d910585 _dispatch_call_block_and_release + 12
6 libdispatch.dylib 0x000000010d931792 _dispatch_client_callout + 8
7 libdispatch.dylib 0x000000010d917237 _dispatch_queue_serial_drain + 1022
8 libdispatch.dylib 0x000000010d91798f _dispatch_queue_invoke + 1053
9 libdispatch.dylib 0x000000010d917d31 _dispatch_queue_override_invoke + 374
10 libdispatch.dylib 0x000000010d919899 _dispatch_root_queue_drain + 813
11 libdispatch.dylib 0x000000010d91950d _dispatch_worker_thread3 + 113
12 libsystem_pthread.dylib 0x000000010dcc55a2 _pthread_wqthread + 1299
13 libsystem_pthread.dylib 0x000000010dcc507d start_wqthread + 13
)
libc++abi.dylib: terminating with uncaught exception of type NSException
問題分析:主要原因是因為swfit新的特性:module??梢钥聪旅娴睦樱?/p>
swift項目
(lldb) po self
<iMessageDemo.ViewController: 0x7f89d0508ad0>
oc項目
(lldb) po self
<ViewController: 0x7fdf86e04330>
可以看到,對于swift項目,在查找一個類的時候需要知道類所在的module。所以上面的crash是因為找到不對應(yīng)的類文件。
解決方案:根據(jù)上述分析,在設(shè)置類的時候,我們把module加上就好了。如下圖

五. MSMessagesAppViewController中處理會話回調(diào)的方法
willBecomeActive
Called when the extension is about to move from the inactive to active state.
This will happen when the extension is about to present UI.
當(dāng)需要展示UI的時候就會被調(diào)用
Use this method to configure the extension and restore previously stored state.
didResignActive
Called when the extension is about to move from the active to inactive state.
This will happen when the user dissmises the extension, changes to a different
conversation or quits Messages.
翻譯:這函數(shù)會在extension消失、切換會話或者退出消息的時候被調(diào)用。
說人話:
extension消失: (1)手指向下滑動時,imessage功能條回到屏幕最下方時,extension消失;
? (2)在extension狀態(tài),點擊輸入框時,切換到輸入狀態(tài),extension消失;
切換會話或者退出消息:頂部導(dǎo)航返回,退出短信
Use this method to release shared resources, save user data, invalidate timers,
and store enough state information to restore your extension to its current state
in case it is terminated later.sys
我在模擬器上,在extension狀態(tài)直接Home時,crash~! 但之前真機上是沒有問題的~!雞肋
didReceive
Called when a message arrives that was generated by another instance of this
extension on a remote device.
收到相同的extension發(fā)來的消息。這里說“a remote device”,我們知道,iMessage是可以自己給自己發(fā)消息的。當(dāng)自己給自己發(fā)送消息的時候,短信界面有兩個會話,一個是自己收,另一個是自己發(fā)。那么問題來了,這算“a remote device”么~!??
搞笑了,這個方法可以在收到同類extension發(fā)來的消息時被調(diào)用,即使此時這個extension不在活躍狀態(tài)。
Use this method to trigger UI updates in response to the message.
didStartSending
Called when the user taps the send button.
didCancelSending
Called when the user deletes the message without sending it.
Use this to clean up state related to the deleted message.
willTransition
Called before the extension transitions to a new presentation style.
Use this method to prepare for the change in presentation style.
didTransition
Called after the extension transitions to a new presentation style.
Use this method to finalize any behaviors associated with the change in presentation style.
最后這兩上放一起說好了。iMessage的界面有兩種狀態(tài),一種是收縮,另一種是展開。收縮狀態(tài)的大小大概和輸入法鍵盤的大小差不多(這里的大小有問題)。展開狀態(tài)就是全屏了。從開發(fā)過程和使用過程,我發(fā)現(xiàn)收縮狀態(tài)時大小存在一定的問題:即收縮狀態(tài)的大小并不統(tǒng)一。所以,有時會看到extension的應(yīng)用選擇界面存在重疊的情況~!尷尬
六. Sticker
一般直接使用MSStickerBrowserViewController來展示sticker。這個VC就是一個被封裝好了的CollectionViewController,并且每個Cell支持Gif。MSStickerBrowserViewController只對外暴露了dataSourceDelegate。下面給出一個簡單的例子。
import UIKit
import Messages
class BGStickerBrowserViewController: MSStickerBrowserViewController {
var dataSource : Array<MSSticker> = []
override func viewDidLoad() {
super.viewDidLoad()
title = "Sticker"
navigationController?.navigationBar.isHidden = false
view.backgroundColor = UIColor.white
let stickerUrls = Bundle.main.urls(forResourcesWithExtension: ".gif", subdirectory: "")
guard stickerUrls != nil ,(stickerUrls?.count)!>0 else {
return
}
for ele in stickerUrls! {
let sticker = try? MSSticker(contentsOfFileURL: ele, localizedDescription: ele.path)
guard sticker != nil else {
continue
}
dataSource.append(sticker!)
}
}
override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int {
return dataSource.count
}
override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker {
return dataSource[index]
}
}
七. 發(fā)送消息
在MSStickerBrowserViewController中點擊sticker就可以發(fā)送貼紙消息,然后MSStickerBrowserViewController并沒有暴露出相應(yīng)的delegate,所以對于點擊事件后的消息生成我們無能為力。
在iMessage中使用MSConversation來完全掌控消息的構(gòu)建與插入。(注意這里是插入,apple多次強調(diào)的一點:消息最終的發(fā)送權(quán)在用戶手里。extension無權(quán)也無法發(fā)送消息。)下面弄一個簡單的例子來示范一下消息的構(gòu)建與插入。代碼不是很多,重點在第一個函數(shù)。
import UIKit
import Messages
class BGSelfDefineViewController: UIViewController {
//創(chuàng)建消息并插入
func handleSendButtonClick(sender:UIButton) {
//BGConversationManager.shared.appDeleagte這個就是MSMessagesAppViewController的實例。
if let image = createImageForMessage(), let conversation = BGConversationManager.shared.appDeleagte?.activeConversation {
//layout還有很多別的屬性值可以設(shè)置,詳細(xì)請查看文檔
let layout = MSMessageTemplateLayout()
layout.image = image
layout.caption = "Stepper Value"
let message = MSMessage()
message.layout = layout
message.url = URL(string: "emptyURL")
//收起頁面,以展示插入的消息
BGConversationManager.shared.appDeleagte?.requestPresentationStyle(.compact)
conversation.insert(message, completionHandler: { (error) in
print(error ?? "")
})
// 其它的消息類型
// conversation.insert(<#T##message: MSMessage##MSMessage#>, completionHandler: <#T##((Error?) -> Void)?##((Error?) -> Void)?##(Error?) -> Void#>) //發(fā)送自定義消息
// conversation.insertText(<#T##text: String##String#>, completionHandler: <#T##((Error?) -> Void)?##((Error?) -> Void)?##(Error?) -> Void#>) //發(fā)送文本消息
// conversation.insert(<#T##sticker: MSSticker##MSSticker#>, completionHandler: <#T##((Error?) -> Void)?##((Error?) -> Void)?##(Error?) -> Void#>) //發(fā)送sticker消息
// 發(fā)送url: 圖片,音頻,視頻的鏈接,詳細(xì)使用請查看文檔
// conversation.insertAttachment(<#T##URL: URL##URL#>, withAlternateFilename: <#T##String?#>, completionHandler: <#T##((Error?) -> Void)?##((Error?) -> Void)?##(Error?) -> Void#>)
}
}
//將view轉(zhuǎn)換成圖片插入,這不是重點。
func createImageForMessage() -> UIImage? {
let background = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
background.backgroundColor = UIColor.white
let label = UILabel(frame: CGRect(x: 75, y: 75, width: 150, height: 150))
label.font = UIFont.systemFont(ofSize: 56.0)
label.backgroundColor = UIColor.red
label.textColor = UIColor.white
label.text = "1"
label.textAlignment = .center
label.layer.cornerRadius = label.frame.size.width/2.0
label.clipsToBounds = true
background.addSubview(label)
background.frame.origin = CGPoint(x: view.frame.size.width, y: view.frame.size.height)
view.addSubview(background)
UIGraphicsBeginImageContextWithOptions(background.frame.size, false, UIScreen.main.scale)
background.drawHierarchy(in: background.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
background.removeFromSuperview()
return image
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
title = "Self Define"
navigationController?.navigationBar.isHidden = false
view.addSubview(sendButton)
sendButton.widthAnchor.constraint(equalToConstant: 140).isActive = true
sendButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
sendButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
sendButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: -25).isActive = true
}
//在界面止添加一個按鍵,用來觸發(fā)消息的生成與插入
lazy var sendButton : UIButton = {
let view : UIButton = UIButton()
view.backgroundColor = UIColor.white
view.layer.borderColor = UIColor.gray.cgColor
view.layer.borderWidth = 0.5
view.setTitle("Send", for: .normal)
view.setTitleColor(UIColor.black, for: .normal)
view.addTarget(self, action: #selector(handleSendButtonClick(sender:)), for: .touchUpInside)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
}
八. 接收消息
在<五>中我們已經(jīng)提到過didReceive用于接收消息。message中的許多內(nèi)容是在發(fā)送的時候設(shè)置的。相關(guān)屬性查看文檔就好了。
override func didReceive(_ message: MSMessage, conversation: MSConversation) {
// Called when a message arrives that was generated by another instance of this
// extension on a remote device.
// Use this method to trigger UI updates in response to the message.
}
這時只強調(diào)一處(以下內(nèi)容來自官方文檔)
A URL that encodes data to be transmitted with the message.
Encode your application’s data in the URL. For example, you can encode data as key-value pairs in the URL’s query string, as shown below:
guard let components = NSURLComponents(string: myBaseURL) else {
fatalError("Invalid base url")
}
let size = NSURLQueryItem(name: "Size", value: "Large")
let count = NSURLQueryItem(name: "Topping_Count", value: "2")
let cheese = NSURLQueryItem(name: "Topping_0", value: "Cheese")
let pepperoni = NSURLQueryItem(name: "Topping_1", value: "Pepperoni")
components.queryItems = [size, count, cheese, pepperoni]
guard let url = components.url else {
fatalError("Invalid URL components.")
}
message.url = url
The message object is delivered to the extension running on the recipient’s device. The extension can access the session’s current state from the message’s URL property, as shown below:
guard let components = NSURLComponents(url: message.url, resolvingAgainstBaseURL: false) else {
fatalError("The message contains an invalid URL")
}
if let queryItems = components.queryItems {
// process the query items here...
}
If the message is selected on macOS, the system loads the URL in a web browser. The URL should point to a web service that returns a meaningful result based on the encoded data.
The URL property must use an HTTP, HTTPS, or data scheme. Custom app schemes are not supported. Additionally, the URL cannot be longer than 5,000 characters.
By default, this property is set to nil.
需要注意的是黑色加粗的內(nèi)容。
九. 點擊消息
點擊消息的時候MSMessagesAppViewController.activeConversation中的selectedMessage就是被點擊的消息,而消息中的內(nèi)容就是MSMessage對象。
那么如何截獲點擊消息這個事件呢?
消息可以被點擊的前提是MSMessagesAppPresentationStyle處于收縮狀態(tài),而消息被點擊后會進入展開狀態(tài)。所以,點擊消息的截獲就在這里了。
override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
super.willTransition(to: presentationStyle)
// Hide child view controllers during the transition.
removeAllChildViewControllers()
}
override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
super.didTransition(to: presentationStyle)
// Present the view controller appropriate for the conversation and presentation style.
guard let conversation = activeConversation else { fatalError("Expected an active converstation") }
presentViewController(for: conversation, with: presentationStyle)
}
十. imessage與主app之間的數(shù)據(jù)通信
這個文章感覺寫得不錯http://blog.csdn.net/shengpeng3344/article/details/52190997 我就不多說了。
十一. 注意事項
- iMessage需要在compact和expended之間來回切換,所以介意不要使用frame來構(gòu)建布局,使用自動布局可以省好很多工夫。
- compact的高度在同一設(shè)備上可能會變化,有時候系統(tǒng)頁面也會出現(xiàn)重疊現(xiàn)象。
- 忘記了