開發(fā)環(huán)境
1.Xcode 15
2.Swift 5+
3.iOS 13+
4.SwiftUI
- 優(yōu)點(diǎn):UI布局開發(fā)簡(jiǎn)化,支持蘋果多平臺(tái)開發(fā),可以跨端于macOS、watchOS和tvOS,實(shí)現(xiàn)代碼的跨平臺(tái)共享。
- 缺點(diǎn):有學(xué)習(xí)成本,目前沒有資源積累,組件庫(kù)缺失,前期需要投入更多時(shí)間
工程推薦
R.Swift
R.Swift能夠使用類似語(yǔ)法R.資源類型.資源名稱來(lái)對(duì)某資源進(jìn)行引用構(gòu)建
R.Swift有著動(dòng)態(tài)生成代碼的機(jī)制, 它具有以下優(yōu)點(diǎn):
- 代碼自動(dòng)補(bǔ)全:就像輸入其他的代碼一樣,R.Swift支持IDE的代碼自動(dòng)補(bǔ)全
- 自動(dòng)檢測(cè): 可以自動(dòng)檢測(cè)代碼是否存在問題, 當(dāng)我們的資源文件名修改的時(shí)候, 這是就會(huì)提示資源引用錯(cuò)誤
XcodeGen
通過解析一個(gè)YAML或JSON文件來(lái)定義你的目標(biāo)、配置、方案、自定義構(gòu)建設(shè)置和許多其他選項(xiàng)。gitignore可以忽略xcodeproj、xcworkspace工程文件,避免合并代碼導(dǎo)致文件變動(dòng)沖突。
架構(gòu)規(guī)范
- 使用MVVM設(shè)計(jì)模式,使用RxSwift搭配ViewModel使用
- 頁(yè)面路由基于 Swinject 設(shè)計(jì)
- 組件之間通信使用現(xiàn)有組件 KamServiceKit
- 頁(yè)面UI禁用xib,使用純代碼自動(dòng)約束布局,SnapKit
- JSON轉(zhuǎn)Model使用 KakaJSON
- 網(wǎng)絡(luò)框架基于 Alamofire 做二次業(yè)務(wù)封裝,在Debug環(huán)境做Api請(qǐng)求次數(shù)統(tǒng)計(jì)以及404彈窗提醒
- 數(shù)據(jù)普通存儲(chǔ)使用當(dāng)前KFoundation中Cache模塊(基于MMKV),App內(nèi)做業(yè)務(wù)封裝關(guān)聯(lián)用戶,關(guān)聯(lián)設(shè)備。數(shù)據(jù)庫(kù)存儲(chǔ)使用 RxRealm
- 內(nèi)存泄漏檢測(cè)接入 LifetimeTracker , 在Debug環(huán)境做彈窗提醒
- 卡頓檢測(cè)接入已有組件 DebugRing , 可啟用FPS監(jiān)控
- 引入自動(dòng)檢查代碼規(guī)范組件 SwiftLint
- 開發(fā)中使用LocalHotReloading(基于injectionIII)進(jìn)行熱重載,減少編譯,提速增效
- 免費(fèi)編程助手插件 CodeiumForXcode , 可以根據(jù)上下文給出代碼提示,可以按需使用
- 代碼格式化 XCFormat , Xcode 插件,可以快速格式化Swift/OC代碼。
架構(gòu)圖
第三方庫(kù)
- SwiftEntryKit 各種彈窗
- PermissionsKit 各種權(quán)限
- SwiftyStoreKit 蘋果內(nèi)購(gòu)
- RxRealm 數(shù)據(jù)庫(kù)的RX調(diào)用
- UIAdapter 尺寸適配
開發(fā)規(guī)范
正確性
努力讓你的代碼在沒有警告的情況下編譯。 這條規(guī)則決定了許多風(fēng)格決策,比如使用 #selector 類型而不是字符串字面量。
命名
描述性和一致性的命名讓軟件更易于閱讀和理解。使用 API 設(shè)計(jì)規(guī)范 中描述的 Swift 命名規(guī)范。 一些關(guān)鍵點(diǎn)包括如下:
- 盡量讓調(diào)用的地方更加簡(jiǎn)明
- 簡(jiǎn)明性優(yōu)先而不是簡(jiǎn)潔性
- 使用駝峰命名法(而不是蛇形命名法)
- 針對(duì)類型(和協(xié)議)使用首字母大寫,其它都是首字母小寫
- 包含所有需要的單詞,同時(shí)省略不必要的單詞
- 基于角色的命名,而不是類型
- 有時(shí)候要針對(duì)弱引用類型信息進(jìn)行補(bǔ)充
- 盡量保持流暢的用法
- 工廠方法以 make 開頭
命名方法的副作用
不可變版本的動(dòng)詞方法要遵循后接 -ed, -ing 的規(guī)則
可變版本的名詞方法要遵循 formX 的規(guī)則
布爾類型應(yīng)該像斷言一樣讀取
描述 這是什么 的協(xié)議應(yīng)該讀作名詞
描述 一種能力 的協(xié)議應(yīng)該以 -able 或者 -ible 結(jié)尾
使用不會(huì)讓專家驚訝或讓初學(xué)者迷惑的術(shù)語(yǔ)
通常要避免縮寫
使用名稱的先例
首選方法和屬性而不是自由函數(shù)
統(tǒng)一向上或向下包裝首字母縮略詞和首字母
為相同含義的方法提供相同的基本名稱
避免返回類型的重載
選擇用于文檔的好的參數(shù)名
為閉包和元組參數(shù)設(shè)置標(biāo)簽
利用默認(rèn)參數(shù)的優(yōu)勢(shì)
類前綴
Swift 的類自動(dòng)被包含在模塊分配的命名空間中。不應(yīng)該再添加類似于 RW 的類前綴。如果不同模塊的兩個(gè)命名沖突,可以在類名前添加模塊名來(lái)消除歧義。無(wú)論如何,僅在少數(shù)可能引起混淆的情況下指明模塊名。
import SomeModule
let myClass = MyModule.UsefulClass()
代理
當(dāng)創(chuàng)建自定義代理方法的時(shí)候,未命名的第一個(gè)參數(shù)應(yīng)該是代理源。 ( UIKit 包含很多這樣的例子。)
推薦:
func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool
不推薦:
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool
代碼組織
用擴(kuò)展將代碼組織為功能邏輯塊。每個(gè)擴(kuò)展都應(yīng)該添加 // MARK: - 注釋,以保證代碼的結(jié)構(gòu)清晰。
協(xié)議遵循
推薦為協(xié)議方法加一個(gè)單獨(dú)的擴(kuò)展,尤其是為一個(gè)模型加入?yún)f(xié)議遵循的時(shí)候。這可以讓有關(guān)聯(lián)的協(xié)議方法被分組在一起,也可以簡(jiǎn)化用類關(guān)聯(lián)方法向這個(gè)類添加協(xié)議的指令。
推薦:
class MyViewController: UIViewController {
// 類填充在這
} `
`
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// table view 的數(shù)據(jù)源方法
}
// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
// scroll view 的代理方法
}
不推薦:
class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
// 所有方法
}
無(wú)用代碼
無(wú)用代碼(僵尸代碼),包括 Xcode 模板代碼和占位注釋,應(yīng)該被移除掉。教程或書籍中教用戶使用的注釋代碼除外。
僅實(shí)現(xiàn)簡(jiǎn)單調(diào)用父類,但與教程無(wú)直接關(guān)聯(lián)的方法應(yīng)該被移除。這里包括任何為空的或無(wú)用的 UIApplicationDelegate 方法。
推薦:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Database.contacts.count
}
不推薦:
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// 任何可以重建資源的處理
}
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning 未完成的實(shí)現(xiàn),返回節(jié)數(shù)。
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning 未完成的實(shí)現(xiàn),返回行數(shù)。
return Database.contacts.count
}
最小引用
引用最小化。舉個(gè)例子,引用 Foundation 就足夠的情況下不要再引用 UIKit 。
空格
用兩個(gè)字符縮進(jìn)比用制表符縮進(jìn)更節(jié)省空間,同時(shí)能防止換行。務(wù)必在 Xcode 和項(xiàng)目中設(shè)置這個(gè)偏好,如下所示:
方法大括號(hào)和其他大括號(hào)( if / else / switch / while 等)總是在和語(yǔ)句相同的行寫左括號(hào),而在新行寫右括號(hào)。
提示:你可以通過選中一些代碼(或按 ?A 選中全部)然后按 Control-I (或在目錄中選擇編輯器 -> 結(jié)構(gòu) -> 重新縮進(jìn))的方式來(lái)重新縮進(jìn)代碼。一些 Xcode 模板代碼會(huì)使用 4 個(gè)空格的制表符硬編碼,這就是一個(gè)修正它的好方法。推薦:
if user.isHappy {
// 做一件事
} else {
// 做另一件事
}
不推薦:
if user.isHappy
{
// 做一件事
}
else {
// 做另一件事
}
方法之間應(yīng)該只有一個(gè)空行,這樣有助于視覺清晰和組織。方法中的空白應(yīng)該按功能分隔代碼,但在一個(gè)方法中有很多段意味著你應(yīng)該將它們封裝進(jìn)不同的方法。
冒號(hào)總是在左邊沒有空格而右邊有空格。比較特殊的是三元運(yùn)算符 ? :、空字典 [:]和帶有未命名參數(shù)(_:) 的#selector 語(yǔ)法 .推薦:
class TestDatabase: Database {
var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
}
不推薦:
class TestDatabase : Database {
var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
}
長(zhǎng)行應(yīng)該在 70 個(gè)字符左右被換行(這里并非硬性限制,可自行調(diào)整)。
避免在行結(jié)尾的地方附上空白。
在每個(gè)文件的結(jié)尾處增加一個(gè)單獨(dú)的換行符。
注釋
需要的時(shí)候,用注釋來(lái)解釋一個(gè)特定的代碼片段 為什么 做某件事。注釋應(yīng)保持要么是最新的,要么就被刪除。
為了避免塊注釋和代碼內(nèi)聯(lián),代碼應(yīng)該盡可能自文檔化。 例外:這不含那些注釋被用于生成文檔的情況 。
類和結(jié)構(gòu)體
使用哪個(gè)?
結(jié)構(gòu)體有 值語(yǔ)義。對(duì)沒有標(biāo)識(shí)的事物應(yīng)用結(jié)構(gòu)體。
類有 引用語(yǔ)義。對(duì)有標(biāo)識(shí)或有具體生命周期的事物應(yīng)用類。
定義的舉例
這是一個(gè)風(fēng)格良好的類定義例子
class Circle: Shape {
var x: Int, y: Int
var radius: Double
var diameter: Double {
get {
return radius * 2
}
set {
radius = newValue / 2
}
}
init(x: Int, y: Int, radius: Double) {
self.x = x
self.y = y
self.radius = radius
}
convenience init(x: Int, y: Int, diameter: Double) {
self.init(x: x, y: y, radius: diameter / 2)
}
override func area() -> Double {
return Double.pi * radius * radius
}
}
extension Circle: CustomStringConvertible {
var description: String {
return "center = (centerString) area = (area())"
}
private var centerString: String {
return "((x),(y))"
}
}
上面的例子遵循了以下風(fēng)格規(guī)范:
用后面有空格而前面沒有空格的冒號(hào),為屬性、變量、常量、參數(shù)聲明和其它語(yǔ)句指定類型,例如:x: Int 和Circle: Shape。
如果多個(gè)變量和結(jié)構(gòu)體共享一個(gè)共同的目的 / 上下文,則可以在同一行中定義。
縮進(jìn) getter、setter 的定義和屬性觀察器。
不要再添加如internal 的默認(rèn)修飾符。類似的,當(dāng)重寫一個(gè)方法時(shí),不要再重復(fù)添加訪問修飾符。
在擴(kuò)展中組織額外功能(例如打印)。
隱藏非共享的實(shí)現(xiàn)細(xì)節(jié),例如 centerString 在擴(kuò)展中使用 private 訪問控制。
self 的使用
為了簡(jiǎn)潔,請(qǐng)避免使用 self 關(guān)鍵詞,Swift 不需要用它來(lái)訪問一個(gè)對(duì)象屬性或調(diào)用它的方法。
僅在編譯器需要時(shí)(在 @escaping 閉包或初始化函數(shù)中,消除參數(shù)與屬性的歧義)才使用 self。換句話說(shuō),如果不需要 self 就能編譯通過,則可以忽略它。
計(jì)算屬性
為了簡(jiǎn)潔,如果一個(gè)計(jì)算屬性是只讀的,則可以忽略 get 子句。僅在提供了 set 子句的情況下才需要 get 子句
Final
類或成員標(biāo)記為 final 會(huì)從主題分散注意力,而且也沒必要。 盡管如此,final 的使用有時(shí)可以表明你的意圖,且值得你這樣做。在下面的例子中,Box 有特定的目的,且并不打算在派生類中自定義它。標(biāo)記為 final 可以使它更清晰。
// 用這個(gè) Box 類將任何一般類型轉(zhuǎn)換為引用類型。
final class Box{
let value: T
init(_ value: T) {
self.value = value
}
}
函數(shù)聲明
在一行中保持較短的方法聲明,包括左括號(hào):
func reticulateSplines(spline: [Double]) -> Bool {
// 在這里寫網(wǎng)格代碼
}
對(duì)于簽名較長(zhǎng)的函數(shù),則需在合適的位置換行,然后在后續(xù)的行中加一個(gè)額外的換行:
func reticulateSplines(spline: [Double], adjustmentFactor: Double,
translateConstant: Int, comment: String) -> Bool {
// 在這里寫網(wǎng)絡(luò)代碼
}
在一行中保持較短的函數(shù)使用,像這樣:
let success = reticulateSplines(splines)
如果是包裝調(diào)用,則需在合適的位置換行,然后在后續(xù)的行中加一個(gè)額外的換行:
let success = reticulateSplines(
spline: splines,
adjustmentFactor: 1.3,
translateConstant: 2,
comment: "normalize the display")
閉包表達(dá)式
僅在參數(shù)列表最后有個(gè)單獨(dú)的閉包表達(dá)式參數(shù)時(shí),使用尾隨閉包語(yǔ)法。給閉包參數(shù)定義一個(gè)描述性的命名
UIView.animate(withDuration: 1.0) {
self.myView.alpha = 0
}
UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
}, completion: { finished in
self.myView.removeFromSuperview()
})
類型
請(qǐng)盡可能多的使用 Swift 原生類型。 Swift 提供了 Objective-C 橋接,所以當(dāng)你需要的時(shí)候你仍然可以使用全套方法。
推薦:
let width = 120.0 // Double
let widthString = (width as NSNumber).stringValue // String
不推薦:
let width: NSNumber = 120.0 // NSNumber
let widthString: NSString = width.stringValue // NSString
可選類型
在可接受 nil 值的情況下,使用 ? 聲明變量和函數(shù)返回類型為可選類型。
用 ! 聲明的隱式解包類型,僅用于稍后在使用前初始化的實(shí)例變量,比如將在 viewDidLoad 中創(chuàng)建子視圖。
當(dāng)訪問一個(gè)可選值時(shí),如果值僅被訪問一次或在鏈中有許多可選項(xiàng)時(shí),使用可選鏈:
self.textContainer?.textLabel?.setNeedsDisplay()
當(dāng)一次性解包和執(zhí)行多個(gè)操作更方便時(shí),使用可選綁定:
if let textContainer = self.textContainer {
// 用 textContainer 做很多事情
}
var subview: UIView?
var volume: Double?
// later on...
if let subview = subview, let volume = volume {
// 使用展開的 subview 和 volume 做某件事
}
延遲初始化
在更細(xì)粒度地控制對(duì)象聲明周期時(shí)考慮使用延遲初始化。 對(duì)于 UIViewController ,延遲初始化視圖是非常正確的。你也可以直接調(diào)用 { }()的閉包或調(diào)用私有工廠方法。例如:
lazy var locationManager: CLLocationManager = self.makeLocationManager()
private func makeLocationManager() -> CLLocationManager {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestAlwaysAuthorization()
return manager
}
注意:
因?yàn)闆]有發(fā)生循環(huán)引用,所以這里不需要[unowned self]。
位置管理器對(duì)彈出 UI 向用戶申請(qǐng)權(quán)限有副作用,所以細(xì)顆粒地控制在這里是有意義的。
類型推斷
優(yōu)先選擇簡(jiǎn)潔緊湊的代碼,讓編譯器為單個(gè)實(shí)例的常量或變量推斷類型。類型推斷也適合于?。ǚ强眨┑臄?shù)組和字典。需要時(shí),請(qǐng)指明特定類型,如 CGFloat 或 Int16。
空數(shù)組和空字典的類型注釋
為空數(shù)組和空字典使用類型注釋。(對(duì)于分配給大型、多行文字的數(shù)組和字典,使用類型注釋。
var names: [String] = []
var lookup: [String: Int] = [:]
語(yǔ)法糖
推薦使用類型聲明簡(jiǎn)短的版本,而不是完整的泛型語(yǔ)法。
推薦:
var deviceModels: [String]
var employees: [Int: String]
var faxNumber: Int?
不推薦:
var deviceModels: Array
var employees: Dictionary<Int, String>
var faxNumber: Optional
內(nèi)存管理
代碼 (甚至非生產(chǎn)環(huán)境、教程演示的代碼)都不應(yīng)該出現(xiàn)循環(huán)引用。分析你的對(duì)象圖并用 weak 和unowned 來(lái)防止強(qiáng)循環(huán)引用?;蛘?,使用值類型( struct、enum )來(lái)徹底防止循環(huán)引用。
延長(zhǎng)對(duì)象的生命周期
使用慣用語(yǔ)法[weak self] 和 guard let strongSelf = self else { return } 來(lái)延長(zhǎng)對(duì)象的生命周期。 在 self 超出閉包生命周期不明顯的地方,[weak self] 更優(yōu)于[unowned self]。 明確地延長(zhǎng)生命周期優(yōu)于可選解包。
推薦:
resource.request().onComplete { [weak self] response in
guard let strongSelf = self else {
return
}
let model = strongSelf.updateModel(response)
strongSelf.updateUI(model)
}
不推薦:
// 如果在響應(yīng)返回前 self 被釋放,則可能導(dǎo)致崩潰
resource.request().onComplete { [unowned self] response in
let model = self.updateModel(response)
self.updateUI(model)
}
不推薦:
// 內(nèi)存回收可以發(fā)生在更新模型和更新 UI 之間
resource.request().onComplete { [weak self] response in
let model = self?.updateModel(response)
self?.updateUI(model)
}
訪問控制
完整的訪問控制注釋會(huì)分散主題且是不必要的。然而,適時(shí)地使用 private 和 fileprivate 會(huì)使代碼更加清晰,也會(huì)有助于封裝。 在合理情況下,private 要優(yōu)于 fileprivate。 使用擴(kuò)展可能會(huì)要求你使用 fileprivate。
只有需要完整的訪問控制規(guī)范時(shí),才顯式地使用 open 、 public 和 internal。
將訪問控制用作前置屬性說(shuō)明符。僅有 static 說(shuō)明符或諸如 @IBAction 、 @IBOutlet 和 @discardableResult 的屬性應(yīng)該放在訪問控制前面。
推薦:
private let message = "Great Scott!"
class TimeMachine {
fileprivate dynamic lazy var fluxCapacitor = FluxCapacitor()
}
不推薦:
fileprivate let message = "Great Scott!"
class TimeMachine {
lazy dynamic fileprivate var fluxCapacitor = FluxCapacitor()
}
控制流
優(yōu)先選擇 for 循環(huán)的 for-in 格式而不是 while-condition-increment 格式。
推薦:
for _ in 0..<3 {
print("Hello three times")
}
for (index, person) in attendeeList.enumerated() {
print("(person) is at position #(index)")
}
for index in stride(from: 0, to: items.count, by: 2) {
print(index)
}
不推薦:
var i = 0
while i < 3 {
print("Hello three times")
i += 1
}
var i = 0
while i < attendeeList.count {
let person = attendeeList[i]
print("(person) is at position #(i)")
i += 1
}
黃金路徑
當(dāng)使用條件語(yǔ)句編碼時(shí),代碼的左邊距應(yīng)該是 「黃金」或「快樂」的路徑。就是不要嵌套 if 語(yǔ)句。多個(gè)返回語(yǔ)句是可以的。guard 語(yǔ)句就是因?yàn)檫@個(gè)創(chuàng)建的。
推薦:
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
guard let context = context else {
throw FFTError.noContext
}
guard let inputData = inputData else {
throw FFTError.noInputData
}
// 用上下文和輸入計(jì)算頻率
return frequencies
}
不推薦:
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
if let context = context {
if let inputData = inputData {
// 用上下文和輸入計(jì)算頻率
return frequencies
} else {
throw FFTError.noInputData
}
} else {
throw FFTError.noContext
}
}
當(dāng)用 guard 或 if let 解包多個(gè)可選值時(shí),在可能的情況下使用最下化復(fù)合版本嵌套。舉例:
推薦:
guard let number1 = number1,
let number2 = number2,
let number3 = number3 else {
fatalError("impossible")
}
// 用數(shù)字做某事:
不推薦:
if let number1 = number1 {
if let number2 = number2 {
if let number3 = number3 {
// 用數(shù)字做某事
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}
失敗防護(hù)
對(duì)于用某些方法退出,防護(hù)語(yǔ)句是必要的。一般地,它應(yīng)該是一行簡(jiǎn)潔的語(yǔ)句,比如: return 、 throw 、 break 、 continue 和 fatalError()。應(yīng)該避免大的代碼塊。如果清理代碼被用在多個(gè)退出點(diǎn),則可以考慮用 defer 塊來(lái)避免清理代碼的重復(fù)。
分號(hào)
在 Swift 中,每條代碼語(yǔ)句后面都不需要加分號(hào)。只有在你希望在一行中結(jié)合多條語(yǔ)句,才需要加分號(hào)。
不要在用分號(hào)分隔的單行中寫多條語(yǔ)句。
括號(hào)
條件周圍的括號(hào)是不必要的,應(yīng)該被忽略。
推薦:
if name == "Hello" {
print("World")
}
不推薦:
if (name == "Hello") {
print("World")
}
