开发环境
1.Xcode 15
2.Swift 5+
3.iOS 13+
4.SwiftUI
- 优点:UI布局开发简化,支持苹果多平台开发,可以跨端于macOS、watchOS和tvOS,实现代码的跨平台共享。
- 缺点:有学习成本,目前没有资源积累,组件库缺失,前期需要投入更多时间
工程推荐
R.Swift
R.Swift能够使用类似语法R.资源类型.资源名称来对某资源进行引用构建
R.Swift有着动态生成代码的机制, 它具有以下优点:
- 代码自动补全:就像输入其他的代码一样,R.Swift支持IDE的代码自动补全
- 自动检测: 可以自动检测代码是否存在问题, 当我们的资源文件名修改的时候, 这是就会提示资源引用错误
XcodeGen
通过解析一个YAML或JSON文件来定义你的目标、配置、方案、自定义构建设置和许多其他选项。gitignore可以忽略xcodeproj、xcworkspace工程文件,避免合并代码导致文件变动冲突。
架构规范
- 使用MVVM设计模式,使用RxSwift搭配ViewModel使用
- 页面路由基于 Swinject 设计
- 组件之间通信使用现有组件 KamServiceKit
- 页面UI禁用xib,使用纯代码自动约束布局,SnapKit
- JSON转Model使用 KakaJSON
- 网络框架基于 Alamofire 做二次业务封装,在Debug环境做Api请求次数统计以及404弹窗提醒
- 数据普通存储使用当前KFoundation中Cache模块(基于MMKV),App内做业务封装关联用户,关联设备。数据库存储使用 RxRealm
- 内存泄漏检测接入 LifetimeTracker , 在Debug环境做弹窗提醒
- 卡顿检测接入已有组件 DebugRing , 可启用FPS监控
- 引入自动检查代码规范组件 SwiftLint
- 开发中使用LocalHotReloading(基于injectionIII)进行热重载,减少编译,提速增效
- 免费编程助手插件 CodeiumForXcode , 可以根据上下文给出代码提示,可以按需使用
- 代码格式化 XCFormat , Xcode 插件,可以快速格式化Swift/OC代码。
架构图
第三方库
- SwiftEntryKit 各种弹窗
- PermissionsKit 各种权限
- SwiftyStoreKit 苹果内购
- RxRealm 数据库的RX调用
- UIAdapter 尺寸适配
开发规范
正确性
努力让你的代码在没有警告的情况下编译。 这条规则决定了许多风格决策,比如使用 #selector 类型而不是字符串字面量。
命名
描述性和一致性的命名让软件更易于阅读和理解。使用 API 设计规范 中描述的 Swift 命名规范。 一些关键点包括如下:
- 尽量让调用的地方更加简明
- 简明性优先而不是简洁性
- 使用驼峰命名法(而不是蛇形命名法)
- 针对类型(和协议)使用首字母大写,其它都是首字母小写
- 包含所有需要的单词,同时省略不必要的单词
- 基于角色的命名,而不是类型
- 有时候要针对弱引用类型信息进行补充
- 尽量保持流畅的用法
- 工厂方法以 make 开头
命名方法的副作用
不可变版本的动词方法要遵循后接 -ed, -ing 的规则
可变版本的名词方法要遵循 formX 的规则
布尔类型应该像断言一样读取
描述 这是什么 的协议应该读作名词
描述 一种能力 的协议应该以 -able 或者 -ible 结尾
使用不会让专家惊讶或让初学者迷惑的术语
通常要避免缩写
使用名称的先例
首选方法和属性而不是自由函数
统一向上或向下包装首字母缩略词和首字母
为相同含义的方法提供相同的基本名称
避免返回类型的重载
选择用于文档的好的参数名
为闭包和元组参数设置标签
利用默认参数的优势
类前缀
Swift 的类自动被包含在模块分配的命名空间中。不应该再添加类似于 RW 的类前缀。如果不同模块的两个命名冲突,可以在类名前添加模块名来消除歧义。无论如何,仅在少数可能引起混淆的情况下指明模块名。
import SomeModule
let myClass = MyModule.UsefulClass()
代理
当创建自定义代理方法的时候,未命名的第一个参数应该是代理源。 ( UIKit 包含很多这样的例子。)
推荐:
func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool
不推荐:
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool
代码组织
用扩展将代码组织为功能逻辑块。每个扩展都应该添加 // MARK: - 注释,以保证代码的结构清晰。
协议遵循
推荐为协议方法加一个单独的扩展,尤其是为一个模型加入协议遵循的时候。这可以让有关联的协议方法被分组在一起,也可以简化用类关联方法向这个类添加协议的指令。
推荐:
class MyViewController: UIViewController {
// 类填充在这
} `
`
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// table view 的数据源方法
}
// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
// scroll view 的代理方法
}
不推荐:
class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
// 所有方法
}
无用代码
无用代码(僵尸代码),包括 Xcode 模板代码和占位注释,应该被移除掉。教程或书籍中教用户使用的注释代码除外。
仅实现简单调用父类,但与教程无直接关联的方法应该被移除。这里包括任何为空的或无用的 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 未完成的实现,返回节数。
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning 未完成的实现,返回行数。
return Database.contacts.count
}
最小引用
引用最小化。举个例子,引用 Foundation 就足够的情况下不要再引用 UIKit 。
空格
用两个字符缩进比用制表符缩进更节省空间,同时能防止换行。务必在 Xcode 和项目中设置这个偏好,如下所示:
方法大括号和其他大括号( if / else / switch / while 等)总是在和语句相同的行写左括号,而在新行写右括号。
提示:你可以通过选中一些代码(或按 ⌘A 选中全部)然后按 Control-I (或在目录中选择编辑器 -> 结构 -> 重新缩进)的方式来重新缩进代码。一些 Xcode 模板代码会使用 4 个空格的制表符硬编码,这就是一个修正它的好方法。推荐:
if user.isHappy {
// 做一件事
} else {
// 做另一件事
}
不推荐:
if user.isHappy
{
// 做一件事
}
else {
// 做另一件事
}
方法之间应该只有一个空行,这样有助于视觉清晰和组织。方法中的空白应该按功能分隔代码,但在一个方法中有很多段意味着你应该将它们封装进不同的方法。
冒号总是在左边没有空格而右边有空格。比较特殊的是三元运算符 ? :、空字典 [:]
和带有未命名参数(_:)
的#selector
语法 .推荐:
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]
}
长行应该在 70 个字符左右被换行(这里并非硬性限制,可自行调整)。
避免在行结尾的地方附上空白。
在每个文件的结尾处增加一个单独的换行符。
注释
需要的时候,用注释来解释一个特定的代码片段 为什么 做某件事。注释应保持要么是最新的,要么就被删除。
为了避免块注释和代码内联,代码应该尽可能自文档化。 例外:这不含那些注释被用于生成文档的情况 。
类和结构体
使用哪个?
结构体有 值语义。对没有标识的事物应用结构体。
类有 引用语义。对有标识或有具体生命周期的事物应用类。
定义的举例
这是一个风格良好的类定义例子
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))"
}
}
上面的例子遵循了以下风格规范:
用后面有空格而前面没有空格的冒号,为属性、变量、常量、参数声明和其它语句指定类型,例如:x: Int
和Circle: Shape
。
如果多个变量和结构体共享一个共同的目的 / 上下文,则可以在同一行中定义。
缩进 getter
、setter
的定义和属性观察器。
不要再添加如internal
的默认修饰符。类似的,当重写一个方法时,不要再重复添加访问修饰符。
在扩展中组织额外功能(例如打印)。
隐藏非共享的实现细节,例如 centerString 在扩展中使用 private
访问控制。
self 的使用
为了简洁,请避免使用 self 关键词,Swift 不需要用它来访问一个对象属性或调用它的方法。
仅在编译器需要时(在 @escaping
闭包或初始化函数中,消除参数与属性的歧义)才使用 self。换句话说,如果不需要 self 就能编译通过,则可以忽略它。
计算属性
为了简洁,如果一个计算属性是只读的,则可以忽略 get
子句。仅在提供了 set 子句的情况下才需要 get
子句
Final
类或成员标记为 final 会从主题分散注意力,而且也没必要。 尽管如此,final 的使用有时可以表明你的意图,且值得你这样做。在下面的例子中,Box 有特定的目的,且并不打算在派生类中自定义它。标记为 final 可以使它更清晰。
// 用这个 Box 类将任何一般类型转换为引用类型。
final class Box{
let value: T
init(_ value: T) {
self.value = value
}
}
函数声明
在一行中保持较短的方法声明,包括左括号:
func reticulateSplines(spline: [Double]) -> Bool {
// 在这里写网格代码
}
对于签名较长的函数,则需在合适的位置换行,然后在后续的行中加一个额外的换行:
func reticulateSplines(spline: [Double], adjustmentFactor: Double,
translateConstant: Int, comment: String) -> Bool {
// 在这里写网络代码
}
在一行中保持较短的函数使用,像这样:
let success = reticulateSplines(splines)
如果是包装调用,则需在合适的位置换行,然后在后续的行中加一个额外的换行:
let success = reticulateSplines(
spline: splines,
adjustmentFactor: 1.3,
translateConstant: 2,
comment: "normalize the display")
闭包表达式
仅在参数列表最后有个单独的闭包表达式参数时,使用尾随闭包语法。给闭包参数定义一个描述性的命名
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()
})
类型
请尽可能多的使用 Swift 原生类型。 Swift 提供了 Objective-C 桥接,所以当你需要的时候你仍然可以使用全套方法。
推荐:
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 值的情况下,使用 ? 声明变量和函数返回类型为可选类型。
用 ! 声明的隐式解包类型,仅用于稍后在使用前初始化的实例变量,比如将在 viewDidLoad
中创建子视图。
当访问一个可选值时,如果值仅被访问一次或在链中有许多可选项时,使用可选链:
self.textContainer?.textLabel?.setNeedsDisplay()
当一次性解包和执行多个操作更方便时,使用可选绑定:
if let textContainer = self.textContainer {
// 用 textContainer 做很多事情
}
var subview: UIView?
var volume: Double?
// later on...
if let subview = subview, let volume = volume {
// 使用展开的 subview 和 volume 做某件事
}
延迟初始化
在更细粒度地控制对象声明周期时考虑使用延迟初始化。 对于 UIViewController
,延迟初始化视图是非常正确的。你也可以直接调用 { }()
的闭包或调用私有工厂方法。例如:
lazy var locationManager: CLLocationManager = self.makeLocationManager()
private func makeLocationManager() -> CLLocationManager {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestAlwaysAuthorization()
return manager
}
注意:
因为没有发生循环引用,所以这里不需要[unowned self]
。
位置管理器对弹出 UI 向用户申请权限有副作用,所以细颗粒地控制在这里是有意义的。
类型推断
优先选择简洁紧凑的代码,让编译器为单个实例的常量或变量推断类型。类型推断也适合于小(非空)的数组和字典。需要时,请指明特定类型,如 CGFloat
或 Int16
。
空数组和空字典的类型注释
为空数组和空字典使用类型注释。(对于分配给大型、多行文字的数组和字典,使用类型注释。
var names: [String] = []
var lookup: [String: Int] = [:]
语法糖
推荐使用类型声明简短的版本,而不是完整的泛型语法。
推荐:
var deviceModels: [String]
var employees: [Int: String]
var faxNumber: Int?
不推荐:
var deviceModels: Array
var employees: Dictionary<Int, String>
var faxNumber: Optional
内存管理
代码 (甚至非生产环境、教程演示的代码)都不应该出现循环引用。分析你的对象图并用 weak
和unowned
来防止强循环引用。或者,使用值类型( struct、enum )
来彻底防止循环引用。
延长对象的生命周期
使用惯用语法[weak self] 和 guard let strongSelf = self else { return }
来延长对象的生命周期。 在 self 超出闭包生命周期不明显的地方,[weak self]
更优于[unowned self]
。 明确地延长生命周期优于可选解包。
推荐:
resource.request().onComplete { [weak self] response in
guard let strongSelf = self else {
return
}
let model = strongSelf.updateModel(response)
strongSelf.updateUI(model)
}
不推荐:
// 如果在响应返回前 self 被释放,则可能导致崩溃
resource.request().onComplete { [unowned self] response in
let model = self.updateModel(response)
self.updateUI(model)
}
不推荐:
// 内存回收可以发生在更新模型和更新 UI 之间
resource.request().onComplete { [weak self] response in
let model = self?.updateModel(response)
self?.updateUI(model)
}
访问控制
完整的访问控制注释会分散主题且是不必要的。然而,适时地使用 private
和 fileprivate
会使代码更加清晰,也会有助于封装。 在合理情况下,private
要优于 fileprivate
。 使用扩展可能会要求你使用 fileprivate
。
只有需要完整的访问控制规范时,才显式地使用 open
、 public
和 internal
。
将访问控制用作前置属性说明符。仅有 static
说明符或诸如 @IBAction
、 @IBOutlet
和 @discardableResult
的属性应该放在访问控制前面。
推荐:
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()
}
控制流
优先选择 for 循环的 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
}
黄金路径
当使用条件语句编码时,代码的左边距应该是 「黄金」或「快乐」的路径。就是不要嵌套 if 语句。多个返回语句是可以的。guard 语句就是因为这个创建的。
推荐:
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
guard let context = context else {
throw FFTError.noContext
}
guard let inputData = inputData else {
throw FFTError.noInputData
}
// 用上下文和输入计算频率
return frequencies
}
不推荐:
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
if let context = context {
if let inputData = inputData {
// 用上下文和输入计算频率
return frequencies
} else {
throw FFTError.noInputData
}
} else {
throw FFTError.noContext
}
}
当用 guard
或 if let
解包多个可选值时,在可能的情况下使用最下化复合版本嵌套。举例:
推荐:
guard let number1 = number1,
let number2 = number2,
let number3 = number3 else {
fatalError("impossible")
}
// 用数字做某事:
不推荐:
if let number1 = number1 {
if let number2 = number2 {
if let number3 = number3 {
// 用数字做某事
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}
失败防护
对于用某些方法退出,防护语句是必要的。一般地,它应该是一行简洁的语句,比如: return
、 throw
、 break
、 continue
和 fatalError()
。应该避免大的代码块。如果清理代码被用在多个退出点,则可以考虑用 defer 块来避免清理代码的重复。
分号
在 Swift 中,每条代码语句后面都不需要加分号。只有在你希望在一行中结合多条语句,才需要加分号。
不要在用分号分隔的单行中写多条语句。
括号
条件周围的括号是不必要的,应该被忽略。
推荐:
if name == "Hello" {
print("World")
}
不推荐:
if (name == "Hello") {
print("World")
}