13-发布微博

发布微博

课程目标

  1. 界面搭建
  2. 自定义文字输入框
  3. 自定义显示照片的View
  4. 底部 toolBar 自定义(UIStackView)
  5. 表情键盘(一个复杂的自定义 View 如何一步一步实现出来的)

界面搭建

导航栏内容

  • 标题视图懒加载
// MARK: - 懒加载

/// 顶部标题视图
private lazy var titleView: UILabel = {
    let label = UILabel()
    // 设置多行
    label.numberOfLines = 0
    // 字体大小
    label.font = UIFont.systemFontOfSize(14)
    // 文字居中
    label.textAlignment = NSTextAlignment.Center
    // 如果有用户昵称
    if let name = HMUserAccountViewModel.sharedInstance.userAccount?.name {
        // 初始化一个带有属性的文字
        var attr = NSMutableAttributedString(string: "发微博\n\(name)")
        // 获取到要添加的属性的范围
        let range = (attr.string as NSString).rangeOfString(name)
        // 添加属性
        attr.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(12), range: range)
        attr.addAttribute(NSForegroundColorAttributeName, value: UIColor.lightGrayColor() ,range: range)
        label.attributedText = attr
    }else{
        label.text = "发微博"
    }
    label.sizeToFit()
    return label
}()
  • 右边按钮懒加载
/// 右边按钮
private lazy var rightButton: UIButton = {
    let button = UIButton()

    // 添加点击事件
    button.addTarget(self, action: "send", forControlEvents: UIControlEvents.TouchUpInside)

    // 设置文字属性
    button.titleLabel?.font = UIFont.systemFontOfSize(13)
    button.setTitle("发送", forState: UIControlState.Normal)

    // 设置不同状态的文字
    button.setTitleColor(UIColor.grayColor(), forState: UIControlState.Disabled)
    button.setTitleColor(UIColor.whiteColor(), forState: UIControlState.Normal)

    // 设置不同状态的背景图片
    button.setBackgroundImage(UIImage(named: "common_button_white_disable"), forState: UIControlState.Disabled)
    button.setBackgroundImage(UIImage(named: "common_button_orange"), forState: UIControlState.Normal)
    button.setBackgroundImage(UIImage(named: "common_button_orange_highlighted"), forState: UIControlState.Highlighted)

    // 设置宽高
    button.height = 30
    button.width = 44

    return button
}()
  • 实现 send 方法
@objc private func send(){
    printLog("发送")
}
  • 设置导航栏内容
// 设置导航栏内容
private func setupNav(){
    // 设置左边 Item
    navigationItem.leftBarButtonItem = UIBarButtonItem.item(title: "返回", target: self, action: "back")
    // 设置中间 titleView
    navigationItem.titleView = titleView
    // 设置右边 Item
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightButton)
    // 默认为不可用状态
    navigationItem.rightBarButtonItem?.enabled = false
}

运行测试

文字输入框

  1. 带有占位文字
  2. 可以像 UITextView 一样输入多行
  3. 自定义一个输入框继承于 UITextView,向里面添加一个 label
  • 代码实现
class HMTextView: UITextView {

    /// 重写的是指定构造函数
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)

        // 添加占位控件
        addSubview(placeholderLabel)

        // 添加约束
        placeholderLabel.snp_makeConstraints { (make) -> Void in
            make.width.lessThanOrEqualTo(self.snp_width).offset(-10)
            make.leading.equalTo(self.snp_leading).offset(5)
            make.top.equalTo(self.snp_top).offset(8)
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // 占位文字控件
    private lazy var placeholderLabel: UILabel = {
        let label = UILabel()
        // 设置文字颜色以及大小
        label.font = UIFont.systemFontOfSize(12)
        label.textColor = UIColor.lightGrayColor()
        label.text = "请输入文字"

        // 多行
        label.numberOfLines = 0
        return label
    }()
}
  • 添加到 controller 中使用
// 懒加载控件
private lazy var textView: HMTextView = {
    let textView = HMTextView()
    return textView
}()

// setupUI 方法中添加子控件并设置约束

view.addSubview(textView)
textView.snp_makeConstraints { (make) -> Void in
    make.edges.equalTo(self.view.snp_edges)
}

运行测试

  • HMTextView 中提供给外界设置占位文字的属性
// 添加 placeholder 属性,代外界设置值
var placeholder: String? {
    didSet{
        placeholderLabel.text = placeholder
    }
}
  • 重写 font 属性,以让占位文字与输入的文字字体大小一样
override var font: UIFont? {
    didSet{
        placeholderLabel.font = font
    }
}
  • 外界设置文字大小
textView.font = UIFont.systemFontOfSize(16)

运行测试:占位文字与输入的文字一样大

  • 监听文字改变的时候,去执行占位控件的隐藏与显示逻辑
// 监听文字改变的通知
NSNotificationCenter.defaultCenter().addObserver(self, selector: "textDidChange", name: UITextViewTextDidChangeNotification, object: self)
  • 文字改变之后调用的方法
/// 文字改变的时候会调用这个方法,当前如果有文字的话就隐藏占位 label
@objc private func textDidChange(){
    placeholderLabel.hidden = hasText()
}

运行测试。注:监听文字改变在这个地方不要使用代理,因为自己一般不成为自己的代理。

底部 ToolBar 初始化

//设置约束
        toolBar.snp_makeConstraints { (make) -> Void in
            make.left.right.bottom.equalTo(self.view)
        }
        var items = [UIBarButtonItem]()
        //添加 UIBarButtonItem类型的对象到数据源数组中
        let itemSettings = [["imageName": "compose_toolbar_picture","actionName": "selectPicture"],
            ["imageName": "compose_mentionbutton_background"],
            ["imageName": "compose_trendbutton_background"],
            ["imageName": "compose_emoticonbutton_background", "actionName": "selectEmoticon"],
            ["imageName": "compose_add_background"]]

        for item in itemSettings {
            let imageName = item["imageName"]
            let btn = UIButton()
            btn.setImage(UIImage(named: imageName!), forState: .Normal)
            btn.setImage(UIImage(named: imageName! + "_highlighted"), forState: .Highlighted)
            btn.sizeToFit()
            if let actionName = item["actionName"] {
                btn.addTarget(self, action: Selector(actionName), forControlEvents: .TouchUpInside)
            }

            let barItem = UIBarButtonItem(customView: btn)
            //添加到数组中
            items.append(barItem)
            //添加弹簧类型的item  FlexibleSpace: 可伸缩的弹簧
            let space = UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target: nil, action: nil)
            items.append(space)
        }

        items.removeLast()
        toolBar.items = items
  • HMComposeViewController 中懒加载控件
