Swift自定義表情鍵盤+錄音

老規矩,一圖勝千言。Demo 傳送門 點我就行html

運行環境

  • Xcode10
  • swift 4.0

前言

這裏沒有乾貨,也沒有教程,請各位大神手下留情。這個 demo 是平時本身在工做之餘學習 swift 寫的,由於天天學習時間有限因此這個 demo 先後寫了一個月左右,裏面的語法和命名都不是很規範,也沒有作大量的機型和版本測試,總體語法偏向於OC。在寫的期間也查詢了許多資料以及API 的用法,其中有一部分邏輯和 emoji 表情資源是來自於VernonVan的這篇博客,我也並沒有抄襲之意,只是單純的去練習和使用swift語法僅此而已。其餘素材均來自於iconfontgit

看圖知意

這個思惟導圖展現的是各個子控件的層級關係,也包含了部分邏輯。demo中頁面聯動和旋轉適配未作。github

花開兩朵各表一枝

從 emoji 提及

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他的工程中的正則表達式是有點瑕疵的。在正則表達式上我作了改進,匹配規則以下:安全

  • 你好[smile] -> [smile]
  • 你好[smile.png] -> [smile.png]
  • 你好[smile_] -> [smile_]
  • 你好[a[smile]] -> [smile]
  • 你好[][[[smile]] -> []、[smile]

只作了 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))
複製代碼
輸入框與表情頁切換動畫

引用別人的話:「真正的鍵盤也就是說調起表情鍵盤時輸入框是有光標的,能進行拖拽光標、選中區域等的操做,這樣的體驗纔是與系統鍵盤一致的。其實系統已經提供好了接口給咱們直接使用,UITextViewUITextField都有的inputViewinputAccessoryView就是用來實現自定義鍵盤的」。可是有一種狀況是:若是表情鍵盤的高度低於系統字體鍵盤的高度,那麼在切換表情鍵盤與文字鍵盤的時候是有落差的,這個落差致使textView在回落的過程當中,字體鍵盤瞬間切換表情鍵盤會有一個間隙把當前頁面的內容暴露出來個零點幾秒,很是影響美觀,而系統的文字鍵盤高度和 emoji 鍵盤高度時一致的因此沒有這個問題。解決辦法我暫時就想起來兩種:服務器

  1. textViewkeyboardWillShow通知執行時,將textViewsuperView的高度等於 textView.height + emojiView.height 這樣supeView的高度就會很大,這樣在回落的過程當中就不會顯示位移縫隙,還能夠爲 emoji 視圖加向上滾動的動畫,這樣切換就會更加銜接。
  2. 不用textViewinputView屬性,作一個假的 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
        }
        
    }
}
複製代碼
語音邊錄邊轉的實現

語音錄製邏輯是這樣嬸的。

  1. 每點擊一次錄音按鈕便建立一個錄音機,建立錄音機的同時會建立兩個路徑:.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
    複製代碼
  2. 錄製時間爲60秒,前1S內爲初始化錄音機時間,若是1S內取消錄製則提示"錄音時間過短",執行取消錄製方法,刪除兩個文件;若是沒有取消則繼續錄製,展現錄製動畫,增長手勢滑動效果,增長語音消息呼吸燈動畫,當錄製完畢後在轉換回調中刪除錄製相對應的.caf文件,拋出轉換成功的.mp3文件路徑。

  3. 錄製成功後,拿到相對應的.mp3文件路徑上傳到服務器,由於在上傳過程必爲異步上傳(若是爲主線程那不就卡了),有可能當前文件未上傳成功後續又有文件要上傳,因此要記得加鎖,加鎖,加鎖保證數據的安全。demo 中這一部分並無實現。

  4. 取消發送,則刪除兩個對應的文件,結束轉換

  5. 錄製時間到直接發送,上滑取消,聲波監測等等。。。

具體實現見demo內Utils->Keyboard->Tool->Recorder文件,邊錄邊轉的實現見ConverAudioFile文件,轉換是用的lame.framework的三方庫。

swift基本語法

經常使用屬性

只讀屬性(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    
        
    }
}
複製代碼
協議擴展

像上面的例子中UITableviewCellUICollectionCell中他們所實現的方法都是同樣的,只是二者的類型不一樣,則不必定義兩個協議,只須要寫一個協議便可,這時就能夠在協議中使用關聯類型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作類型限定,即方法擁有者只能是限定的類型。

爲何使用協議擴展

由於咱們項目裏有不少地方是對UIViewUIColor等經常使用類進行extension

  1. 在進行多人開發的時候對同一類型作相同操做是很常有的事,他也寫了一個和你命名方式同樣的方法,可是他也新建了一個文件,而後大家兩個方法就衝突了,而後再進行一頓排查。
  2. 隨着需求的增多,你擴展的方法也就更多,而後將這些方法寫成工具類,當進行下次開發時能夠直接拖進工程中快速使用,可是卻與其餘人的方法衝突了,很尷尬啊。

上面的協議擴展能夠很好的解決這個問題,並且在寫法上能夠帶一個本身的標誌,逼格很高。像一些三方庫都有這種操做的:view.snp.makeConstraints()imageView.kf.setImage(with: <>)

由於類型不少,要擴展出來的方法也不少,總不能每一個類或者結構體都寫一個協議吧,其實,寫一個就夠了,將這些協議抽離出一個通用的便可。demo 中就是這樣作的,將協議抽離出一個通用的來。

總結

在寫的過程當中並無按照別人的代碼照抄照搬而是吸收精華,棄去糟粕。寫 demo 不是目的,更多的是爲了提升本身的知識面,並且 swift 語言版本也日漸穩定,swift 做爲 iOS 的新語言潛力仍是比較大的。由於對 swift 學習的比較少,理解的也比較淺,文中或 demo 裏確定有不穩當的地方,因此是接受批評和教育的。

轉載請說明出處。

參考資料

juejin.im/post/5a6b3f…

www.jianshu.com/p/971fff236…

onevcat.com/2016/11/pop…

onevcat.com/2016/12/pop…

www.jianshu.com/p/c06ebd6de…

www.cnblogs.com/muzijie/p/6…

相關文章
相關標籤/搜索