iOS 编程零碎要点

本文总结了我在学习 Swift 开发 iOS App 过程中的零碎知识, 目前已经通过所学成功上架一款软件到 App Store.

img

变量 & 常量

  • 除非必要, 一般情况下只使用常量.
  • Swift 函数中的参数默认传入都是常量, 如果确实要在函数内部改变传入的参数值的话需要将其标明为变量, 使用 inout 可以完成
  • 常量与变量的本质区别: 每个动作在执行完之后, 其内含的本地变量会被从内存中擦除, 在下一次执行此动作时会重新建立一个同名的本地变量, 但是此变量与上一次执行中的变量只有名称相同的关系而已, 是完全不同的两个本地变量. 常量一旦被建立在一次方法的执行过程中只能赋值一次, 但是变量可以被赋值多次.
  • 变量类型:
    • 成员变量: 写在类声明的大括号中的变量叫成员变量 (也叫属性 / 实例变量); 成员变量存储在堆中 (当前对象对应的堆的存储空间中) 不会被系统自动释放 只能有程序员手动释放
    • 局部变量: 写在代码块或函数中的变量为局部变量; 局部变量存储在栈里面系统会自动释放
    • 全员变量: 写在函数外或大括号外的变量就是全局变量; 全局变量存储在静态区中 程序启动时就会分配存储空间 直到程序结束才会释放

面向协议编程 (Protocol oriented programming)

  • 继承会带来耦合, 面向对象编程经常使用继承, 经常忍受着愈加繁杂和庞大的体系获得代码的可重用性, 但是随着项目越来越大, 代码复杂性加速增长, bug 也越来越难以发现. 而面向协议可以最大程度减少耦合.
  • swift 是一门面向协议编程 pop 的语言 (Protocol oriented programming), 有物件导向编程 oop 的特性 (object oriented programming). pop 观点极为新颖, 教学资源极少, 现阶段还是学习 oop.
  • 代理与协议: 协议相当于合同, 让开发者不必要知道如何进行但是可以安排下去执行的命令. 代理则是代表的意思. 由于 iOS 开发中严格遵守 MVC 模式, 视图只负责显示不存储数据, 数据由 model 进行存储, 视图控制器则起到传输数据给视图并从视图返回用户交互事件的作用. 因此视图控制器既是数据源的代理. 代码世界中术业有专攻, 让让每个对象专注在自己所擅长的工作中, 这样整个系统更加清晰明了. 表视图的代理分为两个: UITableViewDelegateUITableViewDataSource
  • 方法名: 例如 tableView(_:numberOfRowsInSection:) 其中 tableView 并不是方法名, 方法名是 tableView 加上后面的参数列表
  • 表格视图与普通视图控制器之间必须有 delegateDataSource 协议链接, 表视图控制器自带这两个协议. 而且在 viewDidLoad 方法中必须声明代理
    • delegate 协议负责滑动操作等
    • DataSource 负责…
  • 协议既是规定一定要用什么属性, 一定要用什么方法, 可以代替子类别与父类别概念, 还有以下好处
    • 可以服从多个 Protocol
    • 有时父类别方法属性多, 子类别忘记了必须覆写的某个属性方法, Protocol 保证一定会实作
    • 某几个子类别有相同的方法, 重复添加比较麻烦, 通过 Protocol 方便添加
  • 好的面向对象的设计法则是让对象管理自己的状态.
  • Swift 既是面向对象的, 又是函数式的编程语言. 说 Swift 是面向对象的语言, 是因为 Swift 支持类的封装, 继承, 和多态, 从这点上来看与 Java 这类纯面向对象的语言几乎毫无差别. 说 Swift 是函数式编程语言, 是因为 Swift 支持 map, reduce, filter, flatmap 这类去除中间状态, 数学函数式的方法, 更加强调运算结果而不是中间过程.
  • swift 面向协议编程的两大基石一个是 struct, 一个是 Protocol. struct 没有也不需要继承的功能, 因为 swift 提倡面向协议, 为了实现某个功能, struct 去遵从某个协议即可
  • Swift 是一门支持多编程范式的语言, 既支持面向对象编程, 也支持面向协议编程, 同时还支持函数式编程. 在项目开发过程中, 控制器和视图部分由于使用系统框架, 应更多采用面向对象编程的方式; 而模型或业务逻辑等自定义类型部分, 则应优先考虑面向协议编程.

iOS 启动

himg

  • Pre-main Time: 指 main 函数执行之前的加载时间, 包括 dylib 动态库加载, Mach-O 文件加载, Rebase/Binding, Objective-C Runtime 加载等;

    在 Xcode 中 Edit Scheme -> Run -> Auguments 添加环境变量 DYLD_PRINT_STATISTICS 并把其值设为 1, 如下图:

    himg

    himg

  • Loading Time: 指 main 函数开始执行到 AppDelegate 的 applicationDidBecomeActive: 回调方法执行 (App 被激活) 的时间间隔, 这个时间包含了的 App 启动时各初始化项的执行时间 (一般写在 application:didFinishLaunchingWithOptions: 方法里) , 同时包含首页 UI 被渲染并显示出来的耗时.

    我们可以在 main 函数开始执行和 applicationDidBecomeActive: 方法执行末尾时分别记录一个时间点, 然后计算两者时间差即可,

