iMessage App 是一种全新的应用扩展,载体是 iOS 系统的 Message 应用,通过 iMessage App,用户可以发送更加丰富的消息内容,享受更具交互性的会话体验。
从 iOS 10 开始,消息扩展(Messages Extension)可以独立于宿主App(Container App) 开发,并提供了全新的消息模式。用户可以通过 Message 发送文本、表情包(sticker)、多媒体文件,以及其他可交互消息。

Messages App Store


Messages App Store 独立于之前的 App Store,只存在于系统的 Message 中,这里只显示 iMessage-related 应用。它可以干什么呢?

  • 显示iMessage App;
  • 为未安装应用的用户提供安装途径(Inline App Attribution);
  • 提供iap,Apple Pay和访问相机功能。

iMessage App 可以独立开发(iOS 10之后),也可以作为 Extension 添加在现有项目中。

  • 在现有项目中添加:File->New->Target->Application Extension->Sticker Pack Extension/iMessage Extension
  • 独立开发:File->New->Project->Application->Sticker Pack Application/iMessage Application

基本表情包 (Sticker Packs)

Sticker 是 iOS 10 iMessage 引入的一种新的交互方式,可以当做消息发送,也可以附加在已有消息上。创建表情包不需要任何代码。图片文件需满足以下条件:

  • 图片格式:PNG, APNG, GIF, JPEG;
  • 文件大小:小于 500 KB;
  • 图片大小区间:[100 x 100,206 x 206]

只需提供最大像素图片(@3x,[300 x 300,618 x 618]),系统会在需要的时候自动完成缩放。

苹果建议的表情文件大小:

  • Small: 100 x 100 pt @3x scale (300 x 300 pixel image)
  • Medium: 136 x 136 pt @3x scale (378 x 378 pixel image)
  • Large: 206 x 206 pt @3x scale (618 x 618 pixel image)

打开 Xcode,创建新工程 BasicStickerPack:File->New->Project->Application->Sticker Pack Application
左边侧栏有个Stickers.xcstickers,包含了iMessage APP的icon 和表情,添加一组图片。如果要更改表情包默认名称和大小,可在右侧Attributes inspector 中更改。

运行,表情包将在 Message 的 Message App Store 打开,点击任何一个表情即可添加到当前信息并发送:

自定义表情包

基本的表情应用程序提供的模板可能不完全满足需求,我们创建一个较复杂的表情包应用程序 CustomStickerPack,选择 iMessage Application 模版。

创建好的工程相比上面的 Sticker Pack Application 多了 MessagesExtension 文件夹。该文件夹包含四个部分:

  • MessagesViewController.swift : iMessage app的程序入口;
  • MainInterface.storyboard: 可视化操作;
  • Assets.xcassets: 图片集合;
  • Info.plist : 配置一些扩展信息;

MessagesViewControllerMSMessagesAppViewController 的子类,它是用来展示消息扩展(Message Extension)的界面。自定义 Sticker 需要自定义MSStickerBrowserViewControllerMSStickerBrowserViewController是用来显示 Sticker 的,它有两个协议方法需要实现:

@protocol MSStickerBrowserViewDataSource <NSObject>
- (NSInteger)numberOfStickersInStickerBrowserView:(MSStickerBrowserView *)stickerBrowserView;//返回Sticker数量
- (MSSticker *)stickerBrowserView:(MSStickerBrowserView *)stickerBrowserView stickerAtIndex:(NSInteger)index;//返回MSSticker对象

MSStickerBrowserViewController用法跟UITableViewDataSource很像,有一个stickerBrowserView对象:

@property (nonatomic, strong, readonly) MSStickerBrowserView *stickerBrowserView;

刷新数据,调用:

[stickerBrowserView reloadData];

一张图说明一下上述几个对象的关系:

将 sticker 放入 MessagesExtension 文件夹,创建一个 MSSticker 数组存储表情包,并写一个加载表情包的方法:

var stickers = [MSSticker]()
func loadStickers() {
    for i in 1...2 {
        if let url = Bundle.main.url(forResource: "Sticker \(i)", withExtension: "png"){
            do {
                let sticker = try MSSticker(contentsOfFileURL: url, localizedDescription: "")
                stickers.append(sticker)
            } catch {
                print(error)
            }
        }
    }
}

实现createStickerBrowser方法, 初始化MSStickerBrowserViewController作为根视图, 并设置宽高约束:

