IOS架构设计与规范

开发环境

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工程文件,避免合并代码导致文件变动冲突。

架构规范

  1. 使用MVVM设计模式,使用RxSwift搭配ViewModel使用
  2. 页面路由基于 Swinject 设计
  3. 组件之间通信使用现有组件 KamServiceKit
  4. 页面UI禁用xib,使用纯代码自动约束布局,SnapKit
  5. JSON转Model使用 KakaJSON
  6. 网络框架基于 Alamofire 做二次业务封装,在Debug环境做Api请求次数统计以及404弹窗提醒
  7. 数据普通存储使用当前KFoundation中Cache模块(基于MMKV),App内做业务封装关联用户,关联设备。数据库存储使用 RxRealm
  8. 内存泄漏检测接入 LifetimeTracker , 在Debug环境做弹窗提醒
  9. 卡顿检测接入已有组件 DebugRing , 可启用FPS监控
  10. 引入自动检查代码规范组件 SwiftLint
  11. 开发中使用LocalHotReloading(基于injectionIII)进行热重载,减少编译,提速增效
  12. 免费编程助手插件 CodeiumForXcode , 可以根据上下文给出代码提示,可以按需使用
  13. 代码格式化 XCFormat , Xcode 插件,可以快速格式化Swift/OC代码。
    架构图

第三方库

开发规范

正确性

努力让你的代码在没有警告的情况下编译。 这条规则决定了许多风格决策,比如使用 #selector 类型而不是字符串字面量。

命名

描述性和一致性的命名让软件更易于阅读和理解。使用 API 设计规范 中描述的 Swift 命名规范。 一些关键点包括如下:

  1. 尽量让调用的地方更加简明
  2. 简明性优先而不是简洁性
  3. 使用驼峰命名法(而不是蛇形命名法)
  4. 针对类型(和协议)使用首字母大写,其它都是首字母小写
  5. 包含所有需要的单词,同时省略不必要的单词
  6. 基于角色的命名,而不是类型
  7. 有时候要针对弱引用类型信息进行补充
  8. 尽量保持流畅的用法
  9. 工厂方法以 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: IntCircle: Shape
如果多个变量和结构体共享一个共同的目的 / 上下文,则可以在同一行中定义。
缩进 gettersetter 的定义和属性观察器。
不要再添加如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 向用户申请权限有副作用,所以细颗粒地控制在这里是有意义的。
类型推断
优先选择简洁紧凑的代码,让编译器为单个实例的常量或变量推断类型。类型推断也适合于小(非空)的数组和字典。需要时,请指明特定类型,如 CGFloatInt16
空数组和空字典的类型注释
为空数组和空字典使用类型注释。(对于分配给大型、多行文字的数组和字典,使用类型注释。
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

内存管理

代码 (甚至非生产环境、教程演示的代码)都不应该出现循环引用。分析你的对象图并用 weakunowned 来防止强循环引用。或者,使用值类型( 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)
} 

访问控制

完整的访问控制注释会分散主题且是不必要的。然而,适时地使用 privatefileprivate 会使代码更加清晰,也会有助于封装。 在合理情况下,private 要优于 fileprivate。 使用扩展可能会要求你使用 fileprivate
只有需要完整的访问控制规范时,才显式地使用 openpublicinternal
将访问控制用作前置属性说明符。仅有 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
  }
}

当用 guardif 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")
} 

失败防护

对于用某些方法退出,防护语句是必要的。一般地,它应该是一行简洁的语句,比如: returnthrowbreakcontinuefatalError()。应该避免大的代码块。如果清理代码被用在多个退出点,则可以考虑用 defer 块来避免清理代码的重复。

分号

在 Swift 中,每条代码语句后面都不需要加分号。只有在你希望在一行中结合多条语句,才需要加分号。
不要在用分号分隔的单行中写多条语句。
括号
条件周围的括号是不必要的,应该被忽略。
推荐:

if name == "Hello" {
  print("World")
} 
不推荐:
if (name == "Hello") {
  print("World")
} 
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容