UIKit

  • UIView 的 convert(_ rect: CGRect, to view: UIView?) -> CGRect 可以将 receiver 中的 rect 转换到 to view 的坐标系上

    比如 priceView的坐标为 (10, 10, 20, 20), priceView.inputField 的坐标为 (5, 5, 10, 10), 那么:

    1
    2
    var rect1 = vc.priceView.inputField.convert(vc.priceView.inputField.bounds, to: vc.view) // (15, 15, 10, 10)
    var rect2 = vc.priceView.inputField.convert(CGRect.zero, to: vc.view) // (15, 15, 0, 0)
  • UIDatePicker 继承自 UIControl, 其通过 addTarget() 方法来添加用户交互事件. 其不是 UIPickerView 的子类, 不可通过协议的方式进行配置或用户交互.

  • 设置 layer 的层次可以使用 layer.zPosition = Int, 数字越大层次越高, 越不被遮挡

  • 设置 view 的层级可以使用 bringSubview(toFront view: UIView)sendSubview(toBack view: UIView) 两个方法调整层次

  • 代码中但凡以 UI 开头的都属于 UIKit 的一部分

  • scrollView 最重要的就是 contentSize 属性, 设置时将其内的 view 上下左右全部对齐到 contentSize, 然后将 view 的宽度对齐到 frameSize 就可以保证视图只能上下移动.

  • 将图片上方覆盖一层视图, 调整视图为黑色, 透明度为 0.2, 可以上图片上的文字标签更加易读

  • xcode 中的视图顺序为最下方在最上层.

  • 每个单元格都必须设置独立的相对应的 view 文件

  • 动态形态即 iOS 系统中的字体放大功能, 苹果推荐开发者开发的 APP 都支持动态形态, 即字体的 Text Style

  • CALayer 与 view 区别

    • 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示, 并且 UIView 的尺寸样式都由内部的 Layer 所提供. 两者都有树状层级结构, layer 内部有 SubLayers, View 内部有 SubViews. 但是 Layer 比 View 多了个 AnchorPoint
    • View 显示的时候, UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display
    • CALayer 是默认修改属性支持隐式动画的, 在给 UIView 的 Layer 做动画的时候, View 作为 Layer 的代理, Layer 通过 actionForLayer:forKey: 向 View 请求相应的 action(动画行为)
    • layer 内部维护着三分 layer tree, 分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树), 在做 iOS 动画的时候, 我们修改动画的属性, 在动画的其实是 Layer 的 presentLayer 的属性值, 而最终展示在界面上的其实是提供 View 的 modelLayer
    • 两者最明显的区别是 View 可以接受并处理事件, 而 Layer 不可以
    • 总结: view 负责了与人的动作交互以及对 layer 的管理, layer 则负责了所有能让人看到的东西.
  • 如果要展示或弹出某个操作的话一定要用到 present 命令

  • Storyboard 中项目变多后会变得难以管理, 这时可以将项目中与其他视图没有关联的视图独立出来. 一是为了方便管理, 二也可以分配给专人进行设计, 互不打扰. 命令为”Editor”选单中”Refactor to Storyboard”. 注: 重构之后标签的名称不可再改变! 可能是 xcode 的 bug

  • 如果要在 APP 中嵌入一个有良好体验的网页, 可以使用 SFSafariViewController, 如果想简单方便可以使用 WKWebView.

  • UIPickerViewUITableView 类似, 需要与普通控制器建立 DataSourcedelegate

  • scrollViewimageView 会截获 touch, 传递不到父视图 view 上, 需要 extension 里面对 touchesBegan 方法进行重载后将其能传递到父视图 view 中.

  • 代码设置字体

    1
    2
    3
    label.font = [UIFont fontWithName:@"Helvetica-Bold" size:20];// 加粗
    label.font = [UIFont fontWithName:@"Helvetica-Oblique" size:20];// 加斜
    label.font = [UIFont fontWithName:@"Helvetica-BoldOblique" size:20];// 又粗又斜
  • 设置一个 view 位于屏幕的三分之一处, 可以设置屏幕的 bottom 与 view 的 topbottom 对齐, 然后设置 multiplier. 以下图为例:

    himg

    在图中的例子中, 可以理解为将 view 的 bottom 与屏幕的 bottom 合并为一条线附着于 view 上, 然后依据 multiplier 的值设置此线在屏幕中的位置

  • Xcode 中的默认可以使用的字体与 macOS 的安装字体不同, xcode 中的所有可显示的字体都在 IPhone 中默认存在

  • 除了系统的字体有 systemFontWeight 方法, 其他的所有自定义字体的粗细都只能通过字体的文件来区分, 比如细字体是一个字体文件, 粗字体是一个字体文件

  • 导入自定义字体并使用

  • LaunchScreen 修改图片无响应

    • 判断 Info.plist 中 launchscreen 的名称是否设置正确
    • 修改 launchscreen 的图片后, 需要删除 app, 再重新安装
  • button 的 titleLabel 设置: 必须使用 setTitle("", for: .touchUpInside) 指定状态, 然后才可以使用 titleLabel. https://www.jianshu.com/p/53dcf361236b

  • tableView 的 sectionHeaderView 的背景颜色不可通过 backgroundColor 直接进行设置, 可以通过 contentView.backgroundColor 进行设置, 但是由于 contentView 是与 safeArea 对齐的, 超出 safeArea 区域的其他地方的背景颜色仍然是默认的白色, 因此最直接的办法是 backgroundView = LineView(UIColor.red)

  • tableView 的 cell 中有, 自身, backgroundView, selectedBackgroundView, contentView 他们层次的上下关系是:

    自身 -> backgroundView -> selectedBackgroundView -> contentView -> 其他添加的 view

    在布局时, 我们切记记得一定要将自定义添加的 view 添加到 contentView 上, 因为系统对 editStyle 的 cell 进行处理时都是针对 contentView 进行

    contentView 默认是根据 safeArea 对齐的, 因此如果添加的 view 对齐了 contentView, 那么就不用操心对齐到 safeArea 了

  • 在长按 cell 弹出菜单的方法中, 我们必须要设置 canBecomeFirstRespondercanPerformAction, 然后在长按手势方法中声明 recognizer.view?.becomeFirstResponder(), 切记不要在 cell 每次赋值时指定 cell.becomeFirstResponder, 否则会发生 AXError 错误!

  • 在 tableView 的 reloadRows 方法执行时, 如果我们同时使用了 tableView.reloadData(), 那么将会出现单元格缺失的现象, 这应该与单元格的复用有关. 相应的 iOS 13 的方法 UITableViewDiffableDataSource 在增量更新时如果进行 tableView.reloadData() 也会发生 cell 的断层现象

  • tableView 的复用机制

    通常, 我们会在 tableView.register(HomeTableViewCell.self, forCellReuseIdentifier: "tableCell") 进行注册 cell, 之后在 cellForRowAt 方法中使用 let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! HomeTableViewCell 来进行复用后.

    想象一下, 如果不进行复用, 而是直接在 cellForRowAt 中返回一个我们自定义的 cell, 可不可以呢?

    答案是可以的, 但是这样的话每次向下滑动, 显示一个新的 cell 时, 系统就会对那一个新的 cell 进行初始化, 对旧的进行释放, 这样是极其浪费系统计算资源的. 而使用了复用机制后, 系统就可以直接调用指定标识符 (指定类型) 的 cell, 不用涉及到初始化, 大大减轻了系统负担.

    复用的简单流程如下:

    假设一个屏幕 (显示范围) 只能显示 tableView 的 5 个 cell

    1. 在添加一个 tableView 到主 view 的时候会随之创建一个复用池, 用于存放通过标识符和指定类型创建的 注册 cell
    2. 系统在显示 tableView 的第一个 cell 时调用其代理方法 cellForRowAt, 因为代理方法中使用了 dequeueReusableCell(withIdentifier: "tableCell", for: indexPath), 那么会检测当前复用池有无 cell, 没有则新建 (初始化) 一个, 返回此 cell 到屏幕并将此 cell 添加到复用池
    3. 系统在显示第 2 个 cell 的时候, 再次调用代理方法 cellForRowAt, 检查复用池有无 cell, 虽然有 cell, 但是当前屏幕 (显示范围) 还没有被铺满, 因此再次创建 (即初始化) 一个 cell, 返回此 cell 到屏幕并将其添加到复用池
    4. 重复第 3 步, 直至显示到第 7 个 cell, 此时屏幕 (显示范围) 已经被填满, 显示新的 cell 的同时对旧 cell 进行回收到复用池并从复用池拉取之前被回收的 cell 进行复用.

    himg

    UITableViewCell 的复用机制是, 在 tableview 中存在一个复用池. 这个复用池是一个队列或一个链表. 然后通过 dequeueReusableCellWithIdentifier: 获取一个 cell, 如果当前 cell 不存在, 即新建一个 cell, 并将当前 cell 添加进复用池中. 如果当前的 cell 数量已经到过 tableview 所能容纳的个数, 则会在滚动到下一个 cell 时, 自动取出之前的 cell 并设置内容.

    tableView, tableView 的 headerView, collectionView, collectionView 的 headerView 都是这个套路

  • UIView 的如下属性是可以有动画效果的, 其他的则不行, 比如 isHidden 属性

    • frame
    • bounds
    • center
    • transform
    • alpha
    • backgroundColor
  • backgroundColor, alpha, isHidden, opaque 区别

    • hidden: 此属性为 BOOL 值, 用来表示 UIView 是否隐藏. 关于隐藏大家都知道就是让 UIView 不显示而已, 但是需要注意的是:

      • 当前 UIView 的所有 subview 也会被隐藏, 忽略 subviewhidden 属性. UIView 中的 subview 就相当于 UIView 的死忠小弟, 老大干什么我们就跟着老大, 同进同退, 生死与共!
      • 当前 UIView 也会从响应链中移除. 你想你都不显示了, 就不用在响应链中接受事件了.
    • alpha: 此属性为浮点类型的值, 取值范围从 0.0 到 1.0, 表示从完全透明到完全不透明, 其特性有:

      • 当前 UIViewalpha 值会被其所有 subview 继承. 因此, alpha 值会影响到 UIView 跟其所有 subview.
      • alpha 具有动画效果. 当 alpha 为 0 时, 跟 hiddenYES 时效果一样, 但是 alpha 主要用于实现隐藏的动画效果, 在动画块中将 hidden 设置为 YES 没有动画效果.
    • backgroundColoralpha(Clear Color): 此属性为 UIColor 值, 而 UIColor 可以设置 alpha 的值, 其特性有:

      • 设置 backgroundColoralpha 值只影响当前 UIView 的背景, 并不会影响其所有 subview. 这点是同 alpha 的区别, Clear Color 就是 backgroundColoralpha 为 0.0.
      • alpha 值会影响 backgroundColor 最终的 alpha. 假设 UIView 的 alpha 为 0.5, backgroundColor 的 alpha 为 0.5, 那么 backgroundColor 最终的 alpha 为 0.25(0.5 乘以 0.5).
    • opaque:

      此属性为 BOOL 值. 要搞清楚这个属性的作用, 就要先了解绘图系统的一些原理: 屏幕上的每个像素点都是通过 RGBA 值 (Red, Green, Blue 三原色再配上 Alpha 透明度) 表示的, 当纹理 (UIView 在绘图系统中对应的表示项) 出现重叠时, GPU 会按照下面的公式计算重叠部分的像素 (这就是所谓的 “合成”):

      1
      Result = Source + Destination * (1 - SourceAlpha)

      Result 是结果 RGB 值, Source 为处在重叠顶部纹理的 RGB 值, Destination 为处在重叠底部纹理的 RGB 值. 通过公式发现: 当 SourceAlpha 为 1 时, 绘图系统认为下面的纹理全部被遮盖住了, Result 等于 Source, 直接省去了计算! 尤其在重叠的层数比较多的时候, 完全不同考虑底下有多少层, 直接用当前层的数据显示即可, 这样大大节省了 GPU 的工作量, 提高了效率. (多像现在一些 “美化墙”, 不管后面的环境多破烂, “美化墙” 直接遮盖住了, 什么都看不到, 不用整治改进, 省心省力). 更详细的可以读下 objc.io 中 < 绘制像素到屏幕上 > 这篇文章.

      那什么时候 SourceAlpha 为 1 呢? 这时候就是 opaque 上场的时候啦! 当 opaque 为 YES 时, SourceAlpha 为 1. opaque 就是绘图系统向 UIView 开放的一个性能开关, 开发者根据当前 UIView 的情况 (这些是绘图系统不知道的, 所以绘图系统也无法优化), 将 opaque 设置为 YES, 绘图系统会根据此值进行优化. 所以, 如果在开发时某 UIView 是不透明的, 就将 opaque 设置为 YES, 能优化显示效率.

      需要注意的是:

    • UIViewopaqueYES 时, 其 alpha 必须为 1.0, 这样才符合 opaque 为 YES 的场景. 如果 alpha 不为 1.0, 最终的结果将是不可预料的 (unpredictable).

    • opaque 只对 UIView 及其 subclass 生效, 对系统提供的类 (像 UIButton, UILabel) 是没有效果的.

  • UITableView 代理方法呼叫的顺序:

    1. 首先执行 numberOfRowsInSection: 方法, 返回 cell 个数为 10.
    2. 其次执行的就是 heightForRowAtIndexPath: 方法, 如上图, 此时执行该方法会将所有 cell 的高度全部返回.
    3. 这时候就开始执行 cellForRowAtIndexPath: 方法, 因为当前页面只能布局 3 条 cell, 所以该方法会被执行三次. 并且, 执行一次 cellForRowAtIndexPath: 方法紧接着就会执行一次 heightForRowAtIndexPath: 方法返回 cell 高度.

    因此, 当我们从网络或者本地缓存中获取到所需数据 (array) 后, 可以直接执行代码: self.tableView reloadData, 然后就会调用 cellForRowAtIndexPath: 方法和 heightForRowAtIndexPath: 方法.

  • UIView animate 的动画选项

    • layoutSubviews
    • allowUserInteraction: 允许在动画执行过程中对用户交互进行反馈
    • beginFromCurrentState: 从当前状态继续执行另一动画
    • repeat: 让动画一直重复执行
    • autoreverse: 配合. repeat 使用, 使动画反转并持续执行
    • overrideInheritedDuration: 强制使用提交动画时使用的动画时长
    • overrideInheritedCurve: 强制使用提交动画时使用的曲线
    • overrideInheritedOptions: 强制使用提交动画时使用的选项
    • allowAnimatedContent: 允许在动画过程中直接动态改变 view 的属性 (默认不设置此值时动画过程中使用的是此 view 的快照)
    • showHideTransitionViews: 允许在动画过程中隐藏或显示正在进行动画的 view
    • curveEaseInOut: 相当于 [.curveEaseIn, .curveEaseOut] 的组合, 在开始加速和在结束动画时减速
    • curveEaseIn: 在动画开始时加速
    • curveEaseOut: 在动画结束时减速
    • curveLinear: 让动画保持匀速
    • transitionFlipFromLeft: 从左边翻转
    • transitionFlipFromRight: 从右边翻转
    • transitionCurlUp: 卷上去
    • transitionCurlDown: 卷下去
    • transitionCrossDissolve: 交叉溶解
    • transitionFlipFromTop: 从顶部翻转
    • transitionFlipFromBottom: 从底部翻转
    • preferredFramesPerSecond60: 每秒 60 帧
    • preferredFramesPerSecond30: 每秒 30 帧
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    UIView.animate(withDuration: 1, // 动画总时长
    delay: 2, // 执行动画前的延时
    options: [.curveEaseIn, .curveEaseOut], // 动画的属性, 可以使多个属性配合, 也可以是单个属性, 如果是 [] 的话则使用默认
    animations: {print(1) }, // 闭包, 动画作用的目标
    completion: nil) // 动画执行结束后的回调闭包

    UIView.animate(withDuration: 1,
    delay: 1,
    usingSpringWithDamping: 0.5, // 设置弹性动画的阻尼 (范围: 0.0~1.0), 越接近 0.0 弹性越大, 反之则越小.
    initialSpringVelocity: 0.5, // 控制动画初始速度.
    options: [.curveEaseIn],
    animations: {print(2) },
    completion: nil)