/// composeToolBar
private lazy var composeToolBar: HMComposeToolBar = HMComposeToolBar(frame: CGRectZero)
  • HMComposeViewControllersetupUI 方法中添加控件与约束
view.addSubview(composeToolBar)

// 添加约束
composeToolBar.snp_makeConstraints { (make) -> Void in
    make.bottom.equalTo(self.view.snp_bottom)
    make.width.equalTo(self.view.snp_width)
    make.height.equalTo(44)
}

底部 ToolBar 跟随键盘移动

  • 监听键盘 frame 改变通知
// 监听键盘 frame 改变通知
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillChangeFrame:", name: UIKeyboardWillChangeFrameNotification, object: nil)
  • 注销通知
deinit{
    NSNotificationCenter.defaultCenter().removeObserver(self)
}
  • 在键盘 frame 改变做更新约束的逻辑
/// 键盘 frame 改变通知调用的方法
@objc private func keyboardWillChangeFrame(noti: NSNotification){

    let endFrame = (noti.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()

    // 更新约束
    composeToolBar.snp_updateConstraints { (make) -> Void in
        make.bottom.equalTo(self.view.snp_bottom).offset(endFrame.origin.y - self.view.height)
    }

    UIView.animateWithDuration(0.25) { () -> Void in
        self.composeToolBar.layoutIfNeeded()
    }
}
  • 拖动 textView 的时候退下键盘:打开 textView 垂直方向弹簧效果,并设置代理
textView.alwaysBounceVertical = true
textView.delegate = self
  • 实现协议,并实现协议方法
func scrollViewDidScroll(scrollView: UIScrollView) {
    self.view.endEditing(true)
}
  • 实现 textViewDidChange 的方法,当textView有文字输入的时候右边按钮可用
func textViewDidChange(textView: UITextView) {
    //设置占位的文本的隐藏或者显示
        placeholderLabel.hidden = textView.hasText()
        //设置 发布按钮的 交互 和不可交互状态
        //有文本就允许交互
        navigationItem.rightBarButtonItem?.enabled = textView.hasText()
}

选择照片

目标

  • 在独立的项目中开发独立的功能,或者直接切换根控制器
  • 开发完毕后再整合到现有项目中
  • 提高工作效率,专注开发品质 😄
  • 选择照片
  • 重建控件布局

项目准备

  • 新建文件夹 PictureSelector
  • 新建 PictureSelectorViewController 继承自 UICollectionViewController
  • AppDelegate 添加以下代码
window?.rootViewController = PictureSelectorViewController()

运行测试

代码实现

设置布局

  • 添加控制器构造函数,简化外部调用
/// 可重用标识符号
private let WBPictureSelectorViewCellID = "WBPictureSelectorViewCellID"

/// 照片选择控制器
class PictureSelectorViewController: UICollectionViewController {

    // MARK: - 构造函数
    init() {
        let layout = UICollectionViewFlowLayout()

        super.init(collectionViewLayout: layout)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // 注册可重用 Cell
        self.collectionView!.registerClass(UICollectionViewCell.self,
            forCellWithReuseIdentifier: WBPictureSelectorViewCellID)
    }
}
  • 设置背景颜色
collectionView?.backgroundColor = UIColor.lightGrayColor()

注意在 CollectionViewController 中,collectionView 不是 view

  • 修改数据源方法
// MARK: 数据源
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 10
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(WBPictureSelectorViewCellID, forIndexPath: indexPath)

    // Configure the cell
    cell.backgroundColor = UIColor.redColor()

    return cell
}
  • 设置 cell 尺寸
init() {
    let layout = UICollectionViewFlowLayout()
    // 屏幕越大,显示的内容应该越多
    layout.itemSize = CGSize(width: 80, height: 80)
    layout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 20)

    super.init(collectionViewLayout: layout)
}

从 iPhone 6 开始,就需要考虑越大的屏幕显示越多的内容

自定义 Cell

  • 添加素材

  • 自定义 Cell

/// 照片选择单元格
private class PictureSelectorViewCell: UICollectionViewCell {

    // MARK: - 构造函数
    override init(frame: CGRect) {
        super.init(frame: frame)

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    /// 设置界面
    private func setupUI() {
        // 添加控件
        contentView.addSubview(addButton)
        contentView.addSubview(removeButton)

        // 自动布局
        addButton.snp_makeConstraints { (make) -> Void in
            make.edges.equalTo(contentView.snp_edges)
        }
        removeButton.snp_makeConstraints { (make) -> Void in
            make.top.equalTo(contentView.snp_top)
            make.right.equalTo(contentView.snp_right)
        }
    }

    // MARK: - 懒加载控件
    /// 添加按钮
    private var addButton = UIButton(imageName: "compose_pic_add", backImageName: nil)
    /// 删除按钮
    private var removeButton = UIButton(imageName: "compose_photo_close", backImageName: nil)
}
  • 修改注册的 Cell
// 注册可重用 Cell
collectionView!.registerClass(PictureSelectorViewCell.self,
    forCellWithReuseIdentifier: WBPictureSelectorViewCellID)
  • 修改数据源
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(WBPictureSelectorViewCellID, forIndexPath: indexPath) as! PictureSelectorViewCell
  • 按钮监听方法
// MARK: - 监听方法
/// 添加照片
@objc private func addPicture() {
    print("添加照片")
}

/// 删除照片
@objc private func removePicture() {
    print("删除照片")
}
  • 添加监听方法
// 监听方法
addButton.addTarget(self, action: "addPicture", forControlEvents: .TouchUpInside)
removeButton.addTarget(self, action: "removePicture", forControlEvents: .TouchUpInside)

利用代理传递按钮点击事件

  • 定义协议传递消息
/// 照片选择单元格代理
private protocol PictureSelectorViewCellDelegate: NSObjectProtocol {
    /// 添加照片
    func pictureSelectorViewCellDidAdd(cell: PictureSelectorViewCell)
    /// 删除照片
    func pictureSelectorViewCellDidRemove(cell: PictureSelectorViewCell)
}
  • 设置代理
/// 照片选择代理
weak var pictureDelegate: PictureSelectorViewCellDelegate?
  • 修改监听方法
// MARK: - 监听方法
/// 添加照片
@objc private func addPicture() {
    pictureDelegate?.pictureSelectorViewCellDidAdd(self)
}

/// 删除照片
@objc private func removePicture() {
    pictureDelegate?.pictureSelectorViewCellDidRemove(self)
}
  • extension 中实现协议方法
// MARK: - PictureSelectorViewCellDelegate
extension PictureSelectorViewController: PictureSelectorViewCellDelegate {
    private func pictureSelectorViewCellDidAdd(cell: PictureSelectorViewCell) {
        print("添加照片")
    }

