Swift-核心之面向協議開發
阿新 • • 發佈:2019-02-05
Swift is a Protocol-Oriented Programming Language
Swift 是一門面向協議 (POP) 開發的語言
Swift 的核心是面向協議程式設計
WWDC 對 OOP 很好的詮釋:
POP 面向協議的程式設計
面向協議的程式設計的核心是抽象(Abstraction)和簡化(Simplicity)
協議的高階使用是協議的延展
協議(protocol) + 結構體(struct) > 類(class)
面向物件與面向協議比較
- 面向物件是一個很古老的軟體開發模式,通過類來實現
- 面向協議是蘋果在 swift 中主推的,通過協議和結構體,可以代替類
- Swift 中的很多物件都改成了結構體和協議
- 並不是所有的類都可以被協議+結構體替代,但大多數是可以被替換的
- 面向協議使程式碼更加靈活,類似於元件化開發,符合工廠方法模式
例項比較:給一個類新增額外的方法
- 通過繼承
- 建立一個繼承類的子類,在子類中新增方法,以後使用子類即可獲取這個方法
- 通過協議
- 為這個方法定義一個協議,那個類需要實現這個方法,協議即可
使用繼承的缺點:
- 通過繼承新增的方法,不一定每個子類都會使用,使程式碼冗餘
- 擁有太多的子類,使類冗餘
- 對於父類的方法太過依賴,當父類的方法更改後,影響子類的過載方法
- …
什麼時候使用面向物件的類呢?
協議的高階使用是協議的延展以及和結構體配合
為協議新增屬性, 屬性為可讀或可寫
protocol MessageModelProtocol {
var name: String {get set}
var age: Int { set get }
}
定義一個接受協議的結構體
struct MessageModel: MessageModelProtocol {
var name: String = ""
var age: Int = 0
var isMale: Bool = false
init(with dict: [String: Any]) {
self .name = (dict["name"] as? String) ?? ""
self.age = (dict["age"] as? Int) ?? 0
self.isMale = (dict["isMale"] as? Bool) ?? false
}
}
對協議進行延展
extension MessageModelProtocol {
mutating func test() {
self.name = "Hello iPhone 8"
}
}
協議的協議
protocol DemoMessageModelProtocol: MessageModelProtocol {
var date: Date { set get }
}
具體例項運用:
- 給 UIViewController 新增 一個數據為空檢視
- 給 UIViewController 新增 一個遮擋提示檢視
- 給 xib 新增一個快速獲取示例方法
- ……(對控制器依賴比較小的檢視等)
資料為空或者網路請求失敗提示介面
import UIKit
enum EmptyType {
case emptyData
case networkError
}
protocol EmptyDataSetProtocol { }
extension EmptyDataSetProtocol where Self : UIViewController {
func addEmptyView(type: EmptyType? = .emptyData, iconName: String, tipTitle: String, action: Selector? = nil) {
let emptyView = UIView(frame: view.bounds)
emptyView.backgroundColor = UIColor.white
emptyView.tag = 1024
view.addSubview(emptyView)
let icomViewW: CGFloat = 100
let imageView = UIImageView(image: UIImage(named: iconName))
imageView.frame.size = imageView.image?.size ?? CGSize(width: icomViewW, height: icomViewW)
imageView.contentMode = .center
imageView.center = CGPoint(x: emptyView.center.x, y: emptyView.center.y - 100)
emptyView.addSubview(imageView)
let tipLabel = UILabel()
let margin: CGFloat = 20
tipLabel.numberOfLines = 0
tipLabel.font = UIFont.systemFont(ofSize: 14)
tipLabel.textColor = UIColor.lightGray
if tipTitle.contains("\n") {
let style = NSMutableParagraphStyle()
style.lineSpacing = 5 // 設定行間距
style.alignment = .center // 文字居中
let tipString = NSAttributedString(string: tipTitle, attributes: [NSParagraphStyleAttributeName: style])
tipLabel.attributedText = tipString
} else {
tipLabel.text = tipTitle
}
tipLabel.adjustsFontSizeToFitWidth = true
tipLabel.textAlignment = .center
tipLabel.sizeToFit()
tipLabel.frame = CGRect(x: margin, y: imageView.frame.maxY + margin, width: UIScreen.main.bounds.width - margin*2, height: tipLabel.bounds.height)
emptyView.addSubview(tipLabel)
// 網路請求失敗
if type == .networkError {
let reloadButton = UIButton(type: .system)
reloadButton.frame.size = CGSize(width: 100, height: 36)
reloadButton.center = CGPoint(x: emptyView.center.x, y: tipLabel.frame.maxY + margin*2)
reloadButton.backgroundColor = UIColor(red: 255/255.0, green: 42/255.0, blue: 102/255.0, alpha: 1.0)
reloadButton.layer.cornerRadius = 18
reloadButton.layer.masksToBounds = true
reloadButton.setTitle("重新載入", for: .normal)
reloadButton.setTitleColor(UIColor.white, for: .normal)
reloadButton.titleLabel?.font = UIFont.systemFont(ofSize: 16)
reloadButton.addTarget(self, action: action!, for: .touchUpInside)
emptyView.addSubview(reloadButton)
}
}
func hideEmptyView() {
view.subviews.filter({ $0.tag == 1024 }).first?.removeFromSuperview()
}
}
具體使用:
class ViewController: UIViewController, EmptyDataSetProtocol {...}
/// 顯示資料為空檢視
func showEmptyDataView() {
addEmptyView(type: .emptyData, iconName: "emptyData", tipTitle: "資料為空")
}
/// 顯示請求失敗重新載入檢視
func showNetworkErrorReloadView() {
addEmptyView(type: .networkError, iconName: "network_error", tipTitle: "網路出問題了,請檢查網路", action: #selector(reloadData))
}
/// 移除空檢視/重新載入檢視
func removeEmptyView() {
hideEmptyView()
}
新增 guide 檢視 到 window 上
import UIKit
protocol GuideViewProtocol { }
extension GuideViewProtocol where Self : UIViewController {
func showGuideView(with title: String, imageName: String, buttonName: String, sureAction: Selector, cancelAction: Selector) {
let kScreenW: CGFloat = UIScreen.main.bounds.width
let kMargine: CGFloat = 30
let backgroundView = UIView(frame: UIScreen.main.bounds)
backgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.3)
backgroundView.tag = 1024
UIApplication.shared.keyWindow?.addSubview(backgroundView)
let containerView = UIView()
containerView.frame.size = CGSize(width: kScreenW-kMargine*2, height: (kScreenW-kMargine*2) + 20)
containerView.backgroundColor = UIColor.white
containerView.center = backgroundView.center
containerView.layer.cornerRadius = 15
backgroundView.addSubview(containerView)
let tipLabel = UILabel(frame: CGRect(x: kMargine, y: 30, width: containerView.bounds.width-kMargine*2, height: kMargine))
tipLabel.font = UIFont.systemFont(ofSize: 20)
tipLabel.textColor = UIColor.red
tipLabel.textAlignment = .center
tipLabel.text = title
containerView.addSubview(tipLabel)
let sureButton = UIButton(type: .system)
sureButton.frame.size = CGSize(width: 200, height: 30)
sureButton.setTitle(buttonName, for: .normal)
sureButton.setTitleColor(UIColor.white, for: .normal)
sureButton.backgroundColor = UIColor.red
sureButton.titleLabel?.font = UIFont.systemFont(ofSize: 18)
sureButton.frame.origin.x = (containerView.bounds.width - sureButton.bounds.width)*0.5
sureButton.center.y = containerView.bounds.height - 20 - sureButton.bounds.height
sureButton.layer.cornerRadius = 15
sureButton.addTarget(self, action: sureAction, for: .touchUpInside)
containerView.addSubview(sureButton)
let centerImageView = UIImageView(image: UIImage(named: imageName))
centerImageView.contentMode = .scaleAspectFit
centerImageView.frame = CGRect(x: 30, y: tipLabel.frame.maxY+20, width: containerView.bounds.width-60, height: sureButton.frame.minY - tipLabel.frame.maxY - 40)
containerView.addSubview(centerImageView)
let cancelButton = UIButton(type: .custom)
cancelButton.setBackgroundImage(UIImage(named: "cancelButton"), for: .normal)
cancelButton.frame.size = cancelButton.currentBackgroundImage?.size ?? CGSize.zero
cancelButton.center.x = UIScreen.main.bounds.width * 0.5
cancelButton.center.y = containerView.frame.maxY + 50
cancelButton.addTarget(self, action: cancelAction, for: .touchUpInside)
backgroundView.addSubview(cancelButton)
}
func hideGuideView() {
UIView.animate(withDuration: 0.25) {
UIApplication.shared.keyWindow?.subviews.filter({ $0.tag == 1024 }).first?.removeFromSuperview()
}
}
}
具體使用
class ViewController: UIViewController, GuideViewProtocol {...}
/// 新增指示檢視到當前檢視的 window 上
func addGuideView() {
showGuideView(with: "您有VIP禮包待領取", imageName: "vip_image", buttonName: "立即領取", sureAction: #selector(sureAction), cancelAction: #selector(removeGuideView))
}
/// 提示檢視上按鈕的點選事件
func sureAction() {
show(FirstViewController(), sender: nil)
hideGuideView()
}
/// 移除指示檢視
func removeGuideView() {
hideGuideView()
}
修改導航欄左側按鈕
protocol LeftBarButtonChangeable { }
extension LeftBarButtonChangeable where Self : UIViewController {
func changeLeftBarButton(_ imageName: String, action: Selector ) {
let itemButton = UIButton(type: .custom)
itemButton.setImage(UIImage(named: imageName), for: .normal)
itemButton.sizeToFit()
itemButton.addTarget(self, action: action, for: .touchUpInside)
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: itemButton)
}
}
具體使用
class ViewController: UIViewController, LeftBarButtonChangeable {
override func viewDidLoad() {
super.viewDidLoad()
changeLeftBarButton("back_image", action: #selector(backAction))
}
func backAction() {
navigationController?.popViewController(animated: true)
}
}