Window 操作

  • 通过 .isHidden 来控制隐藏及显示

  • window?.makeKeyAndVisible() 的作用是显示一个 UIWindow, 同时设置为 keyWindow, 并将其显示在同一 windowLevel 的其它任何 UIWindow 之上, 等效于:

    1
    2
    window?.makeKey()
    window?.isHidden = false
  • window 的显示问题

    • 对于 hidden 的 setter 方法, 最终显示的以最后执行过 .isHidden=false 的 UIWindow 为准, 且执行 .isHidden=false 之前 isHidden 的值为 true. (isHidden 如果是从 false 改为 false 的不算最后改变 UIWindow 的显示状态)
    • 对于 makeKeyAndVisible 方法, 最终显示的以最后 执行过 makeKeyAndVisible 的 UIWindow 为准.
    • 对于先后分别用 makeKeyAndVisible 方法和 isHidden 的 setter 方法, 还是先后分别用 isHidden 的 setter 方法和 makeKeyAndVisible 方法, 结局同样以最后改变显示状态的 UIWindow 为准.
  • window level 问题

    • windowLevel 数值越大的显示在窗口栈的越上面
    • 显示层的优先级 为: alert > statusBar > normal
    • 系统给 UIWindow 默认的 windowLevel 为 normal

控制器

  • 给定的导航流程只有一个导航控制器; 一个导航控制器可以管理多个视图控制器; 导航体系的每个视图控制器都有到导航控制器的引用.

  • UIPageViewControllerUINavigationController 都属于容器控制器.

  • 如果控制器被包裹在 navigationController 中, 则必须在 navigationController 中设置状态列才有效果

  • 为什么必须要添加 required init?(coder decoder: NSCoder)

    • 名称: 必要初始化器
    • 上下文: 当继承了遵守 NSCoding protocol 的类 (如 UIView, UIViewController 等) 时
    • 显性添加的条件: 当在子类定义了指定初始化器或 override 了父类的初始化器后, 那么必须显性实现 (其他情况下会隐性实现, 不需要我们管)
    • fatalError 含义: 默认的在必要初始化器中系统会给我们添加 fatalError 命令, 其含义是无条件停止执行并打印

    如果是代码实现界面, 当重写或自定义了初始化器时, 系统会自动提示我们添加此必要初始化器, 按照系统的要求进行 fix 即可