func createStickerBrowser() {
    let controller = MSStickerBrowserViewController(stickerSize: .large)

    addChildViewController(controller)
    view.addSubview(controller.view)

    controller.stickerBrowserView.backgroundColor = UIColor.blue
    controller.stickerBrowserView.dataSource = self

    view.topAnchor.constraint(equalTo: controller.view.topAnchor).isActive = true
    view.bottomAnchor.constraint(equalTo: controller.view.bottomAnchor).isActive = true
    view.leftAnchor.constraint(equalTo: controller.view.leftAnchor).isActive = true
    view.rightAnchor.constraint(equalTo: controller.view.rightAnchor).isActive = true
}

实现MSStickerBrowserViewDataSource的代理方法:

func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int {
    return stickers.count
}

func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker {
    return stickers[index]
}

viewDidLoad 中加载 sticker 、创建 MSStickerBrowserViewController:

override func viewDidLoad() {
    super.viewDidLoad()

    loadStickers()
    createStickerBrowser()
}

运行,跟上一个表情包差不多,但背景换成了自定义的颜色。

自定义 APP

本例将利用 iMessage App 创建一个独特的可交互消息。
创建 iMessage Application MessageApp,在MainInterface.storyboard添加一个 stepper 和 button:

选择 stepper 打开 Attributes inspector, 改变最小最大值为0和10:

介绍几个关键对象:

MSConversation

指当前打开的会话,可以通过 MSConversation 插入消息。
MSMessagesAppViewController中可以通过成员变量activeConversation获取当前的MSConversation,随后可调用以下方法插入不同的消息:

// 添加交互型消息.
open func insert(_ message: MSMessage, completionHandler: (@escaping (Error?) -> Swift.Void)? = nil)

// 添加 sticker.
open func insert(_ sticker: MSSticker, completionHandler: (@escaping (Error?) -> Swift.Void)? = nil)

// 添加文本.
open func insertText(_ text: String, completionHandler: (@escaping (Error?) -> Swift.Void)? = nil)

// 添加多媒体(音视频).
open func insertAttachment(_ URL: URL, withAlternateFilename filename: String?, completionHandler: (@escaping (Error?) -> Swift.Void)? = nil)

MSMessage

代表单个消息体,包含两个部分:

  • MSSession:用来描述消息如何发送;
  • MSMessageLayout:用来描述消息如何展示。
    在 iMessage 中自定义 MSMessage 必须要设置两个属性:layouturl。 url 是链接某个 web 页面的一些内容, 这样 MacOS 用户可以看到 iMessage 的内容。

MSMessageLayout是一个抽象类,目前系统只提供了一种展现方法MSMessageTemplateLayout,里边有许多属性和空间用来自定义message;:

左上角的空间是展示icon, 所有的属性都是可选的,提供任何标题字符串将摆脱底部部分的布局。
添加createImageForMessage()方法,将当前的stepper的数值显示到圆形的label中, 然后将label放在UIImage对象中。

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 = "\(Int(stepper.value))"
    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
}

设置 button 点击方法didPress,该方法中先创建message的布局, 并且设置 image 和 caption, 接着创建 MSMessage 对象并插入到会话中:

@IBAction func didPress(button sender: AnyObject) {
    if let image = createImageForMessage(), let conversation = activeConversation {
        let layout = MSMessageTemplateLayout()
        layout.image = image
        layout.caption = "Stepper Value"

        let message = MSMessage()
        message.layout = layout
        message.url = URL(string: "emptyURL")

        conversation.insert(message, completionHandler: { (error) in
            print(error)
        })
    }
}

在这个例子中, 我们只是创建了简单的字符 url,如果用户点击该消息并且 url 是 http(s) 类型的,系统会通过浏览器打开相应的页面。
运行 App:

点击 button 发送消息:

stepper+2:

iMessage App LifeCycle

看下 iMessage App 的生命周期。

启动过程

// Message Extension启动。
- (void)didBecomeActiveWithConversation:(MSConversation *)conversation;
- (void)viewWillAppear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated;

销毁过程

- (void)viewWillDisappear:(BOOL)animated;
- (void)viewDidDisappear:(BOOL)animated;
- (void)willResignActiveWithConversation:(MSConversation *)conversation;
// Message Extension被系统销毁。

唤起过程

-(void)willTransitionToPresentationStyle:(MSMessagesAppPresentationStyle)presentationStyle;
-(void)didTransitionToPresentationStyle:(MSMessagesAppPresentationStyle)presentationStyle;

上面列出的回调方法均出现在MSMessagesAppViewController中,因此,iMessage App 的生命周期就是MSMessagesAppViewController的生命周期。

官方文档 Messages Framework Reference
WWDC Session1
WWDC Session2
官方 demo