    private func pictureSelectorViewCellDidRemove(cell: PictureSelectorViewCell) {
        print("删除照片")
    }
}
  • 在数据源方法中设置代理
cell.pictureDelegate = self

注意:如果协议是私有的,那么协议方法也必须是私有的

选择照片

  • 判断是否支持访问相册
// 添加照片
private func pictureSelectorViewCellDidAdd(cell: PictureSelectorViewCell) {
    // 判断是否支持照片选择
    if !UIImagePickerController.isSourceTypeAvailable(.PhotoLibrary) {
        print("无法访问照片库")
        return
    }
}
  • 访问相册
// 访问相册
let picker = UIImagePickerController()
presentViewController(picker, animated: true, completion: nil)
  • 设置代理
// 设置代理
picker.delegate = self
  • 遵守协议并实现方法
// MARK: - UIImagePickerControllerDelegate, UINavigationControllerDelegate
extension PictureSelectorViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    /// 选中媒体代理方法
    ///
    /// - parameter picker: 照片选择器
    /// - parameter info:   信息字典 allowsEditing = true 适合选择头像
    func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
        print(info)

        dismissViewControllerAnimated(true, completion: nil)
    }
}

注意:一旦实现了代理方法,则需要用代码 dismiss 控制器

设置图片数据源

  • 定义照片数组
/// 照片数组
private lazy var pictures = [UIImage]()
  • 在代理方法中插入照片
let image = info[UIImagePickerControllerOriginalImage] as! UIImage

pictures.append(image)
collectionView?.reloadData()

dismissViewControllerAnimated(true, completion: nil)
  • 在 cell 中添加 image 属性
/// 照片
private var image: UIImage? {
    didSet {
        addButton.setImage(image, forState: .Normal)
    }
}
  • 修改数据源中的图像数量函数
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return pictures.count + 1
}

保证末尾有一个加号按钮添加照片

  • 在数据源方法中设置图像
cell.image = (indexPath.item == pictures.count) ? nil : pictures[indexPath.item]
  • 扩展 image 属性的 didSet 函数
/// 照片
private var image: UIImage? {
    didSet {
        addButton.setImage(image ?? UIImage(named: "compose_pic_add"), forState: .Normal)
        addButton.setImage(image ?? UIImage(named: "compose_pic_add_highlighted"), forState: .Highlighted)
    }
}

细节处理

记录用户点击按钮的索引

  • 定义当前选中照片索引
/// 当前选中照片索引
private var currentIndex = 0
  • 在代理方法中记录当前用户点击 cell 的索引
// 记录当前用户选中索引
currentIndex = collectionView!.indexPathForCell(cell)!.item
  • 在照片选择控制器的代理方法中设置对应的图像
if currentIndex < pictures.count {
    pictures[currentIndex] = image
} else {
    pictures.append(image)
}
collectionView?.reloadData()

设置照片填充模式

// 设置照片填充模式
addButton.imageView?.contentMode = .ScaleAspectFill

删除照片

  • 删除照片操作
// 删除照片
private func pictureSelectorViewCellDidRemove(cell: PictureSelectorViewCell) {

    guard let indexPath = collectionView?.indexPathForCell(cell) else {
        return
    }

    pictures.removeAtIndex(indexPath.item)
    collectionView?.deleteItemsAtIndexPaths([indexPath])
}
  • 默认隐藏删除按钮
/// 照片
private var image: UIImage? {
    didSet {
        addButton.setImage(image ?? UIImage(named: "compose_pic_add"), forState: .Normal)
        addButton.setImage(image ?? UIImage(named: "compose_pic_add_highlighted"), forState: .Highlighted)

        removeButton.hidden = (image == nil)
    }
}

设置最多选择照片数量

  • 定义最多照片常量
/// 最大照片数量
private let WBPictureSelectorViewMaxPictureCount = 9
  • 修改数据源方法
// MARK: 数据源
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return pictures.count + (pictures.count < WBPictureSelectorViewMaxPictureCount ? 1 : 0)
}

内存处理

  • 缩放图片
extension UIImage {

    /// 将图像缩放到指定宽度
    ///
    /// - parameter width: 指定宽度,如果图片尺寸比指定宽度小,直接返回
    ///
    /// - returns: 等比例缩放后的图像
    func scaleImage(width: CGFloat) -> UIImage {

        // 1. 判断图像尺寸
        if size.width < width {
            return self
        }

        // 2. 计算比例
        let height = size.height * width / size.width
        let rect = CGRect(x: 0, y: 0, width: width, height: height)

        // 3. 核心绘图
        // 1> 开启上下文
        UIGraphicsBeginImageContext(rect.size)

        // 2> 绘图
        drawInRect(rect)

        // 3> 获得结果
        let result = UIGraphicsGetImageFromCurrentImageContext()

        // 4> 关闭上下文
        UIGraphicsEndImageContext()

        // 5> 返回结果
        return result
    }
}

  • 修改照片选择控制器代理方法
/// 选中媒体代理方法
///
/// - parameter picker: 照片选择器
/// - parameter info:   信息字典 allowsEditing = true 适合选择头像
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {

    let image = info[UIImagePickerControllerOriginalImage] as! UIImage
    let scaleImage = image.scaleImage(300)

    if currentIndex < pictures.count {
        pictures[currentIndex] = scaleImage
    } else {
        pictures.append(scaleImage)
    }
    collectionView?.reloadData()

    dismissViewControllerAnimated(true, completion: nil)
}

整合照片选择控制器

准备 文件

  • PhotoSelector 拖拽至项目
  • UIImage+Extension.swift 拖拽至项目

整合照片选择控制器

  • 定义控制器属性
/// 照片选择控制器
private lazy var pictureSelectorViewController = PictureSelectorViewController()
  • 准备照片视图