数组 & 集合 & 字典

  • 字典的类型都是 Optional, 如果要使用键找对应的值, 应该使用 if let 绑定结构进行解包
  • Swift 中有三种元组类型, array, set, dictionary. dictionary 使用键值映射模式, 即通过一个确定的键可以找到一个确定的值 (键必须唯一, 值可以重复).
  • ArrayString 不同, Array 的下标可以使 Int 类型, 但是 String 的下标必须是严格的 Index 类型.
  • 从字典中拿出来的值都是 Optional 类型, 可能有值, 可能没有值 (nil)
  • array 只可以存同一类型, tuple 可以存各种类型 (是小括号), 甚至可以存 array

Optional

  • as! 代表向下转型 (Downcasting), 让一个对象从其所属的类别转到一个衍生类别中, 是向下的. 如果很清楚转换可以进行则使用 as!, 如果不是很清楚是否可以成功则使用 as?

  • 空的含义并非是变量为 nil, Swift 中只有 Optional 类型的变量可以为 nil

  • 在普通类型的后面加上符号 ? 即可将其转换为 Optional 类型. Optional 的意义是对目前某个变量不确定是否包含量值时进行包装, 如果此变量没有量值则为 nil, 如果有赋值了则通过拆包的方式获得其具体量值. ? 可以在类型或者实例后面: 如果在类型后则表示 Optional 包装, 如果在实例后面则表示可选链调用.

  • xcodeSwift 中碰到 Optional 一定要使用 if-let 结构去解绑, 千万不要使用 !, 因为要考虑到 “” 也是一个字符串, 可能会转型失败, 使程序崩溃.

  • ! 跟在类型后面表示隐式解包可选值, 隐式解包可选值定义的属性默认值为 nil, 他表示大部分时间变量都是有值的, 或者变量一旦被设值后会一直有值. 使用时不需要可选绑定就能访问它. ! 跟在实例后面表示强制解包.

  • ? 跟在类型后面表示定义可选值, ? 跟在实例后面表示可选链调用

  • 可选链

    可选链是一个调用和查询可选属性, 方法和下标的过程, 它可能为 nil . 如果可选项包含值, 属性, 方法或者下标的调用成功; 如果可选项是 nil , 属性, 方法或者下标的调用会返回 nil . 多个查询可以链接在一起, 如果链中任何一个节点是 nil , 那么整个链就会得体地失败. 多个 ? 可以链接在一起 如果链中任何一个节点是 nil, 那么整个链就会调用失败

target 与 project 与 xcworkspace 的关系

  • 一个 xcworkspace 可以包含多个 project
  • 一个 target 只能对应一个 product
  • 一个 xcworkspace 编译时可以选择多个项目中的不同 target, 如图中可以选择 3 个 target
  • 一个文件可以映射到同一个 xcworkspace 中的多个 target 中, 可以用在开发 vip 版本与普通版本这一需求上
  • Swift Package Manager 是对应于一整个 xcworkspace 的, 即, 在同一个 xcworkspace 中的任意一个 project 中引入了第三方库, 那么在任意一个 project 中都可以使用

himg

guard 用法

  • guardif 比较起来, 代码更清楚, 更易读 (if 有一重重括号, guard 则是从上到下一行行, 如果都能经过即代表全部符合条件)
  • guard 后面跟随希望成立的条件
  • guard 必须跟随 else
  • guard 后的 else 代码必须向下执行, 不能返回来执行条件成立的结果, 可以使用 return, break, continue.
  • guard let else() 实在条件不成立时进行, if let 是在条件成立时进行.
  • guard let 的常数在 else 外面也可以使用, if let 后面的常数只可以在 else 里面使用.

switch 用法

默认必须要将所有情况 case 完全, 否则要加 default

默认只执行一个 case, 然后跳出 switch, 如果想执行多个 case, 需要用到 fallthrough 关键字

支持 整型, 浮点型, 字符串, 布尔, 区间运算符, 元组

支持元组时可使用值绑定 (value binding)

1
2
3
4
5
let request = (0,"success")
switch request {
case (0, let state): state // 被输出: success
case (let errorCode, _): "error code is \(errorCode)"
} // 涵盖了所有可能的 case, 不用写 default 了

MapKit

  • 前向地址编码 (Forward Geocoding): 将文字地址转换为全球地理坐标
  • 反向地理编码 (Reverse Geocoding): 将经纬度值转回地址
  • IOS 10 之后规定如果要使用照片库或者相机则必须要将原因列在 info.plist 文件中. 这样可以在 APP 要使用照片或相机时给用户提醒.
  • 如果要与图片选择器进行互动, 必须遵守 UIImagePickerControllerDelegateUINavigationControllerDelegate

AutoLayout

  • constrain to marginxcodeAutoLayout 的防触摸边缘功能, 选中后自动缩进 20
  • autolayout 即设置物件的长宽以及横纵坐标
    • 长宽: 通过物件长或宽与屏幕 view 的比值确定物件长宽
    • 纵横坐标: 通过居中及物件之间相对距离确认
  • Content Hugging Priority(视图抗拉伸优先级): 值越小, 越先被拉伸
  • Content Compression Resistance(抗压缩优先级): 值越小, 越先被压缩
  • autolayout 中设置百分比宽度 / 高度: 先设定等宽, 然后再属性设置面板中 multuplier 调整为百分比
  • 纯代码写视图布局时需要注意, 要手动调用 loadView 方法, 而且不要调用父类的 loadView 方法. 纯代码和用 IB 的区别仅存在于 loadView 方法及其之前, 编程时需要注意的也就是 loadView 方法.

控制转移语句

在 guard-else 中必须使用控制转移语句, 在 switch 中也可以使用 break 及 fallthrough

  • return: 在需要返回值的方法体中直接返回某值, 完全结束本方法

    himg

  • break: 结束整个 (循环) 控制流的执行, 同时可用于 switch 中

    himg

  • continue: 立刻停止本次循环, 重新开始下次循环 (代表着已经完成本次循环)

    himg

  • throw: 错误抛出

  • fallthrough: 贯穿, 在 switch 一个 case 执行完后默认会结束整个 switch, 如果需要按照顺序向下执行 case, 需要使用 fallthrough 贯穿

类方法与实例方法

  1. 类方法

    • 类方法是属于类对象的
    • 类方法只能通过类对象调用
    • 类方法中的 self 是类对象
    • 类方法可以调用其他的类方法, 不能调用对象方法
    • 类方法中不能访问成员变量
  2. 实例方法

    • 实例方法是属于实例对象的
    • 实例方法只能通过实例对象调用
    • 实例方法的 self 是实例对象
    • 实例方法中可以直接调用实例方法, 也可以调用类方法 (通过类名)
    • 实例方法中可以访问成员变量

可变参数 (可替代数组)

当调用函数的时候你可以利用可变形式参数来声明形式参数可以被传入值的数量是可变的. 可以通过在形式参数的类型名称后边插入三个点符号 (…) 来书写可变形式参数.

