iOS 10 by Tutorials 笔记(四)

Chapter 4: Beginning Message Apps

iOS 10 允许往 iMessage 上添加自定义的表情包了,本章我们会学习如何制作一款表情包 App,也顺便学习一下 Messages 框架。

还是先从最简单的创建表情包应用(sticker packs)开始,这或许最简单的 App 了,一行代码也不需要写!

创建新工程,选择 iOS\Application\Sticker Pack Application 模板

新工程只包含一项内容,一个名叫 Stickers.xcstickers 的资源目录,里面有 app iconSticker Pack 文件夹(用来存放表情),我们拖一些表情图片到 Sticker Pack 文件中

一切 OK,运行打开 Messages,现在就可以选择我们自定义的表情包了

这里有几个注意事项说明一下:

  • 表情图片必须是 PNG,APNG,GIF,JPEG 这几种类型,且大小要小于 500KB
  • Messages 中显示的表情都是统一尺寸
  • 你只用提供 3x 分辨率的图片

Creating a sticker application

sticker packs 有点简单,接下来我们实现一个表情(Sticker)应用,可以在运行时控制表情包(sticker)的显示。

该应用我们用了一个秀色可餐的名字 Stickerlicious,这一次创建选择 iOS\Application\iMessage Application 模板

先来看一下大体构造:

  • application target,因为整个应该其实是一个 extensions。但是针对 Messages extensions,它的宿主程序不用做任何事情,甚至不会出现在屏幕上。
  • 一个 Messages extension,这是真正运行在 Messages 中的程序,也是我们要实现的
  • Messages extension 目录下包含一个 storboard,一个 asset catalog 和 MessagesViewController.swift(MSMessagesAppViewController 的子类)

所有的 Messages apps 都生存在 MSMessagesAppViewController 的子类中,它提供了一组构建复杂 message apps 的方法和属性。第六章我们会详细介绍

The sticker browser view controller

Messages 框架包含一对类文件 MSStickerBrowserViewMSStickerBrowserViewController,可以用来显示表情(sticker),二者的关系类似于 UITableViewUITableViewController

MSMessagesAppViewController 作为 Messages extension 的基类,我们在其基础上添加一个子 VC 用来显示表情。

先创建这个子 VC,命名 CandyStickerBrowserViewController.swift

import Messages  
class CandyStickerBrowserViewController: MSStickerBrowserViewController {

}

然后去 Storyboard 上拖一个 Container View 到 Messages View Controller 上,选择嵌入的 VC,把它的类修改为刚定义的 CandyStickerBrowserViewController

返回代码 CandyStickerBrowserViewController,添加数据源 stickers 数组

var stickers = [MSSticker]()  

我们的图片都存放在 candy 文件夹下

用一个常量数组来存储放图片名

let stickerNames = ["CandyCane", "Caramel", "ChocolateBar", "ChocolateChip", "DarkChocolate", "GummiBear", "JawBreaker", "Lollipop", "SourCandy"]  

接着遍历这个数组创建贴图 sticker 对象,并将结果放置在数据源 stickers 数组中

extension CandyStickerBrowserViewController {  
  func loadStickers() {
    stickers = stickerNames.map({ name in
      let url = Bundle.main.url(forResource: name,
        withExtension: "png")!
      return try! MSSticker(
        contentsOfFileURL: url,
        localizedDescription: name)
    })
  } 
}

让我们一开始就载入数据源

override func viewDidLoad() {  
  super.viewDidLoad()
  loadStickers()
  stickerBrowserView.backgroundColor = #colorLiteral(
    red:  0.9490196078, green: 0.7568627451,
    blue: 0.8196078431, alpha: 1)
}

最后实现 sticker browser view 的两个 data source 方法,非常类似写 table view

//MARK: MSStickerBrowserViewDataSource
extension CandyStickerBrowserViewController {  
  override func numberOfStickers(in stickerBrowserView:
    MSStickerBrowserView) -> Int {
    return stickers.count
  }
  override func stickerBrowserView(_ stickerBrowserView:
    MSStickerBrowserView, stickerAt index: Int) -> MSSticker {
    return stickers[index]
  }
}

运行,这和上面通过 sticker packs 的方式创建的完全一样嘛,好吧下一步我们来动态添加表情包。

Adding dynamic stickers

我们来增加一个 Chocoholic 模式的开关,开启它后只显示和巧克力相关的表情。先在 storyboard 上修改一波,增加一个 switch 开关

注意 switch 我们是加在了基类 MessagesViewController 上(不是 CandyStickerBrowserViewController),为了将 switch 结果传递给 CandyStickerBrowserViewController,这里选择用 protocol 实现。

创建一个 Chocoholicable 协议,接收 switch 开关的状态

protocol Chocoholicable {
func setChocoholic(_ chocoholic: Bool) }

从 switch 到 MessagesViewController 拉一个 @IBAction,在该方法中将 switch 开关的状态传给子类代理

@IBAction func handleChocoholicChanged(_ sender: UISwitch) {
  childViewControllers.forEach({ vc in
    guard let vc = vc as? Chocoholicable else { return }
    vc.setChocoholic(sender.isOn)
  })
}

为了配合 Chocoholic 模式的演出,回到 CandyStickerBrowserViewController.swift 的数据源载入方法中,即 Chocoholic 模式开启时,数据源 stickers 数组中只包含 Chocolate 表情。

func loadStickers(_ chocoholic: Bool = false) {  
  stickers = stickerNames.filter { name in
    return chocoholic ? name.contains("Chocolate") : true
  }.map { name in
    let url = Bundle.main.url(forResource: name, withExtension: "png")!
    return try! MSSticker(contentsOfFileURL: url, localizedDescription: name)
  }
}