/// 准备照片视图
private func preparePictureView() {
    // 添加视图
    view.addSubview(pictureSelectorViewController.view)

    // 自动布局
    pictureSelectorViewController.view.snp_makeConstraints { (make) -> Void in
        make.bottom.equalTo(view.snp_bottom)
        make.left.equalTo(view.snp_left)
        make.right.equalTo(view.snp_right)
        make.height.equalTo(view.snp_height).multipliedBy(0.6)
    }
}

运行测试,发现选中照片结束后,提示错误:

Presenting view controllers on detached view controllers is discouraged

  • 添加子控制器
// 添加子控制器
addChildViewController(pictureSelectorViewController)
  • 修改照片选择视图层次
// 添加视图
view.insertSubview(pictureSelectorViewController.view, belowSubview: toolbar)

运行会发现照片选择视图跑到了 textView 和 toolBar 的后面

重建控件布局

  • 修改照片选择视图的高度
make.height.equalTo(0)
  • 在选择照片监听方法中重建控件索引
// 选择照片
@objc private func selectPhoto() {

    if pictureSelectorViewController.view.bounds.height == 0 {
        // 修改布局高度
        pictureSelectorViewController.view.snp_remakeConstraints { (make) -> Void in
            make.bottom.equalTo(view.snp_bottom)
            make.left.equalTo(view.snp_left)
            make.right.equalTo(view.snp_right)
            make.height.equalTo(view.snp_height).multipliedBy(0.6)
        }
        // 修改文本视图的底部约束
        textView.snp_remakeConstraints { (make) -> Void in
            make.top.equalTo(view.snp_top)
            make.left.equalTo(view.snp_left)
            make.right.equalTo(view.snp_right)
            make.bottom.equalTo(pictureSelectorViewController.view.snp_top)
        }

        UIView.animateWithDuration(0.25, animations: { () -> Void in
            self.view.layoutIfNeeded()
        })
    }
}
  • 关闭键盘
textView.resignFirstResponder()

运行测试

  • 调整 viewDidAppear 如果已经显示照片选择视图,则不再激活键盘
override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)

    if pictureSelectorViewController.imageList.count == 0 {
        textView.becomeFirstResponder()
    }
}

发布微博

发布文字微博

接口定义

参数 说明
access_token 采用OAuth授权方式为必填参数,其他授权方式不需要此参数,OAuth授权后获得
status 要发布的微博文本内容,必须做URLencode,内容不超过140个汉字

连续两次发布的微博不可以重复

  • HMNetworkTools 中添加 update 方法
/// 发布文字微博
func update(accessToken: String, text: String, finished: HMRequestCallBack){
    // 请求地址
    let urlString = "https://api.weibo.com/2/statuses/update.json"
    // 请求参数
    let params = [
        "access_token": accessToken,
        "status": text
    ]
    request(.POST, url: urlString, params: params, finished: finished)
}
  • HMComposeViewController 中调用
/// 发送文字微博
private func update(){
    HMNetworkTools.shareTools.update(HMUserAccountViewModel.sharedUserAccount.accessToken!, text: textView.emoticonText) { (result, error) -> () in
        if error != nil {
            print(error)
            SVProgressHUD.showErrorWithStatus("发表失败")
            return
        }
        print(result)
        SVProgressHUD.showSuccessWithStatus("发表成功")
    }
}

发布图片微博

接口定义

文档地址

http://open.weibo.com/wiki/2/statuses/upload

接口地址

https://upload.api.weibo.com/2/statuses/upload.json

HTTP 请求方式

  • POST

请求参数

参数 说明
access_token 采用OAuth授权方式为必填参数,其他授权方式不需要此参数,OAuth授权后获得
status 要发布的微博文本内容,必须做URLencode,内容不超过140个汉字
pic 要上传的图片,仅支持JPEG、GIF、PNG格式,图片大小小于5M

请求必须用POST方式提交,并且注意采用multipart/form-data编码方式

代码实现

  • HMNetworkTools 中添加上传图片的方法
func upload(accessToken: String, text: String, image: UIImage, finished: HMRequestCallBack){
    // url
    let url = "https://upload.api.weibo.com/2/statuses/upload.json"

    let params = [
        "access_token": accessToken,
        "status": text
    ]

    POST(url, parameters: params, constructingBodyWithBlock: { (formData) -> Void in
        let data = UIImagePNGRepresentation(image)!
        /**
            1. data: 二进制数据
            2. name: 服务器定义的字段名称
            3. fileName: 保存在服务器的文件名,通常可以乱写,服务器自己会做处理
            4. mimeType: 告诉服务器文件类型
                - 大类型 / 小类型
                    image/jepg, image/png
                    text/plain, text/html
                - 如果不想告诉服务器准确类型:
                    application/octet-stream

        */
        formData.appendPartWithFileData(data, name: "pic", fileName: "xxaaa", mimeType: "image/jpeg")
        }, success: { (_, response) -> Void in
            guard let dict = response as? [String: AnyObject] else {
                // 如果不是字典,返回错误
                let error = NSError(domain: "com.itheima.error", code: -1001, userInfo: ["message": "The response data type isn't a [String: AnyObject]"])
                finished(result: nil, error: error)
                return
            }
            finished(result: dict, error: nil)
        }) { (_, error) -> Void in
            finished(result: nil, error: error)
    }
}

表情键盘

在实际开发中对于比较独立的模块,可以直接新建一个项目,在新项目上演练,测试,等待模块开发完毕之后再移植到项目中,方便项目的测试

实现效果

表情键盘效果图.png.jpeg

实现思路

  1. 从最简单的 View 开始做起
  2. 底部切换表情类型的 View 可以使用 UIStackView 来实现
  3. 表情显示的 View 可以使用 UICollectionView 实现
  4. 每一页表情对应一个 Cell 来表示
  5. 每一种表情对应 UICollectionView 中的一组

自定义 HMEmoticonKeyboard

  • 自定义 HMEmoticonKeyboard 继承于 UIView
