还记得去年的 WWDC 吗?当人们都在关注新出来的 Swift3、SiriKit、User Notifications 时,我却关注起了它,没错,就是 iMessage Apps。那么它到底是什么东西呢?
它是什么
在 WWDC 的 Session 204 中如此描述它:
You will be able to write apps that run within the context of the Messages application.
直白点说就是我们可以通过编写 iMessage App 来内嵌到系统的 Message 中,并进行各种交互。用户通过系统的 Message 入口来使用自己开发的 iMessage App 丰富和扩展用户在消息互动过程中的更多乐趣和功能。
目前 iMessage Apps 主要为用户提供三种内容形式:
- 第一种是交互式信息内容(Interactive message),发送者和接收者相互产生各种有趣交互效果。
- 第二种是贴图(Stickers)式的信息内容,通常贴图以静态图片、动态图片为主。
- 第三种是照片、视频、链接等样式的信息内容。
好了,既然我们知道了 iMessage Apps 是什么,那么我们又该如何去开发呢?我们接着往下说:
如何开发
开发须知
- 依赖的 Message.framework 框架(仅仅支持iOS10以及更高版本)。
- 支持使用 Apple Pay支付、In-app purchase内购、Camera access相机访问。
- 开发的 iMessage Apps 也仅仅只在 iOS 10 以及更高版本中运行。
- 目前在 WatchOS 平台上可以接收和发送,Mac OS 平台上则支持可以接收的。
- Sticker文件大小最大不超过 500 KB。
- 如果是动态Stickers的话,仅仅支持APNG、GIF 两种格式。
- Sticker支持这些格式:PNG、APNG、JPEG、GIF,当然苹果最推荐还是APNG、PNG。
- 当然了,到目前为止,iMessage App 的定位还是 Application 的Extension。如下图所示:
开发 Sticker Pack Application
Sticker Pack Application 是一个由 Apple 提供的快速开发 iMessage App 模板,大家使用它来开发时仅仅只需要根据要求指定 Stickers.xcstickers 中的图片即可。
- 首先打开 Xcode,选择 Sticker Pack Application ,创建之后,会看到当然项目中仅仅包含一个 Stickers.xcstickers 图片集。如下图所示:
该 Stickers.xcstickers 中包含了两部分,分别是 iMessage App Icon 和 Stick Pack。想必大家应该都猜到了,iMessage App icon 指的是在系统的 Message 中展示的 iMessage App 图标,而 Stick Pack 里面的图片对应在大家开发的 iMessage App 中展示的各个贴图。如下图所示:
默认情况下,所展示的 Sticker Pack Application 中的图片列表大小是 Regular型,除此之外,开发者还可以选择 Small、Large 两种尺寸。每一种尺寸类型都对应着不同的最大尺寸。这里苹果也是建议大家尽可能选择使用3x的图片,以便达到最好的展示效果。
Small 100 * 100pt @3x
Medium 136 * 136pt @3x
Large 206 * 206pt @3x
优点: 上手方便、快捷。
缺点: 无法满足开发者的自定义需求。
开发简单 Custom Sticker Application
相比较之前的 Sticker Pack Application 开发,Custom Sticker Application 要复杂一些、可定制化一些。它可以做到:
- 自定义自己的 Application UI
- 动态创建 Stickers
- 应用中使用相机、使用内购
在开发过程中大家会涉及到如下类:
- MessagesViewController
- MSStickerBrowserViewController
- MSStickerBrowserView
- MSStickerBrowserViewDataSource
- MSMessagesAppViewController
MessagesViewController 类个人觉得更像是一个容器类,它来承载和将要展示的视图所在的控制器作为它的子控制器添加上去。同时,MessagesViewController 类当中的有些方法是用来处理子控制器上的事件回掉,而 MSStickerBrowserViewController 类则继承了 UIViewController 且接受 MSStickerBrowserViewDataSource 协议(额……说的有点饶了),我们还是来看下面的图:
MSMessagesAppViewController 通常不会直接使用,使用是继承于 MSMessagesAppViewController 的 MSMessagesViewController。
每一个贴图就是一个 MSSticker对象,而 MSStickerView 对象中又包含了 MSSticker对象等等,这个,我们后面会有更详细的介绍。
接着我们:
打开 Xcode 选择 iMessage Application,创建后会发现默认有了 MessagesViewController,相信大家刚开始看到 MessagesViewController 会有点懵掉,里面实现了很多方法,不过没关系我们暂时只看 viewDidLoad() 就行。
对于简单的 Custom Sticker Application 开发,Apple 已经为了我们提供MSStickerBrowserViewController,我们只需要通过继承该类并实现相关数据源协议方法。如下所示:
//返回 Sticker 的个数
override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int {
return stickers.count
}
//返回每一个 Sticker 对象
override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker {
return stickers[index]
}
大家看到这里是不是有点像平时使用的 UITableView 或者 UICollectionView 的数据源协议呢?的确比较类似。这里,Stickers 数组包含了我要展示的所有 Sticker 数据,每一个元素都是 MSSticker 类型。如下是创建数组中元素的方法:
//创建 MSStricker 对象的数组
var stickers = [MSSticker]()
//创建 MSSSticker 对象
func createSticker(asset: String, localizedDescription: String) {
//获取图片路径
guard let stickerPath = Bundle.main.path(forResource: asset, ofType:"png") else {
print("获取图片路径失败")
return
}
let stickerURL = URL(fileURLWithPath: stickerPath)
let sticker: MSSticker
do {
try sticker = MSSticker(contentsOfFileURL: stickerURL, localizedDescription: localizedDescription)
//将创建的 MSStricker 对象添加到数组中
stickers.append(sticker)
} catch {
print(error)
return
}
}
因为所有的图片资源都放在 Bundle 中,所以通过获取每一个图片所在路径,通过 URL 的 fileURLWithPath 方法转化为 URL 对象,再调用 MSSticker 的contentsOfFileURL 方法得到 MSSticker 对象。
- 到此,我们已经实现了 MSStickerBrowserViewController 中的数据源协议方法,回到 MessagesViewController 中,在 viewDidLoad() 中去创建GjStickerBrowerViewController 对象的实例,设置实例对象的视图的 frame,最后将 GjStickerBrowerViewController 实例和相关 view 视图都添加到当前的 MSMessagesViewController 及它的 View 视图上。(当然了,添加的时候切记一定是先将 GjStickerBrowerViewController 控制器添加到
MessagesViewController 上,然后再添加视图到 MessagesViewController 的 view 上。) 如下所示:
//创建实例
gjBrowerViewController = GjStickerBrowerViewController(stickerSize: .regular);
//设置它的frame
gjBrowerViewController.view.frame = self.view.frame
//添加控制器及其视图
self.addChildViewController(gjBrowerViewController)
gjBrowerViewController.didMove(toParentViewController: self)
self.view.addSubview(gjBrowerViewController.view)
接着再去调用 GjStickerBrowerViewController 实例的加载数据方法。
//加载数据
gjBrowerViewController.loadStickers()
gjBrowerViewController.stickerBrowserView.reloadData()
至此运行就会看到效果啦。
优点: 可以根据开发者的需求进行简单自定义
缺点: 还是无法做到深度需求自定义
除了上面的那种简单自定义方式之外,可能大家还是会觉得自定义的程度不够或者不满足自己的需求,没关系,我们接着往下说另一种:
开发深度 Custom Sticker Application
除了上面的那种简单自定义的 Sticker Application 之外,可能大家还是会觉得自定义的程度不够或者不满足自己的需求,那么下面的这种绝对适合你。这是一种更深度自定义的 Sticker Application。
- 首先依然通过 Xcode 选择 iMessage Application 项,那么既然是要深度自定义,干脆我们直接创建一个继承于 UICollectionView Controller 好了,(当然,这里你也可以选择继承其他任意的控制器都行)。这个类主要用来承载展示各个贴图的 view 所在控制器。我们先定义一个枚举类型,用于 CollectionView 的 Item 类型。如下所示:
enum CollectionViewItem {
case sticker(MSSticker)
case addSticker
}
Sticker(MSSticker): 指代上图中每一个贴图图片。
AddSticker : 指代上图中第一个加号图片。
- 其次,我们可以当前该控制器的 viewdidLoad() 中调用同上的填充数组数据的方法。loadStickers(),当然了,既然使用到了 UICollectionView 那就一定要实现它的数据源代理方法。如下所示:
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let item = items[indexPath.row]
switch item {
case .addSticker:
return dequeueAddStickerCell(at: indexPath)
case .sticker(let sticker):
return dequeueStickerCell(for: sticker, at: indexPath)
}
}
在上面的代码中,分别对应创建了加号 Cell 类型和其余图片 Cell 类型,如下所示:
class AddStickerCell: UICollectionViewCell {
static let reuseIdentifier: String = "AddStickerCellIdentify"
lazy var addImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.backgroundColor = UIColor.blue
addImageView.center = self.contentView.center
addImageView.image = UIImage(named: "add.png")
self.addSubview(addImageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private func dequeueAddStickerCell(at indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView?.dequeueReusableCell(withReuseIdentifier: AddStickerCell.reuseIdentifier, for: indexPath) as! AddStickerCell
return cell
}
class StickerCell: UICollectionViewCell {
static let reuseIdentifier = "StickerCellIdenfity"
lazy var stickerView = MSStickerView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(stickerView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private func dequeueStickerCell(for sticker: MSSticker, at indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView?.dequeueReusableCell(withReuseIdentifier: StickerCell.reuseIdentifier, for: indexPath) as! StickerCell
cell.stickerView.sticker = sticker
return cell
}
- 这个时候这个 UICollectionView 所在的页面就基本上完成,我们再来到 MessagesViewController 类中,在其 viewDidLoad() 中创建 GJStickerCollectionViewController 类的实例并添加到MessagesViewController 上以及相应的视图 view。如下所示:
private func loadStickerCollectionViewController() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 100, height: 100)
gjStickerViewController = GJStickerCollectionViewController(collectionViewLayout: layout)
gjStickerViewController.view.frame = self.view.frame
self.addChildViewController(gjStickerViewController)
gjStickerViewController.didMove(toParentViewController: self)
self.view.addSubview(gjStickerCollecitonViewController.view)
gjStickerViewController.delegate = self
}
- 接下来,我们在 GJStickerCollectionViewController 类中创建 GJStickerCollectionViewControllerDelegate 协议,并为协议添加如下方法:
protocol GJStickerCollectionViewControllerDelegate: class {
func gjStickerCollectionViewControllerDidSelectAdd(_ controller: GJStickerCollectionViewController)
}
它主要用于处理点击加号事件并在设置代理:
weak var delegate: GJStickerCollectionViewControllerDelegate?
与此同时,在 MessagesViewController 类中接收并实现该协议:
extension MessagesViewController: GJStickerCollectionViewControllerDelegate {
func gjStickerCollectionViewControllerDidSelectAdd(_ controller: GJStickerCollectionViewController) {
//用于切换贴图的展示样式
requestPresentationStyle(.expanded)
}
}
到这里,一个可以深度自定义的简单 iMessage Application 就算是完成了。
优点: 可以满足自己多方面的自定义需求。
缺点: 需要自己开发的比较多,开发起来相对复杂。
Interactive Message
- 第一,我觉得关于 Interactive Mesaage,大家应该先知道 Presentation Styles,它指的是 iMessage Application 的展示页面样式,分别是如下两种:
1、Compact : 指的是拥挤的可以上下滑动的展示效果,
2、Expanded: 指的是扩散的,占满全屏显示的效果。
- 第二,我们把与他人进行会话称为一个 MSConversation,把每次发送出去的消息称为一个 MSMessage。
每一个 Message 关联了两部分:
1、MsSession:用于创建和更新一个Message。
2、MSMessageTemplateLayout:用于会话的界面UI布局。
那么我们具体该如何去创建一个消息对象呢?
//创建Message的UI布局
let layout = MSMessageTemplateLayout()
//创建Message的消息体
let message = MSMessage(session: MSSession())
message.url = URL(fileURLWithPath: "XXX")
message.accessibilityLabel = "描述"
message.summaryText = "summaryText"
//设置Message的UI布局
message.layout = layout
可以通过调用 insert 方法在当前对话中插入一个消息:
//获取当前会话
let converson = self.activeConversation
converson?.insert(message, completionHandler: { error in
print("error")
})
当然了,我们还可以通过如下一些方法来监听事件:
override func didStartSending(_ message: MSMessage, conversation: MSConversation) {
//用户点击发送按钮会触发方法
}
override func didReceive(_ message: MSMessage, conversation: MSConversation) {
//接收到另一个远端设备发送来的信息会触发方法
}
....
iMessage Applicaton 的生命周期
iMessage Application 的生命周期是非常简单的,主要分成两部分:
1、Becoming active(开始激活)
当用户点击了 Message Application 的应用图标之后,它就会开始启动,然后分别调用didBecomeActive、viewWillAppear、viewDidAppear 方法。
2、Resigning active
当用户关掉当前屏幕中的对话页面,内部会启动 Resigning active,然后调用viewWillDisappear、viewDidDisappear、willResignActive 方法进而进程终止。
总结
整体而言,今天梳理了下关于 Messsage.framework、iMessage Application 开发等方面的知识,当然了,在我看来我仅仅写了冰山一角,还有很多东西值得去研究。希望这篇文章能给大家带来一定的帮助。
参考来源:
- https://chengwey.com/ios-10-by-tutorials-bi-ji-si/
- https://www.appcoda.com/message-sticker-app/
- https://developer.apple.com/videos/play/wwdc2016/204
- https://developer.apple.com/videos/play/wwdc2016/224
- https://www.shinobicontrols.com/blog/ios-10-day-by-day-day-1-messages