作为 MessagesViewController 的子 VC,我们在 CandyStickerBrowserViewController.swift 中实现 Chocoholicable 协议方法

extension CandyStickerBrowserViewController: Chocoholicable {  
  func setChocoholic(_ chocoholic: Bool) {
    loadStickers(chocoholic)
    stickerBrowserView.reloadData()
  }
}

运行,开启 Chocoholic 模式可以把巧克力表情过滤出来

Creating a custom sticker browser

MSStickerBrowserView 提供的定制性还是太少,想要完全掌控 sticker app,你需要 MSStickerView,它更加强大,创建它只需要给它传递一个 MSSticker 对象

既然要用 MSStickerView 替代 MSStickerBrowserView,之前的 CandyStickerBrowserViewController 也就不需要了,分别删除相关 storyboard 和代码文件。我们重新拖一个 UICollectionViewController 来替代,在 Collection View 中开启 Section Header,然后拖一个 UILable 到 Section Header 上、拖一个 UIView 到 collection view cell 中,并将 cell 里的通用 View 类修改为 MSStickerView

接下来创建相关子类代码文件,SectionHeaderUICollectionReusableView),StickerCollectionViewCellUICollectionViewCell),StickerCollectionViewControllerUICollectionViewController),相应的 storyboad 中的也别忘了设置对应的 custom class

这一次的表情名字我们换成字典来归类存储,相应的数据源用一个结构体来封装一下

let stickerNameGroups: [String: [String]] = [  
  "Crunchy":   ["CandyCane","JawBreaker","Lollipop"],
  "Chewy":     ["Caramel","GummiBear","SourCandy"],
  "Chocolate": ["ChocolateBar","ChocolateChip","DarkChocolate"]
]
// 表情结构体
struct StickerGroup {  
  let name: String
  let members: [MSSticker]
}

然后在 StickerCollectionViewController 中根据字典生成结构体,最后这些结构体组成数据源。

先来创建一个空的数据源数组

var stickerGroups = [StickerGroup]()  

遍历字典创建结构体 StickerGroup,装入数据源数组 stickerGroups

extension StickerCollectionViewController {  
  // 1
  func loadStickers(_ chocoholic: Bool = false) {
    // 2 根据名字过滤巧克力模式
    stickerGroups = stickerNameGroups.filter({ (name, _) in
      // 3
      return chocoholic ? name == "Chocolate" : true
    }).map { (name, stickerNames) in
// 4 取出字典中的 value 对象(数组),继续遍历,创建 MSSticker 数组
      let stickers: [MSSticker] = stickerNames.map { name in
        let url = Bundle.main.url(forResource: name,
          withExtension: "png")!
        return try! MSSticker(contentsOfFileURL: url,
          localizedDescription: name)
      }
// 5 创建 StickerGroup 结构体
      return StickerGroup(name: name, members: stickers)
    }
// 6 对数据源数组进行排序
    stickerGroups.sort(by: { $0.name < $1.name })
  }
}

下面让我们在载入时生成这个数据源数组

override func viewDidLoad() {  
  super.viewDidLoad()
  loadStickers()
  if let layout = collectionView?.collectionViewLayout as?
    UICollectionViewFlowLayout {
    layout.sectionHeadersPinToVisibleBounds = true
  }
  collectionView?.backgroundColor = #colorLiteral(
    red:  0.9490196078, green: 0.7568627451,
    blue: 0.8196078431, alpha: 1)
}

还记得之前在 iPhone 6 上展示的是三列表情,这里我们做个小限制,也让它展示三列(每个贴纸不超过 136 point)

// MARK: UICollectionViewDelegateFlowLayout
extension StickerCollectionViewController {  
  func collectionView(_ collectionView: UICollectionView,
    layout collectionViewLayout: UICollectionViewLayout,
    sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
    let edge = min(collectionView.bounds.width / 3, 136)
    return CGSize(width: edge, height: edge)
  } 
}

上面每个 cell 设置为一个正方形,边长是整个宽度的 1/3 或 136(二者较小的值)

实现 UICollectionView 的数据源方法

extension StickerCollectionViewController {  
  override func numberOfSections(
    in collectionView: UICollectionView) -> Int {
    return stickerGroups.count
  }
  override func collectionView(_ collectionView:
    UICollectionView,
    numberOfItemsInSection section: Int) -> Int {
    return stickerGroups[section].members.count
  }
  override func collectionView(_ collectionView: UICollectionView,
    cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(
      withReuseIdentifier: "StickerCollectionViewCell",
      for: indexPath) as! StickerCollectionViewCell

    let sticker =
      stickerGroups[indexPath.section].members[indexPath.row]
    cell.stickerView.sticker = sticker

    return cell 
  }
  override func collectionView(_ collectionView: UICollectionView,
    viewForSupplementaryElementOfKind kind: String,
    at indexPath: IndexPath) -> UICollectionReusableView {
    guard kind == UICollectionElementKindSectionHeader else {
      fatalError()
    }
    let header = collectionView.dequeueReusableSupplementaryView(
      ofKind: kind, withReuseIdentifier: "SectionHeader",
      for: indexPath) as! SectionHeader
    header.label.text = stickerGroups[indexPath.section].name
    return header
  }
}

最最最后一步,别忘了实现 Chocoholicable 协议,不然就过滤不了巧克力了

extension StickerCollectionViewController: Chocoholicable {  
  func setChocoholic(_ chocoholic: Bool) {
    loadStickers(chocoholic)
    collectionView?.reloadData()
  }
}

运行,一切 OK


-EOF-

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!