class HMEmoticonKeyboard: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupUI()
    }

    private func setupUI(){
        // 设置背景颜色
        backgroundColor = UIColor(patternImage: UIImage(named: "emoticon_keyboard_background")!)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
  • HMComposeViewController 中添加切换键盘的方法 switchKeyboard
/// 切换键盘
private func switchKeyboard(){
}
  • 在点击 HMComposeToolBar 上的表情按钮的时候调用方法
// MARK: - HMComposeToolBarDelegate
func composeToolBarButtonDidSelected(type: ComposeToolBarButtonType) {
    switch type {
    case ...
    case .Emoticon:
        switchKeyboard()
    }
}
  • 懒加载键盘
/// 键盘
private lazy var emoticonKeyboard: HMEmoticonKeyboard = {
    let keyboard = HMEmoticonKeyboard()
    keyboard.size = CGSizeMake(SCREENW, 216)
    return keyboard
}()

表情类型切换视图

  • 自定义 HMEmoticonToolBar
class HMEmoticonToolBar: UIStackView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        // 设置布局方向
        axis = UILayoutConstraintAxis.Horizontal
        // 设置子控件的分布方式 -> 填充,大小相等
        distribution = UIStackViewDistribution.FillEqually

        setupUI()
    }

    private func setupUI(){
        // 添加 4 个按钮
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
  • 提供添加 3 个按钮的方法
private func addChildItem(title: String, bgImageName: String) {
    let button = UIButton()

    // 设置文字以及字体大小
    button.titleLabel?.font = UIFont.systemFontOfSize(14)
    button.setTitle(title, forState: UIControlState.Normal)

    // 设置不同状态的背景图片
    button.setBackgroundImage(UIImage(named: "\(bgImageName)_normal"), forState: UIControlState.Normal)
    button.setBackgroundImage(UIImage(named: "\(bgImageName)_selected"), forState: UIControlState.Selected)

    // 设置不同状态的文字颜色
    button.setTitleColor(UIColor.whiteColor(), forState: UIControlState.Normal)
    button.setTitleColor(UIColor.grayColor(), forState: UIControlState.Selected)

    addArrangedSubview(button)
}
  • setupUI 中添加按钮
private func setupUI(){
    // 添加 3 个按钮
    addChildItem("默认", bgImageName: "compose_emotion_table_right")
    addChildItem("Emoji", bgImageName: "compose_emotion_table_mid")
    addChildItem("浪小花", bgImageName: "compose_emotion_table_right")
}
  • HMEmoiticonKeyboard 中添加 HMEmoticonToolBar
// 懒加载控件
/// 底部切换表情类型的toolBar
private lazy var emoticonToolBar: HMEmoticonToolBar = HMEmoticonToolBar(frame: CGRectZero)
  • setupUI 方法中添加控件以及约束
// 添加子控件
addSubview(emoticonToolBar)

// 添加约束
emoticonToolBar.snp_makeConstraints { (make) -> Void in
    make.bottom.equalTo(self.snp_bottom)
    make.leading.equalTo(self.snp_leading)
    make.right.equalTo(self.snp_right)
    make.height.equalTo(37)
}

运行测试:按钮背景图片没有拉伸方式有问题

  • 更改拉伸方式:点击Assets.xcassets --> 选中对应的背景图片 --> 查看右边属性面板 --> 在 Slicing 区设置 SlicesHorizontal,设置 centerStretches

    • 有些情况下 Xcode 会出 Bug,需要设置 SlicesHorizontal And Vertical
  • 监听子按钮点击

private func addChildItem(title: String, bgImageName: String) {
    let button = UIButton()
    // 添加点击事件
    button.addTarget(self, action: "childButtonClick:", forControlEvents: UIControlEvents.TouchUpInside)
    ...
}
  • 实现响应方法
/// 子控件点击
///
/// - parameter button: 当前点击的 button
@objc private func childButtonClick(button: UIButton){
    // 按钮点击方法
}
  • 实现选中一个按钮的时候取消选中之前的按钮

    • 记录当前选中的按钮
    • 当点击下一个按钮的时候取消选中记录的按钮,选中当前按钮
    • 再次记录当前选中的按钮
  • 定义 currentSelectedButton 属性记录当前选中的按钮

/// 当前选中的按钮
var currentSelectedButton: UIButton?
  • childButtonClick 实现按钮点击逻辑
/// 子按钮点击
///
/// - parameter button: 当前点击的 button
@objc private func childButtonClick(button: UIButton){

    // 如果当前选中的 button 与即将要选中的button相同,则直接返回
    if button == currentSelectedButton {
        return
    }
    // 取消选中之前的
    currentSelectedButton?.selected = false
    // 选中现在点击的
    button.selected = true
    // 再次记录现在选的按钮
    currentSelectedButton = button
}

运行测试

  • 按钮点击的时候需要让 HMEmoticonKeyboard 知道哪一个按钮点击了
    • 给按钮添加tag
    • 添加协议,在按钮点击的时候调用协议方法
  • 更新 setupUI 方法中调用方式
private func setupUI(){
    // 添加 3 个按钮
    addChildItem("默认", bgImageName: "compose_emotion_table_left", index: 0)
    addChildItem("Emoji", bgImageName: "compose_emotion_table_mid", index: 1)
    addChildItem("浪小花", bgImageName: "compose_emotion_table_right", index: 2)
}
  • 定义协议
protocol HMEmoticonToolBarDelegate: NSObjectProtocol {
    func emoticonToolBarButtonDidSelected(index: Int)
}
  • 添加代理属性
/// 代理
weak var delegate: HMEmoticonToolBarDelegate?
  • 在按钮点击的时候调用代理身上的方法
/// 子按钮点击
///
/// - parameter button: 当前点击的 button
@objc private func childButtonClick(button: UIButton){
    ...
    // 调用代理方法
    delegate?.emoticonToolBarButtonDidSelected(button.tag)
}
  • HMEmoticonKeyboard 继承 HMEmoticonToolBarDelegate 协议
class HMEmoticonKeyboard: UIView, HMEmoticonToolBarDelegate {
    ...
}
  • HMEmoticonKeyboard 中设置 HMEmoticonToolBar 的代理为自己
/// 底部切换表情类型的toolBar
private lazy var emoticonToolBar: HMEmoticonToolBar = {
    let toolBar = HMEmoticonToolBar(frame: CGRectZero)
    toolBar.delegate = self
    return toolBar
}()
  • 实现代理方法
// MARK: - HMEmoticonToolBarDelegate
func emoticonToolBarButtonDidSelected(index: Int) {
    print(index)
}

运行测试

表情显示视图

  • HMEmoticonKeyboard 中添加 UICollectionView
/// 懒加载控件
/// 显示表情的视图
private lazy var emoticonCollectionView: UICollectionView = {
    let collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: UICollectionViewFlowLayout())
    collectionView.backgroundColor = RandomColor()
    return collectionView
}()
  • 添加控件与约束
// 添加子控件
addSubview(emoticonCollectionView)
// 添加约束
emoticonCollectionView.snp_makeConstraints { (make) -> Void in
    make.width.equalTo(self.snp_width)
    make.top.equalTo(self.snp_top)
    make.bottom.equalTo(self.emoticonToolBar.snp_top)
    make.leading.equalTo(self)
}

