老规矩,一图胜千言。Demo 传送门 点我就行html
这里没有干货,也没有教程,请各位大神手下留情。这个 demo 是平时本身在工做之余学习 swift 写的,由于天天学习时间有限因此这个 demo 先后写了一个月左右,里面的语法和命名都不是很规范,也没有作大量的机型和版本测试,总体语法偏向于OC。在写的期间也查询了许多资料以及API 的用法,其中有一部分逻辑和 emoji 表情资源是来自于VernonVan的这篇博客,我也并没有抄袭之意,只是单纯的去练习和使用swift语法仅此而已。其余素材均来自于iconfont。git
这个思惟导图展现的是各个子控件的层级关系,也包含了部分逻辑。demo中页面联动和旋转适配未作。github
demo 总体业务逻辑占很大内容,其余都是子控件的堆叠并无很高的难度系数,只要处理好控件之间的逻辑关系就能很好的实现动画效果。正则表达式
在作 emoji 表情的时候还在想怎么实现表情与文字的转换,如:😂 -> [笑哭] 这种形式,由于与服务器进行数据交互将表情做为图片作数据传递是很是不合理的,而且还要考虑到表情与文字之间的相互转化关系,因此demo 中用到的是将 emoji 当中富文本的Attachment
属性来处理而后给相应的表情打Tag。来看一下具体代码:编程
//点击 emoji 事件
func didClickEmoji(with model: MYEmojiModel) {
guard let image = UIImage.image(name: model.imageName!, path: "emoji") else {
print("图片找不到")
return
}
// 记录textView光标当前位置
let selectedRange = self.textView.selectedRange
// 将 emoji 标记为[name] 这种形式
let emojiString = "[\(model.emojiDescription!)]"
// 经过字体大小设置 emoji 大小
let font = UIFont.systemFont(ofSize: MYTextViewTextFont)
let emojiHeight = font.lineHeight
// emoji 图片附件
let attachment = NSTextAttachment()
attachment.image = image
attachment.bounds = .init(x: 0, y: font.descender, width: emojiHeight, height: emojiHeight)
let attachString = NSAttributedString(attachment: attachment)
// 将图片附件转为 NSMutableAttributedString
let emojiAttributedString = NSMutableAttributedString(attributedString: attachString)
// 将这段文字打上标记,key 本身定义,value 为[name],这样作方便遍历和表情与文字替换
emojiAttributedString.addAttribute(NSAttributedString.Key(rawValue: MYAddEmojiTag), value: emojiString, range: .init(location: 0, length: attachString.length))
// 获取输入框中的富文本
let attributedText = NSMutableAttributedString(attributedString: self.textView.attributedText)
// 将打好标记的富文本替换到光标位置
attributedText.replaceCharacters(in: selectedRange, with: emojiAttributedString)
self.textView.attributedText = attributedText
self.textView.selectedRange = .init(location: selectedRange.location + emojiAttributedString.length, length: 0)
// 从新设置 font 是为了不 emoji 在文字末尾致使光标变小
self.textView.font = font
//从新计算文字高度,来作自适应
self.textViewDidChange(self.textView)
}
复制代码
由于在 emoji 被点击的时候就被打上相应的tag,value 是对应的文字描述,因此在富文本转字符串时就比较方便了。swift
//将 string 转为 NSString为了方便作字符串截取
let string = attribute.string as NSString
//遍历富文本,筛选出被打标记的富文本
attribute.enumerateAttribute(NSAttributedString.Key(rawValue: MYAddEmojiTag),
in: range, options: NSAttributedString.EnumerationOptions.longestEffectiveRangeNotRequired) { (value, range, stop) in
if value != nil {
// value即 emoji 对应的描述信息
let tagString = value as! String
result = result + tagString
}else{
let rangString = string.substring(with: range)
result = result + rangString
}
}
复制代码
经过上面的代码就已经实现文字<=>富文本的相互转换了,由于textView
自带复制粘贴功能,而UIPasteboard
粘贴板是没有attributedText
属性的,当复制或剪切时只能将textView.attributedText
转为文字,当粘贴的时候只能将文字转为富文本。由于在emoji 键盘被点击的时候你已经知道 emoji 相对应的文字描述,而若是粘贴为纯文字,那如何知道相对应的 emoji 呢?是的,用的是正则匹配,也是盗用别人的逻辑,可是VernonVan他的工程中的正则表达式是有点瑕疵的。在正则表达式上我作了改进,匹配规则以下:安全
只作了 a-z 下划线和.的匹配,若是想匹配更多内容本身添加规则便可。正则表达式不是很会写,只是尝试着想了这几种规则,想要验证和学习的能够去正则验证网站学习。具体代码实如今工程:Targets->Utils->Keyboard->Resources->MYMatchingEmojiManager
文件中bash
//正则验证网站:https://c.runoob.com/front-end/854
//表达式: \[([a-z_.])+?\]
let regex = try! NSRegularExpression.init(pattern: "\\[([a-z_.])+?\\]")
//用表达式匹配结果
let results = regex.matches(in: string
, options: NSRegularExpression.MatchingOptions.reportProgress, range: .init(location: 0, length: string.count))
复制代码
引用别人的话:“真正的键盘也就是说调起表情键盘时输入框是有光标的,能进行拖拽光标、选中区域等的操做,这样的体验才是与系统键盘一致的。其实系统已经提供好了接口给咱们直接使用,UITextView
和UITextField
都有的inputView
和inputAccessoryView
就是用来实现自定义键盘的”。可是有一种状况是:若是表情键盘的高度低于系统字体键盘的高度,那么在切换表情键盘与文字键盘的时候是有落差的,这个落差致使textView
在回落的过程当中,字体键盘瞬间切换表情键盘会有一个间隙把当前页面的内容暴露出来个零点几秒,很是影响美观,而系统的文字键盘高度和 emoji 键盘高度时一致的因此没有这个问题。解决办法我暂时就想起来两种:服务器
textView
的keyboardWillShow
通知执行时,将textView
的superView
的高度等于 textView.height + emojiView.height
这样supeView
的高度就会很大,这样在回落的过程当中就不会显示位移缝隙,还能够为 emoji 视图加向上滚动的动画,这样切换就会更加衔接。textView
的inputView
属性,作一个假的 emoji 表情页,微信的键盘就是一个假的,由于当切换到表情页时,textView
就失去了响应,光标就消失了,这样就形成了键盘回落而 emoji 键盘向上滚动的效果,我在工程中就是用的这种方式。不管是文字切换语音、文字切表情、语音切表情或者其余功能的任意切换,都是通过如下方法(具体实现见 demo):微信
private var keyboardType : MYKeyboardInputViewEnum.KeyboardType = .None {
//默认没有任何属性,为.None
//至关于OC中的重写 set 方法
willSet{
if keyboardType == newValue {
//若是将要改变的值与当前值同样,则不作任何操做,即同一种模式
return
}
//不相同则从新赋值
self.keyboardType = newValue;
switch newValue {
//判断哪一种模式,处理相应的逻辑,具体实现见工程代码
case .Emoji:
break
case .System:
break
case .Funcs:
break
case .Record:
break
default:
break
}
}
}
复制代码
语音录制逻辑是这样婶的。
每点击一次录音按钮便建立一个录音机,建立录音机的同时会建立两个路径:.caf
路径和.mp3
路径,.caf
路径是录音机录制的文件存放路径,.mp3
路径则为转换后的文件路径。以及录音机的一些必要参数:
/// 设置录音格式 默认kAudioFormatLinearPCM
var formatIDKey : AudioFormatID = kAudioFormatLinearPCM
/// 设置录音采样率(Hz) 8000/44100/96000(影响音频的质量) 默认 44100
var sampleRateKey : NSInteger = 44100
/// 录音通道数 1 或 2 默认为2
var channelsKey : NSInteger = 2
/// 线性采样位数 八、1六、2四、32 默认 16
var bitDepthKey : NSInteger = 16
/// 录音的质量 默认QualityMin
var qualityKey : AVAudioQuality = .min
复制代码
录制时间为60秒,前1S内为初始化录音机时间,若是1S内取消录制则提示"录音时间过短",执行取消录制方法,删除两个文件;若是没有取消则继续录制,展现录制动画,增长手势滑动效果,增长语音消息呼吸灯动画,当录制完毕后在转换回调中删除录制相对应的.caf
文件,抛出转换成功的.mp3
文件路径。
录制成功后,拿到相对应的.mp3
文件路径上传到服务器,由于在上传过程必为异步上传(若是为主线程那不就卡了),有可能当前文件未上传成功后续又有文件要上传,因此要记得加锁,加锁,加锁保证数据的安全。demo 中这一部分并无实现。
取消发送,则删除两个对应的文件,结束转换
录制时间到直接发送,上滑取消,声波监测等等。。。
具体实现见demo内Utils->Keyboard->Tool->Recorder
文件,边录边转的实现见ConverAudioFile
文件,转换是用的lame.framework
的三方库。
只读属性(readonly)
在OC语法中由于存在.h
和.m
两个文件,因此想暴露给外部使用的接口和方法是所有定义在.h
文件中的而 swift 则是所有写在同一个文件中的。若是你想定义一个属性为只读属性:
OC写法
.h文件定义
@property (nonatomic, assign,readonly) BOOL isHidden;
.m文件实现
- (BOOL)isHidden{}
复制代码
swift写法
//只实现 get 方法
var isHidden : BOOL {
get{
return true
}
}
复制代码
有时候你须要定义一个属性,外部为只读而内部能够读写,OC是很是好实现的
.h文件定义
@property (nonatomic, assign,readonly) BOOL isHidden;
.m文件实现
@property (nonatomic, assign) BOOL isHidden;
这样就可实现一个外部只读内部读写功能
复制代码
而 swift 实现方法有不少种,你能够定义一个方法,内部定义一个为private
的属性,将这个属性返回出去。还有更简便的写法
//意思是内部实现 set 方法,外部只可调用 get 方法
private(set) var isHidden = true
复制代码
设置代理
OC 中是这样写的
@protocol MYEmojiProtocolDelegate <NSObject>
//必须实现
- (void) didClickDelete;
@optional 可选实现
- (void) didClickSend;
@end
复制代码
设置代理属性:
@property (nonatomic, weak) id<MYEmojiProtocolDelegate> delegate;
复制代码
swift 写法
protocol MYEmojiProtocolDelegate : NSObjectProtocol {
func didClickDelete()
}
复制代码
设置代理属性:
weak var pageDelegate : MYEmojiProtocolDelegate?
复制代码
若是 swift 代理方法想设置成option
可选方法,则方法须要加@objc
前缀,protocol
前也是须要加@objc
的,被标识为@objc属性,使得它兼容OC代码,拥有可选方法的协议只能被类遵照而枚举和结构体是不能遵照协议的。还有一种作法就是对协议进行方法扩展:
extension MYEmojiProtocolDelegate {
//扩展代理的方法是必须实现的
func didClickSend() {
}
}
复制代码
在学习 swift 的时候发现OC中的代理与 swift 中的协议,这是两种不一样的概念,咱们也知道 swift 是一门面向协议的编程,由于是初学 swift 对其理解仍是比较浅的,下面谈谈我对面向协议的理解。
protocol
是一些方法或属性的名称,自我理解像是方法和属性(或者属性)的集合。只定义接口或者属性而不实现任何功能,若是某个类型
(不是类;类型包括:类,枚举,结构体)想要遵照一个协议,那它须要实现这个协议所定义的全部这些内容。swift 里的protocol
不只能够定义方法还可定义属性,这与OC
里的有所不一样。
举个栗子:为UITableViewcell
实现点击事件即:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//增长个点击调用方法
didClick()
}
复制代码
由于有不一样的UITableViewcell
的子类都须要实现这个方法,那咱们应该怎么作呢?
继承
能够很好的解决这个问题,可是缺点是带来耦合性。若是再实现一个呼吸效果呢,就又在Base
类中实现相应的代码,很快Base
类就变得臃肿,且任何代码均可以写进去,而子类也彻底不知道实现了父类的哪些方法。
Extension/Category
你们确定在项目中用到的比较多,也很实用。直接为UITableViewcell
写一个扩展,那意味着项目里全部的UITableViewcell
对象均可以访问这个方法,若是UICollectionCell
也须要上面的方法呢?也写扩展,粘贴复制一样的代码,咱们都知道这两个类都继承自UIView
,那直接给UIView
添加扩展,这样项目中全部继承自UIView
的对象均可以访问这个方法,为了一个类就污染了其余对象,由于这些对象根本不须要这些方法。
定义一个protocol
protocol MYCompatible {
// 定义属性
//必须明确指定该属性支持的操做:只读(get)或者是可读写(get set)
// 要用 var 定义属性,即便只有 get 方法
var name: String {get set}
var birthday : String {get}
// 定义方法
//protocol中的约定方法,当方法中有参数时是不能有默认值的
func eat(food: String)
//若是须要改变自身的值,须要在方法前面加mutating关键字
mutating func changeName(name: String)
}
复制代码
定一个类或者结构体实现该协议
//遵照协议,实现协议的方法 就上面例子而言,只须要将`UITableviewCell`类遵照协议便可
class MYExtension: MYCompatible {
var name: String = "xiaoma"
let birthday: String = "1994"
func eat(food: String = "KFC") {
if food == "KFC" {
print("好吃")
} else {
print("想吃KFC")
}
}
//若是协议中方法有mutating关键字,若是结构体来遵照协议则须要mutating
func changeName(name: String) {
self.name = name
}
}
复制代码
若是只但愿协议只被类class遵照,只须要在定义协议的时候在后面加上AnyObject
便可
protocol MYCompatible : AnyObject {
var name: String {get set}
...
}
复制代码
若是协议中定义了构造函数(init),则实现协议的类必须实现这个构造函数
protocol MYCompatible {
var name: String {get set}
var birthday: String {get}
// 定义构造函数
init(name: String)
}
class MYExtension: MYCompatible {
var name: String = "xiaoma"
let birthday: String = "1994"
//若是该类被定义为final 则 required 不写
required init(name: String) {
self.name = name
}
}
复制代码
像上面的例子中UITableviewCell
和UICollectionCell
中他们所实现的方法都是同样的,只是二者的类型不一样,则不必定义两个协议,只须要写一个协议便可,这时就能够在协议中使用关联类型associatedtype
public protocol MYCompatible {
associatedtype MYCompatibleType
var my : MYCompatibleType { get }
}
final class MYExtension: MYCompatible {
typealias MYCompatibleType = Bool
var my: MYCompatibleType {
return true
}
}
复制代码
咱们知道协议中定义的属性或者方法是不提供实现方式的,咱们能够经过协议扩展的形式,在扩展中实现相应的代码:
//定一个协议
public protocol MYCompatible {
//使用关联类型
associatedtype MYCompatibleType
//建立属性 属性类型为关联的协议
var my : MYCompatibleType { get }
}
//构建一个类,实现协议
public final class MYExtension<Base>: MYCompatible {
// Base 为泛型
public let my: Base
// 构造方法
public init(_ my:Base) {
self.my = my
}
}
复制代码
给协议添加默认实现,用where
关键字对协议作条件限定(where 类型限定) 这里 MYCompatibleType 关联类型,能够是类或者是结构体,若是是结构体能够用 MYCompatibleType == Data
若是是类则能够 MYCompatibleType: UIView
extension MYCompatible where MYCompatibleType : UIView {
public var width: CGFloat {
get {
return my.frame.size.width
}
set {
my.frame.size.width = newValue
}
}
}
在想要扩展的类中添加MYExtension 类或者结构体,这个类是继承MYCompatible的协议的,因此就拥有了MYCompatible协议里面默认的实现方法,即刚才那个用 `where` 限定的类型
extension UIView {
var my: MYExtension <UIView> {
return MYExtension(my: self)
}
}
//调用则是
let view = UIView()
view.my.width = 20
复制代码
咱们如今回过头来看看这个扩展协议,首先定义一个名为MYCompatible
的协议,而后关联类型associatedtype MYCompatibleType
,定义属性为var my : MYCompatibleType { get }
返回的类型为关联的类型,再定义一个类MYExtension <Base>
Base为泛型,实现协议,则实现my
属性,再构造MYExtension
类的init
方法,如今对UIView
进行扩展
extension UIView {
var my: MYExtension <UIView> {
return MYExtension(my: self)
}
}
复制代码
如今 UIView
的对象里的属性my
就实现了MYCompatible
协议,即拥有该协议的方法,由于协议默认是不提供方法的实现的,因此要对协议进行扩展,在扩展的时候使用了where
作类型限定,即方法拥有者只能是限定的类型。
由于咱们项目里有不少地方是对UIView
、UIColor
等经常使用类进行extension
上面的协议扩展能够很好的解决这个问题,并且在写法上能够带一个本身的标志,逼格很高。像一些三方库都有这种操做的:view.snp.makeConstraints()
、imageView.kf.setImage(with: <>)
由于类型不少,要扩展出来的方法也不少,总不能每一个类或者结构体都写一个协议吧,其实,写一个就够了,将这些协议抽离出一个通用的便可。demo 中就是这样作的,将协议抽离出一个通用的来。
在写的过程当中并无按照别人的代码照抄照搬而是吸收精华,弃去糟粕。写 demo 不是目的,更多的是为了提升本身的知识面,并且 swift 语言版本也日渐稳定,swift 做为 iOS 的新语言潜力仍是比较大的。由于对 swift 学习的比较少,理解的也比较浅,文中或 demo 里确定有不稳当的地方,因此是接受批评和教育的。
转载请说明出处。