Swift is a Protocol-Oriented Programming Language
Swift 是一门面向协议 (POP) 开发的语言
Swift 的核心是面向协议编程
WWDC 对 OOP 很好的诠释:
Protocol-Oriented Programming in Swift
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)
}
}