iOS10 之 iMessage Apps

还记得去年的 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。如下图所示:
14920048410015.jpg

开发 Sticker Pack Application

Sticker Pack Application 是一个由 Apple 提供的快速开发 iMessage App 模板,大家使用它来开发时仅仅只需要根据要求指定 Stickers.xcstickers 中的图片即可。

  • 首先打开 Xcode,选择 Sticker Pack Application ,创建之后,会看到当然项目中仅仅包含一个 Stickers.xcstickers 图片集。如下图所示:
8F460AB0-B0F0-496A-B878-F615407C3B1D.jpeg

该 Stickers.xcstickers 中包含了两部分,分别是 iMessage App Icon 和 Stick Pack。想必大家应该都猜到了,iMessage App icon 指的是在系统的 Message 中展示的 iMessage App 图标,而 Stick Pack 里面的图片对应在大家开发的 iMessage App 中展示的各个贴图。如下图所示:

showStickerPackApplication.png

默认情况下,所展示的 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 协议(额……说的有点饶了),我们还是来看下面的图:

showCustomStickerStructure.png

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()

至此运行就会看到效果啦。

show.png

优点: 可以根据开发者的需求进行简单自定义
缺点: 还是无法做到深度需求自定义

除了上面的那种简单自定义方式之外,可能大家还是会觉得自定义的程度不够或者不满足自己的需求,没关系,我们接着往下说另一种:

开发深度 Custom Sticker Application

除了上面的那种简单自定义的 Sticker Application 之外,可能大家还是会觉得自定义的程度不够或者不满足自己的需求,那么下面的这种绝对适合你。这是一种更深度自定义的 Sticker Application。

  • 首先依然通过 Xcode 选择 iMessage Application 项,那么既然是要深度自定义,干脆我们直接创建一个继承于 UICollectionView Controller 好了,(当然,这里你也可以选择继承其他任意的控制器都行)。这个类主要用来承载展示各个贴图的 view 所在控制器。我们先定义一个枚举类型,用于 CollectionView 的 Item 类型。如下所示:
    enum CollectionViewItem {
        case sticker(MSSticker)
        case addSticker
    }
14920923598280.jpg
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 的展示页面样式,分别是如下两种:
14919275404464.jpg
1、Compact : 指的是拥挤的可以上下滑动的展示效果,
2、Expanded: 指的是扩散的,占满全屏显示的效果。
  • 第二,我们把与他人进行会话称为一个 MSConversation,把每次发送出去的消息称为一个 MSMessage。
14919286077612.jpg

每一个 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(开始激活)

14919298443959.jpg

当用户点击了 Message Application 的应用图标之后,它就会开始启动,然后分别调用didBecomeActive、viewWillAppear、viewDidAppear 方法。

2、Resigning active

14919298895436.jpg

当用户关掉当前屏幕中的对话页面,内部会启动 Resigning active,然后调用viewWillDisappear、viewDidDisappear、willResignActive 方法进而进程终止。

总结

整体而言,今天梳理了下关于 Messsage.framework、iMessage Application 开发等方面的知识,当然了,在我看来我仅仅写了冰山一角,还有很多东西值得去研究。希望这篇文章能给大家带来一定的帮助。

参考来源:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容