运行测试

  • 设置 emoticonCollectionView 的数据源以及注册 cell
/// 显示表情的视图
private lazy var emoticonCollectionView: UICollectionView = {
    let collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: UICollectionViewFlowLayout())
    collectionView.backgroundColor = RandomColor()
    // 设置数据源
    collectionView.dataSource = self
    // 注册 cell
    collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: HMEmoticonKeyboardCellId)
    return collectionView
}()
  • 继承协议
class HMEmoticonKeyboard: UIView, HMEmoticonToolBarDelegate, UICollectionViewDataSource {
    ...
}
  • 实现协议方法
extension HMEmoticonKeyboard {

    /// 返回表情一共有多少页
    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // 为了测试,先默认返回10个
        return 10
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(HMEmoticonKeyboardCellId, forIndexPath: indexPath)
        // 测试返回随机颜色
        cell.backgroundColor = RandomColor()
        return cell
    }
}

运行测试:

  • 调整每一个 cell 的大小
    • 因为每一个 cell 的大小与 collectionView 一样大
    • 而调整完 collectionView 大小要调用的方法就是 layoutSubviews
    • 所以在 layoutSubviews 调整每一个 cell 的大小
override func layoutSubviews() {
    super.layoutSubviews()

    // 设置每一个 cell 的大小
    let layout = emoticonCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
    layout.itemSize = emoticonCollectionView.size
}

运行测试:每一行之间有间距,而且滚动方向不对

  • 在初始化 emoticonCollectionView 的时候设置滚动方向以及 cell 间距 (UICollectionViewFlowLayout 身上的属性)
/// 显示表情的视图
private lazy var emoticonCollectionView: UICollectionView = {
    let layout = UICollectionViewFlowLayout()
    // 设置滚动方向:水平滚动
    layout.scrollDirection = UICollectionViewScrollDirection.Horizontal
    // 设置每一个 cell 之间的间距
    layout.minimumLineSpacing = 0

    let collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: layout)
    collectionView.backgroundColor = RandomColor()
    // 设置数据源
    collectionView.dataSource = self
    // 注册 cell
    collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: HMEmoticonKeyboardCellId)
    return collectionView
}()
  • 开启分页 & 隐藏水平滚动条 & 关闭弹簧效果 (UIScrollView 身上的属性)
// 开启分页 & 隐藏水平滚动条
collectionView.pagingEnabled = true
collectionView.showsHorizontalScrollIndicator = false
// 关闭弹簧效果
collectionView.bounces = false
  • 自定义 HMEmoticonPageCell 为表情键盘的 Cell
class HMEmoticonPageCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupUI(){
        backgroundColor = RandomColor()
    }
}
  • 替换注册的 cell
// 注册 cell
collectionView.registerClass(HMEmoticonPageCell.self, forCellWithReuseIdentifier: HMEmoticonKeyboardCellId)
  • 为了测试效果,在 HMEmoticonPageCell中添加一个测试的 label
class HMEmoticonPageCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupUI()
    }

    private func setupUI(){

        contentView.addSubview(label)

        label.snp_makeConstraints { (make) -> Void in
            make.center.equalTo(contentView.snp_center)
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    /// 测试用的 label
    private lazy var label: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFontOfSize(35)
        return label
    }()
}
  • 提供 indexPath: NSIndexPath 属性,显示当前滚动到哪个位置
var indexPath: NSIndexPath? {
    didSet{
        label.text = "第\(indexPath!.section)组,第\(indexPath!.item)页"
    }
}
  • 在返回 cell 的时候设置 indexPath
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(HMEmoticonKeyboardCellId, forIndexPath: indexPath) as! HMEmoticonPageCell
    // 测试返回随机颜色
    cell.backgroundColor = RandomColor()
    cell.indexPath = indexPath
    return cell
}

运行测试

读取表情数据

  • 在iTunesStore中下载最新版本的新浪微博安装包,获取素材文件
  • 支持iOS6.0的项目的素材是可以直接获取的但是如果不支持iOS6.0的设置是无法获取素材的,建议保存一些有些App的素材,大部分是没有版权的

三种文件夹的区别

  • 黄色文件夹: 编译后,资源文件在 mainBundle 中,源代码程序需要通过这种方式拖拽添加, 效率高
  • 蓝色文件夹:编译后,资源文件在 mainBundle 中的对应文件夹中,游戏文件的素材一般通过这种方式拖拽添加,用于换肤应用,游戏场景, 不同路径下的相同文件名

*白色 Bundle:编译后,资源文件在 mainBundle 中仍然以包的形式存在,可以路径形式访问,拖拽文件更简单,主要用于第三方框架包装资源素材

  • 新键 HMEmoticonManager 类,里面加载表情数据,对外提供表情数据,和一些配置信息
class HMEmoticonManager: NSObject {
static let shareEmoticonManager: EmoticonManager = EmoticonManager()

    lazy var packages: [EmoticonPackages] = [EmoticonPackages]()

    private override init() {
        super.init()
        loadEmoticons()
    }

    func loadEmoticons() {
        let path = NSBundle.mainBundle().pathForResource("emoticons.plist", ofType: nil, inDirectory: "Emoticons.bundle")!
        let dict = NSDictionary(contentsOfFile: path) as! [String : AnyObject]

        let array = dict["packages"] as! [[String : AnyObject]]

        for item in array {
            //获取id
            let id = item["id"] as! String
            loadPackages(id)
        }
    }

    private func loadPackages(id: String) {
        //通过id获取 分组名称
        let path = NSBundle.mainBundle().pathForResource("info.plist", ofType: nil, inDirectory: "Emoticons.bundle/" + id)!
        //通过分组名称加载分组中的 info.plist 文件
        let dict = NSDictionary(contentsOfFile: path)!
        let group_name_cn = dict["group_name_cn"] as! String
        //获取表情数据
        let array = dict["emoticons"] as! [[String : String]]

        let p = EmoticonPackages(id: id, title: group_name_cn,array: array)
        packages.append(p)
    }
}
  • 定义表情模型
class HMEmoticon: NSObject {

    /// 表情文字描述
    var chs: String?
    /// 表情图片名字 (仅对图片表情有效)
    var png: String?


    /// Emoji表情的 code
    var code: String?
    /// 是否是Emoji表情
    var isEmoji: Bool = false

