1. 程式人生 > >Swift-核心之面向協議開發

Swift-核心之面向協議開發

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)
    }
}