1
2
3
4
5
6
7
8
9
10
11
func sum(_ numbers: Int...) -> Int{
var total = 0
for item in numbers {
total += item
}
return total
}

sum([1, 2, 3]) // 报错! 只能使用下面的方式调用
sum(1, 2, 3)
sum(1)

print 函数即使用了可变参数

1
2
3
4
5
6
7
/// - Parameters:
/// - items: Zero or more items to print.
/// - separator: A string to print between each item. The default is a single
/// space (`" "`).
/// - terminator: The string to print after all items have been printed. The
/// default is a newline (`"\n"`).
public func print(_ items: Any..., separator: String = "", terminator: String ="\n")

元类型

表示 任意 概念的词有 Any, AnyObject, AnyClass.

1
typealias AnyClass = AnyObject.Type

AnyClass 即元类型, 表示类型的类型

1
2
3
4
5
6
7
8
class A {
class func method() {
print("Hello")
}
}

let typeA: A.Type = A.self // typeA 是类型 A 本身, 与 A 等价
typeA.method() // 因此可以直接呼叫类型方法

匹配模式

Swift 中模式有

  • 通配符模式 (Wildcard Pattern)
  • 标识符模式 (Identifier Pattern)
  • 值绑定模式 (Value-Binding Pattern)
  • 元祖模式 (Tuple Pattern)
  • 枚举 Case 模式 (Enumeration Case Pattern)
  • 可选模式 (Optional Pattern)
  • 类型转换模式 (Type-Casting Pattern)
  • 表达式模式 (Expression Pattern)

计算属性与存储属性

swift 中的属性一共有两种, 存储属性与计算属性, 在计算属性中的 set 与 willSet/didSet 永远不能在一起

计算属性

计算属性可设置 get, set

  • 计算属性不是属性是方法
  • 计算属性在内存中是找不到变量的映射地址的
  • 方法中嵌套了两个函数
    • get: 一定要有返回值, 返回的就是计算的结果
    • set: 一定不能有返回值, 是对某个属性赋值, set 函数有一个自带的形参 newValue 携带的就是为某个属性符的值
  • 计算属性的 get 和 set 方法可以不同时出现, 如果只是对某个属性赋值而不需要返回, 那么只需要写 set 方法就可以, 如果某个属性是只读的不能修改, 那么只需要写 get 方法
  • 在 set 中可以使用 self.** = newValue, 这样就相当于让计算属性拥有了存储属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var sum :Int? {
// 获取值
get{
return self.sum
}
// 设置值
set{
self.sum = newValue
}
}

// 也可以这样简写 get 方法
var sum :Int? {
return self.sum
}

@ 命令

  • @_exported import

    • 当依赖引用了另一个依赖, 除非标明 @exported 关键字, 否则只使用此依赖中的属性及对象不能使用此依赖的依赖中的对象. 如果要使用, 需要手动再次 import 依赖的依赖
    • 全局引入, 在一个文件中引入了这些类后, 全局都可以使用不用再次写 import
  • discardableResult, 一个函数有返回值, 但是没有被使用时, 默认会警告, 如果加上此关键字则不会警告

    1
    2
    3
    4
    5
    6
    7
    8
    /* 进行定义 */
    @discardableResult
    func add(a: Int, b: Int) -> Int {
    return a + b
    }

    /* 进行使用, 虽然没有使用结果值, 但是不会报错 */
    add(a: 1, b: 2)

存储属性

存储属性的值直接存储在内存地址中

属性观察者

属性观察者是对属性的观察, 一旦属性的值发生变化, 则属性观察者的 didSet/willSet/Set 将会被触发并调用其内的方法.

与其定义相同, 属性观察者即可以观察计算属性, 又可以观察存储属性. 不过有一点: 计算属性中不允许出现 didSetwillSet, 只能有 set

willSet 与 didSet 属性观察者, 指在当前类型内对属性进行监测, 并作出响应. 在初始化器中设置属性值不会触发 willSetdidSet

  • willSet: 会传递新值, 默认叫 newValue
  • didSet: 含有一个设置新值之前的旧值, 默认叫 oldValue.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Test {
var test1: String {
willSet {
print("willSet")
}
didSet {
print("didSet")
}
}

var test2 = "2" {
willSet {
print("willSet - test2")
}
didSet {
print("didSet - test2")
}
}

init() {
test1 = ""
}
}

var t1 = Test()
t1.test1 = "3"
print(t1.test1)

t1.test2
t1.test2 = "4"

class 与 static 区别

  • class 不能修饰存储属性, static 可以修饰存储属性
  • class 修饰的计算属性可以被重写, static 修饰的计算属性不能被重写
  • class 修饰的计算属性被重写时, 可以使用 static 转为静态属性
  • class 修饰的类方法可以被重写, static 修饰的静态方法不能被重写
  • class 修饰的类方法被重写时, 可以使用 static 转为静态方法
  • class 只能在类中使用, static 可以在类, 结构体, 枚举中使用

影响编译时间的因素

在 Build Settings ➔ Swift Compiler - Custom Flags ➔ Other Swift Flags 中添加如下代码可查看耗时编译代码

1
2
3
4
/// <limit> 为 warning 的编译时间阈值
-Xfrontend -warn-long-function-bodies=<limit>

-Xfrontend -warn-long-expression-type-checking=<limit>

himg

影响因素如下:

  • 硬件

  • 配置

  • 代码书写方式

    • 使用 + 拼接可选字符串会极其耗时

      1
      2
      3
      4
      5
      /* 优化前 372ms */
      let finalResult = (dbWordModel?.vocabularyModel?.justSentenceExplain ?? "") +"<br/>"+ (dbWordModel?.vocabularyModel?.justSentence ??"")

      /* 优化后 20ms */
      let finalResult = "\(dbWordModel?.vocabularyModel?.justSentenceExplain ??"")<br/>\(dbWordModel?.vocabularyModel?.justSentence ?? "")"
    • 可选值使用?? 赋默认值再嵌套其他运算会极其耗时.

      1
      2
      3
      4
      5
      6
      7
      /* 优化前 372 ms */
      let finalResult = (dbWordModel?.vocabularyModel?.justSentenceExplain ?? "") +"<br/>"+ (dbWordModel?.vocabularyModel?.justSentence ??"")

      /* 优化后 63 ms */
      guard let dbSentenceExp = dbWordModel?.vocabularyModel?.justSentenceExplain,
      let dbSentence = dbWordModel?.vocabularyModel?.justSentence else {return}
      let finalResult = "\(dbSentenceExp)<br/>\(dbSentence)"
    • 将长计算式代码拆分 最后组合计算

      1
      2
      3
      4
      5
      6
      7
      /* 优化前 736 ms */
      let totalTime = (timeArray.first?.float()?.int ?? 0) * 60 + (timeArray.last?.float()?.int ?? 0)

      /* 优化后 22 ms */
      let firstPart: Int = (timeArray.first?.float()?.int ?? 0)
      let lastPart: Int = (timeArray.last?.float()?.int ?? 0)
      let totalTime = firstPart * 60 + lastPart
    • 与或非和 >=,<=,== 逻辑运算嵌套 Optional 会比较耗时

      1
      2
      3
      4
      5
      6
      7
      /* 优化前 10420 ms */
      let finalResult = (dbWordModel?.vocabularyModel?.justSentenceExplain ?? "") +"<br/>"+ (dbWordModel?.vocabularyModel?.justSentence ??"")

      /* 优化后 21 ms */
      let leftValue: CGFloat = homeMainVC?.scrollview.contentOffset.y ?? 0
      let rightValue: CGFloat = (homeMainVC?.headHeight ?? 0.0) - (homeMainVC?.ignoreTopSpeace ?? 0.0)
      if leftValue == rightValue {...}
    • 手动增加类型推断会降低编译时间.

      1
      2
      3
      4
      5
      6
      7
      /* 优化前 21 ms */
      let leftValue = homeMainVC?.scrollview.contentOffset.y ?? 0
      let rightValue = (homeMainVC?.headHeight ?? 0.0) - (homeMainVC?.ignoreTopSpeace ?? 0.0)

      /* 优化后 16 ms */
      let leftValue: CGFloat = homeMainVC?.scrollview.contentOffset.y ?? 0
      let rightValue: CGFloat = (homeMainVC?.headHeight ?? 0.0) - (homeMainVC?.ignoreTopSpeace ?? 0.0)