    init(dictionary: [String: AnyObject]) {
        super.init()
        setValuesForKeysWithDictionary(dictionary)
    }

    override func setValue(value: AnyObject?, forUndefinedKey key: String) {}
}
  • 与界面对应需要向上抽取一个package对应的模型
  • title toolBar中显示的title
  • sectionEmoticon toolBar每个按钮对应的大数组
class EmoticonPackages: NSObject {
    var title: String?
    lazy var sectionEmoticon = [[Emoticon]]()
}
  • EmoticonPackages添加构造方法
init(id: String, title: String, array: [[String : String]]) {
        super.init()
        self.title = title

        //遍历数组 转换为模型 再将模型转换为

        var emoticonArray: [Emoticon] = [Emoticon]()
        for item in array {
            let e = Emoticon(id: id, dict: item)
            emoticonArray.append(e)
        }
    }
  • 处理数据, 将模型数组[HMEmoticon]类型处理为 [[HMEmoticon]]
private func sectionEmoticonArray(array: [Emoticon]) -> [[Emoticon]]{
        //获取表情数量 这些数组每页21个 能装多少组
        let pageCount = (array.count - 1 ) / 21 + 1

        var sectionEm = [[Emoticon]]()
        for i in 0..<pageCount {
            //每页截取从大数组中截取21个表情   不足21个的会造成数组索引越界
            let loc = i * SectionEmoticonCount
            var length = SectionEmoticonCount
            if loc + length > array.count {
                length = array.count - loc
            }
            let subArray = (array as NSArray).subarrayWithRange(NSRange(location: loc, length: length))
            sectionEm.append(subArray as! [Emoticon])
        }
        return sectionEm
    }
  • 返回 HMEmoticonKeyboardcollectionView 所需要的数据
    • 数据结构分析如下
表情数据结构分析.png

运行测试

底部 HMEmoticonToolBar 与 显示表情的 collectionView 联动

点击底部表情类型按钮,切换到对应表情

  • HMEmoticonToolBar 的代表方法中使用 collectionView 滚动到对应的组
// MARK: - HMEmoticonToolBarDelegate
func emoticonToolBarButtonDidSelected(index: Int) {
    print(index)
            let indexPath = NSIndexPath(forItem: 0, inSection: index)
            self.collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: false)
}

运行测试

当滚动到某种表情页时,选中对应表情按钮

实现思路

  • 实现监听 collectionView 滚动的位置
    • scrollView 的代理方法 scrollViewDidScroll
  • 获取到 collectionView 的中心
  • 定位滚动到对应cell的中心点
  • 判断当前屏幕中显示的两个cell 谁的frame包含了目标中心点
  • 循环当前屏幕中显示的cell对应的item(最多两个)
  • 当cell的frame 包含该了中心点的时候 就更新页面信息

实现代码

 func scrollViewDidScroll(scrollView: UIScrollView) {
        //确定cell的中心点
        var center = collectionView.center
        center.x = center.x + collectionView.contentOffset.x
        let indexPaths = collectionView.indexPathsForVisibleItems()

        for indexPath in indexPaths {
            //最多两个  最少一个
            let cell = collectionView.cellForItemAtIndexPath(indexPath)!
            if cell.frame.contains(center) {
                toolBar.setBtnSelected(indexPath.section)
                updatePageControlData(indexPath)
            }
        }
    }

运行测试

  • HMEmoticonToolBar 中提示 selectButtonWithSection 方法供选中按钮方法
/// 通过 section 选中某一个按钮
func selectButtonWithSection(section: Int) {
    // 通过 section 获取到对应的 button,让其选中
    let button = viewWithTag(section)! as! UIButton
    childButtonClick(button)
}
  • HMEmoticonKeyboardscrollViewDidScroll 方法中调用此方法
func scrollViewDidScroll(scrollView: UIScrollView) {
    ...
    emoticonToolBar.selectButtonWithSection(section)
}

运行测试:崩溃 Could not cast value of type 'WeiBo.HMEmoticonToolBar' (0x10bad2dc0) to 'UIButton' (0x10dcc2320). 原因是当前 section 为 0,调用 viewWithTag 方法取到的是 toolBar 自己,强转出错,所以把每一个按钮对应的枚举值给定一个基数

  • 在调用 viewWithTag 方法的时候添加一个基数
/// 通过 section 选中某一个按钮
func selectButtonWithSection(section: Int) {
    // 通过 section 获取到对应的 button,让其选中
    let button = viewWithTag(section + 1000)! as! UIButton
    childButtonClick(button)
}

运行测试:在从第0组滑动过一半的时候,很快速的切换到第1组表情去了,原因就是调用 childButtonClick 方法会执行代理方法,代理方法会回调滚动 collectionView,所以在这个地方只需要切换 button 选中状态

  • 提取按钮切换状态的方法 changeButtonState
/// 改变按钮状态,把当前选中的 button 取消选中,把传入的 button 设置选中
private func changeButtonState(button: UIButton){
    // 如果当前选中的 button 与即将要选中的button相同,则直接返回
    if button == currentSelectedButton {
        return
    }
    // 取消选中之前的
    currentSelectedButton?.selected = false
    // 选中现在点击的
    button.selected = true
    // 再次记录现在选的按钮
    currentSelectedButton = button
}
  • selectButtonWithSection 调用 changeButtonState 方法
/// 通过 section 选中某一个按钮
func selectButtonWithSection(section: Int) {
    // 通过 section 获取到对应的 button,让其选中
    let button = viewWithTag(section + 1000)! as! UIButton
    // 更改按钮选中状态
    changeButtonState(button)
}
  • 替换 childButtonClick 方法内实现
/// 子按钮点击
///
/// - parameter button: 当前点击的 button
@objc private func childButtonClick(button: UIButton){
    // 如果当前选中的 button 与即将要选中的button相同,则直接返回
    if button == currentSelectedButton {
        return
    }
    // 改变按钮状态
    changeButtonState(button)

    // 调用代理方法
    delegate?.emoticonToolBarButtonDidSelected(HMEmoticonType(rawValue: button.tag)!)
}

运行测试

表情显示

设置子控件

  • HMEmoticonPageCell 中添加 20 个按钮表情按钮
