老規(guī)矩,一圖勝千言。Demo 傳送門 點(diǎn)我就行
運(yùn)行環(huán)境
- Xcode10
- swift 4.0
前言
這里沒(méi)有干貨,也沒(méi)有教程,請(qǐng)各位大神手下留情。這個(gè) demo 是平時(shí)自己在工作之余學(xué)習(xí) swift 寫的,因?yàn)槊刻鞂W(xué)習(xí)時(shí)間有限所以這個(gè) demo 前后寫了一個(gè)月左右,里面的語(yǔ)法和命名都不是很規(guī)范,也沒(méi)有做大量的機(jī)型和版本測(cè)試,整體語(yǔ)法偏向于OC。在寫的期間也查詢了許多資料以及API 的用法,其中有一部分邏輯和 emoji 表情資源是來(lái)自于VernonVan的這篇博客,我也并無(wú)抄襲之意,只是單純的去練習(xí)和使用swift語(yǔ)法僅此而已。其他素材均來(lái)自于iconfont。
看圖知意
這個(gè)思維導(dǎo)圖展示的是各個(gè)子控件的層級(jí)關(guān)系,也包含了部分邏輯。demo中頁(yè)面聯(lián)動(dòng)和旋轉(zhuǎn)適配未做。
花開(kāi)兩朵各表一枝
從 emoji 說(shuō)起
demo 整體業(yè)務(wù)邏輯占很大內(nèi)容,其他都是子控件的堆疊并沒(méi)有很高的難度系數(shù),只要處理好控件之間的邏輯關(guān)系就能很好的實(shí)現(xiàn)動(dòng)畫效果。
在做 emoji 表情的時(shí)候還在想怎么實(shí)現(xiàn)表情與文字的轉(zhuǎn)換,如:?? -> [笑哭] 這種形式,因?yàn)榕c服務(wù)器進(jìn)行數(shù)據(jù)交互將表情作為圖片做數(shù)據(jù)傳遞是非常不合理的,并且還要考慮到表情與文字之間的相互轉(zhuǎn)化關(guān)系,所以demo 中用到的是將 emoji 當(dāng)中富文本的Attachment
屬性來(lái)處理然后給相應(yīng)的表情打Tag。來(lái)看一下具體代碼:
//點(diǎn)擊 emoji 事件
func didClickEmoji(with model: MYEmojiModel) {
guard let image = UIImage.image(name: model.imageName!, path: "emoji") else {
print("圖片找不到")
return
}
// 記錄textView光標(biāo)當(dāng)前位置
let selectedRange = self.textView.selectedRange
// 將 emoji 標(biāo)記為[name] 這種形式
let emojiString = "[\(model.emojiDescription!)]"
// 通過(guò)字體大小設(shè)置 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)
// 將圖片附件轉(zhuǎn)為 NSMutableAttributedString
let emojiAttributedString = NSMutableAttributedString(attributedString: attachString)
// 將這段文字打上標(biāo)記,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)
// 將打好標(biāo)記的富文本替換到光標(biāo)位置
attributedText.replaceCharacters(in: selectedRange, with: emojiAttributedString)
self.textView.attributedText = attributedText
self.textView.selectedRange = .init(location: selectedRange.location + emojiAttributedString.length, length: 0)
// 重新設(shè)置 font 是為了避免 emoji 在文字末尾導(dǎo)致光標(biāo)變小
self.textView.font = font
//重新計(jì)算文字高度,來(lái)做自適應(yīng)
self.textViewDidChange(self.textView)
}
因?yàn)樵?emoji 被點(diǎn)擊的時(shí)候就被打上相應(yīng)的tag,value 是對(duì)應(yīng)的文字描述,所以在富文本轉(zhuǎn)字符串時(shí)就比較方便了。
//將 string 轉(zhuǎn)為 NSString為了方便做字符串截取
let string = attribute.string as NSString
//遍歷富文本,篩選出被打標(biāo)記的富文本
attribute.enumerateAttribute(NSAttributedString.Key(rawValue: MYAddEmojiTag),
in: range, options: NSAttributedString.EnumerationOptions.longestEffectiveRangeNotRequired) { (value, range, stop) in
if value != nil {
// value即 emoji 對(duì)應(yīng)的描述信息
let tagString = value as! String
result = result + tagString
}else{
let rangString = string.substring(with: range)
result = result + rangString
}
}
復(fù)制粘貼的實(shí)現(xiàn)
通過(guò)上面的代碼就已經(jīng)實(shí)現(xiàn)文字<=>富文本的相互轉(zhuǎn)換了,因?yàn)?code>textView自帶復(fù)制粘貼功能,而UIPasteboard
粘貼板是沒(méi)有attributedText
屬性的,當(dāng)復(fù)制或剪切時(shí)只能將textView.attributedText
轉(zhuǎn)為文字,當(dāng)粘貼的時(shí)候只能將文字轉(zhuǎn)為富文本。因?yàn)樵趀moji 鍵盤被點(diǎn)擊的時(shí)候你已經(jīng)知道 emoji 相對(duì)應(yīng)的文字描述,而如果粘貼為純文字,那如何知道相對(duì)應(yīng)的 emoji 呢?是的,用的是正則匹配,也是盜用別人的邏輯,但是VernonVan他的工程中的正則表達(dá)式是有點(diǎn)瑕疵的。在正則表達(dá)式上我做了改進(jìn),匹配規(guī)則如下:
- 你好[smile] -> [smile]
- 你好[smile.png] -> [smile.png]
- 你好[smile_] -> [smile_]
- 你好[a[smile]] -> [smile]
- 你好[][[[smile]] -> []、[smile]
只做了 a-z 下劃線和.的匹配,如果想匹配更多內(nèi)容自己添加規(guī)則即可。正則表達(dá)式不是很會(huì)寫,只是嘗試著想了這幾種規(guī)則,想要驗(yàn)證和學(xué)習(xí)的可以去正則驗(yàn)證網(wǎng)站學(xué)習(xí)。具體代碼實(shí)現(xiàn)在工程:Targets->Utils->Keyboard->Resources->MYMatchingEmojiManager
文件中
//正則驗(yàn)證網(wǎng)站:https://c.runoob.com/front-end/854
//表達(dá)式: \[([a-z_.])+?\]
let regex = try! NSRegularExpression.init(pattern: "\\[([a-z_.])+?\\]")
//用表達(dá)式匹配結(jié)果
let results = regex.matches(in: string
, options: NSRegularExpression.MatchingOptions.reportProgress, range: .init(location: 0, length: string.count))
輸入框與表情頁(yè)切換動(dòng)畫
引用別人的話:“真正的鍵盤也就是說(shuō)調(diào)起表情鍵盤時(shí)輸入框是有光標(biāo)的,能進(jìn)行拖拽光標(biāo)、選中區(qū)域等的操作,這樣的體驗(yàn)才是與系統(tǒng)鍵盤一致的。其實(shí)系統(tǒng)已經(jīng)提供好了接口給我們直接使用,UITextView
和UITextField
都有的inputView
和inputAccessoryView
就是用來(lái)實(shí)現(xiàn)自定義鍵盤的”。但是有一種情況是:如果表情鍵盤的高度低于系統(tǒng)字體鍵盤的高度,那么在切換表情鍵盤與文字鍵盤的時(shí)候是有落差的,這個(gè)落差導(dǎo)致textView
在回落的過(guò)程中,字體鍵盤瞬間切換表情鍵盤會(huì)有一個(gè)間隙把當(dāng)前頁(yè)面的內(nèi)容暴露出來(lái)個(gè)零點(diǎn)幾秒,非常影響美觀,而系統(tǒng)的文字鍵盤高度和 emoji 鍵盤高度時(shí)一致的所以沒(méi)有這個(gè)問(wèn)題。解決辦法我暫時(shí)就想起來(lái)兩種:
- 當(dāng)
textView
的keyboardWillShow
通知執(zhí)行時(shí),將textView
的superView
的高度等于textView.height + emojiView.height
這樣supeView
的高度就會(huì)很大,這樣在回落的過(guò)程中就不會(huì)顯示位移縫隙,還可以為 emoji 視圖加向上滾動(dòng)的動(dòng)畫,這樣切換就會(huì)更加銜接。 - 不用
textView
的inputView
屬性,做一個(gè)假的 emoji 表情頁(yè),微信的鍵盤就是一個(gè)假的,因?yàn)楫?dāng)切換到表情頁(yè)時(shí),textView
就失去了響應(yīng),光標(biāo)就消失了,這樣就造成了鍵盤回落而 emoji 鍵盤向上滾動(dòng)的效果,我在工程中就是用的這種方式。
無(wú)論是文字切換語(yǔ)音、文字切表情、語(yǔ)音切表情或者其他功能的任意切換,都是經(jīng)過(guò)以下方法(具體實(shí)現(xiàn)見(jiàn) demo):
private var keyboardType : MYKeyboardInputViewEnum.KeyboardType = .None {
//默認(rèn)沒(méi)有任何屬性,為.None
//相當(dāng)于OC中的重寫 set 方法
willSet{
if keyboardType == newValue {
//如果將要改變的值與當(dāng)前值一樣,則不做任何操作,即同一種模式
return
}
//不相同則重新賦值
self.keyboardType = newValue;
switch newValue {
//判斷哪種模式,處理相應(yīng)的邏輯,具體實(shí)現(xiàn)見(jiàn)工程代碼
case .Emoji:
break
case .System:
break
case .Funcs:
break
case .Record:
break
default:
break
}
}
}
語(yǔ)音邊錄邊轉(zhuǎn)的實(shí)現(xiàn)
語(yǔ)音錄制邏輯是這樣?jì)鸬摹?/p>
-
每點(diǎn)擊一次錄音按鈕便創(chuàng)建一個(gè)錄音機(jī),創(chuàng)建錄音機(jī)的同時(shí)會(huì)創(chuàng)建兩個(gè)路徑:
.caf
路徑和.mp3
路徑,.caf
路徑是錄音機(jī)錄制的文件存放路徑,.mp3
路徑則為轉(zhuǎn)換后的文件路徑。以及錄音機(jī)的一些必要參數(shù):/// 設(shè)置錄音格式 默認(rèn)kAudioFormatLinearPCM var formatIDKey : AudioFormatID = kAudioFormatLinearPCM /// 設(shè)置錄音采樣率(Hz) 8000/44100/96000(影響音頻的質(zhì)量) 默認(rèn) 44100 var sampleRateKey : NSInteger = 44100 /// 錄音通道數(shù) 1 或 2 默認(rèn)為2 var channelsKey : NSInteger = 2 /// 線性采樣位數(shù) 8、16、24、32 默認(rèn) 16 var bitDepthKey : NSInteger = 16 /// 錄音的質(zhì)量 默認(rèn)QualityMin var qualityKey : AVAudioQuality = .min
- 錄制時(shí)間為60秒,前1S內(nèi)為初始化錄音機(jī)時(shí)間,如果1S內(nèi)取消錄制則提示"錄音時(shí)間太短",執(zhí)行取消錄制方法,刪除兩個(gè)文件;如果沒(méi)有取消則繼續(xù)錄制,展示錄制動(dòng)畫,增加手勢(shì)滑動(dòng)效果,增加語(yǔ)音消息呼吸燈動(dòng)畫,當(dāng)錄制完畢后在轉(zhuǎn)換回調(diào)中刪除錄制相對(duì)應(yīng)的
.caf
文件,拋出轉(zhuǎn)換成功的.mp3
文件路徑。 - 錄制成功后,拿到相對(duì)應(yīng)的
.mp3
文件路徑上傳到服務(wù)器,因?yàn)樵谏蟼鬟^(guò)程必為異步上傳(如果為主線程那不就卡了),有可能當(dāng)前文件未上傳成功后續(xù)又有文件要上傳,所以要記得加鎖,加鎖,加鎖保證數(shù)據(jù)的安全。demo 中這一部分并沒(méi)有實(shí)現(xiàn)。 - 取消發(fā)送,則刪除兩個(gè)對(duì)應(yīng)的文件,結(jié)束轉(zhuǎn)換
- 錄制時(shí)間到直接發(fā)送,上滑取消,聲波監(jiān)測(cè)等等。。。
具體實(shí)現(xiàn)見(jiàn)demo內(nèi)Utils->Keyboard->Tool->Recorder
文件,邊錄邊轉(zhuǎn)的實(shí)現(xiàn)見(jiàn)ConverAudioFile
文件,轉(zhuǎn)換是用的lame.framework
的三方庫(kù)。
swift基本語(yǔ)法
常用屬性
只讀屬性(readonly)
在OC語(yǔ)法中因?yàn)榇嬖?code>.h和.m
兩個(gè)文件,所以想暴露給外部使用的接口和方法是全部定義在.h
文件中的而 swift 則是全部寫在同一個(gè)文件中的。如果你想定義一個(gè)屬性為只讀屬性:
OC寫法
.h文件定義
@property (nonatomic, assign,readonly) BOOL isHidden;
.m文件實(shí)現(xiàn)
- (BOOL)isHidden{}
swift寫法
//只實(shí)現(xiàn) get 方法
var isHidden : BOOL {
get{
return true
}
}
有時(shí)候你需要定義一個(gè)屬性,外部為只讀而內(nèi)部可以讀寫,OC是非常好實(shí)現(xiàn)的
.h文件定義
@property (nonatomic, assign,readonly) BOOL isHidden;
.m文件實(shí)現(xiàn)
@property (nonatomic, assign) BOOL isHidden;
這樣就可實(shí)現(xiàn)一個(gè)外部只讀內(nèi)部讀寫功能
而 swift 實(shí)現(xiàn)方法有很多種,你可以定義一個(gè)方法,內(nèi)部定義一個(gè)為private
的屬性,將這個(gè)屬性返回出去。還有更簡(jiǎn)便的寫法
//意思是內(nèi)部實(shí)現(xiàn) set 方法,外部只可調(diào)用 get 方法
private(set) var isHidden = true
設(shè)置代理
OC 中是這樣寫的
@protocol MYEmojiProtocolDelegate <NSObject>
//必須實(shí)現(xiàn)
- (void) didClickDelete;
@optional 可選實(shí)現(xiàn)
- (void) didClickSend;
@end
設(shè)置代理屬性:
@property (nonatomic, weak) id<MYEmojiProtocolDelegate> delegate;
swift 寫法
protocol MYEmojiProtocolDelegate : NSObjectProtocol {
func didClickDelete()
}
設(shè)置代理屬性:
weak var pageDelegate : MYEmojiProtocolDelegate?
如果 swift 代理方法想設(shè)置成option
可選方法,則方法需要加@objc
前綴,protocol
前也是需要加@objc
的,被標(biāo)識(shí)為@objc屬性,使得它兼容OC代碼,擁有可選方法的協(xié)議只能被類遵守而枚舉和結(jié)構(gòu)體是不能遵守協(xié)議的。還有一種做法就是對(duì)協(xié)議進(jìn)行方法擴(kuò)展:
extension MYEmojiProtocolDelegate {
//擴(kuò)展代理的方法是必須實(shí)現(xiàn)的
func didClickSend() {
}
}
在學(xué)習(xí) swift 的時(shí)候發(fā)現(xiàn)OC中的代理與 swift 中的協(xié)議,這是兩種不同的概念,我們也知道 swift 是一門面向協(xié)議的編程,因?yàn)槭浅鯇W(xué) swift 對(duì)其理解還是比較淺的,下面談?wù)勎覍?duì)面向協(xié)議的理解。
protocol
是一些方法或?qū)傩缘拿Q,自我理解像是方法和屬性(或者屬性)的集合。只定義接口或者屬性而不實(shí)現(xiàn)任何功能,如果某個(gè)類型
(不是類;類型包括:類,枚舉,結(jié)構(gòu)體)想要遵守一個(gè)協(xié)議,那它需要實(shí)現(xiàn)這個(gè)協(xié)議所定義的所有這些內(nèi)容。swift 里的protocol
不僅可以定義方法還可定義屬性,這與OC
里的有所不同。
從實(shí)現(xiàn)方法說(shuō)起
舉個(gè)栗子:為UITableViewcell
實(shí)現(xiàn)點(diǎn)擊事件即:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//增加個(gè)點(diǎn)擊調(diào)用方法
didClick()
}
因?yàn)橛胁煌?code>UITableViewcell的子類都需要實(shí)現(xiàn)這個(gè)方法,那我們應(yīng)該怎么做呢?
繼承
可以很好的解決這個(gè)問(wèn)題,但是缺點(diǎn)是帶來(lái)耦合性。如果再實(shí)現(xiàn)一個(gè)呼吸效果呢,就又在Base
類中實(shí)現(xiàn)相應(yīng)的代碼,很快Base
類就變得臃腫,且任何代碼都可以寫進(jìn)去,而子類也完全不知道實(shí)現(xiàn)了父類的哪些方法。
Extension/Category
大家肯定在項(xiàng)目中用到的比較多,也很實(shí)用。直接為UITableViewcell
寫一個(gè)擴(kuò)展,那意味著項(xiàng)目里所有的UITableViewcell
對(duì)象都可以訪問(wèn)這個(gè)方法,如果UICollectionCell
也需要上面的方法呢?也寫擴(kuò)展,粘貼復(fù)制同樣的代碼,我們都知道這兩個(gè)類都繼承自UIView
,那直接給UIView
添加擴(kuò)展,這樣項(xiàng)目中所有繼承自UIView
的對(duì)象都可以訪問(wèn)這個(gè)方法,為了一個(gè)類就污染了其他對(duì)象,因?yàn)檫@些對(duì)象根本不需要這些方法。
使用協(xié)議解決問(wèn)題
定義一個(gè)protocol
protocol MYCompatible {
// 定義屬性
//必須明確指定該屬性支持的操作:只讀(get)或者是可讀寫(get set)
// 要用 var 定義屬性,即使只有 get 方法
var name: String {get set}
var birthday : String {get}
// 定義方法
//protocol中的約定方法,當(dāng)方法中有參數(shù)時(shí)是不能有默認(rèn)值的
func eat(food: String)
//如果需要改變自身的值,需要在方法前面加mutating關(guān)鍵字
mutating func changeName(name: String)
}
定一個(gè)類或者結(jié)構(gòu)體實(shí)現(xiàn)該協(xié)議
//遵守協(xié)議,實(shí)現(xiàn)協(xié)議的方法 就上面例子而言,只需要將`UITableviewCell`類遵守協(xié)議即可
class MYExtension: MYCompatible {
var name: String = "xiaoma"
let birthday: String = "1994"
func eat(food: String = "KFC") {
if food == "KFC" {
print("好吃")
} else {
print("想吃KFC")
}
}
//如果協(xié)議中方法有mutating關(guān)鍵字,如果結(jié)構(gòu)體來(lái)遵守協(xié)議則需要mutating
func changeName(name: String) {
self.name = name
}
}
如果只希望協(xié)議只被類class遵守,只需要在定義協(xié)議的時(shí)候在后面加上AnyObject
即可
protocol MYCompatible : AnyObject {
var name: String {get set}
...
}
如果協(xié)議中定義了構(gòu)造函數(shù)(init),則實(shí)現(xiàn)協(xié)議的類必須實(shí)現(xiàn)這個(gè)構(gòu)造函數(shù)
protocol MYCompatible {
var name: String {get set}
var birthday: String {get}
// 定義構(gòu)造函數(shù)
init(name: String)
}
class MYExtension: MYCompatible {
var name: String = "xiaoma"
let birthday: String = "1994"
//如果該類被定義為final 則 required 不寫
required init(name: String) {
self.name = name
}
}
協(xié)議擴(kuò)展
像上面的例子中UITableviewCell
和UICollectionCell
中他們所實(shí)現(xiàn)的方法都是一樣的,只是兩者的類型不同,則沒(méi)必要定義兩個(gè)協(xié)議,只需要寫一個(gè)協(xié)議即可,這時(shí)就可以在協(xié)議中使用關(guān)聯(lián)類型associatedtype
public protocol MYCompatible {
associatedtype MYCompatibleType
var my : MYCompatibleType { get }
}
final class MYExtension: MYCompatible {
typealias MYCompatibleType = Bool
var my: MYCompatibleType {
return true
}
}
我們知道協(xié)議中定義的屬性或者方法是不提供實(shí)現(xiàn)方式的,我們可以通過(guò)協(xié)議擴(kuò)展的形式,在擴(kuò)展中實(shí)現(xiàn)相應(yīng)的代碼:
//定一個(gè)協(xié)議
public protocol MYCompatible {
//使用關(guān)聯(lián)類型
associatedtype MYCompatibleType
//創(chuàng)建屬性 屬性類型為關(guān)聯(lián)的協(xié)議
var my : MYCompatibleType { get }
}
//構(gòu)建一個(gè)類,實(shí)現(xiàn)協(xié)議
public final class MYExtension<Base>: MYCompatible {
// Base 為泛型
public let my: Base
// 構(gòu)造方法
public init(_ my:Base) {
self.my = my
}
}
給協(xié)議添加默認(rèn)實(shí)現(xiàn),用where
關(guān)鍵字對(duì)協(xié)議做條件限定(where 類型限定)
這里 MYCompatibleType 關(guān)聯(lián)類型,可以是類或者是結(jié)構(gòu)體,如果是結(jié)構(gòu)體可以用 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
}
}
}
在想要擴(kuò)展的類中添加MYExtension 類或者結(jié)構(gòu)體,這個(gè)類是繼承MYCompatible的協(xié)議的,所以就擁有了MYCompatible協(xié)議里面默認(rèn)的實(shí)現(xiàn)方法,即剛才那個(gè)用 `where` 限定的類型
extension UIView {
var my: MYExtension <UIView> {
return MYExtension(my: self)
}
}
//調(diào)用則是
let view = UIView()
view.my.width = 20
我們現(xiàn)在回過(guò)頭來(lái)看看這個(gè)擴(kuò)展協(xié)議,首先定義一個(gè)名為MYCompatible
的協(xié)議,然后關(guān)聯(lián)類型associatedtype MYCompatibleType
,定義屬性為var my : MYCompatibleType { get }
返回的類型為關(guān)聯(lián)的類型,再定義一個(gè)類MYExtension <Base>
Base為泛型,實(shí)現(xiàn)協(xié)議,則實(shí)現(xiàn)my
屬性,再構(gòu)造MYExtension
類的init
方法,現(xiàn)在對(duì)UIView
進(jìn)行擴(kuò)展
extension UIView {
var my: MYExtension <UIView> {
return MYExtension(my: self)
}
}
現(xiàn)在 UIView
的對(duì)象里的屬性my
就實(shí)現(xiàn)了MYCompatible
協(xié)議,即擁有該協(xié)議的方法,因?yàn)閰f(xié)議默認(rèn)是不提供方法的實(shí)現(xiàn)的,所以要對(duì)協(xié)議進(jìn)行擴(kuò)展,在擴(kuò)展的時(shí)候使用了where
做類型限定,即方法擁有者只能是限定的類型。
為什么使用協(xié)議擴(kuò)展
因?yàn)槲覀冺?xiàng)目里有很多地方是對(duì)UIView
、UIColor
等常用類進(jìn)行extension
- 在進(jìn)行多人開(kāi)發(fā)的時(shí)候?qū)ν活愋妥鱿嗤僮魇呛艹S械氖拢矊懥艘粋€(gè)和你命名方式一樣的方法,但是他也新建了一個(gè)文件,然后你們兩個(gè)方法就沖突了,然后再進(jìn)行一頓排查。
- 隨著需求的增多,你擴(kuò)展的方法也就更多,然后將這些方法寫成工具類,當(dāng)進(jìn)行下次開(kāi)發(fā)時(shí)可以直接拖進(jìn)工程中快速使用,但是卻與其他人的方法沖突了,很尷尬啊。
上面的協(xié)議擴(kuò)展可以很好的解決這個(gè)問(wèn)題,而且在寫法上可以帶一個(gè)自己的標(biāo)志,逼格很高。像一些三方庫(kù)都有這種操作的:view.snp.makeConstraints()
、imageView.kf.setImage(with: <>)
因?yàn)轭愋秃芏啵獢U(kuò)展出來(lái)的方法也很多,總不能每個(gè)類或者結(jié)構(gòu)體都寫一個(gè)協(xié)議吧,其實(shí),寫一個(gè)就夠了,將這些協(xié)議抽離出一個(gè)通用的即可。demo 中就是這樣做的,將協(xié)議抽離出一個(gè)通用的來(lái)。
總結(jié)
在寫的過(guò)程中并沒(méi)有按照別人的代碼照抄照搬而是吸取精華,棄去糟粕。寫 demo 不是目的,更多的是為了提高自己的知識(shí)面,而且 swift 語(yǔ)言版本也日漸穩(wěn)定,swift 作為 iOS 的新語(yǔ)言潛力還是比較大的。因?yàn)閷?duì) swift 學(xué)習(xí)的比較少,理解的也比較淺,文中或 demo 里肯定有不妥當(dāng)?shù)牡胤剑允墙邮芘u(píng)和教育的。
參考資料
https://juejin.im/post/5a6b3f016fb9a01ca267b6db
http://www.lxweimin.com/p/971fff236881
https://onevcat.com/2016/11/pop-cocoa-1/
https://onevcat.com/2016/12/pop-cocoa-2/