发布微博
课程目标
- 界面搭建
- 自定义文字输入框
- 自定义显示照片的View
- 底部 toolBar 自定义(UIStackView)
- 表情键盘(一个复杂的自定义 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
}
运行测试
文字输入框
- 带有占位文字
- 可以像 UITextView 一样输入多行
- 自定义一个输入框继承于 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)
- 在
HMComposeViewController
的setupUI
方法中添加控件与约束
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()
}
}
发布微博
发布文字微博
接口定义
-
文档地址
-
接口地址
-
HTTP 请求方式
- POST
请求参数
参数 | 说明 |
---|---|
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)
}
}
表情键盘
在实际开发中对于比较独立的模块,可以直接新建一个项目,在新项目上演练,测试,等待模块开发完毕之后再移植到项目中,方便项目的测试
实现效果
实现思路
- 从最简单的 View 开始做起
- 底部切换表情类型的 View 可以使用
UIStackView
来实现 - 表情显示的 View 可以使用
UICollectionView
实现 - 每一页表情对应一个
Cell
来表示 - 每一种表情对应
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
区设置Slices
为Horizontal
,设置center
为Stretches
- 有些情况下 Xcode 会出 Bug,需要设置
Slices
为Horizontal And Vertical
- 有些情况下 Xcode 会出 Bug,需要设置
监听子按钮点击
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
}
- 返回
HMEmoticonKeyboard
中collectionView
所需要的数据- 数据结构分析如下
运行测试
底部 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)
}
- 在
HMEmoticonKeyboard
的scrollViewDidScroll
方法中调用此方法
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
}
- 在
emoticons
的didSet
方法中显示表情
/// 当前页显示的表情数据
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?
- 更新
HMEmoticonPageCell
中emoticons
的didSet
方法
/// 当前页显示的表情数据
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 表情数据
- 演练Emoji表情, 拖入
String+Emoji
分类到项目中, - 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模型中添加空表情和删除表情的构造方法和对应的标记
运行测试 解决页面复用的问题