感悟

  • iOS 开发账户等级

    • 个人

      • 费用: 99 美元 / 年
      • 创建 Apple ID: 需要
      • App Store 上架: 是
      • 最大 udid 支持数: 每种设备各 100 台, 续费时可以从新编辑
      • 协作人数: 1 人 (开发者自己)
      • 该账号在 App Store 销售者只能显示个人的 ID
    • 公司
      费用: 99 美元 / 年

      • 创建 Apple ID: 需要
      • App Store 上架: 是
      • 最大 udid 支持数: 每种设备 100 台, 每次续费时可更新
      • 协作人数: 多人
      • 该账号该账号在 App Store 销售者可以显示类似 Studios, 或者自定义的团队名称, 例如: Game omiga
      • 分 4 种管理级别权限:
        • Admin Legal 权限 (Agent 账号) : 超级管理员. 可以管理开发者和管理 app store 中的应用.
        • Admin 权限: 管理员, 可以管理开发者. 添加测试机子和管理团队证书.
        • Member 权限: 是普通开发者. 只能下载证书和使用证书
        • No Access 权限: 没有相应的权限.
      • 公司账号可以自己定义一定数量的开发者子账号, 不过只能由主账号来执行提交, 发布等操作.
      • 需要填写公司的邓百氏编码 (D-U-N-S Number)
    • 企业

      • 费用: 299 美元 / 年
      • 创建 Apple ID: 需要
      • App Store 上架: 否 (该账号下的 app 不能发布到 App Store 中) 即该账号开发应用不能发布到 App Store, 直接扫码下载, 苹果的 iOS 设备 UDID 数量不限制.
      • 最大 udid 支持数: 不限制
      • 协作人数: 多人
      • 企业开发者不能通过 appstore 途径发 app, 但是可以直接无上限的分发 app (in-house 发布)
      • 需要填写公司的邓百氏编码 (D-U-N-S Number)
      • 申请难度: 难

      企业账号不能上线应用到 App Store, 适用于那些不希望公开发布应用的企业且还需要大量安装使用的公司. 企业开发者账号打包的 ipa 可以在 “蒲公英”, “fir.im”, ” 公孙测 “上发布, 之后生成链接供下载, 针对企业级 ipa, 需要在 iPhone 的 “设置”-“通用”-“设备管理” 里面信任该企业证书, app 在手机上才能正常使用.

  • iOS 测试与分发渠道

    • 测试渠道
      • Personal Team
      • Ad Hoc
      • TestFlight
    • 分发渠道
      • App Store
      • In-House: 企业证书才能使用本项
      • Custom Apps
  • 持久化存储时如果要频繁写入或读取最好使用 CoreData 或其他数据库而不是使用文件以减少 I/O 次数

    • OS Cache: 性能最好的一层, 使用 logical I/O, 由于是储存在内存中, 所以 I/O 操作很高效 (使用 logical I/O)

    • Disk Cache: 磁盘储存的物理映射. (使用 physical I/O)

    • Permanent Storage: 最终用于持久化数据的介质, 对于 iOS 来说, 就是闪存 (使用 physical I/O)

      缓存有以上几个层级, 对于 app 来说, 离 cpu 越近的 cache, 性能就越好, 但同时我们也希望 cache 能确实地落在磁盘中. 数据在内存当中时对于 app 而言速度是最快的, 也没有任何的 IO 开销, 但是当我们需要将数据从内存一层一层地注入到闪存时, 就需要注意 IO 开销了.

      himg

      面是单单的更新 plist 操作, 调用了系统的 writeToFile 函数, 最后再调用栈上系统为我们调用了 fsync, 所以数据就会直接由 OS cache 层一直写入到 Disk cache 层, 并从 OS cache 层被清除, 如果在写入后我们仍然要继续使用数据, 就会失去了 OS cache 这一层的缓存, 而需要重新开启 IO 去磁盘中读取数据

      因此使用 plist 这类文件来储存需要频繁读写的数据, 是非常不合适的

  • 程序派发的目的是为了告诉 CPU 需要被调用的函数在哪里, 进而调用确定的内存地址, 编译型语言有三种派发方式:

    • 直接派发: c, 效率最高, 不灵活, 不能继承

    • 函数表派发: java, 通过数组保存函数位置, 调用时查找内存地址对应的函数并调用

      himg

    • 消息派发: objc, 运行时会顺着类的继承关系向上查找应该被调用的函数, 第一次运行查找很慢, 之后有了缓存后就和函数表派发一样快了

      himg

      Swift 使用了以上三种派发方式, 在不同的情况下会执行不同的派发方式 (Swift 的默认都会将派发方式最优化为直接派发). 具体如下图所示

      himg

  • UITableView 的几个交互属性

    • isDragging: 是否正在被手指拖动 (必须手指与屏幕接触, 需要滑动一小段距离才能使此值设为 true)
    • isZooming: 当前 tableView 是否正在缩放 (放大或缩小)
    • isFocused: 是否是当前 UIScreen 的 focusedView
    • isTracking: 是否被手指按住以开始一个滑动事件 (只要手指放上哪怕没有滚动也会为 true 值)
    • isDecelerating: 是否在惯性滑动, 即手指已经离开屏幕但是 scrollView 仍然在滚动的情况. 因此本属性与 isDragging 不可能同时为 true
    • isDecendant(of: UIView): 是否是某 viewsubview
    • isZoomBouncing: 是否正在缩放的惯性动画中
    • isExclusiveTouch: 当设置了 isExclusiveTouch = true 的控件 (View) 是事件的第一响应者, 那么到你的所有手指离开屏幕前, 其他的控件 (View) 是不会响应任何触摸事件的. 如果设置类别较多, 可直接设置全局 UIView.appearance().isExclusiveTouch = true
    • isFirstResponder: 是否是第一响应者
  • UITableView 的行高

    默认情况下如果我们实现了 cellForRowAtIndexPath 方法, 那么如果有 500 行, 那么在 reloadData 的时候就会调用高度方法 heightForRowAt 500 次.

    最好的优化办法是如果 cell 高度都统一, 那么就直接使用 tableView.rowHeight = .. 来确定高度, 这样不会调用高度方法那么多次.

    如果我们的 tableView 含有不同的 cell 高度, 那么可以使用自动行高来将高度计算推迟到滚动时发生

    1
    2
    3
    4
    5
    6
    7
    8
    // ios 10 及以下

    tableView.estimatedRowHeight = 100.0
    tableView.rowHeight = UITableView.automaticDimension

    // ios 11 及以上

    tableView.rowHeight = UITableView.automaticDimension

    然后 cell 就会依据内部的所有空间自动计算行高

    另外设置 func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { } 是没有意义的

    或者直接通过代理方法实现:

    1
    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { }

    自动行高与非自动行高有如下区别:

    1. 在禁用 cell 预估高度的情况下, 系统会先把所有 cell 实际高度先计算出来, 也就是先执行 tableView:heightForRowAtIndexPath: 代理方法, 接着用获取的 cell 实际高度总和来参与计算 contentSize, 然后才显示 cell 的内容. 在这个过程中, 如果实际高度计算比较复杂的话, 可能会消耗更多的性能.

    2. 在使用 cell 预估高度的情况下, 系统会先执行所有 cell 的预估高度, 也就是先执行 tableView:estimatedHeightForRowAtIndexPath: 代理方法, 接着用所有 cell 预估高度总和来参与计算 contentSize, 然后才显示 cell 的内容. 这时候从下往上滚动 tableView, 当有新的 cell 出现的时候, 如果 cell 预估值高度减去实际高度 (实际高度根据 cell 中所持有控件约束计算得出) 的差值不等于 0, contentSize 的高度会以这个差值来动态变化, 如果差值等于 0, contentSize 的高度不再变化. 在这个过程中, 由之前的所有 cell 实际高度一次性先计算变成了现在预估高度一次性先计算, 然后实际高度分步计算. 正如苹果官方文档所说, 减少了实际高度计算时的性能消耗, 但是这种实际高度和预估高度差值的动态变化在滑动过快时可能会产生 跳跃 现象, 所以此时的预估高度和真实高度越接近越好 (为了解决这种问题可以使用字典缓存所有的预估高度然后在代理方法中返回当前 cell 的高度).

  • UIScrollView 如果被设置 contentOffset 或者 setContentOffset() 的话, 会触发其 scrillViewDidScroll 代理方法

  • contencontentSizeviewDidLoad 中设置后, 如果之后没有进行边距与宽高的约束的话是起作用的, 在 viewdidload 中设置是为了方便, 更严格的应该全部使用约束

  • CaseIterable 作用是为了 allCases, 只适用于简单的枚举, 如果有关联值则不能实现, 可以在枚举中添加 static var allCases 来实现

    1
    2
    3
    4
    5
    6
    7
    8
    enum MartialStatus : CaseIterable {
    case single
    case married(spouse: String)

    static var allCases: [MartialStatus] {
    return [.single, .married(spouse: "Leon")]
    }
    }
  • UITableViewCellcontentViewbgColor 位于 bgColor 之上

  • UIScrollView 如果没有被正确释放, 并且其代理方法中会发送 Notification 的话, 那么可能会触发各种灵异事件.

  • didset 在初始化时不会被调用, 包括在直接设置的 label.text = "123" 也不会被调用, 只有设置 label = UILabel(title: "123") 才会调用 labeldidset

  • Void 是空元组的意思, 与 () 等价, () -> () 表示一个不接受任何类型参数且不返回任何除空元组以外的任何元素, e.g. func abc() -> Void { } 表示不需要返回值或可返回一个空元组的一个方法. 与 func abc() -> Void { return () } 等价

    Void 是空元组, 但是不是 nil, Void 在内存有空间, 但是 nil 是完全在内存中没有空间的, 类似的还有 let str = "", str 是空字符串, 但是在内存中是有空间的, 与 nil 的含义截然不同

    在 RxSwift 中, 我们可以定义一个 void 类型的监听序列, 表示他不接受任何类型参数, 只进行一个最简单的信号传递. 在需要传递信号的时候发送空元组即可, e.g.

    1
    2
    3
    let test = PublishSubjec<Void>() // 创建一个只接受空元组的 PublishSubject 可监听序列
    test.subscirbe(onNext: { print("123") }) // 可监听序列 test 订阅一个监听者, 这个监听者会响应事件 (事件为打印 123)
    test.onNext(()) // 使 test 自产生一个空元组元素, 此时由于在上一步中监听序列已经订阅了一个监听者, 因此会打印出 123
  • 时间戳: 格林威治时间 1970 年 01 月 1 日 00 时 00 分 00 秒起至现在的总秒数, 所以所有时区的时间戳都是一样的, 但是同样的时间戳在不同的时区会显示不同的日期时间, 因为中国是 UTC + 8 时区, 因此在北京时间 1970-01-01 08:00:00 的时候, 时间戳为零, 一个普通的时间戳如果放到中国时区来计算的话就会以 8:00 为基准, 计算差值

    • NSDate: 网络时间, 属于 Foundation (单位秒, 保留到微秒)
    • CFAbsoluteTimeGetCurrent(): 网络时间, 属于 CoreFoundatio(单位秒, 保留到微秒, 默认为 the reference date (epoch) is 00:00:00 1 January 2001) 相当于 NSDate().timeIntervalSinceReferenceDate
    • mach_absolute_time(): 内建时钟 (单位秒, 保留到纳秒) , 不会因为外部时间变化而变化 (例如时区变化, 夏时制, 秒突变等), 系统重启后 CACurrentMediaTime() 会被重置.
    • CACurrentMediaTime(): 内建时钟, 属于 QuartzCore (单位秒, 保留到纳秒) , 不会因为外部时间变化而变化 (例如时区变化, 夏时制, 秒突变等), 系统重启后 CACurrentMediaTime() 会被重置.
  • 类的实例被称为对象, 除了类的实例都不能被称为对象. 实例很宽泛, class 的实例和 struct 的实例都可以被简称为 实例.

  • let 修饰的是 不可变变量, var 修饰的是 可变变量, 一个变量可不可变完全就根据 let 与 var 定义的

  • class, struct, enum, protocol 中的变量被称为属性. 变量很宽泛, 所有属性都是变量, 但是全局变量不是属性 (因为不属于任何 class, struct 等)

  • isKindOfClass 不仅用来确定一个对象是否是一个类的成员, 也可以用来确定一个对象是否派生自该类的类成员, 而 isMemberOfClass 只能做到第一点

  • printdebugPrint 区别: debugPrint 输出的信息更全面, 放在网络请求中尤为明显

  • 三目运算符格式   问题 : 答案一 : 答案二   如果问题成立, 使用答案一, 如果问题不成立, 使用答案二

  • let path = Bundle.main.path(forResource: String, ofType: String) 会得到一个全局的文件路径, 即, 哪怕这个文件不在根目录下也会被搜索到, 非常强大. 同时, 也要保证同一个项目中不要有两个同名文件, 这样会导致冲突

  • 不同的类一定要用不同的文件

  • ?? 是空合并运算符, 即会检查前面的属性是否有一个值, 如果没有会使用 ?? 之后的默认值

  • omnigraffle 做标注图美化方法

    • 加阴影, 增加高级感
    • 截图方角变圆角, 增加圆润感
    • 箭头使用直角箭头并将直角倒角为圆角
  • 在遍历结构中要适当地加入 break, 这样遇到符合条件会自动停止, 而不会一直无休止运行下去. 可以节省时间

  • Linux 作为操作系统, ApacheNginx 作为 Web 服务器, MySQL 作为数据库, PHP/Perl/Python 作为服务器端脚本解释器. 由于这四个软件都是免费或开放源码软件 (FLOSS), 因此使用这种方式不用花一分钱 (除开人工成本) 就可以建立起一个稳定, 免费的网站系统, 被业界称为 “LAMP“或 “LNMP” 组合.

  • @IBOutlet@IBAction 区别

    • IBOutlet 只是将标签等不需要与使用者互动的原件进行连接, 通过代码控制界面
    • IBAction 连接的是按钮一样的与使用者互动的原件, 通过界面控制代码
  • 为什么 IBOutlet 后面有 !, 类别中的属性被定义后一定要被初始化 (有值), 但如果是 Optional 类型则可以不初始化, 使用! 表示不用进行解包一定会有值.

  • addTarget 方法和 @IBAction 链接的意义是相同的, 都是通过用户交互事件来执行相关方法.

  • 事件区别:

    • touch up inside 触发: 手指按下, 在按钮区域抬起
    • touch up outside 触发: 手指按下, 在按钮区域外抬起
    • touch down 触发: 手指按下
  • 在方法中 indexPath 可翻译为某路径, indexPath.row 则翻译为某路径所在的行数

  • row 属于数据, cell 属于视图, 表视图控制器通过数据源和代理方法将两者关联在一起.

  • for-in 循环结构中, 如果 in 后跟的是一个范围, 则 index 获取到的是从左到右依次遍历到的范围数; 如果 in 后跟的是一个集合, 则变量 index 会自动遍历集合中的所有元素. in 前面的参数为捕获参数, 每次从 in 后参数中捕获的元素都会赋值给他.

  • throws 需要配合 try, do, catch 使用

  • playground

    • 可以多页面, 使用 ⌘ 1 显示多页面
    • 可以显示 Markdown 语法, 使用 /*: 配合 Markdown 语法效果更佳
  • swift 高级函数 filter map reduce

    • filter 过滤筛选 (只留下符合闭包内条件的元素): evenArray = numbers.filter({ $0 % 2 == 0 }) (过滤结果为数组)
    • map 映射 (将所有元素映射出来以供接下来的运算): arrayInThreeTimes = numbers.map({ $0 * 3}) (映射结果为数组)
    • reduce 累加 / 乘运算 (将所有元素进行累叠): sum = numbers.reduce(0, { $0 + $1 }) 或者简写为 sum = numbers.reduce(0, +)
  • map 不能将元素映射成可选类型, flatmap 可以

  • 真实主机, > 云主机, >vps> 虚拟主机

    • 云主机: 就像是一栋大楼, 楼房中公寓的墙壁都是打穿了的是一个超大的空间, 你需要多大的空间, 就用隔离板给你隔离出多大的空间, 在空间内是一个独立, 空间外面是完全不影响的, 如果你突然觉得空间不够了, 那么还可以把隔离板移动来扩大空间, 具有很好的扩展性.
    • VPS: 同样是一大套房分隔出来的 N 个房间, 但是房间里面有厕所, 有洗衣机, 这些你是独立的, 你还可以安装其他家电, 就想是一个独立的小公寓一样, 房间与房间之间没有任何的共享资源, 都是独立的.
    • 虚拟机: 是一套房, 隔离出来了 N 多个房间, 房间只有基本的床, 凳子等私人的东西, 然而厕所, 厨房, 洗衣机等等这些都是公共使用了, 相互之间使用是有影响的.
  • shortcutItem 快速启动菜单思路分析:

    • 在主页面的 viewDidload 方法中创建相关的菜单动作, 然后注入到系统管理的 APP 中 UIApplication.shared.shortcutItems = [shortcutItem1, shortcutItem2, shortcutItem3]
    • 创建一个 shortcutItem 实例, 从两个地方抓取动作信息赋值给他.
      • 从应用启动的方法中抓取场景创建信息的方法中的快捷动作 application(_:configurationForConnecting:options:)
      • 从后台挂起状态快捷动作激活的方法中抓取动作信息 windowScene(_:performActionFor:completionHandler:)
    • 然后在 becomeActive 方法中对 shortcutItem 存的信息进行分析, 进而执行相关方法.
  • 物件导向可以指 swift 中的 class, struct, enum, 一般只指代 class 所描述的, 如果是 struct 或者 enum 描述的最好不用物件来向别人表达. 三者的使用选择需要经过大量实战练习之后才能掌握, 开始阶段先用 class, 等过几年融会贯通之后再尝试用其他.

  • objc 的分类和扩展对应到 swift 中只有 extension 了, extension 中增加的方法和属性在整个项目中起作用. extension 可以做的工作:

    • 定义实例方法和类型方法
    • 提供新的初始化方法
    • 定义和使用新的内嵌类型
    • 创建已存在 Protocolextension, 为 Protocol 提供可选方法
  • 泛型 (generic) 可以使我们在程序代码中定义一些可变的部分, 在运行的时候指定. 使用泛型可以最大限度地重用代码, 保护类型的安全以及提高性能. 泛型可以将类型参数化, 提高代码复用率, 减少代码量.

  • Swift 中, 可选型是为了表达一个变量为空的情况, 当一个变量为空, 他的值就是 nil

  • 一个函数如果可以以某一个函数作为参数, 或者是返回值, 那么这个函数就称之为高阶函数, 如 map, reduce, filter

  • copy-on-write

    值类型 (比如: struct), 在复制时, 复制对象与原对象实际上在内存中指向同一个对象, 当且仅当修改复制的对象时, 才会在内存中创建一个新的对象, 为了提升性能, Struct, String, Array, Dictionary, Set 采取了 Copy On Write 的技术 比如仅当有 “写” 操作时, 才会真正执行拷贝操作 对于标准库值类型的赋值操作, Swift 能确保最佳性能, 所有没必要为了保证最佳性能来避免赋值

  • 比较 Swift 和 OC 中的初始化方法 (init) 有什么不同?

    swift 的初始化方法, 更加严格和准确, swift 初始化方法需要保证所有的非 optional 的成员变量都完成初始化, 同时 swfit 新增了 conveniencerequired 两个修饰初始化器的关键字 convenience 只提供一种方便的初始化器, 必须通过一个指定初始化器来完成初始化 required 是强制子类重写父类中所修饰的初始化方法

  • 函数重载: 函数名称相同, 函数的参数个数不同, 或者参数类型不同, 或参数标签不同, 返回值类型与函数重载无关. swift 支持函数重载

  • 延迟存储属性 lazy var

    默认情况下建立一个类别的实例时, 其内所有属性必须被初始化 (property 有内容或是 Optional), 不过有些属性需要大量计算, 因此可以使用懒加载方式, 构建类别实例的时候不进行初始化, 在调用时系统才进行计算. (类似 OC 中的懒加载)

    • lazy 属性必须是 var, 不能是 let
    • let 必须在实例对象的初始化方法完成之前就拥有值
    • 如果多条线程同时第一次访问 lazy 属性, 无法保证属性只被初始化 1 次
    • lazy 不能用于计算属性
    • 全局属性默认已经是 lazy 了
    • 如果一个属性依靠同一个类别的另一个属性, 则必须使用 lazy, 因此如果不使用 lazy 的话另外两个属性在类别被初始化之前都没有正确被设定, 不能直接拿来计算.

    UIViewController 的属性会在 viewDidLoad 之前被初始化, 因此如果对 UIViewController 的属性使用 lazy 关键字, 那么在 viewDidLoad 时该属性不会被初始化, 直到此属性被调用 (set 或者 get 都是被调用)

    himg

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Avatar {
    static let defaultSmallSize = CGSize(width: 64, height: 64)

    lazy var smallImage: UIImage = {
    let size = CGSize(
    width: min(Avatar.defaultSmallSize.width, self.largeImage.size.width),
    height: min(Avatar.defaultSmallSize.height, self.largeImage.size.height)
    )
    return self.largeImage.resizedTo(size)
    }()
    var largeImage: UIImage

    init(largeImage: UIImage) {
    self.largeImage = largeImage
    }
    }
  • try, catch, do, throw

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // 创建字典
    let user:[String:Any] = ["name":"yang", "age":20]

    enum UserError:Swift.Error{
    case noKey(message:String) // key 无效
    case ageBeyond // 年级超出
    }

    func testAction() throws {
    guard let name = user["name"] else { throw UserError.noKey(message: "没有此人") }
    guard let value = user["age"] else { throw UserError.noKey(message: "年龄无效") }

    let ageValue = value as! Int

    guard ageValue > 100 else {throw UserError.ageBeyond}
    }

    //try testAction() // 在此处直接 try 不 catch 会抛异常报错

    func getUser() throws {
    do {
    try testAction()
    } catch let UserError.noKey(message){
    print("error:\(message)")
    } catch UserError.ageBeyond {
    print("年龄不合适")
    } catch {
    print("other error")
    }
    }

    try getUser() // 在此处 try 不需要 catch, 因为 getUser() 中已经根据异常做了处理

参考