可能有很多同学在一开始写swift代码时都不知道一些相关的代码规范,常量变量如何定义等等。
这里我推荐关于代码规范的三份指导文章, 对于一些同学的代码规范性
会有很大的提升。
我们项目中用的是Realm 团队的swiftLint
安装比较简单 大部分的警告
和Error(不影响运行)
可以给你一些代码规范的指导
公司项目中不改动任何代码的二次编译时间需要3.82s
添加swiftLint后时间为4.279s
,有的时候会更长一些。
推荐使用nicklockwood大神写的SwiftFormat
以XCode插件的形式添加到XCode中,一键格式化当前Swift文件。非常方便。
得益于Swift中 外部参数命名
和_ 代表的忽略外部参数命名
Swift中的API命名会更强于Objective-C
Swift 官方对Swift API 设计命名有一套规范文档
这里是中文翻译
最好的学习方式是模仿Swift标准库的API设计的感觉去写自己项目的方法名。
个人感悟: 好的方法名读起来应该是一句精炼流畅的英文句子
在开发过程中有遇到几次崩溃都是没有考虑到这种情况。😿
刚刚从Objective-C过渡到Swift时候的我的写法:
/// 统一音乐模型转换类
class MusicConvertManager {
/// 将儿歌的音乐模型转换成统一音乐模型
/// - Parameter childernSongModel: 儿歌模型
/// - Returns: 统一的音乐模型
static func convertChildrenSong(of childernSongModel: ChildernSongModel) -> GenernalMusicModel {
let genernalMusic = GenernalMusicModel()
genernalMusic.id = childernSongModel._id
genernalMusic.url = childernSongModel.musicURL
genernalMusic.name = childernSongModel.title
return genernalMusic
}
/// 将播客的音乐模型转换成统一音乐模型
/// - Parameter childernSongModel: 播客音乐模型
/// - Returns: 统一的音乐模型
static func convertChildrenSong(of podcastModel: PodcastModel) -> GenernalMusicModel {
let genernalMusic = GenernalMusicModel()
genernalMusic.id = PodcastModel.pid
genernalMusic.url = childernSongModel.url
genernalMusic.name = childernSongModel.name
return genernalMusic
}
}
/// 具体使用 不建议这样,每次写到这里都需要先想到MusicConvertManager类,再思考用哪个具体的方法。❎
MusicManager.shared.currentModel = MusicConvertManager.convertChildrenSong(of: jsonModel.childrenModel)
建议写法: 通过给具体的模型创建extension
, 在extension中创建generalMusicModel的计算型属性
方便阅读和使用。
/// 通过genernalMusicModel计算型属性转换统一的音乐模型。 PodcastModel转换同理。
extension ChildernSongModel {
/// 统一的音乐模型 (如果是耗时操作建议缓存转换后的结果)
var genernalMusicModel: GenernalMusicModel {
let genernalMusic = GenernalMusicModel()
genernalMusic.id = _id
genernalMusic.url = musicURL
genernalMusic.name = title
return genernalMusic
}
}
/// 具体使用 这样写便于阅读及使用方便。 ✅
MusicManager.shared.currentModel = jsonModel.childrenModel.genernalMusicModel
以后有自定义协议的时候,命名可以参照这三种情况去规范命名。
有时候我们判断一个array? 是否为空会写出下面这样代码
var array:[String]?
/// 一番array 操作后
if array?.count != 0 {
///当数组长度不为0时
doSomething()
}
// 取一个集合中第一个大于0的数
let numberArray = [-4,1,-1,2,3,9]
let firstPositiveNumber = numberArray.first(where: { $0 > 0 }) ✅
let firstPositiveNumber = numberArray.filter { $0 > 0 }.first ❌
// 第一个方法遍历到符合条件的元素后即停止, 第二个方法在所有元素都遍历完一遍后再去找第一个。
// 同上面还有 取出集合中的最大最小元素
let minNumber = numberArray.min() ✅
let maxNumber = numberArray.max() ✅
let minNumber = numberArray.sorted().first ❌
let maxNumber = numberArray.sorted().last ❌
// 判断是不是所有的元素都是大于0 isAllPositive为Bool
let isAllPositive = numberArray.allSatisfy { $0 > 0 } ✅✅✅
let isAllPositive = numberArray { $0 > 0 }.isEmpty ❌❌❌
// 第一个方法在遇到第一个元素不不符合条件就遍历结束 直接返回false
// 第二个方法需要把所有的元素都遍历一遍后再去看是否是isEmpty 长集合会性能低下。
// 判断是否包含 -1 这个元素
let isContiansNagtiveOne = numberArray.contains(-1) ✅
let isContiansNagtiveOne = numberArray.filter { $0 == -1 }.isEmpty == false ❌
let isContiansNagtiveOne = numberArray.first(where: { $0 == -1 }) != nil ❌
// 其原因同上。
extension UIFont {
/// APP中大标题的字体
static let appLargeTitle = UIFont.systemFont(ofSize: 24)
}
extension UIColor {
/// APP主题色
static let appMain = UIColor.yellow
}
let titleLabel = UILabel()
titleLabel.font = .appLargeTitle
titleLabel.backgroundColor = .appMain
具体用法可看之前写过的一篇文章
@autoclosure
算是使用机会比较少的一个关键字了,唯一的作用是使代码变的美观一些。使闭包的描述不再使用{}
, 而是更参数化用()
。 不太能理解@autoclosure
的同学可以看一下Swift中文文档闭包章节的最后一个知识点。 这个tip只做了解就好。
如果觉得编译报错太烦可以使用swift 5 出来的@unknown 关键字修饰default 分支 让新添加的case以编译警告的形式出现。
enum Animal: String {
case human = "H"
case dog = "D"
case cat = "C"
}
enum TimeUtile: Int {
case second = 1
case minute = 60
case hour = 3600
}
var animal: Animal = .human
var time: TimeUtile = .second
print(animal) // human
print(animal.rawValue) // H
print(time) // second
print(time.rawValue) // 1
// 使用 if let 嵌套太多 不利于维护 ❌
if let realOptionalA = optionalA {
print("had A")
if let realOptionalB = optionalB {
print("had A and B")
if let realOptionalC = optionalC {
print("had A、B and C")
}
}
}
// 使用 guard let 调理清楚 便于阅读 ✅
guard let realOptionalA = optionalA else { return }
print("had A")
guard let realOptionalB = optionalB else { return }
print("had A and B")
guard let realOptionalC = optionalC else { return }
print("had A、B and C")
多用guard let 去解包可以在很多情况下大幅度的减小一些耗时函数的编译时间
,具体可以参考Swift编译加速Tips这篇文章。
在struct中, 编译器会自动生成带有属性的初始化方法。
struct User {
let name: String?
var age: Int?
}
// 可直接调用
User(name: String?, age: Int?)
但对于class就没有对于的初始化方法。我们可以使用XCode提供的辅助功能来生成对应的初始化方法。
class Book {
let name: String?
let pageCount: Int?
}
//使用后:
class Book {
// 编译器自动补全的方法
internal init(name: String?, pageCount: Int?) {
self.name = name
self.pageCount = pageCount
}
let name: String?
let pageCount: Int?
}
enum MyEnum {
case ok
case error
case none ❌
}
// 这个时候myEnum实际上是一个Optional的枚举值 而Optional 也有一个 none的枚举选项。
var myEnum : MyEnum? = .none
//可以通过指定类型解决 但不建议这样
var myEnum : MyEnum? = Optional.none
var myEnum : MyEnum? = MyEnum.none
这个时候编译器会报警告 而且你的switch中会多一个case .some(.none):的选项。
假设某电商app首页的tableView有4个section
// 电商首页的tableView 分组
//CaseIterable 用来获取枚举项个数
enum HomeSectionType: Int, CaseIterable {
// banner位
case banner = 1
// 合辑
case menu = 2
// 推荐
case recommend = 3
// 商品
case goods = 4
// 枚举内部封装组头高度的计算方法
var headerHeight: CGFloat {
switch self :
case banner:
return 88.88
.....
}
}
// tableView 代理
func numberOfSections(in tableView: UITableView) -> Int {
return HomeSectionType.allCases.count
}
// 获取组头高度
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard let sectionType = HomeSectionType(rawValue: section) else { return 0.0 }
return sectionType.headerHeight
}
这样就可以让tableView的代理看起来简洁明了。
CaseIterable
协议可以让你的枚举具备Array相关的属性,如count
还有一个好处就是当产品某个版本想要调换section的顺序
的时候 可以直接 修改枚举项的Int值
即可。
有时候为了图方便就会使用计算型属性,保证每次都会拿到最新的数据。但如果是一些耗时操作
建议添加缓存,或者使用普通的存储型属性。 缓存存在就直接返回缓存值,不存在的时候再去调用计算方法。
我在SwiftUI中没有过多考虑性能问题,大部分使用的计算型属性,导致有些地方性能消耗过多。。。 分享出来以示警醒。。
日常写法是let vc = UIViewController()
navigationController?.pushViewController(vc)
或者 present(vc, animated: true, completion: nil)
可以给UIViewController添加extension方法, 对 push 和 present 的封装
extension UIViewController {
func bePushed(by currentVC: UIViewController?) {
currentVC?.navigationController?.pushViewController(self, animated: true)
}
func bePresented(by currentVC: UIViewController?) {
currentVC?.present(self, animated: true, completion: nil)
}
}
// 在ViewController中使用如下: 个人感觉这样去弹出一个控制器语义上会更连贯便捷。
UIViewController().bePushed(by: self)
UIViewController().bePresented(by: self)
个人一开始在OC转Swift的时候会对如何更好的在Swift中写通知名感到有些疑惑。
苹果在Swift4.0
中Swift官方库
对通知名的使用做过一次修改。 改成了类的静态属性的形式,如下:
如监听UItextView的内容改变:
NotificationCenter.default.addObserver(self, selector: #selector(textViewNotifitionAction), name: UITextView.textDidChangeNotification, object: nil)
我们再日常开发中就会在对于监听的类的extension中去定义通知名
extension MyClass {
public class let MyNotification: NSNotification.Name = ....
}
如果想全局管理
你的通知,而且更方便
的使用通知名可以使用下面这种方式:
/// 创建一个通知名协议
public protocol NotificationName {
var name: Notification.Name { get }
}
extension RawRepresentable where RawValue == String, Self: NotificationName {
public var name: Notification.Name {
get {
return Notification.Name(self.rawValue)
}
}
}
//最后用一个枚举去管理你所有的通知名
/// 所有通知名
public enum Notifications: String, NotificationName {
/// 自定义某某通知
case myNotification
}
// 使用
NotificationCenter.default.addObserver(self, selector: #selector(fromMyNotification(notification:)), name: Notifications.myNotification.name, object: nil)
优点:1.规避了通知名同名的可能性。 2.书写起来更方便。 3.方便查找管理。
假设你需要对一个字符串数组strArray进行一些处理,但是当元素为a的时候直接忽略。 你可能会写成:
let strArray = ["a","a","b","c","d"]
for str in strArray {
if str == "a" { continue }
// do something here
}
或者你也可以使用where
关键字进行限定:
for str in strArray where str != "a" {
// do something here
}
这样你的代码也会看起来更整洁一些,不过你如果用filter函数的话另讲。
假设我们有一个处理图书的运用,一本书包括不同的章节,不同的章节又包括不同的页面,可以像下面这样表示。
struct Page { }
// 章节
var myChapter: [Page] = []
// 一本书
var myBook: [[Page]] = []
但如果我们用别名去定义章节类型和书类型
// 章节
typealias Chapter = [Page]
// 书
typealias Book = [Chapter]
var myChapter: Chapter = []
var myBook: Book = []
这样的好处可以让代码可读性更强,以后在项目中看到Chapter 和 Book 就知道表示的是章节和书了。
现在有一个需求: 在一个引导之后去顺序执行其他的引导, 或者一个网络请求完去顺序执行另外的网络请求。
我们可能会写出这个样子的代码:
func asyncTask1(success: @escaping ()->Void) {
// After a while
success()
}
func asyncTask2(success: @escaping ()->Void) {
// After a while
success()
}
func asyncTask3(success: @escaping ()->Void) {
// After a while
success()
}
// 方法调用 多重闭包嵌套 阅读成本太大 不建议 ❌
func start() {
asyncTask1 {
asyncTask2 {
asyncTask3 {
// finished, do something here
}
}
}
}
在Swift中我们可以自定义运算符, 可以通过自定义操作符写出下面这样的代码
// >--> 为我们自定义的运算符 >-->左边的方法执行完再去执行右边的方法
// 所有的方法都执行完后会调用 finish 方便阅读 ✅
asyncTask1 >--> asyncTask2 >--> asyncTask3 {
// finished,do something here
}
运算符的具体定义如下
typealias MyVoidClosure = ()->Void
/// 定义优先级组
precedencegroup MyAsyncPrecedencegroup {
associativity: left // 从左往右执行
assignment: false // 不可以赋值
}
infix operator >-->: MyAsyncPrecedencegroup // 遵守 MyAsyncPrecedencegroup 优先级组
/// 这里的逃逸闭包写的有点丑 本来想用alias来简化 发现语法不支持。
func >-->(lhs:@escaping ((@escaping MyVoidClosure) -> Void),
rhs: @escaping ((@escaping MyVoidClosure) -> Void))
-> (@escaping MyVoidClosure) -> Void {
return { complete in
lhs {
rhs {
complete()
}
}
}
}
这样我们就可以通过自定义运算符来通俗易懂的表示出异步顺序执行事件了。
具体运算符的定义和使用:Swift文档_自定义运算符
为了明确区分flatMap函数的使用场景,在Swift4.1时候推出compactMap函数来加以区分。 一般使用是用来过滤集合中的nil。
var array: [String?]?
我们想要安全的取出array里面的第一个元素
平时的使用:
// 这个时候array1元素的类型是 [String?], 如果需要安全的使用array1 需要二次对array1解包 ❌
guard let array1 = array, let firstObject1 = array1.first, let realFirstObject = firstObject1 else { return }
使用 compactMap
// 使用compactMap后 array的类型是[String] ✅
guard let array2 = array?.compactMap({$0}), let firstObject2 = array2.first else { return }
在iOS的开发中,圆角带阴影都是一件比较头疼的事情。
但是利用Swift泛型
和Core Animation
的一些知识,可以写出很优雅简洁的圆角阴影代码。
如下
/// 阴影圆角的视图
class CornerShadowView<T: UIView>: UIView {
var childView: T = T()
override init(frame: CGRect) {
super.init(frame: frame)
configBaseUI()
}
private func configBaseUI() {
childView = T()
addSubview(childView)
childView.frame = bounds
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
使用:
// 设置泛型的具体类为 UIButton
let cornerShadowView = CornerShadowView<UIButton>(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
// UIButton的基本属性设置
cornerShadowView.childView.setTitle("Hi", for: .normal)
// UIButton的圆角属性设置 可以进行二次封装,略。
cornerShadowView.childView.backgroundColor = .red
cornerShadowView.childView.layer.cornerRadius = 50
cornerShadowView.childView.layer.masksToBounds = true
// 阴影设置 可以进行二次封装,略。
cornerShadowView.layer.shadowColor = UIColor.black.cgColor
cornerShadowView.layer.shadowOffset = .zero
cornerShadowView.layer.shadowRadius = 20
cornerShadowView.layer.shadowOpacity = 0.8
具体可以看iOS中圆角视图加阴影的方案对比及封装一文的讲解。
关于属性包装最有用的实际使用就是对UserDefaults的封装, 具体可以看这篇文章的讲解
/// 当前用户的名称为 "liaoWorking"
var name:String = "liaoWorking"
/// 对UserDefaults的封装类
struct UserDefaultsConfig {
@UserDefault("had_shown_guide_view\(name)", defaultValue: false)
static var hadShownGuideView: Bool
}
假设当前用户引导显示完毕,将hadShownGuideView 置为true
UserDefaultsConfig.hadShownGuideView = true
假设用户liaoWorking
在没杀死app
的情况下切换到用户zhangMing
这个时候再去读 UserDefaultsConfig.hadShownGuideView 的值 是true 还是 false ?🤔
实际这个时候 UserDefaultsConfig.hadShownGuideView 的key 还是 "had_shown_guide_viewliaoWorking
"
因为是static, UserDefaultsConfig.hadShownGuideView的key在第一次调用的时候就确定了。
你可以将hadShownGuideView改为成员变量,保证每一次获取UserDefaults的时候key都为包含当前用户名的key。
struct UserDefaultsConfig {
/// 去掉Static 改为成员变量
@UserDefault("had_shown_guide_view\(name)", defaultValue: false)
var hadShownGuideView: Bool
}
/// 保证每次调用的时候UserDefaults的key都是当前的用户的name
UserDefaultsConfig().hadShownGuideView = true
假设我们有一个处理图书的运用,一本书包括不同的章节,不同的章节又包括不同的页面,可以像下面这样表示。
struct Page { }
// 章节
var myChapter: [Page] = []
// 一本书
var myBook: [[Page]] = []
但如果我们用别名去定义章节类型和书类型
// 章节
typealias Chapter = [Page]
// 书
typealias Book = [Chapter]
var myChapter: Chapter = []
var myBook: Book = []
这样的好处可以让代码可读性更强,以后在项目中看到Chapter 和 Book 就知道表示的是章节和书了。
在项目中经常会用到时间戳格式转换成字符串, 每次用的时候都是找之前的代码复制粘贴。 再去修改成对应的时间格式。 这里容易出现类似于把yyyy写成YYYY之类的错误。
为了避免写重复代码及错误的发生, 可以将所有的DateFormatter类型封装在枚举中使用。
如下:
/// 项目中常用的时间格式枚举
enum DateFormatterType: String {
/// yyyy-MM-dd
case yyyy_MM_dd = "yyyy-MM-dd"
/// yyyy.MM.dd
case yyyy_MM_dd_dot = "yyyy.MM.dd"
/// yyyy 年 MM 月
case yyyy_MM_chinese = "yyyy 年 MM 月"
/// yyyy年MM月
case yyyyMM_chinese = "yyyy年MM月"
/// yyyy/MM/dd
case yyyyMMdd_slash = "yyyy/MM/dd"
/// yy 年 M 月 d 日 HH:mm
case yy_M_d_HH_mm_chinese = "yy 年 M 月 d 日 HH:mm"
func makeDateString(of timeStamp: Double, isMileSecond: Bool = true) -> String {
//把时间戳转换成指定的格式
}
}
// 具体的调用
DateFormatterType.yyyy_MM_dd.makeDateString(of: 1626318786)
这样使用起来时间戳转具体的时间格式就更简洁和安全一些。(关于系统库的NSDateFormatter的性能问题这里就不做讨论了。可依据具体项目进行优化。)
前几天遇到一个需求: 有一个多选框,用户选择不同的选项并执行对应的方法。
在Swift中函数是一等公民(first-class type)
可以作为参数或者返回值去使用。 利用这个特性,我们就可以很好的解决这个需求。
func funcA() { print("A") }
func funcB() { print("B") }
// 使用数组来存储函数
var funcArray:[()->()] = [funcA, funcB]
// 遍历函数数组中的元素,并依次执行。
funcArray.forEach({$0()})
这样下来整个需求的代码逻辑就清晰可见。也更方便维护。
很多同学在OC转到Swift的时候,在使用VC或者View的属性时都喜欢在前面加一个self。
/// 例如在VC中,设置view的背景色
self.view.backgroundColor = .red
在闭包的使用中因为语法的原因又不得不使用self.xxx 有的同学就会有疑问,到底什么时候需要使用self 什么时候不需要使用self
这里的回答是能不使用self,就不要使用self。能省就省。 不过像在闭包或者init方法中需要使用self是语法原因,这里得加上self。
一般我们在创建一个空数组的时候,都喜欢像下面这样写
//Person为自定义类
var persons:[Person] = []
借鉴标准库中的一些写法 例如: var size: CGSize = .zero
我们可以添加Array的一个分类 让创建空数组变的更具有语义化:
extension Array {
static var empty: Self { [] }
}
/// 在使用的时候 我们就可以像下面这样去创建空数组:
var persons:[Person] = .empty
使代码更具有可读性。
我在一开始的Swift项目开发中 总喜欢把模型属性设置成可选型,包括用一些模型生成工具生成的属性也都默认是可选。
这里以User模型为例。
class User {
var name: String?
}
这样会使你在项目中大部分使用name的地方。 都会使用 ?? 来赋默认值.
使代码变得不雅观, 并且考虑默认值是什么。
于是在后面的开发中,像name属性 都统一改成了有默认值
class User {
var name: String = ""
}
省去了很多无用的赋默认值的操作。也不用在使用时考虑默认值是多少。
但如果某个属性的确存在为nil的情况需要考虑。还是老老实实写成可选型。
在平时开发中,print打印多个参数我们会写成下面的样子:
let paraA = "A"
let paraB = "B"
let paraC = "C"
// 打印结果为 "A____B____C"
print("\(paraA)____\(paraB)____\(paraC)")
其实在这里直接使用print的多参数打印,写法上会更简单:
// 打印结果为 "A B C"
print(paraA,paraB,paraC)
在前面tip13 已经提到了关于优化编译时长的一些建议 下面重点讲讲最近在适配Xcode14时遇到的一个编译问题。
let vedioCount = 4
let musicCount = 3
let sectionCount = 4
// 不要写像成下面这种 很长的一段计算表达式的形式
// 下面这一行代码在2019款16寸顶配mbp上编译耗时也需要110多秒 这里不是毫秒 是秒
let cellWidth = CGFloat(224 * vedioCount + musicCount * 150 + 233 + (sectionCount - 1) * 30 + sectionCount) ❌
需要将上面的一行长代码拆分成多个短的代码。 并且在声明变量的时候主动指定类型,别让XCode自己去推断。 如:
let videoCount: Int = 4
这条不算是Swift的建议,而是iOS开发过程中的一个建议: 在UIView中创建局部变量表示图的宽高时不建议使用 height, width 种命名方式
let width: CGFloat = 30.0
let height: CGFloat = 40.0
因为在Swift中view中已经存在叫width和height的属性,虽然这样可以使用。 但在日后如果想修改局部变量的名称width 或者 height 为其他名称时。 就会产生查找修改的麻烦。(帮别人改代码有感)
建议命名为 itemWidth, viewHeight 之类的名称作以区分:
let itemWidth: CGFloat = 30.0
let itemHeight: CGFloat = 40.0
如果对项目的内存管理有严格要求的话推荐使用struct. 不严格测试 在遍历创建一百万次拥有两个属性的Person对象时。 struct的内存占用是class的0.6倍。 其他方面性能差异不大。
to be continued⏱.