/// 添加表情按钮
private func addEmoticonButtons(){
    et leftMargin: CGFloat = 5
        let bottomMargin: CGFloat = 30
        let bW = (UIScreen.mainScreen().bounds.width - 2 * leftMargin) / CGFloat(EmoticonColCount)
        let bH = (bounds.height - bottomMargin) / CGFloat(EmoticonRowCount)
        for i in 0..<SectionEmoticonCount {
            let btn = EmoticonButton()
            btn.addTarget(self, action: "btnDidClick:", forControlEvents: .TouchUpInside)
            btn.titleLabel?.font = UIFont.systemFontOfSize(32)
            let row = i / EmoticonColCount
            let col = i % EmoticonColCount
            let x = leftMargin + CGFloat(col) * bW
            let y = bH * CGFloat(row)
            btn.frame = CGRect(x: x, y: y, width: bW, height: bH)
            contentView.addSubview(btn)
            buttonArray.append(btn)
        }
}
  • setupUI 方法中调用此方法
private func setupUI(){
    // 添加子控件
    addEmoticonButtons()
    ...
}


> 运行测试

### 显示图片表情数据

- 在 `HMEmoticonPageCell` 中提供 `emoticons` 属性,供外界设置表情数据

```swift
var emoticons: [HMEmoticon]?
  • HMEmoticonKeyboard 中的 collectionView 数据源方法里面给 cell 设置数据
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(HMEmoticonKeyboardCellId, forIndexPath: indexPath) as! HMEmoticonPageCell
    cell.indexPath = indexPath
    // 设置表情数据
    cell.emoticons = HMEmoticonTools.allEmoticons()[indexPath.section][indexPath.row]
    return cell
}
  • emoticonsdidSet 方法中显示表情
/// 当前页显示的表情数据
var emoticons: [HMEmoticon]? {
    didSet{
        // 遍历当前设置的表情数据
        for (index,value) in emoticons!.enumerate() {
            let button = emoticonButtons[index]
            if !value.isEmoji {
                let image = UIImage(named: value.png!)
                button.setImage(image, forState: UIControlState.Normal)
            }
        }
    }
}

运行测试:表情没有显示出来,加载表情的图片地址不正确,因为表情图片是放在 Emoticons.bundle 中的,所以需要拼接前面的路径,而这前面的路径就是表情所对应的 info.plist 文件所在的路径

  • HMEmoticon 中添加 path 属性
var png: String? {
        didSet {
            imagePath = Emoticon.bundlePath + "/Emoticons.bundle/" + (id ?? "") + "/\(png ?? "")"
        }
    }

    var imagePath: String?
  • 更新 HMEmoticonPageCellemoticonsdidSet 方法
/// 当前页显示的表情数据
var emoticons: [HMEmoticon]? {
    didSet{
        // 遍历当前设置的表情数据
        for (index,value) in emoticons!.enumerate() {
            let button = emoticonButtons[index]
            if !value.isEmoji {
                let image = UIImage(named: "\(value.path!)/\(value.png!)")
                button.setImage(image, forState: UIControlState.Normal)
            }
        }
    }
}

运行测试:图片表情显示出来了,但是 cell 复用 导致没有表情的页面也显示过表情,所以在遍历设置表情之后需要先将所有的 显示表情的button 隐藏掉

  • 先隐藏所有显示表情的 button,遍历几个表情显示几个
/// 当前页显示的表情数据
var emoticons: [HMEmoticon]? {
    didSet{

        // 先隐藏所有的表情按钮
        for value in emoticonButtons {
            value.hidden = true
        }

        // 遍历当前设置的表情数据
        for (index,value) in emoticons!.enumerate() {
            let button = emoticonButtons[index]
            // 显示当前遍历到的表情按钮
            button.hidden = false
            if !value.isEmoji {
                let image = UIImage(named: "\(value.path!)/\(value.png!)")
                button.setImage(image, forState: UIControlState.Normal)
            }
        }
    }
}

显示 Emoji 表情数据

  1. 演练Emoji表情, 拖入 String+Emoji 分类到项目中,
  2. Emoji 表情其实就是字符串
  • 设置 Emoji 表情数据

    button.setTitle(em.codeStr(), forState: UIControlState.Normal)
}
  • 运行测试:Emoji 表情显示太小,调整 button 的文字大小即可解决
/// 添加表情按钮
private func addEmoticonButtons(){
    for _ in 0..<HMEmoticonPageNum {
        let button = UIButton()
        button.titleLabel?.font = UIFont.systemFontOfSize(36)
        contentView.addSubview(button)
        emoticonButtons.append(button)
    }
}

运行测试

  • 更改 HMEmoticonKeyboard 中的 collectionView 的背景颜色为透明色
/// 显示表情的视图
private lazy var emoticonCollectionView: UICollectionView = {
    ...
    collectionView.backgroundColor = UIColor.clearColor()
    ...
    return collectionView
}()
  • 去掉 HMEmoticonPageCell 中显示 section 的 label

运行测试

  • 提升表情数据
  • 每页的最后一个添加一个删除按钮
  • 每页不足21个表情需要补足空白表情
  • 空白表情的最后一个应该是删除表情

在HMEmoticonPackages提升数据

init(id: String, title: String, array: [[String : String]]) {
        super.init()
        self.title = title

        //遍历数组 转换为模型 再将模型转换为

        var emoticonArray: [Emoticon] = [Emoticon]()
        var index = 0
        for item in array {
            let e = Emoticon(id: id, dict: item)
            emoticonArray.append(e)
            index++
            if index == 20 {
                //每页的最后一个添加一个删除表情
                let delete = Emoticon(isDelete: true)
                emoticonArray.append(delete)
                index = 0
            }
        }


        //不足21个的补空白表情
        let delta = emoticonArray.count % 21
        if delta != 0 {
            for _ in delta..<20 {
                let empty = Emoticon(isEmpty: true)
                emoticonArray.append(empty)
            }
            emoticonArray.append(Emoticon(isDelete: true))
        }
        //将模型数组处理成 分组的模型数组
        //分组规则 21 个表情为一页
        self.sectionEmoticon = sectionEmoticonArray(emoticonArray)
    }
  • 在HMEmoticon模型中添加空表情和删除表情的构造方法和对应的标记

运行测试 解决页面复用的问题

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,975评论 4 60
  • 2017.02.22 可以练习,每当这个时候,脑袋就犯困,我这脑袋真是神奇呀,一说让你做事情,你就犯困,你可不要太...
    Carden阅读 1,316评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,559评论 18 139
  • 在宿舍住了一年多一点了,养花也快一年了,规模逐渐扩大,由开始的花市上买的盆栽到后来自己用塑料瓶切开的养着的路边的...
    嘎嘎222阅读 294评论 2 2