├── .DS_Store ├── Assets ├── git-hook-output.png ├── swift-alipay.png ├── swift-wechat.png └── xcode-new-project.png ├── LICENSE ├── README.md └── resource ├── .DS_Store ├── 10 在 Swift图表中使用Foudation库中的测量类型.md ├── 12 iOS16 中的三种新字体宽度样式.md ├── 13 如何在SwiftUI中创建条形图.md ├── 14 SwiftUI中的水平条形图.md ├── 15 在iOS 16中用SwiftUI Charts创建一个折线图 .md ├── 16 在iOS16中用SwiftUI图表定制一个线图.md ├── 17 Sendable 和 @Sendable 闭包——代码实例详解.md ├── 18 SwiftUI布局协议-Part2.md ├── 19 Swift 中的async:await ——代码实例详解.md ├── 19.md ├── 20 Swift AsyncSequence —— 代码实例详解.md ├── 21 Swift AsyncThrowingStream 和 AsyncStream ——— 代码实例详解.md ├── 22 Swift 中的 MainActor使用和主线程调度.md ├── 22 SwiftUI布局协议-Part2.md ├── 25 Flutter 多引擎渲染,在稿定 App 的实践.md ├── 25 Swift AsyncThrowingStream 和 AsyncStream ——— 代码实例详解.md ├── 26 Flutter 多引擎渲染,在稿定 App 的实践(二):原理篇.md ├── 26 Swift 中的 MainActor使用和主线程调度.md ├── 27 Flutter 多引擎渲染,在稿定 App 的实践(三):躺坑篇.md ├── 27 Swift中的Actors 使用以如何及防止数据竞争.md ├── 37 在 SwiftUI 中创建一个环形 Slider.md ├── 38 使用HSB而不是RGB来定义颜色.md ├── 39 Swift 单元测试入门.md ├── 40 在 Swift 中使用 async let 并发运行后台任务.md ├── 41 当今Swift包中的二进制目标.md ├── 42 如何使用 SwiftUI 中新地图框架 MapKit.md ├── 43 实现模块化应用的本地化.md ├── 44 使用 Swift 的并发系统并行运行多个任务.md ├── 45 使用 Swift Package 插件生成代码.md ├── 46 使用 SwiftUI 的 Eager Grids.md ├── 47 项目中第三方库并不是必须的.md ├── 48 在Swift中编写脚本:Git Hooks.md ├── 49 逐步实现基于源码的 Swift 代码覆盖率.md ├── 50 iOS-16-主要功能和提升.md ├── 51 Swift 中的动态成员查找.md ├── 52 Swift 中的热重载.md ├── 53 Swift中的幻象类型.md ├── 54 Swift中的类型占位符 .md ├── 55 SwiftUI 之 HStack 和 VStack 的切换.md ├── 56 SwiftUI 中的自定义导航.md ├── 57 SwiftUI 状态管理系统指南.md ├── 58 WWDC 23 之后的 SwiftUI 有哪些新功能.md ├── 59 使用 Swift 6 语言模式构建 Swift 包.md ├── 60 如何使用 Swift 中的 GraphQL.md ├── 61 SwiftUI 在 WWDC 24 之后的新变化.md ├── 62 在 SwiftUI 中 accessibilityChildren 视图修饰符的作用.md ├── 63.Swift 并发中的任务让步Yielding和Debouncing.md ├── 64.「实战指南 」Swift 并发中的任务取消机制.md ├── 7个大型iOS项目的Xcode快捷方式.pdf ├── 8 SwiftUI 锁屏小组件.md ├── 9 SwiftUI布局协议-Part1.md ├── Sourcery的Swift Package命令行插件 ├── .DS_Store ├── Sourcery的Swift Package命令行插件.md └── assets │ ├── sourcery-command-cli.mp4 │ └── sourcery-command-xcode.mp4 └── 用SwiftLint保持Swift风格一致 ├── .DS_Store ├── assets ├── .DS_Store ├── disable-swiftlint-code.png ├── extra-blank-space-warning.png ├── fix-before-exclude-codable.png ├── identifier-name-violations.png ├── identifier-names-with-underscores.png ├── integrate-swiftlint-xcode.png ├── swiftlint-fix-automatically-xcode.png ├── todo-causes-warning.png └── todo-no-warning-yml-file.png └── 用SwiftLint保持Swift风格一致.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/.DS_Store -------------------------------------------------------------------------------- /Assets/git-hook-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/Assets/git-hook-output.png -------------------------------------------------------------------------------- /Assets/swift-alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/Assets/swift-alipay.png -------------------------------------------------------------------------------- /Assets/swift-wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/Assets/swift-wechat.png -------------------------------------------------------------------------------- /Assets/xcode-new-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/Assets/xcode-new-project.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SwiftCommunityRes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /resource/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/.DS_Store -------------------------------------------------------------------------------- /resource/10 在 Swift图表中使用Foudation库中的测量类型.md: -------------------------------------------------------------------------------- 1 | # 在 Swift 图表中使用 Foudation 库中的测量类型 2 | 3 | 4 | ## 前言 5 | 6 | 在这篇文章中,我们将建立一个条形图,比较基督城地区自然散步的持续时间。我们将使用今年推出的新的Swift `Charts` 框架,并将看到如何绘制默认不符合 `Plottable` 协议的类型的数据,如 `Measurement`。 7 | 8 | ## 定义图表的数据 9 | 10 | 让我们先定义一下要在图表中展现的数据。 11 | 12 | 我们声明了一个包含标题和步行时间(小时)的 `Walk` 结构体。我们使用 `Foundation` 框架中的测量类型[Measurement](https://developer.apple.com/documentation/foundation/measurement "Measurement")和单位类型[UnitDuration](https://developer.apple.com/documentation/foundation/unitduration "UnitDuration")来表示每次步行的时间。 13 | 14 | ```swift 15 | struct Walk { 16 | let title: String 17 | let duration: Measurement 18 | } 19 | ``` 20 | 21 | 我们在数组 `works` 中存储要在图表中显示的数据。 22 | 23 | ```swift 24 | let walks = [ 25 | Walk( 26 | title: "Taylors Mistake to Sumner Beach Coastal Walk", 27 | duration: Measurement(value: 3.1, unit: .hours) 28 | ), 29 | Walk( 30 | title: "Bottle Lake Forest", 31 | duration: Measurement(value: 2, unit: .hours) 32 | ), 33 | Walk( 34 | title: "Old Halswell Quarry Loop", 35 | duration: Measurement(value: 0.5, unit: .hours) 36 | ), 37 | ... 38 | ] 39 | ``` 40 | 41 | ## 在图表中使用测量值 42 | 43 | 尝试直接在图表中使用测量值 44 | 45 | 让我们定义一个 `Chart`,并将 `walks` 数组作为数据参数传递给它。因为我们知道我们的`walk` 标题是唯一的,所以我们可以直接使用它们作为 `id`,但你也可以将你的数据模型改为 `Identifiable`。 46 | 47 | ```swift 48 | Chart(walks, id: \.title) { walk in 49 | BarMark( 50 | x: .value("Duration", walk.duration), 51 | y: .value("Walk", walk.title) 52 | ) 53 | } 54 | ``` 55 | 56 | 注意,因为 `Measurement` 没有遵守 `Plottable` 协议,我们会得到一个错误:「Initializer 'init(x:y:width:height:stacking:)' requires that 'Measurement' conform to 'Plottable'」 57 | 58 | `BarkMark` 的初始化器期望收到一个用于 x 和 y 的 `PlottableValue` 参数。而且 `PlottableValue` 的值类型必须符合 `Plottable` 协议。 59 | 60 | 我们有几个选择来解决这个错误。我们可以提取测量值的 `value`,它是一个 `Double` 类型,它是默认符合 `Plottable` 的,我们可以扩展具有 `Plottable` 一致性的 `Measurement`,或者我们可以定义一个包装了测量的类型并使其符合 `Plottable` 协议。 61 | 62 | 如果我们简单地从测量值中提取,我们就会失去上下文,不知道用什么单位来创建测量值。这意味着,我们将无法正确格式化图表的标签来向用户表示单位。虽然我们可以记住我们在创建测量时使用了小时 `hours`,但这并不理想。例如,我们可以决定以后改变数据模型,以分钟为单位存储持续时间,或者数据可能来自其他地方,所以手动重构单位并不是一个完美的解决方案。 63 | 64 | 用 `Plottable` 的一致性来扩展 `Measurement` 是可行的,但根据 Swift 中关于外部类型的追溯一致性的警告 ([Warning for Retroactive Conformances of External Types](https://github.com/apple/swift-evolution/blob/main/proposals/0364-retroactive-conformance-warning.md "Warning for Retroactive Conformances of External Types")),如果 Swift Charts 在未来添加了这种一致性,它可能会被破坏。 65 | 66 | 67 | 68 | 我们将研究如何定义我们自己的类型来包装 `measurement`,并为我们的自定义类型添加 `Plottable` 的一致性。 69 | 70 | ## 设计一个包装器类型 71 | 72 | 设计一个符合 Plottable 标准的包装器类型 73 | 74 | 我们将定义一个自定义的 `PlottableMeasurement` 类型,并使其成为通用的,所以它可以容纳任何类型的单位的测量类型。 75 | 76 | ```swift 77 | struct PlottableMeasurement { 78 | var measurement: Measurement 79 | } 80 | ``` 81 | 82 | 然后,我们将为 `PlottableMeasurement` 添加 `Plottable` 的一致性,其单位为 `UnitDuration` 类型。我们可以在将来添加对其他单位的支持。 83 | 84 | ```swift 85 | extension PlottableMeasurement: Plottable where UnitType == UnitDuration { 86 | var primitivePlottable: Double { 87 | self.measurement.converted(to: .minutes).value 88 | } 89 | 90 | init?(primitivePlottable: Double) { 91 | self.init( 92 | measurement: Measurement( 93 | value: primitivePlottable, 94 | unit: .minutes 95 | ) 96 | ) 97 | } 98 | } 99 | ``` 100 | 101 | `Plottable` 协议有两个要求: `primitivePlottable` 属性必须返回原始类型之一,如 `Double`、`String` 或 `Date`,以及一个可失败的初始化器,从原始 `plottable` 类型创建一个值。 102 | 103 | 我决定将测量值转换为分钟,但你可以选择适合你需要的任何其他单位。只是在与原始值转换时要使用相同的单位,这一点很重要。 104 | 105 | 106 | 107 | 我们现在可以更新我们的图表,以使用我们的自定义 `Plottable` 类型。 108 | 109 | ```swift 110 | Chart(walks, id: \.title) { walk in 111 | BarMark( 112 | x: .value( 113 | "Duration", 114 | PlottableMeasurement(measurement: walk.duration) 115 | ), 116 | y: .value("Walk", walk.title) 117 | ) 118 | } 119 | ``` 120 | 121 | 它可以工作,但X轴上的标签没有格式化,没有向用户显示测量单位。我们接下来要解决这个问题。 122 | 123 | ![](https://nilcoalescing.com/static/blog/UsingMeasurementsFromFoundationAsValuesInSwiftCharts/Chart-without-formatting.GXK9NAqspTDEYy9wm4szJRGI8sasR0fo_xmhJSdYgQA.png) 124 | 125 | ## 显示格式化标签 126 | 127 | 显示带有测量单位的格式化标签 128 | 129 | 为了定制X轴上的标签,我们将使用`chartXAxis(content:)`修改器,并用传递给我们的值重构x轴的标记。 130 | 131 | ```swift 132 | Chart(walks, id: \.title) { ... } 133 | .chartXAxis { 134 | AxisMarks { value in 135 | AxisGridLine() 136 | AxisValueLabel(""" 137 | \(value.as(PlottableMeasurement.self)! 138 | .measurement 139 | .converted(to: .hours), 140 | format: .measurement( 141 | width: .narrow, 142 | numberFormatStyle: .number.precision( 143 | .fractionLength(0)) 144 | ) 145 | ) 146 | """) 147 | } 148 | } 149 | ``` 150 | 151 | 我们首先添加网格线,然后重构给定值的标签。 152 | 153 | `AxisValueLabel`在初始化器中接受一个`LocalizedStringKey`,它可以通过插值测量和指定其格式风格来构建。 154 | 155 | 我们收到的值是使用我们在 `Plottable` 一致性中定义的初始化器创建的,所以在我们的案例中,测量值是以分钟为单位提供的。但我相信对于这个特定的图表,使用小时会更好。我们可以很容易地将测量值转换为插值内部所需的单位。在这里,我们确定该值是 `PlottableMeasurement` 类型的,所以我们可以强制解包类型转换。 156 | 157 | 我选择了缩小的格式和小数点后零位数作为数字样式,但你可以根据你的具体图表调整这些设置。 158 | 159 | 最后的结果是在X轴上显示以小时为单位的格式化持续时间。 160 | 161 | ![](https://nilcoalescing.com/static/blog/UsingMeasurementsFromFoundationAsValuesInSwiftCharts/Chart-with-formatting.gOyj3M-zKoErXg8eGC4TcKBVM43CpFVE2lNU3k8zF9E.png) 162 | 163 | 你可以从我们的 GitHub repo 中获得这篇文章中使用的项目的完整 [示例代码](https://github.com/SwiftCommunityRes/SwiftUI-Code-Examples/blob/main/Using-Measurements-from-Foundation-as-values-in-Swift-Charts/Using-Measurements-from-Foundation-as-values-in-Swift-Charts.swift)。 164 | 165 | > 来源:[Using Measurements from Foundation for values in Swift Charts](https://nilcoalescing.com/blog/UsingMeasurementsFromFoundationAsValuesInSwiftCharts/) 166 | -------------------------------------------------------------------------------- /resource/12 iOS16 中的三种新字体宽度样式.md: -------------------------------------------------------------------------------- 1 | # SF字体 Expend,Condensed 和 Compressed :iOS16 中的三种新字体宽度样式 2 | 3 | ## 前言 4 | 5 | 在 iOS 16 中,Apple 引入了三种新的宽度样式字体到 SF 字体库。 6 | 7 | 1. Compressed 8 | 9 | 2. Condensed 10 | 11 | 3. Expend 12 | 13 | ![](https://images.xiaozhuanlan.com/photo/2022/f9a30607ad412d7b23ba4e43f5396ade.png) 14 | 15 | ## UIFont.Width 16 | 17 | Apple 引入了新的结构体 `UIFont.Width`,这代表了一种新的宽度样式。 18 | 19 | 目前已有的四种样式。 20 | 21 | * standard:我们总是使用的默认宽度。 22 | 23 | * compressed:最窄的宽度样式。 24 | 25 | * condensed:介于压缩和标准之间的宽度样式。 26 | 27 | * expanded:最宽的宽度样式。 28 | 29 | ![](https://images.xiaozhuanlan.com/photo/2022/0a80f9d3f6deb35081eb1e6ce611ab62.png) 30 | 31 | ## SF 字体和新的宽度样式 32 | 33 | 如何将 SF 字体和新的宽度样式一起使用 34 | 35 | 为了使用新的宽度样式,Apple 有一个新的 `UIFont` 的类方法来接收新的 `UIFont.Width` 。 36 | 37 | ```swift 38 | class UIFont : NSObject { 39 | class func systemFont( 40 | ofSize fontSize: CGFloat, 41 | weight: UIFont.Weight, 42 | width: UIFont.Width 43 | ) -> UIFont 44 | } 45 | ``` 46 | 47 | 你可以像平常创建字体那样来使用新的方法。 48 | 49 | ```swift 50 | let condensed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .condensed) 51 | let compressed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .compressed) 52 | let standard = UIFont.systemFont(ofSize: 46, weight: .bold, width: .standard) 53 | let expanded = UIFont.systemFont(ofSize: 46, weight: .bold, width: .expanded) 54 | ``` 55 | 56 | ## SwiftUI 57 | 58 | > 更新:在 Xcode 14.1 中,SwiftUI 提供了两个新的 API 设置这种新的宽度样式。 59 | `width(_:)` 和 `fontWidth(_:)`。 60 | 61 | 目前(Xcode 16 beta 6),这种新的宽度样式和初始值设定只能在 UIKit 中使用,幸运的是,我们可以在 SwiftUI 中轻松的使用它。 62 | 63 | 有很多种方法可以将 UIKit 集成到 SwiftUI 。我将会展示在 SwiftUI 中使用新宽度样式的两种方法。 64 | 65 | 1. 将 UIfont 转为 Font。 66 | 2. 创建 Font 扩展。 67 | 68 | ## 将 UIfont 转为 Font 69 | 70 | 我们从 [在 SwiftUI 中如何将 UIFont 转换为 Font](https://www.jianshu.com/p/56ee0d1ea0e1 "在 SwiftUI 中如何将 UIFont 转换为 Font") 中了解到,Font 有初始化方法可以接收 UIFont 作为参数。 71 | 72 | 步骤如下 73 | 74 | 1. 你需要创建一个带有新宽度样式的 UIFont。 75 | 2. 使用该 UIFont 创建一个 Font 。 76 | 3. 然后像普通 Font 一样使用它们。 77 | 78 | ```swift 79 | struct NewFontExample: View { 80 | // 1 81 | let condensed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .condensed) 82 | let compressed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .compressed) 83 | let standard = UIFont.systemFont(ofSize: 46, weight: .bold, width: .standard) 84 | let expanded = UIFont.systemFont(ofSize: 46, weight: .bold, width: .expanded) 85 | 86 | var body: some View { 87 | VStack { 88 | // 2 89 | Text("Compressed") 90 | .font(Font(compressed)) 91 | Text("Condensed") 92 | .font(Font(condensed)) 93 | Text("Standard") 94 | .font(Font(standard)) 95 | Text("Expanded") 96 | .font(Font(expanded)) 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | * 创建带有新宽度样式的 UIFont。 103 | * 用 UIFont 初始化 Font, 然后传递给 .font 修改。 104 | 105 | ## 创建一个 Font 扩展 106 | 107 | 这种方法实际上和将 UIfont 转为 Font 是同一种方法。我们只需要创建一个新的 Font 扩展在 SwiftUI 中使用起来更容易一些。 108 | 109 | ```swift 110 | extension Font { 111 | public static func system( 112 | size: CGFloat, 113 | weight: UIFont.Weight, 114 | width: UIFont.Width) -> Font 115 | { 116 | // 1 117 | return Font( 118 | UIFont.systemFont( 119 | ofSize: size, 120 | weight: weight, 121 | width: width) 122 | ) 123 | } 124 | } 125 | ``` 126 | 127 | 创建一个静态函数传递 `UIFont` 需要的参数。然后,初始化 `UIFont `和创建 `Font` 。 128 | 129 | 我们就可以像这样使用了。 130 | 131 | ```swift 132 | Text("Compressed") 133 | .font(.system(size: 46, weight: .bold, width: .compressed)) 134 | Text("Condensed") 135 | .font(.system(size: 46, weight: .bold, width: .condensed)) 136 | Text("Standard") 137 | .font(.system(size: 46, weight: .bold, width: .standard)) 138 | Text("Expanded") 139 | .font(.system(size: 46, weight: .bold, width: .expanded)) 140 | ``` 141 | 142 | ## 如何使用新的宽度样式 143 | 144 | 你可以在你想使用的任何地方使用。不会有任何限制,所有的新宽度都有一样的尺寸,同样的高度,只会有宽度的变化。 145 | 146 | 这里是拥有同样文本,同样字体大小和同样字体样式的不同字体宽度样式展示。 147 | 148 | ![](https://images.xiaozhuanlan.com/photo/2022/78876c1d6091d96ece0fd96e66762bb0.png) 149 | 150 | ## 新的宽度样式优点 151 | 152 | 你可以使用新的宽度样式在已经存在的字体样式上,比如 `thin` 或者 `bold` ,在你的 app 上创造出独一无二的体验。 153 | 154 | Apple 将它使用在他们的照片app ,在 "回忆'' 功能中,通过组合不同的字体宽度和样式在标题或者子标题上。 155 | 156 | ![](https://images.xiaozhuanlan.com/photo/2022/2b7362eee9f2c91c149993c8fef23404.png) 157 | 158 | 这里有一些不同宽度和样式的字体组合,希望可以激发你的灵感。 159 | 160 | ```swift 161 | Text("Pet Friends") 162 | .font(Font(UIFont.systemFont(ofSize: 46, weight: .light, width: .expanded))) 163 | Text("OVER THE YEARS") 164 | .font(Font(UIFont.systemFont(ofSize: 30, weight: .thin, width: .compressed))) 165 | 166 | Text("Pet Friends") 167 | .font(Font(UIFont.systemFont(ofSize: 46, weight: .black, width: .condensed))) 168 | Text("OVER THE YEARS") 169 | .font(Font(UIFont.systemFont(ofSize: 20, weight: .light, width: .expanded))) 170 | ``` 171 | 172 | ![](https://images.xiaozhuanlan.com/photo/2022/54c2cac00674c8bf7287e539449a42f2.png) 173 | 174 | 你也可以用新的宽度样式来控制文本的可读性。 175 | 176 | 下面的这个例子,说明不同宽度样式如何影响每行的字符数和段落长度 177 | 178 | ![](https://images.xiaozhuanlan.com/photo/2022/648985be4a5c61cad32fc7ede43c1a70.png) 179 | 180 | ## 下载这种字体 181 | 182 | 你可以在 [Apple 字体平台](https://developer.apple.com/fonts/ "Apple 字体平台") 来下载这种新的字体宽度样式。 183 | 184 | 下载安装后,你将会发现一种结合了现有宽度和新宽度样式的新样式。 185 | 186 | ![](https://images.xiaozhuanlan.com/photo/2022/733a1ffba0f1fb24ed42e260357cba1b.png) 187 | 188 | > 基本上,除了在模拟器的模拟系统 UI 中,在任何地方都被禁止使用 SF 字体。请确保你在使用前阅读并理解许可证。 189 | 190 | ## 关于我们 191 | 192 | 我们是由 Swift 爱好者共同维护,我们会分享以 Swift 实战、SwiftUI、Swift 基础为核心的技术内容,也整理收集优秀的学习资料。 193 | 194 | 后续还会翻译大量资料到我们公众号,有感兴趣的朋友,可以加入我们。 195 | 196 | > 来自:[Three New font width styles in iOS 16](https://sarunw.com/posts/sf-font-width-styles/) -------------------------------------------------------------------------------- /resource/17 Sendable 和 @Sendable 闭包——代码实例详解.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | `Sendable` 和 `@Sendable` 是 Swift 5.5 中的并发修改的一部分,解决了结构化的并发结构体和执行者消息之间传递的类型检查的挑战性问题。 4 | 5 | 在深入探讨`Sendable`的话题之前,我鼓励你阅读我围绕 [async/await](https://www.avanderlee.com/swift/async-await/)、[actors](https://www.avanderlee.com/swift/actors/) 和 [actor isolation](https://www.avanderlee.com/swift/nonisolated-isolated/) 的文章。这些文章涵盖了新的并发性变化的基础知识,它们与本文所解释的技术直接相关。 6 | 7 | ## 使用 Sendable 8 | 9 | 应该在什么时候使用 `Sendable`? 10 | 11 | `Sendable`协议和闭包表明那些传递的值的公共API是否线程安全的向编译器传递了值。当没有公共修改器、有内部锁定系统或修改器实现了与值类型一样的复制写入时,公共API可以安全地跨并发域使用。 12 | 13 | 标准库中的许多类型已经支持了`Sendable`协议,消除了对许多类型添加一致性的要求。由于标准库的支持,编译器可以为你的自定义类型创建隐式一致性。 14 | 15 | 例如,整型支持该协议: 16 | 17 | ```swift 18 | extension Int: Sendable {} 19 | ``` 20 | 21 | 一旦我们创建了一个具有 `Int` 类型的单一属性的值类型结构体,我们就隐式地得到了对 `Sendable` 协议的支持。 22 | 23 | ```swift 24 | // 隐式地遵守了 Sendable 协议 25 | struct Article { 26 | var views: Int 27 | } 28 | ``` 29 | 30 | 与此同时,同样的 `Article` 内容的类,将不会有隐式遵守该协议: 31 | 32 | ```swift 33 | // 不会隐式的遵守 Sendable 协议 34 | class Article { 35 | var views: Int 36 | } 37 | ``` 38 | 39 | 类不符合要求,因为它是一个引用类型,因此可以从其他并发域变异。换句话说,该类文章(`Article`)的传递不是线程安全的,所以编译器不能隐式地将其标记为遵守`Sendable`协议。 40 | 41 | ### 使用泛型和枚举时的隐式一致性 42 | 43 | 很好理解的是,如果泛型不符合`Sendable`协议,编译器就不会为泛型添加隐式的一致性。 44 | 45 | ```swift 46 | // 因为 Value 没有遵守 Sendable 协议,所以 Container 也不会自动的隐式遵守该协议 47 | struct Container { 48 | var child: Value 49 | } 50 | ``` 51 | 52 | 然而,如果我们将协议要求添加到我们的泛型中,我们将得到隐式支持: 53 | 54 | ```swift 55 | // Container 隐式地符合 Sendable,因为它的所有公共属性也是如此。 56 | struct Container { 57 | var child: Value 58 | } 59 | ``` 60 | 61 | 对于有关联值的枚举也是如此: 62 | 63 | ![如果枚举值们不符合 Sendable 协议,隐式的Sendable协议一致性就不会起作用。](https://www.avanderlee.com/wp-content/uploads/2021/10/sendable_protocol_swift-1024x183.png) 64 | 65 | 你可以看到,我们自动从编译器中得到一个错误: 66 | 67 | > *Associated value ‘loggedIn(name:)’ of ‘Sendable’-conforming enum ‘State’ has non-sendable type ‘(name: NSAttributedString)’* 68 | 69 | 我们可以通过使用一个值类型`String`来解决这个错误,因为它已经符合`Sendable`。 70 | 71 | ```swift 72 | enum State: Sendable { 73 | case loggedOut 74 | case loggedIn(name: String) 75 | } 76 | ``` 77 | 78 | ### 从线程安全的实例中抛出错误 79 | 80 | 同样的规则适用于想要符合`Sendable`的错误类型。 81 | 82 | ```swift 83 | struct ArticleSavingError: Error { 84 | var author: NonFinalAuthor 85 | } 86 | 87 | extension ArticleSavingError: Sendable { } 88 | ``` 89 | 90 | 由于作者不是不变的(non-final),而且不是线程安全的(后面会详细介绍),我们会遇到以下错误: 91 | 92 | > *Stored property ‘author’ of ‘Sendable’-conforming struct ‘ArticleSavingError’ has non-sendable type ‘NonFinalAuthor’* 93 | 94 | 你可以通过确保`ArticleSavingError`的所有成员都符合`Sendable`协议来解决这个错误。 95 | 96 | ## 如何使用Sendable协议 97 | 98 | 隐式一致性消除了很多我们需要自己为`Sendable`协议添加一致性的情况。然而,在有些情况下,我们知道我们的类型是线程安全的,但是编译器并没有为我们添加隐式一致性。 99 | 100 | 常见的例子是被标记为不可变和内部具有锁定机制的类: 101 | 102 | ```swift 103 | /// User 是不可改变的,因此是线程安全的,所以可以遵守 Sendable 协议 104 | final class User: Sendable { 105 | let name: String 106 | 107 | init(name: String) { self.name = name } 108 | } 109 | ``` 110 | 111 | 你需要用`@unchecked`属性来标记可变类,以表明我们的类由于内部锁定机制所以是线程安全的: 112 | 113 | ```swift 114 | extension DispatchQueue { 115 | static let userMutatingLock = DispatchQueue(label: "person.lock.queue") 116 | } 117 | 118 | final class MutableUser: @unchecked Sendable { 119 | private var name: String = "" 120 | 121 | func updateName(_ name: String) { 122 | DispatchQueue.userMutatingLock.sync { 123 | self.name = name 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | ## 要在同一源文件中遵守 `Sendable`的限制 130 | 131 | `Sendable`协议的一致性必须发生在同一个源文件中,以确保编译器检查所有可见成员的线程安全。 132 | 133 | 例如,你可以在例如 Swift package这样的模块中定义以下类型: 134 | 135 | ```swfit 136 | public struct Article { 137 | internal var title: String 138 | } 139 | ``` 140 | 141 | `Article` 是公开的,而标题`title`是内部的,在模块外不可见。因此,编译器不能在源文件之外应用`Sendable`一致性,因为它对标题属性不可见,即使标题使用的是遵守`Sendable`协议的`String`类型。 142 | 143 | 同样的问题发生在我们想要使一个可变的非最终类遵守`Sendable`协议时: 144 | 145 | ![可变的非最终类无法遵守 Sendable 协议](https://www.avanderlee.com/wp-content/uploads/2021/10/non_final_sendable-1024x185.png) 146 | 147 | 由于该类是非最终的,我们无法符合`Sendable`协议的要求,因为我们不确定其他类是否会继承`User`的非`Sendable`成员。因此,我们会遇到以下错误: 148 | 149 | > *Non-final class ‘User’ cannot conform to `Sendable`; use `@unchecked Sendable`* 150 | 151 | 正如你所看到的,编译器建议使用`@unchecked Sendable`。我们可以把这个属性添加到我们的`User`类中,并摆脱这个错误: 152 | 153 | ```swift 154 | class User: @unchecked Sendable { 155 | let name: String 156 | 157 | init(name: String) { self.name = name } 158 | } 159 | ``` 160 | 161 | 然而,这确实要求我们无论何时从`User`继承,都要确保它是线程安全的。由于我们给自己和同事增加了额外的责任,我不鼓励使用这个属性,建议使用组合、最终类或值类型来实现我们的目的。 162 | 163 | ## 如何使用 `@Sendabele` 164 | 165 | 函数可以跨并发域传递,因此也需要可发送的一致性。然而,函数不能符合协议,所以Swift引入了`@Sendable`属性。你可以传递的函数的例子是全局函数声明、闭包和访问器,如`getters`和`setters`。 166 | 167 | [SE-302](https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md)的部分动机是执行尽可能少的同步 168 | 169 | > 我们希望这样一个系统中的绝大多数代码都是无同步的。 170 | 171 | 使用`@Sendable`属性,我们将告诉编译器,他不需要额外的同步,因为闭包中所有捕获的值都是线程安全的。一个典型的例子是在[Actor isolation](https://www.avanderlee.com/swift/nonisolated-isolated/)中使用闭包。 172 | 173 | ```swift 174 | actor ArticlesList { 175 | func filteredArticles(_ isIncluded: @Sendable (Article) -> Bool) async -> [Article] { 176 | // ... 177 | } 178 | } 179 | ``` 180 | 181 | 如果你用非 `Sendabel` 类型的闭包,我们会遇到一个错误: 182 | 183 | ```swift 184 | let listOfArticles = ArticlesList() 185 | var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword") 186 | let filteredArticles = await listOfArticles.filteredArticles { article in 187 | 188 | // Error: Reference to captured var 'searchKeyword' in concurrently-executing code 189 | guard let searchKeyword = searchKeyword else { return false } 190 | return article.title == searchKeyword.string 191 | } 192 | ``` 193 | 194 | 当然,我们可以通过使用一个普通的`String`来快速解决这种情况,但它展示了编译器如何帮助我们执行线程安全。 195 | 196 | ## Swift 6: 为你的代码启用严格的并发性检查 197 | 198 | Xcode 14 允许您通过 `SWIFT_STRICT_CONCURRENCY` 构建设置启用严格的并发性检查。 199 | 200 | ![启用严格的并发性检查,以修复 Sendable 的符合性](https://www.avanderlee.com/wp-content/uploads/2021/10/swift_6_strict_concurrency_checking_sendable-1024x348.jpg) 201 | 202 | 这个构建设置控制编译器对`Sendable`和`actor-isolation`检查的执行水平: 203 | 204 | - *Minimal* : 编译器将只诊断明确标有`Sendable`一致性的实例,并等同于Swift 5.5和5.6的行为。不会有任何警告或错误。 205 | - *Targeted*: 强制执行`Sendable`约束,并对你所有采用`async/await`等并发的代码进行`actor-isolation`检查。编译器还将检查明确采用`Sendable`的实例。这种模式试图在与现有代码的兼容性和捕捉潜在的数据竞赛之间取得平衡。 206 | - *Complete*: 匹配预期的 Swift 6语义,以检查和消除数据竞赛。这种模式检查其他两种模式所做的一切,并对你项目中的所有代码进行这些检查。 207 | 208 | 严格的并发检查构建设置有助于 Swift 向数据竞赛安全迈进。与此构建设置相关的每一个触发的警告都可能表明你的代码中存在潜在的数据竞赛。因此,必须考虑启用严格并发检查来验证你的代码。 209 | 210 | ### Enabling strict concurrency in Xcode 14 211 | 212 | 你会得到的警告数量取决于你在项目中使用并发的频率。对于[Stock Analyzer](https://stock-analyzer.app/),我有大约17个警告需要解决: 213 | 214 | ![并发相关的警告,表明潜在的数据竞赛.](https://www.avanderlee.com/wp-content/uploads/2021/10/strict_concurrency_checking_warnings-1024x649.jpg) 215 | 216 | 这些警告可能让人望而生畏,但利用本文的知识,你应该能够摆脱大部分警告,防止数据竞赛的发生。然而,有些警告是你无法控制的,因为是外部模块触发了它们。在我的例子中,我有一个与`SWHighlight`有关的警告,它不符合`Sendable`,而苹果在他们的`SharedWithYou`框架中定义了它。 217 | 218 | 在上述`SharedWithYou`框架的例子中,最好是等待库的所有者添加`Sendable`支持。在这种情况下,这就意味着要等待苹果公司为`SWHighlight`实例指明`Sendable`的一致性。对于这些库,你可以通过使用`@preconcurrency`属性来暂时禁用`Sendable`警告: 219 | 220 | ```swift 221 | @preconcurrency import SharedWithYou 222 | ``` 223 | 224 | 重要的是要明白,我们并没有解决这些警告,而只是禁用了它们。来自这些库的代码仍然有可能发生数据竞赛。如果你正在使用这些框架的实例,你需要考虑实例是否真的是线程安全的。一旦你使用的框架被更新为`Sendable`的一致性,你可以删除`@preconcurrency`属性,并修复可能触发的警告。 225 | 226 | > 来源:[Sendable and @Sendable closures explained with code examples](https://www.avanderlee.com/swift/sendable-protocol-closures/) 227 | -------------------------------------------------------------------------------- /resource/19 Swift 中的async:await ——代码实例详解.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | async-await 是在 WWDC 2021 期间的 Swift 5.5 中的结构化并发变化的一部分。Swift 中的并发性意味着允许多段代码同时运行。这是一个非常简化的描述,但它应该让你知道 Swift 中的并发性对你的应用程序的性能是多么重要。有了新的 async 方法和 await 语句,我们可以定义方法来进行异步工作。 4 | 5 | 你可能读过 Chris Lattner 的 Swift 并发性宣言 [Swift Concurrency Manifesto by Chris Lattner](https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782 "Swift Concurrency Manifesto by Chris Lattner"),这是在几年前发布的。Swift社区的许多开发者对未来将出现的定义异步代码的结构化方式感到兴奋。现在它终于来了,我们可以用 async-await 简化我们的代码,使我们的异步代码更容易阅读。 6 | 7 | ## 什么是 async? 8 | 9 | async 是异步的意思,可以看作是一个明确表示一个方法是执行异步工作的一个属性。这样一个方法的例子看起来如下: 10 | 11 | ```swift 12 | func fetchImages() async throws -> [UIImage] { 13 | // .. 执行数据请求 14 | } 15 | ``` 16 | 17 | `fetchImages` 方法被定义为异步且可以抛出异常,这意味着它正在执行一个可失败的异步作业。如果一切顺利,该方法将返回一组图像,如果出现问题,则抛出错误。 18 | 19 | ## async 如何取代完成回调闭包 20 | 21 | async 方法取代了经常看到的完成回调。完成回调在 Swift 中很常见,用于从异步任务中返回,通常与一个结果类型的参数相结合。上述方法一般会被写成这样: 22 | 23 | ```swift 24 | func fetchImages(completion: (Result<[UIImage], Error>) -> Void) { 25 | // .. 执行数据请求 26 | } 27 | ``` 28 | 29 | 在如今的 Swift 版本中,使用完成闭包来定义方法仍然是可行的,但它有一些缺点,async 却刚好可以解决。 30 | 31 | - 你必须确保自己在每个可能的退出方法中调用完成闭包。如果不这样做,可能会导致应用程序无休止地等待一个结果。 32 | - 闭包代码比较难阅读。与结构化并发相比,对执行顺序的推理并不那么容易。 33 | - 需要使用弱引用 `weak references` 来避免循环引用。 34 | - 实现者需要对结果进行切换以获得结果。无法从实现层面使用 `try catch` 语句。 35 | 36 | 这些缺点是基于使用相对较新的 `Result` 枚举的闭包版本。很可能很多项目仍然在使用完成回调,而没有使用这个枚举: 37 | 38 | ```swift 39 | func fetchImages(completion: ([UIImage]?, Error?) -> Void) { 40 | // .. 执行数据请求 41 | } 42 | ``` 43 | 44 | 像这样定义一个方法使我们很难推理出调用者一方的结果。`value` 和 `error` 都是可选的,这要求我们在任何情况下都要进行解包。对这些可选项解包会导致更多的代码混乱,这对提高可读性没有帮助。 45 | 46 | ## 什么是 await? 47 | 48 | await 是用于调用异步方法的关键字。你可以把它们 (async-await) 看作是 Swift 中最好的朋友,因为一个永远不会离开另一个,你基本上可以这样说: 49 | 50 | > "Await 正在等待来自他的伙伴 async 的回调" 51 | 52 | 尽管这听起来很幼稚,但这并不是骗人的! 我们可以通过调用我们先前定义的异步方法 `fetchImages` 方法来看一个例子: 53 | 54 | ```swift 55 | do { 56 | let images = try await fetchImages() 57 | print("Fetched \(images.count) images.") 58 | } catch { 59 | print("Fetching images failed with error \(error)") 60 | } 61 | ``` 62 | 63 | 也许你很难相信,但上面的代码例子是在执行一个异步任务。使用 `await` 关键字,我们告诉我们的程序等待 `fetchImages` 方法的结果,只有在结果到达后才继续。这可能是一个图像集合,也可能是一个在获取图像时出了什么问题的错误。 64 | 65 | ## 什么是结构化并发? 66 | 67 | 使用 async-await 方法调用的结构化并发使得执行顺序的推理更加容易。方法是线性执行的,不用像闭包那样来回走动。 68 | 69 | 为了更好地解释这一点,我们可以看看在结构化并发到来之前,我们如何调用上述代码示例: 70 | 71 | ```swift 72 | // 1. 调用这个方法 73 | fetchImages { result in 74 | // 3. 异步方法内容返回 75 | switch result { 76 | case .success(let images): 77 | print("Fetched \(images.count) images.") 78 | case .failure(let error): 79 | print("Fetching images failed with error \(error)") 80 | } 81 | } 82 | // 2. 调用方法结束 83 | ``` 84 | 85 | 正如你所看到的,调用方法在获取图像之前结束。最终,我们收到了一个结果,然后我们回到了完成回调的流程中。这是一个非结构化的执行顺序,可能很难遵循。如果我们在完成回调中执行另一个异步方法,毫无疑问这会增加另一个闭包回调: 86 | 87 | ```swift 88 | // 1. 调用这个方法 89 | fetchImages { result in 90 | // 3. 异步方法内容返回 91 | switch result { 92 | case .success(let images): 93 | print("Fetched \(images.count) images.") 94 | 95 | // 4. 调用 resize 方法 96 | resizeImages(images) { result in 97 | // 6. Resize 方法返回 98 | switch result { 99 | case .success(let images): 100 | print("Decoded \(images.count) images.") 101 | case .failure(let error): 102 | print("Decoding images failed with error \(error)") 103 | } 104 | } 105 | // 5. 获图片方法返回 106 | case .failure(let error): 107 | print("Fetching images failed with error \(error)") 108 | } 109 | } 110 | // 2. 调用方法结束 111 | ``` 112 | 113 | 每一个闭包都会增加一层缩进,这使得我们更难理解执行的顺序。 114 | 115 | 通过使用 async-await 重写上述代码示例,最好地解释了结构化并发的作用。 116 | 117 | ```swift 118 | do { 119 | // 1. 调用这个方法 120 | let images = try await fetchImages() 121 | // 2.获图片方法返回 122 | 123 | // 3. 调用 resize 方法 124 | let resizedImages = try await resizeImages(images) 125 | // 4.Resize 方法返回 126 | 127 | print("Fetched \(images.count) images.") 128 | } catch { 129 | print("Fetching images failed with error \(error)") 130 | } 131 | // 5. 调用方法结束 132 | ``` 133 | 134 | 执行的顺序是线性的,因此,容易理解,容易推理。当我们有时还在执行复杂的异步任务时,理解异步代码会更容易。 135 | 136 | ## 调用异步方法 137 | 138 | 在一个不支持并发的函数中调用异步方法 139 | 140 | 在第一次使用 async-await 时,你可能会遇到这样的错误。 141 | 142 | ![](https://files.mdnice.com/user/17787/c1dc832f-7da0-4c20-b62d-647e8975efb8.jpg) 143 | 144 | 当我们试图从一个不支持并发的同步调用环境中调用一个异步方法时,就会出现这个错误。我们可以通过将我们的 `fetchData` 方法也定义为异步来解决这个错误: 145 | 146 | ```swift 147 | func fetchData() async { 148 | do { 149 | try await fetchImages() 150 | } catch { 151 | // .. handle error 152 | } 153 | } 154 | ``` 155 | 156 | 然而,这将把错误转移到另一个地方。相反,我们可以使用 `Task.init` 方法,从一个支持并发的新任务中调用异步方法,并将结果分配给我们视图模型中的一个属性: 157 | 158 | ```swift 159 | final class ContentViewModel: ObservableObject { 160 | 161 | @Published var images: [UIImage] = [] 162 | 163 | func fetchData() { 164 | Task.init { 165 | do { 166 | self.images = try await fetchImages() 167 | } catch { 168 | // .. handle error 169 | } 170 | } 171 | } 172 | } 173 | ``` 174 | 175 | 使用尾随闭包的异步方法,我们创建了一个环境,在这个环境中我们可以调用异步方法。一旦异步方法被调用,获取数据的方法就会返回,之后所有的异步回调都会在闭包内发生。 176 | 177 | ## 采用 async-await 178 | 179 | 在一个现有项目中采用 async-await 180 | 181 | 当在现有项目中采用 async-await 时,你要注意不要一下子破坏所有的代码。在进行这样的大规模重构时,最好考虑暂时维护旧的实现,这样你就不必在知道新的实现是否足够稳定之前更新所有的代码。这与 SDK 中被许多不同的开发者和项目所使用的废弃方法类似。 182 | 183 | 显然,你没有义务这样做,但它可以使你更容易在你的项目中尝试使用 async-await。除此之外,Xcode 使重构你的代码变得超级容易,还提供了一个选项来创建一个单独的 async 方法: 184 | 185 | ![](https://files.mdnice.com/user/17787/22ebbd1a-ea91-44ab-8f58-249273ad5be6.jpg) 186 | 187 | 每个重构方法都有自己的目的,并导致不同的代码转换。为了更好地理解其工作原理,我们将使用下面的代码作为重构的输入: 188 | 189 | ```swift 190 | struct ImageFetcher { 191 | func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) { 192 | // .. 执行数据请求 193 | } 194 | } 195 | ``` 196 | 197 | ### 将函数转换为异步 (Convert Function to Async) 198 | 199 | 第一个重构选项将 `fetchImages` 方法转换为异步变量,而不保留非异步变量。如果你不想保留原来的实现,这个选项将很有用。结果代码如下: 200 | 201 | ```swift 202 | struct ImageFetcher { 203 | func fetchImages() async throws -> [UIImage] { 204 | // .. 执行数据请求 205 | } 206 | } 207 | ``` 208 | 209 | ### 添加异步替代方案 (Add Async Alternative) 210 | 211 | 添加异步替代重构选项确保保留旧的实现,但会添加一个可用(available) 属性: 212 | 213 | ```swift 214 | struct ImageFetcher { 215 | @available(*, renamed: "fetchImages()") 216 | func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) { 217 | Task { 218 | do { 219 | let result = try await fetchImages() 220 | completion(.success(result)) 221 | } catch { 222 | completion(.failure(error)) 223 | } 224 | } 225 | } 226 | 227 | 228 | func fetchImages() async throws -> [UIImage] { 229 | // .. 执行数据请求 230 | } 231 | } 232 | ``` 233 | 234 | 可用属性对于了解你需要在哪里更新你的代码以适应新的并发变量是非常有用的。虽然,Xcode 提供的默认实现并没有任何警告,因为它没有被标记为废弃的。要做到这一点,你需要调整可用标记,如下所示: 235 | 236 | ```swift 237 | @available(*, deprecated, renamed: "fetchImages()") 238 | ``` 239 | 240 | 使用这种重构选项的好处是,它允许你逐步适应新的结构化并发变化,而不必一次性转换你的整个项目。在这之间进行构建是很有价值的,这样你就可以知道你的代码变化是按预期工作的。利用旧方法的实现将得到如下的警告。 241 | 242 | ![](https://files.mdnice.com/user/17787/c95cee83-d4bc-4954-b426-d24b1e631af7.jpg) 243 | 244 | 你可以在整个项目中逐步改变你的实现,并使用Xcode中提供的修复按钮来自动转换你的代码以利用新的实现。 245 | 246 | ### 添加异步包装器 (Add Async Wrapper) 247 | 248 | 最后的重构方法将使用最简单的转换,因为它将简单地利用你现有的代码: 249 | 250 | ```swift 251 | struct ImageFetcher { 252 | @available(*, renamed: "fetchImages()") 253 | func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) { 254 | // .. 执行数据请求 255 | } 256 | 257 | func fetchImages() async throws -> [UIImage] { 258 | return try await withCheckedThrowingContinuation { continuation in 259 | fetchImages() { result in 260 | continuation.resume(with: result) 261 | } 262 | } 263 | } 264 | } 265 | ``` 266 | 267 | 新增加的方法利用了 Swift 中引入的 `withCheckedThrowingContinuation` 方法,可以不费吹灰之力地转换基于闭包的方法。不抛出的方法可以使用 `withCheckedContinuation`,其工作原理与此相同,但不支持抛出错误。 268 | 269 | 这两个方法会暂停当前任务,直到给定的闭包被调用以触发 async-await 方法的继续。换句话说:你必须确保根据你自己的基于闭包的方法的回调来调用 `continuation` 闭包。在我们的例子中,这归结为用我们从最初的 `fetchImages` 回调返回的结果值来调用继续。 270 | 271 | ### 为你的项目选择正确的 async-await 重构方法 272 | 273 | 这三个重构选项应该足以将你现有的代码转换为异步的替代品。根据你的项目规模和你的重构时间,你可能想选择一个不同的重构选项。不过,我强烈建议逐步应用改变,因为它允许你隔离改变的部分,使你更容易测试你的改变是否如预期那样工作。 274 | 275 | ## 解决错误 276 | 277 | 解决 "Reference to captured parameter ‘self’ in concurrently-executing code "错误 278 | 279 | 在使用异步方法时,另一个常见的错误是下面这个: 280 | 281 | > “Reference to captured parameter ‘self’ in concurrently-executing code” 282 | 283 | 这大致意思是说我们正试图引用一个不可变的`self`实例。换句话说,你可能是在引用一个属性或一个不可变的实例,例如,像下面这个例子中的结构体: 284 | 285 | ![](https://files.mdnice.com/user/17787/f9eed14b-6840-4b26-9c02-d25c72d8e9d8.jpg) 286 | 287 | 不支持从异步执行的代码中修改不可变的属性或实例。 288 | 289 | 可以通过使属性可变或将结构体更改为引用类型(如类)来修复此错误。 290 | 291 | ## 枚举的终点 292 | 293 | async-await 将是`Result`枚举的终点吗? 294 | 295 | 我们已经看到,异步方法取代了利用闭包回调的异步方法。我们可以问自己,这是否会是 Swift 中 [Result 枚举](https://www.avanderlee.com/swift/result-enum-type/ "Result 枚举")的终点。最终我们会发现,我们真的不再需要它们了,因为我们可以利用 try-catch 语句与 async-await 相结合。 296 | 297 | `Result` 枚举不会很快消失,因为它仍然在整个 Swift 项目的许多地方被使用。然而,一旦 async-await 的采用率越来越高,我就不会惊讶地看到它被废弃。就我个人而言,除了完成回调,我没有在其他地方使用结果枚举。一旦我完全使用 async-await,我就不会再使用这个枚举了。 298 | 299 | ## 结论 300 | 301 | Swift 中的 async-await 允许结构化并发,这将提高复杂异步代码的可读性。不再需要完成闭包,而在彼此之后调用多个异步方法的可读性也大大增强。一些新的错误类型可能会发生,通过确保异步方法是从支持并发的函数中调用的,同时不改变任何不可变的引用,这些错误将可以得到解决。 302 | 303 | > 来自:[Async await in Swift explained with code examples](https://www.avanderlee.com/swift/async-await/) -------------------------------------------------------------------------------- /resource/19.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | async-await 是在 WWDC 2021 期间的 Swift 5.5 中的结构化并发变化的一部分。Swift 中的并发性意味着允许多段代码同时运行。这是一个非常简化的描述,但它应该让你知道 Swift 中的并发性对你的应用程序的性能是多么重要。有了新的 async 方法和 await 语句,我们可以定义方法来进行异步工作。 4 | 5 | 你可能读过 Chris Lattner 的 Swift 并发性宣言 [Swift Concurrency Manifesto by Chris Lattner](https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782 "Swift Concurrency Manifesto by Chris Lattner"),这是在几年前发布的。Swift社区的许多开发者对未来将出现的定义异步代码的结构化方式感到兴奋。现在它终于来了,我们可以用 async-await 简化我们的代码,使我们的异步代码更容易阅读。 6 | 7 | ## 什么是 async? 8 | 9 | async 是异步的意思,可以看作是一个明确表示一个方法是执行异步工作的一个属性。这样一个方法的例子看起来如下: 10 | 11 | ```swift 12 | func fetchImages() async throws -> [UIImage] { 13 | // .. 执行数据请求 14 | } 15 | ``` 16 | 17 | `fetchImages` 方法被定义为异步且可以抛出异常,这意味着它正在执行一个可失败的异步作业。如果一切顺利,该方法将返回一组图像,如果出现问题,则抛出错误。 18 | 19 | ## async 如何取代完成回调闭包 20 | 21 | async 方法取代了经常看到的完成回调。完成回调在 Swift 中很常见,用于从异步任务中返回,通常与一个结果类型的参数相结合。上述方法一般会被写成这样: 22 | 23 | ```swift 24 | func fetchImages(completion: (Result<[UIImage], Error>) -> Void) { 25 | // .. 执行数据请求 26 | } 27 | ``` 28 | 29 | 在如今的 Swift 版本中,使用完成闭包来定义方法仍然是可行的,但它有一些缺点,async 却刚好可以解决。 30 | 31 | - 你必须确保自己在每个可能的退出方法中调用完成闭包。如果不这样做,可能会导致应用程序无休止地等待一个结果。 32 | - 闭包代码比较难阅读。与结构化并发相比,对执行顺序的推理并不那么容易。 33 | - 需要使用弱引用 `weak references` 来避免循环引用。 34 | - 实现者需要对结果进行切换以获得结果。无法从实现层面使用 `try catch` 语句。 35 | 36 | 这些缺点是基于使用相对较新的 `Result` 枚举的闭包版本。很可能很多项目仍然在使用完成回调,而没有使用这个枚举: 37 | 38 | ```swift 39 | func fetchImages(completion: ([UIImage]?, Error?) -> Void) { 40 | // .. 执行数据请求 41 | } 42 | ``` 43 | 44 | 像这样定义一个方法使我们很难推理出调用者一方的结果。`value` 和 `error` 都是可选的,这要求我们在任何情况下都要进行解包。对这些可选项解包会导致更多的代码混乱,这对提高可读性没有帮助。 45 | 46 | ## 什么是 await? 47 | 48 | await 是用于调用异步方法的关键字。你可以把它们 (async-await) 看作是 Swift 中最好的朋友,因为一个永远不会离开另一个,你基本上可以这样说: 49 | 50 | > "Await 正在等待来自他的伙伴 async 的回调" 51 | 52 | 尽管这听起来很幼稚,但这并不是骗人的! 我们可以通过调用我们先前定义的异步方法 `fetchImages` 方法来看一个例子: 53 | 54 | ```swift 55 | do { 56 | let images = try await fetchImages() 57 | print("Fetched \(images.count) images.") 58 | } catch { 59 | print("Fetching images failed with error \(error)") 60 | } 61 | ``` 62 | 63 | 也许你很难相信,但上面的代码例子是在执行一个异步任务。使用 `await` 关键字,我们告诉我们的程序等待 `fetchImages` 方法的结果,只有在结果到达后才继续。这可能是一个图像集合,也可能是一个在获取图像时出了什么问题的错误。 64 | 65 | ## 什么是结构化并发? 66 | 67 | 使用 async-await 方法调用的结构化并发使得执行顺序的推理更加容易。方法是线性执行的,不用像闭包那样来回走动。 68 | 69 | 为了更好地解释这一点,我们可以看看在结构化并发到来之前,我们如何调用上述代码示例: 70 | 71 | ```swift 72 | // 1. 调用这个方法 73 | fetchImages { result in 74 | // 3. 异步方法内容返回 75 | switch result { 76 | case .success(let images): 77 | print("Fetched \(images.count) images.") 78 | case .failure(let error): 79 | print("Fetching images failed with error \(error)") 80 | } 81 | } 82 | // 2. 调用方法结束 83 | ``` 84 | 85 | 正如你所看到的,调用方法在获取图像之前结束。最终,我们收到了一个结果,然后我们回到了完成回调的流程中。这是一个非结构化的执行顺序,可能很难遵循。如果我们在完成回调中执行另一个异步方法,毫无疑问这会增加另一个闭包回调: 86 | 87 | ```swift 88 | // 1. 调用这个方法 89 | fetchImages { result in 90 | // 3. 异步方法内容返回 91 | switch result { 92 | case .success(let images): 93 | print("Fetched \(images.count) images.") 94 | 95 | // 4. 调用 resize 方法 96 | resizeImages(images) { result in 97 | // 6. Resize 方法返回 98 | switch result { 99 | case .success(let images): 100 | print("Decoded \(images.count) images.") 101 | case .failure(let error): 102 | print("Decoding images failed with error \(error)") 103 | } 104 | } 105 | // 5. 获图片方法返回 106 | case .failure(let error): 107 | print("Fetching images failed with error \(error)") 108 | } 109 | } 110 | // 2. 调用方法结束 111 | ``` 112 | 113 | 每一个闭包都会增加一层缩进,这使得我们更难理解执行的顺序。 114 | 115 | 通过使用 async-await 重写上述代码示例,最好地解释了结构化并发的作用。 116 | 117 | ```swift 118 | do { 119 | // 1. 调用这个方法 120 | let images = try await fetchImages() 121 | // 2.获图片方法返回 122 | 123 | // 3. 调用 resize 方法 124 | let resizedImages = try await resizeImages(images) 125 | // 4.Resize 方法返回 126 | 127 | print("Fetched \(images.count) images.") 128 | } catch { 129 | print("Fetching images failed with error \(error)") 130 | } 131 | // 5. 调用方法结束 132 | ``` 133 | 134 | 执行的顺序是线性的,因此,容易理解,容易推理。当我们有时还在执行复杂的异步任务时,理解异步代码会更容易。 135 | 136 | ## 调用异步方法 137 | 138 | 在一个不支持并发的函数中调用异步方法 139 | 140 | 在第一次使用 async-await 时,你可能会遇到这样的错误。 141 | 142 | ![](https://www.avanderlee.com/wp-content/uploads/2021/07/async_await_structured_concurrency-1024x211.png) 143 | 144 | 当我们试图从一个不支持并发的同步调用环境中调用一个异步方法时,就会出现这个错误。我们可以通过将我们的 `fetchData` 方法也定义为异步来解决这个错误: 145 | 146 | ```swift 147 | func fetchData() async { 148 | do { 149 | try await fetchImages() 150 | } catch { 151 | // .. handle error 152 | } 153 | } 154 | ``` 155 | 156 | 然而,这将把错误转移到另一个地方。相反,我们可以使用 `Task.init` 方法,从一个支持并发的新任务中调用异步方法,并将结果分配给我们视图模型中的一个属性: 157 | 158 | ```swift 159 | final class ContentViewModel: ObservableObject { 160 | 161 | @Published var images: [UIImage] = [] 162 | 163 | func fetchData() { 164 | Task.init { 165 | do { 166 | self.images = try await fetchImages() 167 | } catch { 168 | // .. handle error 169 | } 170 | } 171 | } 172 | } 173 | ``` 174 | 175 | 使用尾随闭包的异步方法,我们创建了一个环境,在这个环境中我们可以调用异步方法。一旦异步方法被调用,获取数据的方法就会返回,之后所有的异步回调都会在闭包内发生。 176 | 177 | ## 采用 async-await 178 | 179 | 在一个现有项目中采用 async-await 180 | 181 | 当在现有项目中采用 async-await 时,你要注意不要一下子破坏所有的代码。在进行这样的大规模重构时,最好考虑暂时维护旧的实现,这样你就不必在知道新的实现是否足够稳定之前更新所有的代码。这与 SDK 中被许多不同的开发者和项目所使用的废弃方法类似。 182 | 183 | 显然,你没有义务这样做,但它可以使你更容易在你的项目中尝试使用 async-await。除此之外,Xcode 使重构你的代码变得超级容易,还提供了一个选项来创建一个单独的 async 方法: 184 | 185 | ![](https://www.avanderlee.com/wp-content/uploads/2021/07/async-await-refactor-1024x918.png) 186 | 187 | 每个重构方法都有自己的目的,并导致不同的代码转换。为了更好地理解其工作原理,我们将使用下面的代码作为重构的输入: 188 | 189 | ```swift 190 | struct ImageFetcher { 191 | func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) { 192 | // .. 执行数据请求 193 | } 194 | } 195 | ``` 196 | 197 | ### 将函数转换为异步 (Convert Function to Async) 198 | 199 | 第一个重构选项将 `fetchImages` 方法转换为异步变量,而不保留非异步变量。如果你不想保留原来的实现,这个选项将很有用。结果代码如下: 200 | 201 | ```swift 202 | struct ImageFetcher { 203 | func fetchImages() async throws -> [UIImage] { 204 | // .. 执行数据请求 205 | } 206 | } 207 | ``` 208 | 209 | ### 添加异步替代方案 (Add Async Alternative) 210 | 211 | 添加异步替代重构选项确保保留旧的实现,但会添加一个可用(available) 属性: 212 | 213 | ```swift 214 | struct ImageFetcher { 215 | @available(*, renamed: "fetchImages()") 216 | func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) { 217 | Task { 218 | do { 219 | let result = try await fetchImages() 220 | completion(.success(result)) 221 | } catch { 222 | completion(.failure(error)) 223 | } 224 | } 225 | } 226 | 227 | 228 | func fetchImages() async throws -> [UIImage] { 229 | // .. 执行数据请求 230 | } 231 | } 232 | ``` 233 | 234 | 可用属性对于了解你需要在哪里更新你的代码以适应新的并发变量是非常有用的。虽然,Xcode 提供的默认实现并没有任何警告,因为它没有被标记为废弃的。要做到这一点,你需要调整可用标记,如下所示: 235 | 236 | ```swift 237 | @available(*, deprecated, renamed: "fetchImages()") 238 | ``` 239 | 240 | 使用这种重构选项的好处是,它允许你逐步适应新的结构化并发变化,而不必一次性转换你的整个项目。在这之间进行构建是很有价值的,这样你就可以知道你的代码变化是按预期工作的。利用旧方法的实现将得到如下的警告。 241 | 242 | ![](https://www.avanderlee.com/wp-content/uploads/2021/07/async-await-deprecated-1024x209.png) 243 | 244 | 你可以在整个项目中逐步改变你的实现,并使用Xcode中提供的修复按钮来自动转换你的代码以利用新的实现。 245 | 246 | ### 添加异步包装器 (Add Async Wrapper) 247 | 248 | 最后的重构方法将使用最简单的转换,因为它将简单地利用你现有的代码: 249 | 250 | ```swift 251 | struct ImageFetcher { 252 | @available(*, renamed: "fetchImages()") 253 | func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) { 254 | // .. 执行数据请求 255 | } 256 | 257 | func fetchImages() async throws -> [UIImage] { 258 | return try await withCheckedThrowingContinuation { continuation in 259 | fetchImages() { result in 260 | continuation.resume(with: result) 261 | } 262 | } 263 | } 264 | } 265 | ``` 266 | 267 | 新增加的方法利用了 Swift 中引入的 `withCheckedThrowingContinuation` 方法,可以不费吹灰之力地转换基于闭包的方法。不抛出的方法可以使用 `withCheckedContinuation`,其工作原理与此相同,但不支持抛出错误。 268 | 269 | 这两个方法会暂停当前任务,直到给定的闭包被调用以触发 async-await 方法的继续。换句话说:你必须确保根据你自己的基于闭包的方法的回调来调用 `continuation` 闭包。在我们的例子中,这归结为用我们从最初的 `fetchImages` 回调返回的结果值来调用继续。 270 | 271 | ### 为你的项目选择正确的 async-await 重构方法 272 | 273 | 这三个重构选项应该足以将你现有的代码转换为异步的替代品。根据你的项目规模和你的重构时间,你可能想选择一个不同的重构选项。不过,我强烈建议逐步应用改变,因为它允许你隔离改变的部分,使你更容易测试你的改变是否如预期那样工作。 274 | 275 | ## 解决错误 276 | 277 | 解决 "Reference to captured parameter ‘self’ in concurrently-executing code "错误 278 | 279 | 在使用异步方法时,另一个常见的错误是下面这个: 280 | 281 | > “Reference to captured parameter ‘self’ in concurrently-executing code” 282 | 283 | 这大致意思是说我们正试图引用一个不可变的`self`实例。换句话说,你可能是在引用一个属性或一个不可变的实例,例如,像下面这个例子中的结构体: 284 | 285 | ![](https://www.avanderlee.com/wp-content/uploads/2021/07/Screenshot-2021-07-15-at-20.51.19-1024x361.png) 286 | 287 | 不支持从异步执行的代码中修改不可变的属性或实例。 288 | 289 | 可以通过使属性可变或将结构体更改为引用类型(如类)来修复此错误。 290 | 291 | ## 枚举的终点 292 | 293 | async-await 将是`Result`枚举的终点吗? 294 | 295 | 我们已经看到,异步方法取代了利用闭包回调的异步方法。我们可以问自己,这是否会是 Swift 中 [Result 枚举](https://www.avanderlee.com/swift/result-enum-type/ "Result 枚举")的终点。最终我们会发现,我们真的不再需要它们了,因为我们可以利用 try-catch 语句与 async-await 相结合。 296 | 297 | `Result` 枚举不会很快消失,因为它仍然在整个 Swift 项目的许多地方被使用。然而,一旦 async-await 的采用率越来越高,我就不会惊讶地看到它被废弃。就我个人而言,除了完成回调,我没有在其他地方使用结果枚举。一旦我完全使用 async-await,我就不会再使用这个枚举了。 298 | 299 | ## 结论 300 | 301 | Swift 中的 async-await 允许结构化并发,这将提高复杂异步代码的可读性。不再需要完成闭包,而在彼此之后调用多个异步方法的可读性也大大增强。一些新的错误类型可能会发生,通过确保异步方法是从支持并发的函数中调用的,同时不改变任何不可变的引用,这些错误将可以得到解决。 302 | 303 | > 来自:[Async await in Swift explained with code examples](https://www.avanderlee.com/swift/async-await/) -------------------------------------------------------------------------------- /resource/20 Swift AsyncSequence —— 代码实例详解.md: -------------------------------------------------------------------------------- 1 | > [AsyncSequence explained with Code Examples](https://www.avanderlee.com/concurrency/asyncsequence/) 2 | 3 | 4 | 5 | `AsyncSequence`是并发性框架和[SE-298](https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md)提案的一部分。它的名字意味着它是一个提供异步、顺序和迭代访问其元素的类型。换句话说:它是我们在Swift中熟悉的常规序列的一个异步变体。 6 | 7 | 8 | 9 | 就像你不会经常创建你的自定义序列一样,我不期望你经常创建一个自定义的`AsyncSequence`实现。然而,由于与[AsyncThrowingStream和AsyncStream](https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/)等类型一起使用,你很可能不得不与异步序列一起工作。因此,我将指导你使用`AsyncSequence`实例进行工作。 10 | 11 | 12 | 13 | ## 什么是 AsyncSequence? 14 | 15 | `AsyncSequence`是我们在Swift中熟悉的`Sequence`的一个异步变体。由于它的异步性,我们需要使用` await `关键字,因为我们要处理的是异步定义的方法。如果你没有使用过`async/await`,我鼓励你阅读我的文章:[Swift 中的async/await ——代码实例详解](https://www.avanderlee.com/swift/async-await/)。 16 | 17 | 18 | 19 | 值可以随着时间的推移而变得可用,这意味着一个`AsyncSequence`在你第一次使用它时可能不包含也可能包含一些,或者全部的值。 20 | 21 | 22 | 23 | 重要的是要理解`AsyncSequence`只是一个协议。它定义了如何访问值,但并不产生或包含值。`AsyncSequence`协议的实现者提供了一个`AsyncIterator`,并负责开发和潜在地存储值。 24 | 25 | 26 | 27 | ## 创建一个自定义的 AsyncSequence 28 | 29 | 为了更好地理解`AsyncSequence`是如何工作的,我将演示一个实现实例。然而,在定义你的`AsyncSequence`的自定义实现时,你可能想用`AsyncStream`来代替,因为它的设置更方便。因此,这只是一个代码例子,以更好地理解`AsyncSequence`的工作原理。 30 | 31 | 32 | 下面的例子沿用了原始提案中的例子,实现了一个计数器。这些值可以立即使用,所以对异步序列没有太大的需求。然而,它确实展示了一个异步序列的基本结构: 33 | 34 | ```swift 35 | struct Counter: AsyncSequence { 36 | typealias Element = Int 37 | 38 | let limit: Int 39 | 40 | struct AsyncIterator : AsyncIteratorProtocol { 41 | let limit: Int 42 | var current = 1 43 | mutating func next() async -> Int? { 44 | guard !Task.isCancelled else { 45 | return nil 46 | } 47 | 48 | guard current <= limit else { 49 | return nil 50 | } 51 | 52 | let result = current 53 | current += 1 54 | return result 55 | } 56 | } 57 | 58 | func makeAsyncIterator() -> AsyncIterator { 59 | return AsyncIterator(howHigh: limit) 60 | } 61 | } 62 | ``` 63 | 64 | 如您所见,我们定义了一个实现 `AsyncSequence` 协议的` Counter` 结构体。该协议要求我们返回一个自定义的 `AsyncIterator`,我们使用内部类型解决了这个问题。我们可以决定重写此示例以消除对内部类型的需求: 65 | 66 | ```swift 67 | struct Counter: AsyncSequence, AsyncIteratorProtocol { 68 | typealias Element = Int 69 | 70 | let limit: Int 71 | var current = 1 72 | 73 | mutating func next() async -> Int? { 74 | guard !Task.isCancelled else { 75 | return nil 76 | } 77 | 78 | guard current <= limit else { 79 | return nil 80 | } 81 | 82 | let result = current 83 | current += 1 84 | return result 85 | } 86 | 87 | func makeAsyncIterator() -> Counter { 88 | self 89 | } 90 | } 91 | ``` 92 | 93 | 我们现在可以将`self`作为迭代器返回,并保持所有逻辑的集中。 94 | 95 | 注意,我们必须通过提供[typealias](https://www.avanderlee.com/swift/typealias-usage-swift/)来帮助编译器遵守`AsyncSequence`协议。 96 | 97 | 98 | 99 | `next()`方法负责对整体数值进行迭代。我们的例子归结为提供尽可能多的计数值,直到我们达到极限。我们通过对`Task.isCancelled`的检查来实现取消支持。[你可以在这里阅读更多关于任务和取消的信息](https://www.avanderlee.com/concurrency/tasks/#handling-cancellation)。 100 | 101 | 102 | 103 | ## 异步序列的迭代 104 | 105 | 现在我们知道了什么是`AsyncSequence`以及它是如何实现的,现在是时候开始迭代这些值了。 106 | 107 | 108 | 109 | 以上述例子为例,我们可以使用`Counter`开始迭代: 110 | 111 | ```swift 112 | for await count in Counter(limit: 5) { 113 | print(count) 114 | } 115 | print("Counter finished") 116 | 117 | // Prints: 118 | // 1 119 | // 2 120 | // 3 121 | // 4 122 | // 5 123 | // Counter finished 124 | ``` 125 | 126 | 我们必须使用 `await `关键字,因为我们可能会异步接收数值。一旦不再有预期的值,我们就退出for循环。异步序列的实现者可以通过在`next()`方法中返回`nil`来表示达到极限。在我们的例子中,一旦计数器达到配置的极限,或者迭代取消,我们就会达到这个预期: 127 | 128 | ```swift 129 | mutating func next() async -> Int? { 130 | guard !Task.isCancelled else { 131 | return nil 132 | } 133 | 134 | guard current <= limit else { 135 | return nil 136 | } 137 | 138 | let result = current 139 | current += 1 140 | return result 141 | } 142 | ``` 143 | 144 | 许多常规的序列操作符也可用于异步序列。其结果是,我们可以以异步的方式执行映射和过滤等操作。 145 | 146 | 147 | 148 | 例如,我们可以只对偶数进行过滤: 149 | 150 | ```swift 151 | for await count in Counter(limit: 5).filter({ $0 % 2 == 0 }) { 152 | print(count) 153 | } 154 | print("Counter finished") 155 | 156 | // Prints: 157 | // 2 158 | // 4 159 | // Counter finished 160 | ``` 161 | 162 | 或者我们可以在迭代之前将计数映射为一个`String`: 163 | 164 | ```swift 165 | let counterStream = Counter(limit: 5) 166 | .map { $0 % 2 == 0 ? "Even" : "Odd" } 167 | for await count in counterStream { 168 | print(count) 169 | } 170 | print("Counter finished") 171 | 172 | // Prints: 173 | // Odd 174 | // Even 175 | // Odd 176 | // Even 177 | // Odd 178 | // Counter finished 179 | ``` 180 | 181 | 182 | 183 | 我们甚至可以使用`AsyncSequence`而不使用for循环,通过使用`contains`等方法。 184 | 185 | ```swift 186 | let contains = await Counter(limit: 5).contains(3) 187 | print(contains) // Prints: true 188 | ``` 189 | 190 | *注意*,上述方法是异步的,意味着它有可能无休止地等待一个值的存在,直到底层的`AsyncSequence`完成。 191 | 192 | 193 | 194 | ## 继续你的Swift并发之旅 195 | 196 | 如果你喜欢你所读到的关于异步序列的内容,你可能也会喜欢其他的并发主题: 197 | 198 | - [Sendable and @Sendable closures explained with code examples](https://www.avanderlee.com/swift/sendable-protocol-closures/) 199 | - [AsyncSequence explained with Code Examples](https://www.avanderlee.com/concurrency/asyncsequence/) 200 | - [AsyncThrowingStream and AsyncStream explained with code examples](https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/) 201 | - [Tasks in Swift explained with code examples](https://www.avanderlee.com/concurrency/tasks/) 202 | - [Async await in Swift explained with code examples](https://www.avanderlee.com/swift/async-await/) 203 | - [Nonisolated and isolated keywords: Understanding Actor isolation](https://www.avanderlee.com/swift/nonisolated-isolated/) 204 | - [Async let explained: call async functions in parallel](https://www.avanderlee.com/swift/async-let-asynchronous-functions-in-parallel/) 205 | - [MainActor usage in Swift explained to dispatch to the main thread](https://www.avanderlee.com/swift/mainactor-dispatch-main-thread/) 206 | - [Actors in Swift: how to use and prevent data races](https://www.avanderlee.com/swift/actors/) 207 | 208 | 209 | 210 | ## 结论 211 | 212 | `AsyncSequence`是我们在Swift中熟悉的常规`Sequence`的异步替代品。就像你不会经常自己创建一个自定义`Sequence`一样,你也不太可能创建自定义的异步序列。相反,我建议你看一下[AsyncStreams](https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/)。 -------------------------------------------------------------------------------- /resource/21 Swift AsyncThrowingStream 和 AsyncStream ——— 代码实例详解.md: -------------------------------------------------------------------------------- 1 | - [AsyncThrowingStream and AsyncStream explained with code examples](https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/) 2 | 3 | 4 | 5 | `AsyncThrowingStream` 和 `AsyncStream`是Swift 5.5中由[SE-314](https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md)引入的并发框架的一部分。异步流允许你替换基于闭包或 Combine 发布器的现有代码。 6 | 7 | 8 | 9 | 在深入研究围绕抛出流的细节之前,如果你还没有阅读我的文章,我建议你先阅读我的文章,内容包括async-await。本文解释的大部分代码将使用那里解释的API。 10 | 11 | 12 | 13 | ## 什么是 AsyncThrowingStream? 14 | 15 | 你可以把 `AsyncThrowingStream` 看作是一个有可能导致抛出错误的元素流。他的值随着时间的推移而传递,流可以通过一个结束事件来关闭。一旦发生错误,结束事件既可以是成功,也可以是失败。 16 | 17 | 18 | 19 | ## 什么是 AsyncStream? 20 | 21 | `AsyncStream` 类似于抛出的变体,但绝不会导致抛出错误。一个非抛出型的异步流会根据明确的完成调用或流的取消而完成。 22 | 23 | 24 | 25 | *在这篇文章中,我们将解释如何使用`AsyncThrowingStream`。除了发生错误处理的部分,代码示例与`AsyncStream`类似。* 26 | 27 | 28 | 29 | ## 如何使用 AsyncThrowingStream 30 | 31 | `AsyncThrowingStream`可以很好地替代现有的基于闭包的代码,如进度和完成处理程序。为了更好地理解我的意思,我将向你介绍我们在 WeTransfer 应用程序中遇到的一个场景。 32 | 33 | 34 | 35 | 在我们的应用程序中,我们有一个基于闭包的现有类,叫做`FileDownloader`: 36 | 37 | ```swift 38 | struct FileDownloader { 39 | enum Status { 40 | case downloading(Float) 41 | case finished(Data) 42 | } 43 | 44 | func download(_ url: URL, progressHandler: (Float) -> Void, completion: (Result) -> Void) throws { 45 | // .. Download implementation 46 | } 47 | } 48 | ``` 49 | 50 | 文件下载器接受一个URL,报告进度情况,并完成一个包含下载数据的结果或在失败时显示一个错误。 51 | 52 | 53 | 54 | 文件下载器在文件下载过程中报告一个数值流。在这种情况下,它报告的是一个状态值流,以报告正在运行的下载的当前状态。`FileDownloader`是一个完美的例子,你可以重写一段代码来使用`AsyncThrowingStream`。然而,重写需要你在实现层面上也重写你的代码,所以让我们定义一个重载方法来代替: 55 | 56 | ```swift 57 | extension FileDownloader { 58 | func download(_ url: URL) -> AsyncThrowingStream { 59 | return AsyncThrowingStream { continuation in 60 | do { 61 | try self.download(url, progressHandler: { progress in 62 | continuation.yield(.downloading(progress)) 63 | }, completion: { result in 64 | switch result { 65 | case .success(let data): 66 | continuation.yield(.finished(data)) 67 | continuation.finish() 68 | case .failure(let error): 69 | continuation.finish(throwing: error) 70 | } 71 | }) 72 | } catch { 73 | continuation.finish(throwing: error) 74 | } 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | 正如你所看到的,我们把下载方法包裹在一个`AsyncThrowingStream`里面。我们将流的值`Status`的类型描述为一个通用的类型,允许我们用状态更新来延续流。 81 | 82 | 83 | 84 | 只要有错误发生,我们就会通过抛出一个错误来完成流。在完成处理程序的情况下,我们要么通过抛出一个错误来完成,要么用一个不抛出的完成回调来跟进数据的产生。 85 | 86 | ```swift 87 | switch result { 88 | case .success(let data): 89 | continuation.yield(.finished(data)) 90 | continuation.finish() 91 | case .failure(let error): 92 | continuation.finish(throwing: error) 93 | } 94 | ``` 95 | 96 | 在收到最后的状态更新后,不要忘记`finish()`回调,这一点至关重要。否则,我们将保持流的存活,而实现层面的代码将永远不会继续。 97 | 98 | 99 | 100 | 我们可以通过使用另一个`yield`方法来重写上述代码,接受一个`Result`枚举作为参数: 101 | 102 | ```swift 103 | continuation.yield(with: result.map { .finished($0) }) 104 | continuation.finish() 105 | ``` 106 | 107 | 重写后的代码简化了我们的代码,并去掉了switch-case 代码。我们必须映射我们的`Reslut`枚举以匹配预期的`Status`值。如果我们产生一个失败的结果,我们的流将在抛出包含的错误后结束。 108 | 109 | 110 | 111 | ## AsyncThrowingStream 迭代 112 | 113 | 一旦你配置好你的异步抛出流,你就可以开始在数值流上进行迭代。在我们的`FileDownloader`例子中,它将看起来如下所示: 114 | 115 | ```swift 116 | do { 117 | for try await status in download(url) { 118 | switch status { 119 | case .downloading(let progress): 120 | print("Downloading progress: \(progress)") 121 | case .finished(let data): 122 | print("Downloading completed with data: \(data)") 123 | } 124 | } 125 | print("Download finished and stream closed") 126 | } catch { 127 | print("Download failed with \(error)") 128 | } 129 | ``` 130 | 131 | 132 | 133 | 我们处理任何状态的更新,并且我们可以使用`catch`闭包来处理任何发生的错误。你可以使用基于`AsyncSequence`接口的`for ... in`循环进行迭代,这对`AsyncStream`来说是一样的。 134 | 135 | 136 | 137 | *如果你遇到了类似的编译错误:* 138 | 139 | > ‘async’ in a function that does not support concurrency 140 | 141 | 你可能想读一读我的文章,其中[深入介绍了async-await](https://www.avanderlee.com/swift/async-await/)。 142 | 143 | 144 | 145 | 上述代码示例中的打印语句有助于你理解 `AsyncThrowingStream`的生命周期。你可以替换打印语句来处理进度更新和处理数据,为你的用户实现可视化。 146 | 147 | 148 | 149 | ## 调试 AsyncStream 150 | 151 | 如果一个流不能报告数值,我们可以通过放置断点来调试流产生的回调。虽然也可能是上面的*“Download finished and stream closed”* 的打印语句不会调用,这意味着你在实现层的代码永远不会继续。后者可能是一个未完成的流的结果。 152 | 153 | 154 | 155 | 为了验证,我们可以利用`onTermination`回调: 156 | 157 | ```swift 158 | func download(_ url: URL) -> AsyncThrowingStream { 159 | return AsyncThrowingStream { continuation in 160 | 161 | /// 配置一个终止回调,以了解你的流的生命周期。 162 | continuation.onTermination = { @Sendable status in 163 | print("Stream terminated with status \(status)") 164 | } 165 | 166 | // .. 167 | } 168 | } 169 | ``` 170 | 171 | 回调在流终止时被调用,它将告诉你你的流是否还活着。我推荐你阅读[Sendable 和 @Sendable 闭包——代码实例详解](https://www.avanderlee.com/swift/sendable-protocol-closures/)来理解`@Sendable`属性。 172 | 173 | 174 | 175 | 如果出现了错误,输出结果可能如下: 176 | 177 | ```swift 178 | Stream terminated with status finished(Optional(FileDownloader.FileDownloadingError.example)) 179 | ``` 180 | 181 | 上述输出只有在使用`AsyncThrowingStream`时才能实现。如果是一个普通的`AsyncStream`,完成的输出看起来如下: 182 | 183 | ```swift 184 | Stream terminated with status finished 185 | ``` 186 | 187 | 而取消的结果对这两种类型的流来说都是这样的: 188 | 189 | ```swift 190 | Stream terminated with status cancelled 191 | ``` 192 | 193 | 你也可以在流结束后使用这个终止回调进行任何清理。例如,删除任何观察者或在文件下载后清理磁盘空间。 194 | 195 | 196 | 197 | ## 取消一个 AsyncStream 198 | 199 | 一个`AsyncStream`或`AsyncThrowingStream`可以由于一个封闭的任务被取消而取消。一个例子可以如下: 200 | 201 | ```swift 202 | let task = Task.detached { 203 | do { 204 | for try await status in download(url) { 205 | switch status { 206 | case .downloading(let progress): 207 | print("Downloading progress: \(progress)") 208 | case .finished(let data): 209 | print("Downloading completed with data: \(data)") 210 | } 211 | } 212 | } catch { 213 | print("Download failed with \(error)") 214 | } 215 | } 216 | task.cancel() 217 | ``` 218 | 219 | 一个流在超出范围或包围的任务取消时就会取消。如前所述,取消将相应地触发`onTermination`回调。 220 | 221 | 222 | 223 | ## 继续你的Swift并发之旅 224 | 225 | 如果你喜欢你所读到的关于异步流的内容,你可能也会喜欢其他的并发主题: 226 | 227 | - [Sendable and @Sendable closures explained with code examples](https://www.avanderlee.com/swift/sendable-protocol-closures/) 228 | - [AsyncSequence explained with Code Examples](https://www.avanderlee.com/concurrency/asyncsequence/) 229 | - [AsyncThrowingStream and AsyncStream explained with code examples](https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/) 230 | - [Tasks in Swift explained with code examples](https://www.avanderlee.com/concurrency/tasks/) 231 | - [Async await in Swift explained with code examples](https://www.avanderlee.com/swift/async-await/) 232 | - [Nonisolated and isolated keywords: Understanding Actor isolation](https://www.avanderlee.com/swift/nonisolated-isolated/) 233 | - [Async let explained: call async functions in parallel](https://www.avanderlee.com/swift/async-let-asynchronous-functions-in-parallel/) 234 | - [MainActor usage in Swift explained to dispatch to the main thread](https://www.avanderlee.com/swift/mainactor-dispatch-main-thread/) 235 | - [Actors in Swift: how to use and prevent data races](https://www.avanderlee.com/swift/actors/) 236 | 237 | 238 | 239 | ## 结论 240 | 241 | `AsyncThrowingStream`或`AsyncStream`是重写基于闭包的现有代码到支持 async-awai t的替代品的好方法。你可以提供一个连续的值流,并在成功或失败时完成一个流。你可以使用基于`AsyncSequence` APIs的 for 循环在实现层面上迭代值。 -------------------------------------------------------------------------------- /resource/22 Swift 中的 MainActor使用和主线程调度.md: -------------------------------------------------------------------------------- 1 | [MainActor usage in Swift explained to dispatch to the main thread](https://www.avanderlee.com/swift/mainactor-dispatch-main-thread/) 2 | 3 | 4 | 5 | MainActor 是Swift 5.5中引入的一个新属性,它是一个全局 actor,提供一个在主线程上执行任务的执行器。在构建应用程序时,在主线程上执行UI更新任务是很重要的,在使用几个后台线程时,这有时会很有挑战性。使用`@MainActor`属性将帮助你确保你的UI总是在主线程上更新。 6 | 7 | 8 | 9 | 如果您不熟悉 Swift 中的 Actors,我建议您阅读我的文章[Swift中的Actors 使用以如何及防止数据竞争](https://www.avanderlee.com/swift/actors/),全局Actors的行为类似于Actors,我不会在这篇文章中详细介绍Actors的工作方式。 10 | 11 | 12 | 13 | ## 什么是 MainActor? 14 | 15 | MainActor 是一个全局唯一的 Actor,他在主线程上执行他的任务。它应该被用于属性、方法、实例和闭包,以在主线程上执行任务。提案SE-0316 全局Actor 引入了 MainActor,作为其全局 Actor 的一个例子,它继承了`GlobalActor`协议。 16 | 17 | 18 | 19 | ## 理解全局 Actors 20 | 21 | 全局 Actor 可以看作是单例:每个只有一个实例。如果你的Xcode不支持,请升级到最新版本或者通过启用实验并发来工作。您可以通过在 Xcode 的构建设置中将以下值添加到“Other Swift Flags”中来实现: 22 | 23 | ````swift 24 | -Xfrontend -enable-experimental-concurrency 25 | ```` 26 | 27 | 我们可以定义我们自己的全局 Actor 如下: 28 | 29 | ```swift 30 | @globalActor 31 | actor SwiftLeeActor { 32 | static let shared = SwiftLeeActor() 33 | } 34 | ``` 35 | 36 | 37 | 共享属性是`GlobalActor`协议的一个要求,它可以确保有一个全球唯一的角色实例。一旦被定义,你就可以在整个项目中使用全局Actor,就像你对其他 Actor 一样: 38 | 39 | ```swift 40 | @SwiftLeeActor 41 | final class SwiftLeeFetcher { 42 | // .. 43 | } 44 | ``` 45 | 46 | 47 | 48 | ## 如何在 Swift 中使用 `MainActor`? 49 | 50 | 全局actor可以与属性、方法、闭包和实例一起使用。例如,我们可以将 `MainActor `属性添加到视图模型中,以使其在主线程上执行所有任务: 51 | 52 | ```swift 53 | @MainActor 54 | final class HomeViewModel { 55 | // .. 56 | } 57 | ``` 58 | 59 | 60 | 61 | 使用[nonisolated](https://www.avanderlee.com/swift/actors/#nonisolated-access-within-actors),我们可以确保没有主线程要求的方法尽可能快地执行。如果一个类没有父类,父类使用相同的全局actor注释,或者父类是NSObject,则只能使用全局actor进行注释。 全局 Actor 注释的类的子类必须与同一个全局 Actor 隔离。 62 | 63 | 64 | 65 | 在其他情况下,我们可能希望使用全局Actor定义单个属性: 66 | 67 | ```swift 68 | final class HomeViewModel { 69 | 70 | @MainActor var images: [UIImage] = [] 71 | 72 | } 73 | ``` 74 | 75 | 用`@MainActor`属性标记`images`属性,可以确保它只能从主线程更新: 76 | 77 | ![The MainActor attribute requirements are enforced by the compiler.](https://www.avanderlee.com/wp-content/uploads/2021/07/mainactor_global_actor-1024x195.png) 78 | 79 | 编译器执行MainActor的属性要求,可使用如下代码修复错误: 80 | 81 | ```swift 82 | final class HomeViewModel { 83 | @MainActor var images: [UIImage] = [] 84 | func updateImages() async { 85 | await MainActor.run { 86 | images = [] 87 | } 88 | } 89 | } 90 | // OR 91 | final class HomeViewModel { 92 | @MainActor var images: [UIImage] = [] 93 | @MainActor 94 | func updateImages() { 95 | images = [] 96 | } 97 | } 98 | ``` 99 | 100 | 101 | 102 | 单独的方法也可以用该属性进行标记: 103 | 104 | ```swift 105 | @MainActor func updateViews() { 106 | // Perform UI updates.. 107 | } 108 | ``` 109 | 110 | 甚至可以将闭包标记为在主线程上执行: 111 | 112 | ```swift 113 | func updateData(completion: @MainActor @escaping () -> ()) { 114 | /// Example dispatch to mimic behaviour 115 | DispatchQueue.global().async { 116 | async { 117 | await completion() 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | 124 | 125 | ## 直接使用 MainActor 126 | 127 | Swift 中的 MainActor 带有一个可以直接使用 Actor 的扩展: 128 | 129 | ```swift 130 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) 131 | extension MainActor { 132 | 133 | /// Execute the given body closure on the main actor. 134 | public static func run(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T 135 | } 136 | ``` 137 | 138 | 这允许我们直接在方法中使用 MainActor,即使我们没有使用全局 actor 属性定义它的任何主体: 139 | 140 | ```swift 141 | async { 142 | await MainActor.run { 143 | // Perform UI updates 144 | } 145 | } 146 | ``` 147 | 148 | 换句话说,不再需要使用 `DispatchQueue.main.async`了。 149 | 150 | 151 | 152 | ## 我应该在什么时候使用MainActor属性? 153 | 154 | 在 Swift 5.5 之前,你可能定义了很多调度语句,以确保任务在主线程上运行。一个例子可能是这样的: 155 | 156 | ```swift 157 | func fetchData(completion: @escaping (Result<[UIImage], Error>) -> Void) { 158 | URLSession.shared.dataTask(with: URL(string: "..some URL")) { data, response, error in 159 | // .. Decode data to a result 160 | 161 | DispatchQueue.main.async { 162 | completion(result) 163 | } 164 | } 165 | } 166 | ``` 167 | 168 | 在上面的例子中,我们很确定需要一个调度。然而,在其他情况下,调度可能是不必要的,因为我们已经在主线程上。这样做会导致额外的调度被跳过。 169 | 170 | 171 | 172 | 无论哪种方式,在这些情况下,将属性、方法、实例或闭包定义为一个主行为体是有意义的,以确保任务在主线程上执行。例如,我们可以把上面的例子改写成如下: 173 | 174 | ```swift 175 | func fetchData(completion: @MainActor @escaping (Result<[UIImage], Error>) -> Void) { 176 | URLSession.shared.dataTask(with: URL(string: "..some URL")!) { data, response, error in 177 | // .. Decode data to a result 178 | let result: Result<[UIImage], Error> = .success([]) 179 | 180 | async { 181 | await completion(result) 182 | } 183 | } 184 | } 185 | ``` 186 | 187 | 由于我们现在使用的是一个actor定义的闭包,我们需要使用 async-await 技术来调用我们的闭包。在这里使用`@MainActor`属性可以让Swift编译器对我们的代码进行性能优化。 188 | 189 | 190 | 191 | ### 选择正确的策略 192 | 193 | 使用 actors 时选择正确的策略很重要。在上面的例子中,我们决定让闭包成为一个actor,这意味着无论谁使用我们的方法,完成回调都将使用 `MainActor` 执行。在某些情况下,如果数据请求方法也是从一个不需要在主线程上处理完成回调的地方使用,这可能就没有意义了。 194 | 195 | 196 | 197 | 在这些情况下,让实现者负责调度到正确的队列可能会更好。 198 | 199 | ```swift 200 | viewModel.fetchData { result in 201 | async { 202 | await MainActor.run { 203 | // Handle result 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | 210 | 211 | 继续你的Swift并发之旅 212 | 213 | 并发的变化不仅仅是 async-await,还包括许多新的功能,你可以从你的代码中受益。所以,当你在做这件事的时候,为什么不深入研究一下其他的并发功能呢? 214 | 215 | - [Sendable and @Sendable closures explained with code examples](https://www.avanderlee.com/swift/sendable-protocol-closures/) 216 | - [AsyncSequence explained with Code Examples](https://www.avanderlee.com/concurrency/asyncsequence/) 217 | - [AsyncThrowingStream and AsyncStream explained with code examples](https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/) 218 | - [Tasks in Swift explained with code examples](https://www.avanderlee.com/concurrency/tasks/) 219 | - [Async await in Swift explained with code examples](https://www.avanderlee.com/swift/async-await/) 220 | - [Nonisolated and isolated keywords: Understanding Actor isolation](https://www.avanderlee.com/swift/nonisolated-isolated/) 221 | - [Async let explained: call async functions in parallel](https://www.avanderlee.com/swift/async-let-asynchronous-functions-in-parallel/) 222 | - [MainActor usage in Swift explained to dispatch to the main thread](https://www.avanderlee.com/swift/mainactor-dispatch-main-thread/) 223 | - [Actors in Swift: how to use and prevent data races](https://www.avanderlee.com/swift/actors/) 224 | 225 | 226 | 227 | ## 结论 228 | 229 | 全局Actor是对Swift中的Actor的一个很好的补充。它允许我们重用常见的Actor,并使UI任务的执行成为可能,因为编译器可以在内部优化我们的代码。全局Actor可以用在属性、方法、实例和闭包上,之后编译器会确保要求在我们的代码中得到保证。 230 | -------------------------------------------------------------------------------- /resource/25 Flutter 多引擎渲染,在稿定 App 的实践.md: -------------------------------------------------------------------------------- 1 | # Flutter 多引擎渲染,在稿定 App 的实践 2 | 3 | 发这篇文章的原因主要是关于 [multiple-flutters]() Flutter 多引擎的介绍也好,实践也好,可参考的资源实在太少,包括官方的 issues 也没很多有价值的信息,前几个月确实在坑的泥潭里死去活来。但好在已经走出了一条羊肠小道,可供大家参考。 4 | 5 | 对于 Flutter 多引擎的优劣,笔者在这里不多做介绍,只说最重要的一点:如果有 Native + Flutter 同一页面混合布局的需求(UI 一致性 / 降本增效),但又不能整个 App 或者整个页面替换成 Flutter 的,可以考虑使用 FlutterEngineGroup(multiple-flutters)。 6 | 7 | 闲话少说,先看效果。 8 | 9 | ## APP 展示 10 | 11 | ![1660267286030.jpg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b783d937daef45fa8bdafbca059b96d2~tplv-k3u1fbpfcp-watermark.image?) 12 | 13 | 如上图红框处,即为4个不同引擎的 FlutterView,绘制在同一个 Native 布局中。 14 | 15 | 篇幅有限,就不发视频了,有兴趣的同学可以下载 “稿定设计” 来看下效果(不过还在 AB 放量阶段,不一定能看到新版模版页哈~)。 16 | 17 | ## 为什么市面多引擎用的人那么少? 18 | 19 | > multiple-flutters 绝对是 Flutter 的坑中之王 20 | 21 | 首先,Flutter 版本至少升级到 2.10+,才能有初步的 iOS / Android 多引擎同时布局的可用性。 22 | 但建议升级到 Flutter 3+ ,2.5.3 ~ 2.10.5 版本中,iOS 有内存崩溃风险,详细原因可以见同事发的这篇 [解决 Flutter 引起的 iOS 内存崩溃问题](https://juejin.cn/post/7123765829929762847)。 23 | 24 | > 第一次渡劫历程: 25 | > 26 | > 先是接入 FlutterEngineGroup 时发现,编译没有问题,但就是死活无法正常显示 FlutterView,翻查了大量资料(也没什么有用的资料),跟 Flutter 官方 Demo 对比等方式,耗时2天,最后只能锁定在 Flutter 版本或者 flutter_boost 的问题上,死马当作活马医,直接硬干升级 Flutter 到最新版(2.10.2)及相关依赖升级,发现 Debug 正常了 ... 27 | > 28 | > 再就是在打包 flutter Android 时又发现, flutter_boost 报错,从 github issues 了解到,flutter_boost 并没去支持 Flutter 2.10.x,且还有闪白屏问题。根据 issues 建议,2.8+版本上存在 Release 包不可用的问题,推荐降低到 2.5.3,这才总算是从 FlutterEngineGroup 初步落地的可行性坑中爬了出来。 29 | > 30 | > 因为 2.5.3 同时布局多个 FlutterEngine (3 ~ 10 个不等),导致会发生 ANR 的现象,在寻找解决方案无果的情况下,尝试升级到当时最新版本 Flutter 2.10.5 ,结果正常了 31 | 32 | 这在升级过程中还遇到另一个问题,笔者公司项目里还有很多 flutter_boost 的实现,而 flutter_boost 由于某些原因(可以见他们的 issues) 不支持 Flutter 2.5.3 以上版本。那就还需 Fork 下 flutter_boost 进行修改才可正常使用。 33 | 34 | ### FlutterEngineGroup 离实用太远 35 | 36 | - 缺乏内部固定布局方式,只能通过外部布局位置大小来让 FlutterView 自适应。 37 | - 通信层极其繁琐,从有限的 Demo 中看出需三端各自实现 Bridge Channel。桥方法通过“字符串”作为对应类型,导致个性化开发维护成本非常高。 38 | - 应用场景狭窄,多 FlutterEngine 间只能通过 Native 交互通信。 39 | - Flutter Debug 模式下多引擎 = 内存炸裂,要用 Flutter Release 才可以稳定正常到官方描述的 180K / Engine 的内存占用效果 40 | 41 | ## 我们是怎么做的 42 | 43 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c8f71ba78fa74b718e3b5a50f5d2d877~tplv-k3u1fbpfcp-watermark.image?) 44 | 45 | 利用脚本开发了一套 FGUIComponentAPI 工具链来链接 Native 与 Flutter UI 的关系。 46 | 47 | - 保证 Flutter 开发无感,对于 Flutter 来说,和通常一样开发 UI,并可以在独立调试中直接验证效果。 48 | - 保证 Native 开发无感,对于 Native 来说,只是直接引用使用源生类,无需关心其中实现,开箱即用。 49 | - 额外的带来的好处就是天然的 UI 单元测试,并且只要 Flutter 一端验证即可。 50 | 51 | > 这里特别说一下,内置了官方推荐的 pigeon 插件来处理 model 传输的问题,但 pigeon 插件执行起来效率不高,越多的组件执行起来就越慢,所以后面又增加了文件对比跳过的功能来加速。后续考虑替换掉 pigeon,不用 dart 来实现,改用 python 就能解决效率问题。 52 | 53 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a338910bc1141f293951aec79287ea9~tplv-k3u1fbpfcp-watermark.image?) 54 | 55 | 在开发过程上,笔者使用 YAML 来定义 UI 组件,通过 FGUIComponentAPI 多向生成各类代码及服务。 56 | 57 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/80b309b818144ae19eabc83add4f51af~tplv-k3u1fbpfcp-watermark.image?) 58 | 59 | 上图即为自动生成的开发文档,可以看到 Native 调用上是完全无感知的,右侧的预览页面也是天然使用 Flutter 跨端 Web 的能力,直接把 Flutter Example 输出在文档上。 60 | 61 | ### 还有多少坑 62 | 63 | 笔者也还在一步一踩。 64 | 65 | 比如市面上常见的 pub 也要慎用,特别是有跟 Native 交互的插件,基本上都没有考虑多引擎实现的。 66 | 举个例子,常用的 flutter_cache_manager,它因为使用了 sqlite 数据库做存储,在多引擎同时布局的情况下,Android 设备可能会出现数据库等待导致图片缓存写入/读取失败的问题(当然可以通过定义 cachedKey 来指定使用不同的 db 来解决)。这其实也不是第三方库的问题,而是多引擎市面真实使用的人太少的缘故,没有需求就没有市场。 67 | 68 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/128f5575082d48e6a4dc46ba2be63567~tplv-k3u1fbpfcp-watermark.image?) 69 | 70 | 可以看到笔者已经快踩完整个字母表了 ... 手动狗头 71 | 72 | 篇幅有限,这里不展开说明了,如果有需求的同学可以下方评论,人数多的话单独开一篇来介绍如何优雅的避开其中的坑坑坑坑坑炕钪锟烫烫烫 73 | 74 | ## 后续 75 | 76 | 感谢大家厚爱,会逐步推出后续更详细的内容 77 | 78 | > 作者投稿 79 | > 80 | > 来源:[Flutter 多引擎渲染,在稿定 App 的实践](https://juejin.cn/post/7130811413840429093) -------------------------------------------------------------------------------- /resource/25 Swift AsyncThrowingStream 和 AsyncStream ——— 代码实例详解.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | `AsyncThrowingStream` 和 `AsyncStream`是Swift 5.5中由[SE-314](https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md "SE-314")引入的并发框架的一部分。异步流允许你替换基于闭包或 Combine 发布器的现有代码。 4 | 5 | 在深入研究围绕抛出流的细节之前,如果你还没有阅读我的文章,我建议你先阅读我的文章,内容包括async-await。本文解释的大部分代码将使用那里解释的API。 6 | 7 | ## 什么是 AsyncThrowingStream? 8 | 9 | 你可以把 `AsyncThrowingStream` 看作是一个有可能导致抛出错误的元素流。他的值随着时间的推移而传递,流可以通过一个结束事件来关闭。一旦发生错误,结束事件既可以是成功,也可以是失败。 10 | 11 | ## 什么是 AsyncStream? 12 | 13 | `AsyncStream` 类似于抛出的变体,但绝不会导致抛出错误。一个非抛出型的异步流会根据明确的完成调用或流的取消而完成。 14 | 15 | **注意:** 在这篇文章中,我们将解释如何使用`AsyncThrowingStream`。除了发生错误处理的部分,代码示例与`AsyncStream`类似。 16 | 17 | ## AsyncThrowingStream 18 | 19 | **如何使用 AsyncThrowingStream** 20 | 21 | `AsyncThrowingStream` 可以很好地替代现有的基于闭包的代码,如进度和完成处理程序。为了更好地理解我的意思,我将向你介绍我们在 WeTransfer 应用程序中遇到的一个场景。 22 | 23 | 在我们的应用程序中,我们有一个基于闭包的现有类,叫做 `FileDownloader`: 24 | 25 | ```swift 26 | struct FileDownloader { 27 | enum Status { 28 | case downloading(Float) 29 | case finished(Data) 30 | } 31 | 32 | func download(_ url: URL, progressHandler: (Float) -> Void, completion: (Result) -> Void) throws { 33 | // .. Download implementation 34 | } 35 | } 36 | ``` 37 | 38 | 文件下载器接受一个URL,报告进度情况,并完成一个包含下载数据的结果或在失败时显示一个错误。 39 | 40 | 文件下载器在文件下载过程中报告一个数值流。在这种情况下,它报告的是一个状态值流,以报告正在运行的下载的当前状态。`FileDownloader` 是一个完美的例子,你可以重写一段代码来使用 `AsyncThrowingStream`。然而,重写需要你在实现层面上也重写你的代码,所以让我们定义一个重载方法来代替: 41 | 42 | ```swift 43 | extension FileDownloader { 44 | func download(_ url: URL) -> AsyncThrowingStream { 45 | return AsyncThrowingStream { continuation in 46 | do { 47 | try self.download(url, progressHandler: { progress in 48 | continuation.yield(.downloading(progress)) 49 | }, completion: { result in 50 | switch result { 51 | case .success(let data): 52 | continuation.yield(.finished(data)) 53 | continuation.finish() 54 | case .failure(let error): 55 | continuation.finish(throwing: error) 56 | } 57 | }) 58 | } catch { 59 | continuation.finish(throwing: error) 60 | } 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | 正如你所看到的,我们把下载方法包裹在一个 `AsyncThrowingStream` 里面。我们将流的值 `Status` 的类型描述为一个通用的类型,允许我们用状态更新来延续流。 67 | 68 | 只要有错误发生,我们就会通过抛出一个错误来完成流。在完成处理程序的情况下,我们要么通过抛出一个错误来完成,要么用一个不抛出的完成回调来跟进数据的产生。 69 | 70 | ```swift 71 | switch result { 72 | case .success(let data): 73 | continuation.yield(.finished(data)) 74 | continuation.finish() 75 | case .failure(let error): 76 | continuation.finish(throwing: error) 77 | } 78 | ``` 79 | 80 | 在收到最后的状态更新后,不要忘记 `finish()` 回调,这一点至关重要。否则,我们将保持流的存活,而实现层面的代码将永远不会继续。 81 | 82 | 我们可以通过使用另一个 `yield` 方法来重写上述代码,接受一个 `Result` 枚举作为参数: 83 | 84 | ```swift 85 | continuation.yield(with: result.map { .finished($0) }) 86 | continuation.finish() 87 | ``` 88 | 89 | 重写后的代码简化了我们的代码,并去掉了 switch-case 代码。我们必须映射我们的 `Reslut` 枚举以匹配预期的 `Status` 值。如果我们产生一个失败的结果,我们的流将在抛出包含的错误后结束。 90 | 91 | ## AsyncThrowingStream 迭代 92 | 93 | 一旦你配置好你的异步抛出流,你就可以开始在数值流上进行迭代。在我们的 `FileDownloader` 例子中,它将看起来如下所示: 94 | 95 | ```swift 96 | do { 97 | for try await status in download(url) { 98 | switch status { 99 | case .downloading(let progress): 100 | print("Downloading progress: \(progress)") 101 | case .finished(let data): 102 | print("Downloading completed with data: \(data)") 103 | } 104 | } 105 | print("Download finished and stream closed") 106 | } catch { 107 | print("Download failed with \(error)") 108 | } 109 | ``` 110 | 111 | 我们处理任何状态的更新,并且我们可以使用 `catch` 闭包来处理任何发生的错误。你可以使用基于 `AsyncSequence` 接口的 `for ... in` 循环进行迭代,这对 `AsyncStream` 来说是一样的。 112 | 113 | **如果你遇到了类似的编译错误:** 114 | 115 | > ‘async’ in a function that does not support concurrency 116 | 117 | 你可能想读一读我的文章,其中[Swift 中的 async/await ——代码实例详解](https://mp.weixin.qq.com/s/TL1LPvVdhSFxrYCFWQ2LfQ)。 118 | 119 | 上述代码示例中的打印语句有助于你理解 `AsyncThrowingStream` 的生命周期。你可以替换打印语句来处理进度更新和处理数据,为你的用户实现可视化。 120 | 121 | ## 调试 AsyncStream 122 | 123 | 如果一个流不能报告数值,我们可以通过放置断点来调试流产生的回调。虽然也可能是上面的 **“Download finished and stream closed”** 的打印语句不会调用,这意味着你在实现层的代码永远不会继续。后者可能是一个未完成的流的结果。 124 | 125 | 为了验证,我们可以利用 `onTermination` 回调: 126 | 127 | ```swift 128 | func download(_ url: URL) -> AsyncThrowingStream { 129 | return AsyncThrowingStream { continuation in 130 | 131 | /// 配置一个终止回调,以了解你的流的生命周期。 132 | continuation.onTermination = { @Sendable status in 133 | print("Stream terminated with status \(status)") 134 | } 135 | 136 | // .. 137 | } 138 | } 139 | ``` 140 | 141 | 回调在流终止时被调用,它将告诉你你的流是否还活着。我推荐你阅读 [Sendable 和 @Sendable 闭包代码实例详解](https://mp.weixin.qq.com/s/IA9CgMjZf63_RFwNBB9QqQ)来理解 `@Sendable` 属性。 142 | 143 | 如果出现了错误,输出结果可能如下: 144 | 145 | ```swift 146 | Stream terminated with status finished(Optional(FileDownloader.FileDownloadingError.example)) 147 | ``` 148 | 149 | 上述输出只有在使用 `AsyncThrowingStream` 时才能实现。如果是一个普通的 `AsyncStream`,完成的输出看起来如下: 150 | 151 | ```swift 152 | Stream terminated with status finished 153 | ``` 154 | 155 | 而取消的结果对这两种类型的流来说都是这样的: 156 | 157 | ```swift 158 | Stream terminated with status cancelled 159 | ``` 160 | 161 | 你也可以在流结束后使用这个终止回调进行任何清理。例如,删除任何观察者或在文件下载后清理磁盘空间。 162 | 163 | ## 取消一个 AsyncStream 164 | 165 | 一个 `AsyncStream` 或 `AsyncThrowingStream` 可以由于一个封闭的任务被取消而取消。一个例子可以如下: 166 | 167 | ```swift 168 | let task = Task.detached { 169 | do { 170 | for try await status in download(url) { 171 | switch status { 172 | case .downloading(let progress): 173 | print("Downloading progress: \(progress)") 174 | case .finished(let data): 175 | print("Downloading completed with data: \(data)") 176 | } 177 | } 178 | } catch { 179 | print("Download failed with \(error)") 180 | } 181 | } 182 | task.cancel() 183 | ``` 184 | 185 | 一个流在超出范围或包围的任务取消时就会取消。如前所述,取消将相应地触发 `onTermination` 回调。 186 | 187 | ## 继续你的Swift并发之旅 188 | 189 | 如果你喜欢你所读到的关于异步流的内容,你可能也会喜欢其他的并发主题: 190 | 191 | - [Sendable and @Sendable closures explained with code examples](https://www.avanderlee.com/swift/sendable-protocol-closures/) 192 | - [AsyncSequence explained with Code Examples](https://www.avanderlee.com/concurrency/asyncsequence/) 193 | - [AsyncThrowingStream and AsyncStream explained with code examples](https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/) 194 | - [Tasks in Swift explained with code examples](https://www.avanderlee.com/concurrency/tasks/) 195 | - [Async await in Swift explained with code examples](https://www.avanderlee.com/swift/async-await/) 196 | - [Nonisolated and isolated keywords: Understanding Actor isolation](https://www.avanderlee.com/swift/nonisolated-isolated/) 197 | - [Async let explained: call async functions in parallel](https://www.avanderlee.com/swift/async-let-asynchronous-functions-in-parallel/) 198 | - [MainActor usage in Swift explained to dispatch to the main thread](https://www.avanderlee.com/swift/mainactor-dispatch-main-thread/) 199 | - [Actors in Swift: how to use and prevent data races](https://www.avanderlee.com/swift/actors/) 200 | 201 | ## 结论 202 | 203 | `AsyncThrowingStream` 或 `AsyncStream` 是重写基于闭包的现有代码到支持 async-awai t的替代品的好方法。你可以提供一个连续的值流,并在成功或失败时完成一个流。你可以使用基于 `AsyncSequence` APIs 的 for 循环在实现层面上迭代值。 204 | 205 | > 来自: [AsyncThrowingStream and AsyncStream explained with code examples](https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/) -------------------------------------------------------------------------------- /resource/26 Swift 中的 MainActor使用和主线程调度.md: -------------------------------------------------------------------------------- 1 | [MainActor usage in Swift explained to dispatch to the main thread](https://www.avanderlee.com/swift/mainactor-dispatch-main-thread/) 2 | 3 | 4 | 5 | MainActor 是Swift 5.5中引入的一个新属性,它是一个全局 actor,提供一个在主线程上执行任务的执行器。在构建应用程序时,在主线程上执行UI更新任务是很重要的,在使用几个后台线程时,这有时会很有挑战性。使用`@MainActor`属性将帮助你确保你的UI总是在主线程上更新。 6 | 7 | 8 | 9 | 如果您不熟悉 Swift 中的 Actors,我建议您阅读我的文章[Swift中的Actors 使用以如何及防止数据竞争](https://www.avanderlee.com/swift/actors/),全局Actors的行为类似于Actors,我不会在这篇文章中详细介绍Actors的工作方式。 10 | 11 | 12 | 13 | ## 什么是 MainActor? 14 | 15 | MainActor 是一个全局唯一的 Actor,他在主线程上执行他的任务。它应该被用于属性、方法、实例和闭包,以在主线程上执行任务。提案SE-0316 全局Actor 引入了 MainActor,作为其全局 Actor 的一个例子,它继承了`GlobalActor`协议。 16 | 17 | 18 | 19 | ## 理解全局 Actors 20 | 21 | 全局 Actor 可以看作是单例:每个只有一个实例。如果你的Xcode不支持,请升级到最新版本或者通过启用实验并发来工作。您可以通过在 Xcode 的构建设置中将以下值添加到“Other Swift Flags”中来实现: 22 | 23 | ````swift 24 | -Xfrontend -enable-experimental-concurrency 25 | ```` 26 | 27 | 我们可以定义我们自己的全局 Actor 如下: 28 | 29 | ```swift 30 | @globalActor 31 | actor SwiftLeeActor { 32 | static let shared = SwiftLeeActor() 33 | } 34 | ``` 35 | 36 | 37 | 共享属性是`GlobalActor`协议的一个要求,它可以确保有一个全球唯一的角色实例。一旦被定义,你就可以在整个项目中使用全局Actor,就像你对其他 Actor 一样: 38 | 39 | ```swift 40 | @SwiftLeeActor 41 | final class SwiftLeeFetcher { 42 | // .. 43 | } 44 | ``` 45 | 46 | 47 | 48 | ## 如何在 Swift 中使用 `MainActor`? 49 | 50 | 全局actor可以与属性、方法、闭包和实例一起使用。例如,我们可以将 `MainActor `属性添加到视图模型中,以使其在主线程上执行所有任务: 51 | 52 | ```swift 53 | @MainActor 54 | final class HomeViewModel { 55 | // .. 56 | } 57 | ``` 58 | 59 | 60 | 61 | 使用[nonisolated](https://www.avanderlee.com/swift/actors/#nonisolated-access-within-actors),我们可以确保没有主线程要求的方法尽可能快地执行。如果一个类没有父类,父类使用相同的全局actor注释,或者父类是NSObject,则只能使用全局actor进行注释。 全局 Actor 注释的类的子类必须与同一个全局 Actor 隔离。 62 | 63 | 64 | 65 | 在其他情况下,我们可能希望使用全局Actor定义单个属性: 66 | 67 | ```swift 68 | final class HomeViewModel { 69 | 70 | @MainActor var images: [UIImage] = [] 71 | 72 | } 73 | ``` 74 | 75 | 用`@MainActor`属性标记`images`属性,可以确保它只能从主线程更新: 76 | 77 | ![The MainActor attribute requirements are enforced by the compiler.](https://www.avanderlee.com/wp-content/uploads/2021/07/mainactor_global_actor-1024x195.png) 78 | 79 | 编译器执行MainActor的属性要求,可使用如下代码修复错误: 80 | 81 | ```swift 82 | final class HomeViewModel { 83 | @MainActor var images: [UIImage] = [] 84 | func updateImages() async { 85 | await MainActor.run { 86 | images = [] 87 | } 88 | } 89 | } 90 | // OR 91 | final class HomeViewModel { 92 | @MainActor var images: [UIImage] = [] 93 | @MainActor 94 | func updateImages() { 95 | images = [] 96 | } 97 | } 98 | ``` 99 | 100 | 101 | 102 | 单独的方法也可以用该属性进行标记: 103 | 104 | ```swift 105 | @MainActor func updateViews() { 106 | // Perform UI updates.. 107 | } 108 | ``` 109 | 110 | 甚至可以将闭包标记为在主线程上执行: 111 | 112 | ```swift 113 | func updateData(completion: @MainActor @escaping () -> ()) { 114 | /// Example dispatch to mimic behaviour 115 | DispatchQueue.global().async { 116 | async { 117 | await completion() 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | 124 | 125 | ## 直接使用 MainActor 126 | 127 | Swift 中的 MainActor 带有一个可以直接使用 Actor 的扩展: 128 | 129 | ```swift 130 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) 131 | extension MainActor { 132 | 133 | /// Execute the given body closure on the main actor. 134 | public static func run(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T 135 | } 136 | ``` 137 | 138 | 这允许我们直接在方法中使用 MainActor,即使我们没有使用全局 actor 属性定义它的任何主体: 139 | 140 | ```swift 141 | async { 142 | await MainActor.run { 143 | // Perform UI updates 144 | } 145 | } 146 | ``` 147 | 148 | 换句话说,不再需要使用 `DispatchQueue.main.async`了。 149 | 150 | 151 | 152 | ## 我应该在什么时候使用MainActor属性? 153 | 154 | 在 Swift 5.5 之前,你可能定义了很多调度语句,以确保任务在主线程上运行。一个例子可能是这样的: 155 | 156 | ```swift 157 | func fetchData(completion: @escaping (Result<[UIImage], Error>) -> Void) { 158 | URLSession.shared.dataTask(with: URL(string: "..some URL")) { data, response, error in 159 | // .. Decode data to a result 160 | 161 | DispatchQueue.main.async { 162 | completion(result) 163 | } 164 | } 165 | } 166 | ``` 167 | 168 | 在上面的例子中,我们很确定需要一个调度。然而,在其他情况下,调度可能是不必要的,因为我们已经在主线程上。这样做会导致额外的调度被跳过。 169 | 170 | 171 | 172 | 无论哪种方式,在这些情况下,将属性、方法、实例或闭包定义为一个主行为体是有意义的,以确保任务在主线程上执行。例如,我们可以把上面的例子改写成如下: 173 | 174 | ```swift 175 | func fetchData(completion: @MainActor @escaping (Result<[UIImage], Error>) -> Void) { 176 | URLSession.shared.dataTask(with: URL(string: "..some URL")!) { data, response, error in 177 | // .. Decode data to a result 178 | let result: Result<[UIImage], Error> = .success([]) 179 | 180 | async { 181 | await completion(result) 182 | } 183 | } 184 | } 185 | ``` 186 | 187 | 由于我们现在使用的是一个actor定义的闭包,我们需要使用 async-await 技术来调用我们的闭包。在这里使用`@MainActor`属性可以让Swift编译器对我们的代码进行性能优化。 188 | 189 | 190 | 191 | ### 选择正确的策略 192 | 193 | 使用 actors 时选择正确的策略很重要。在上面的例子中,我们决定让闭包成为一个actor,这意味着无论谁使用我们的方法,完成回调都将使用 `MainActor` 执行。在某些情况下,如果数据请求方法也是从一个不需要在主线程上处理完成回调的地方使用,这可能就没有意义了。 194 | 195 | 196 | 197 | 在这些情况下,让实现者负责调度到正确的队列可能会更好。 198 | 199 | ```swift 200 | viewModel.fetchData { result in 201 | async { 202 | await MainActor.run { 203 | // Handle result 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | 210 | 211 | 继续你的Swift并发之旅 212 | 213 | 并发的变化不仅仅是 async-await,还包括许多新的功能,你可以从你的代码中受益。所以,当你在做这件事的时候,为什么不深入研究一下其他的并发功能呢? 214 | 215 | - [Sendable and @Sendable closures explained with code examples](https://www.avanderlee.com/swift/sendable-protocol-closures/) 216 | - [AsyncSequence explained with Code Examples](https://www.avanderlee.com/concurrency/asyncsequence/) 217 | - [AsyncThrowingStream and AsyncStream explained with code examples](https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/) 218 | - [Tasks in Swift explained with code examples](https://www.avanderlee.com/concurrency/tasks/) 219 | - [Async await in Swift explained with code examples](https://www.avanderlee.com/swift/async-await/) 220 | - [Nonisolated and isolated keywords: Understanding Actor isolation](https://www.avanderlee.com/swift/nonisolated-isolated/) 221 | - [Async let explained: call async functions in parallel](https://www.avanderlee.com/swift/async-let-asynchronous-functions-in-parallel/) 222 | - [MainActor usage in Swift explained to dispatch to the main thread](https://www.avanderlee.com/swift/mainactor-dispatch-main-thread/) 223 | - [Actors in Swift: how to use and prevent data races](https://www.avanderlee.com/swift/actors/) 224 | 225 | 226 | 227 | ## 结论 228 | 229 | 全局Actor是对Swift中的Actor的一个很好的补充。它允许我们重用常见的Actor,并使UI任务的执行成为可能,因为编译器可以在内部优化我们的代码。全局Actor可以用在属性、方法、实例和闭包上,之后编译器会确保要求在我们的代码中得到保证。 230 | -------------------------------------------------------------------------------- /resource/27 Flutter 多引擎渲染,在稿定 App 的实践(三):躺坑篇.md: -------------------------------------------------------------------------------- 1 | # Flutter 多引擎渲染,在稿定 App 的实践(三):躺坑篇 2 | 3 | 这一篇会把笔者总结踩过的坑放出来给大家参考,能给要走 Flutter 多引擎之坑的同学一些帮助,不要轻易放弃,总是能走出一条路来。 4 | 5 | ## FAQ 6 | 7 | ### A. Flutter 为什么需要升级到 ~~2.5.3 2.10.5~~ 3.0.5 8 | 9 | 先是在“稿定设计 APP”中接入 FlutterEngineGroup 发现,编译没有问题,但就是死活无法正常显示 FlutterView,翻查了大量资料(也没什么有用的资料),跟 Demo 工程对比等方式,耗时2天,最后只能锁定在 flutter 版本或者 flutter_boost 的问题上,死马当作活马医,直接硬干升级 flutter 到当时最新版(2.10.2)及相关组件,发现正常 ... 10 | 11 | 再就是在打包 flutter Android 时又发现, flutter_boost 报错,从 github issues 了解到,flutter_boost 并没去支持 flutter 2.10.x,且还有闪白屏问题。根据 issues 建议,2.8+版本上存在 Release 包不可用的问题,推荐降低到 2.5.3,这才总算是从 FlutterEngineGroup 初步落地的可行性坑中爬了出来。 12 | 13 | =========== 14 | 15 | 最新,因为 2.5.3 同时布局多个 Engine,导致会发生 ANR 的现象,在寻找解决方案无果的情况下,尝试升级到最新版本 Flutter, 2.10.5 ,结果正常 16 | 17 | =========== 18 | 19 | Flutter 版本 2.5.3+ ~ 3.0.5- 在 iOS 上会有压缩指针释放导致的崩溃问题,所以建议还是升级到 3.0.5 及其以上 20 | 21 | ### B. 官方 Demo 的坑 22 | 23 | 官方 Samples 地址:  24 | 25 | FluterEngineGroup:  26 | 27 | Pigeon:  28 | 29 | 官方 Demo 最大的坑就是 Demo 都是可用的 ... 30 | 31 | Debug 包可用,Release 包会报 engine 配置找不到的白黑屏问题 32 | 33 | ```objc 34 | - (id)makeEngineWithEntrypoint:(nullable NSString *)entrypoint libraryURI:(nullable NSString *)libraryURI; 35 | ``` 36 | 37 | 原因是 libraryURI 参数为 nil,在 Release 下无法索引到 entrypoint . 38 | 39 | libraryURI 是传你当前入口的包名 + dart,以上一篇的 switch 为例: 40 | 41 | ```objc 42 | entrypoint:@"componentSwitch" 43 | libraryURI:@"package:fgui/ui_components.dart"]; 44 | ``` 45 | 46 | ### C. 如何 Flutter 内部控制 Size 47 | 48 | 外部约束必须提供宽高才可正确显示 FlutterView。如果想要做 FlutterView 基于内部自适应,就需要通过 Flutter 传给 Native 宽高后再确定外部约束。 49 | 50 | 但又引发一个问题,外部如果约束没有宽高,则不会渲染 FlutterView。 这就巧妙的用了 0.1 这个默认约束条件,当然已经内置在 ComponentAPI 中,外部调用无需关心。 51 | 52 | ### D. Android 可行性验证上走过的坑 53 | 54 | - top-level 找不到,渲染白屏,问题最后排查到 debug 包正常,release 包不正常。最后找到该 issue(),这是 flutter/dart 的 bug,在 2.5.3 上可以通过*指定入口所在文件*解决,在 2.8.0 以上版本建议退回 2.5.3 (手动狗头)。 55 | - 在使用 flutter debug 包情况下,每个引擎会多占用 100 M 内存,且在同时渲染 10 个引擎的情况下,会导致页面卡死。(通过  官网说明,JIT 模式下会有内存泄漏问题,推荐使用 AOT release 包)。 56 | - 在 release 包情况下,for 循环同时增加 10 个 FlutterView,直接就 OOM 崩溃 ... 最后排查结果,如果 for 中加一个 delay(1),就显示正常且内存占用也正常,怀疑是 Flutter 本身的 Bug,从 issues 中了解到可能是 dart 的 observe 有问题。这个问题在 Flutter 后续版本修复了,具体没有细追究,大概是 2.8+ 或者 2.10+。 57 | 58 | ### E. 打包以及依赖 59 | 60 | 由于 Flutter 只有一个 main() 入口,所以做不到页面和组件化分开打包引用,这就导致出现了一个依赖问题,我们的 Flutter 包是按项目打包的,那去使用组件的模块很多都是通用模块,不能去依赖 Flutter 包。 61 | 62 | 最终的处理方案是反射解耦,双端生成的调用类不再依赖 Pigeon 生成的 API 类,而是通过反射的形式去调用,外部调用者只需引用 FGUIComponentAPI 模块,即可使用 Flutter UI。减少了直接依赖也就减少了构建时长。同时,FGUIComponentAPI 是自动生成的,所以不会存在维护上的问题。 63 | 64 | ### F. 文字国际化 65 | 66 | 由于 FlutterEngineGroup 不是传统的 main() 入口,也不能继承 MaterialApp 或者 WidgetApp ,所以 Flutter 本身的国际化方案并不适用。 67 | 68 | 最终是做了国际化内置的形式,由源生宿主在创建 FlutterView 时通过 MessageChannel 通知 Flutter 当前是什么语言环境,然后在有限复用现有的 intl 生成国际化方式,解决国际化问题。 69 | 70 | ### G. Flutter-Debug <> Flutter-Release 71 | 72 | 被摧残过才明白,这俩就是不同的物种,生殖隔离的那种 73 | 74 | 除非是非要  `attach to Flutter Progress` ,开发调试上只建议使用 Flutter-Release 75 | 76 | #### a. Flutter-Debug 内存泄漏 77 | 78 | 以 iOS 为例: 79 | 80 | > 真机 + Flutter-Release 模式 = 没有问题,个人观测基本 1 M / Engine (官方说 180K / Engine,民间测试 1.33M / Engine) 81 | > 82 | > 真机 + Flutter-Debug 模式 = 内存 100 M / Engine 83 | 84 | 内存问题在 Flutter Debug 模式下是无解的,这个是因为 Flutter 调试功能会导致内存泄漏和增大问题,是 dart 本身的问题且社区上看暂时没有解决方案。 85 | 86 | #### b. Flutter-Release 存在调用陷阱 87 | 88 | 背景: 89 | 90 | 同时布局多个 FlutterView 91 | 92 | 在 Flutter-Debug 下除了内存加载问题,展示及操作都正常 93 | 94 | 在 Flutter-Release 下发现会产生主线程 pThread 锁死等待,界面卡死现象 95 | 96 | 分析: 97 | 98 | 第一步,经大量测试发现,先去单独加载一个 FlutterView,然后再同时布局多个 FlutterView,结果正常。(比如先进入下设置页面,FlutterEngineGroup 创建的还是 flutter_boost 创建的都可以) 99 | 100 | > 初步怀疑是 Flutter 机制的问题,在复用 isolate 时,如果还未创建 isolate,会去走创建流程,但如果外部是循环加载,而创建 isolate 的过程不是线程安全的(调用了还未创建完成的方法),导致某一段代码出现了死锁。 101 | 102 | 第二步,想到另一个页面也是同时布局多个 FlutterView,但在未先单独加载一个 FlutterView 也可以正常使用,对比代码发现: 103 | 104 | 是因为布局时机上不同: 105 | 106 | ```objc 107 | - (void)init ... { 108 |     super 109 |     ... 110 |     [self setupOneFlutterView]; 111 | } 112 | 113 | // 引发问题的代码,在布局时再去另一个 FlutterView 114 | - (void)layoutSubviews { 115 |     [super layoutSubviews]; 116 |     [self setupTwoFlutterView]; 117 | } 118 | 119 | // 但如果只有一个 FlutterView 在 layoutSubviews 上布局,又是可以的 120 | ``` 121 | 122 | 结论: 123 | 124 | 根本原因是 Flutter 自身 C++ 代码的问题,但真的是因为用的人太少,大部分卡在 Demo 都没玩过去,所以也没人提到这个。 125 | 126 | 类似的,Android 也有这问题,多个同时布局会导致 FlutterJNI 死锁,界面无响应。这个可能是 Flutter 2.5.3 的 Bug,反正官方 issues 就一句话,升级最新版 → issue closed。 127 | 128 | ### I. FlutterView 阴影 129 | 130 | 需要注意是,如果开发的 Flutter 组件需要显示阴影,Native 上的宽高约束需要包括阴影的宽高,超过 FlutterView 的 Size 就会被 Native 截掉,会导致样式上问题。 131 | 132 | ### J. Flutter 手势失效 133 | 134 | 在 iOS 上,由于 Flutter 是使用更底层的 touch 事件,响应优先级比手势低,如果布局上存在 Native 手势,就会被手势拦截,产生 FlutterView 无响应的问题。 135 | 136 | 临时解决方式,iOS 可以在外部源生手势上增加 cancelsTouchesInView = NO (default = YES),让 touch 事件生效。 137 | 138 | 最终解决方式,FGUIComponentAPI 提供了点击、滑动手势竞争者,来保证 FlutterView 作为子视图能优先响应而不被父视图拦截。 139 | 140 | ### K. FlutterView 透明部分无法传递事件的问题 141 | 142 | 在 iOS 上,FlutterView 透明部分想要让底层接收到事件 143 | 144 | 控制 userInteractionEnabled=NO 可以暂时解决 145 | 146 | 但并不是一个最佳的实现方案吧,确实在 FlutterView 和 NativeView 叠加的场景下,事件响应是一个比较麻烦的问题。 147 | 148 | ### M. Flutter 开发需要注意的 Root 不是一个 MaterialApp 会产生的问题 149 | 150 | 由于 Root 不是一个 MaterialApp,所以诸如 MediaQuery 等 API 都不可用。 151 | 152 | 当然,由于 ListView 有个要求,父类需要有 Directionality(这个只有在使用时才会报错,编译时不会报错), MaterialApp 是有封装掉的。 153 | 154 | 解决方式,这个生成模版时,根节点默认已为 Directionality。 155 | 156 | 可能还有更多类似的问题,需要注意。 157 | 158 | 场景持续更新: 159 | 160 | 1. [flutter_easyrefresh](https://pub.flutter-io.cn/packages/flutter_easyrefresh)(Third Party) 不可用,因为它的 footer 使用了 MediaQuery 来做 safeArea 判断。可选 [pull_to_refresh](https://pub.flutter-io.cn/packages/pull_to_refresh) 「但它没再更新了,不支持 Flutter 3.0+ 语法,可以换 pull_to_refresh_flutter3 [手动狗头]」。 161 | 162 | ### N. Flutter 第三方库选择需谨慎 163 | 164 | 由 M 问题拓展出一个新的问题:如果第三方库是一个源生混合型插件,通过 plugin 跟 Native 交互的,也不适合在多引擎场景下使用。 165 | 166 | 1. 因没有去注册 plugin,所以第三方库无法获取到 Native 结果,导致异常。这已持 plugin 注册,但要小心不要滥用。因为以前使用方式下,plugin 不释放也没什么问题,毕竟只有一个 FlutterEngine。但现在多引擎下,注册的 plugin 必须是内存安全可释放的,着重注意出现循环引用。 167 | 2. 但也会存在多引擎间消息不可控的问题 。 168 | 169 | ### Q. 慎用 Timer 170 | 171 | Flutter Timer 在 iOS 会通过 dart:io EventHandler 线程来 IO 通信,如果频繁的 Timer 或者存在多个 Timer 会导致频繁 IO 结果就是 CPU 占用过高。如果非要使用,那尽量不要使用周期性任务。 172 | 173 | 有兴趣的同学可以去搜一下 Flutter Timer 在各端上的实现原理。 174 | 175 | ### I. flutter_cache_manager 的使用误区 176 | 177 | 包括好评 100% 的 [cached_network_image](https://pub.dev/packages/cached_network_image) 都是基于 flutter_cache_manager 来做资源缓存。它的设计跟 SDWebImage 相同,也分为硬盘缓存(sqlite 做索引)、内存缓存。但问题就是因为 Flutter 自身不具备 sqlite、文件存储的能力,其实都是通过 Bridge 来跟 Native 交互的,这就导致从硬盘加载资源的效率(sqlite 查询地址 → 地址加载资源)比不上源生。 178 | 179 | 所以对于需要常驻的资源最好由 dart 持有,一旦被释放,内存持有释放的也特别快(据测试 20 多秒就被回收了)。 180 | 181 | 再从硬盘重新加载就会有短暂延迟,不符合 UI 交互效果。 182 | 183 | ### S. sqlite 使用需谨慎 184 | 185 | 背景是上线前测试发现,部分 Android 设备在第一次安装后出现图片展示失败的问题,但重开后就又正常的。排查上,也并没触发图片加载失败的日志。 186 | 187 | 最后,查到可疑点 188 | 189 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/656eb1b5757742d98843fe31d59a3b9e~tplv-k3u1fbpfcp-watermark.image?) 190 | 191 | 锁定问题,是在多引擎模式下使用 [cached_network_image](https://pub.dev/packages/cached_network_image) 导致。 192 | 193 | 细究原因, 194 | 195 | [cached_network_image](https://pub.dev/packages/cached_network_image) ← flutter_cache_manager ← sqflite ,在 iOS / Android 上缓存的图片路径是用的 sqlite 实现的,而 sqlite 在多引擎模式下被多次同时访问导致出现 lock 的情况。 196 | 197 | 这也说明当下 pub 库中的插件大都是在单引擎模式下设计出来的,在多引擎下确实存在多种陷阱。 198 | 199 | 但问题还是很好处理,flutter_cache_manager 提供了 cachekey 字段,对于需同时做缓存的多引擎资源,使用不同的 cachekey 来区分成多个 DB 索引库。 200 | 201 | 也思考下 iOS 为什么不会出现这个问题,因为 iOS FlutterEngineGroup 设计上,一个 Group 中多个引擎都只使用同一个 iO 线程、raster 线程,所以对 sqlite 来说没有产生并发问题。 202 | 203 | ## 后续 204 | 205 | FGUIComponentAPI 可能也有同学感兴趣是个什么,并没有什么高大上的原理,其实质是一个模版代码处理,语言的话,笔者用的 ruby,也可以换 python,这些脚本语言执行速度还是很可靠的,至少比 dart 做脚本好很多。 206 | 207 | 放一下目录结构吧,可以看到整个 fgui_component_api 就是 ruby 做的脚本执行文件 208 | 209 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c9e6c2ef8fa54b4b9446b8a6671283dd~tplv-k3u1fbpfcp-watermark.image?) 210 | 211 | 有时间会整理下代码,放出来给大家参考。 212 | 213 | > 作者投稿 214 | > 215 | > 来源:[Flutter 多引擎渲染,在稿定 App 的实践](https://juejin.cn/post/7130811413840429093) -------------------------------------------------------------------------------- /resource/27 Swift中的Actors 使用以如何及防止数据竞争.md: -------------------------------------------------------------------------------- 1 | 2 | ## 前言 3 | 4 | 5 | Swift Actors 是Swift 5.5中的新内容,也是WWDC 2021上并发重大变化的一部分。在有 actors 之前,数据竞争是一个常见的意外情况。因此,在我们深入研究具有隔离和非隔离访问的行为体之前,最好先了解什么是[数据竞争](https://www.avanderlee.com/swift/thread-sanitizer-data-races/#what-are-data-races),并了解当前你如何[解决这些问题](https://www.avanderlee.com/swift/thread-sanitizer-data-races/#using-the-thread-sanitizer-to-detect-data-races)。 6 | 7 | Swift 中的 Actors 旨在完全解决数据竞争问题,但重要的是要明白,很可能还是会遇到数据竞争。本文将介绍 Actors 是如何工作的,以及你如何在你的项目中使用它们。 8 | 9 | ## 什么是 Actors? 10 | 11 | Swift 中的 Actor 并不新鲜:它们受到 [Actor Model](https://en.wikipedia.org/wiki/Actor_model) 的启发,该模型将行为视为并发计算的通用基元。然后,[SE-0306](https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md)提案引入了 Actor,并解释了它们解决了哪些问题:数据竞争。 12 | 13 | 14 | 当多个线程在没有同步的情况下访问同一内存,并且至少有一个访问是写的时候,就会发生数据竞争。数据竞争会导致不可预测的行为、内存损坏、不稳定的测试和奇怪的崩溃。你可能会遇到无法解决的崩溃,因为你不知道它们何时发生,如何重现它们,或者如何根据理论来修复它们。我的文章[Thread Sanitizer explained: Data Races in Swift](https://www.avanderlee.com/swift/thread-sanitizer-data-races/)深入解释了如何解决、发现和修复数据竞争。 15 | 16 | 17 | Swift 中的 Actors 可以保护他们的状态免受数据竞争的影响,并且使用它们可以让编译器在编写应用程序时为我们提供有用的反馈。此外,Swift 编译器可以静态地强制执行 Actors 附带的限制,并防止对可变数据的并发访问。 18 | 19 | 您可以使用 `actor` 关键字定义一个 Actor,就像您使用类或结构体一样: 20 | 21 | ```swift 22 | actor ChickenFeeder { 23 | let food = "worms" 24 | var numberOfEatingChickens: Int = 0 25 | } 26 | ``` 27 | 28 | Actor 和其他 Swift 类型一样,它们也可以有初始化器、方法、属性和子标号,同时你也可以用协议和泛型来使用它们。此外,与结构体不同的是:当你定义的属性需要手动定义时,`actor` 需要自定义初始化器。最后,重要的是要认识到 `actor` 是引用类型。 29 | 30 | ### Actor 是引用类型,但与类相比仍然有所不同 31 | 32 | Actor 是引用类型,简而言之,这意味着副本引用的是同一块数据。因此,修改副本也会修改原始实例,因为它们指向同一个共享实例。你可以在我的文章[Swift中的Struct与class的区别](https://www.avanderlee.com/swift/struct-class-differences/)中了解更多这方面的信息。 33 | 34 | **然而,与类相比,Actor 有一个重要的区别:他们不支持继承。** 35 | 36 | ![Swift中的Actor几乎和类一样,但不支持继承。](https://www.avanderlee.com/wp-content/uploads/2021/06/swift_actors_data_races.png) 37 | 38 | Swift中的Actor几乎和类一样,但不支持继承。 39 | 40 | 不支持继承意味着不需要像便利初始化器和必要初始化器、重写、类成员或`open`和`final`语句等功能。 41 | 42 | **然而,最大的区别是由 Actor 的主要职责决定的,即隔离对数据的访问。** 43 | 44 | ## Actors 如何通过同步来防止数据竞争 45 | 46 | Actor 通过创建对其隔离数据的同步访问来防止数据竞争。在Actors之前,我们会使用各种锁来创建相同的结果。这种锁的一个例子是并发调度队列与处理写访问的屏障相结合。受我在[Concurrent vs. Serial DispatchQueue: Concurrency in Swift explained](https://www.avanderlee.com/swift/concurrent-serial-dispatchqueue/)一文中解释的技术的启发。我将向你展示使用 Actor 的前后对比。 47 | 48 | 在 Actor 之前,我们会创建一个线程安全的小鸡喂食器,如下所示: 49 | 50 | ```swift 51 | final class ChickenFeederWithQueue { 52 | let food = "worms" 53 | 54 | /// 私有支持属性和计算属性的组合允许同步访问。 55 | private var _numberOfEatingChickens: Int = 0 56 | var numberOfEatingChickens: Int { 57 | queue.sync { 58 | _numberOfEatingChickens 59 | } 60 | } 61 | 62 | /// 一个并发的队列,允许同时进行多次读取。 63 | private var queue = DispatchQueue(label: "chicken.feeder.queue", attributes: .concurrent) 64 | 65 | func chickenStartsEating() { 66 | /// 使用栅栏阻止写入时的读取 67 | queue.sync(flags: .barrier) { 68 | _numberOfEatingChickens += 1 69 | } 70 | } 71 | 72 | func chickenStopsEating() { 73 | /// 使用栅栏阻止写入时的读取 74 | queue.sync(flags: .barrier) { 75 | _numberOfEatingChickens -= 1 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | 正如你所看到的,这里有相当多的代码需要维护。在访问非线程安全的数据时,我们必须仔细考虑自己使用队列的问题。需要一个栅栏标志来停止读取并允许写入。再一次,我们需要自己来处理这个问题,因为编译器并不强制执行它。最后,我们在这里使用了一个`DispatchQueue`,但是经常有围绕着哪个锁是最好的争论。 82 | 83 | 为了看清这一点,我们可以使用我们先前定义的 Actor 小鸡喂食器来实现上述例子: 84 | 85 | ```swift 86 | actor ChickenFeeder { 87 | let food = "worms" 88 | var numberOfEatingChickens: Int = 0 89 | 90 | func chickenStartsEating() { 91 | numberOfEatingChickens += 1 92 | } 93 | 94 | func chickenStopsEating() { 95 | numberOfEatingChickens -= 1 96 | } 97 | } 98 | ``` 99 | 100 | 你会注意到的第一件事是,这个实例更简单,更容易阅读。所有与同步访问有关的逻辑都被隐藏在Swift标准库中的实现细节里。然而,最有趣的部分发生在我们试图使用或读取任何可变属性和方法的时候: 101 | 102 | ![Methods in Actors are isolated for synchronized access.](https://www.avanderlee.com/wp-content/uploads/2021/06/actor_isolated_methods.png) 103 | 104 | Actors中的方法是隔离的,以便同步访问。 105 | 106 | 在访问可变属性 `numberOfEatingChickens`时,也会发生同样的情况: 107 | 108 | ![Mutable properties can only be accessed from within the Actor.](https://www.avanderlee.com/wp-content/uploads/2021/06/actor_isolated_mutable_property.png) 109 | 110 | 可变的属性只能从Actor内部访问。 111 | 112 | 113 | 114 | 然而,我们被允许编写以下代码: 115 | 116 | ```swift 117 | let feeder = ChickenFeeder() 118 | print(feeder.food) 119 | ``` 120 | 121 | 我们的喂食器上的`food`属性是不可变的,因此是线程安全的。没有数据竞争的风险,因为在读取过程中,它的值不能从另一个线程中改变。 122 | 123 | 然而,我们的其他方法和属性会改变一个引用类型的可变状态。为了防止数据竞争,需要同步访问,允许按顺序访问。 124 | 125 | ## 使用async/await从 Actors 访问数据 126 | 127 | 在 Swift 中,我们可以通过使用 `await `关键字来创建异步访问: 128 | 129 | ````swift 130 | let feeder = ChickenFeeder() 131 | await feeder.chickenStartsEating() 132 | print(await feeder.numberOfEatingChickens) // Prints: 1 133 | ```` 134 | 135 | ## 防止不必要的暂停 136 | 137 | 在上面的例子中,我们正在访问我们 Actor 的两个不同部分。首先,我们更新吃食的鸡的数量,然后我们执行另一个异步任务,打印出吃食的鸡的数量。每个`await`都会导致你的代码暂停,以等待访问。在这种情况下,有两个暂停是有意义的,因为两部分其实没有什么共同点。然而,你需要考虑到可能有另一个线程在等待调用`chickenStartsEating`,这可能会导致在我们打印出结果的时候有两只吃食的鸡。 138 | 139 | 140 | 为了更好地理解这个概念,让我们来看看这样的情况:你想把操作合并到一个方法中,以防止额外的暂停。例如,设想在我们的 `actor` 中有一个通知方法,通知观察者有一只新的鸡开始吃东西: 141 | 142 | ```swift 143 | extension ChickenFeeder { 144 | func notifyObservers() { 145 | NotificationCenter.default.post(name: NSNotification.Name("chicken.started.eating"), object: numberOfEatingChickens) 146 | } 147 | } 148 | ``` 149 | 150 | 我们可以通过使用 `await` 两次来使用此代码: 151 | 152 | ```swift 153 | let feeder = ChickenFeeder() 154 | await feeder.chickenStartsEating() 155 | await feeder.notifyObservers() 156 | ``` 157 | 158 | 然而,这可能会导致两个暂停点,每个`await`都有一个。相反,我们可以通过从`chickenStartsEating`中调用`notifyObservers`方法来优化这段代码: 159 | 160 | ```swift 161 | func chickenStartsEating() { 162 | numberOfEatingChickens += 1 163 | notifyObservers() 164 | } 165 | ``` 166 | 167 | 由于我们已经在Actor内有了同步的访问,我们不需要另一个等待。这些都是需要考虑的重要改进,因为它们可能会对性能产生影响。 168 | 169 | ## Actor 内的非隔离(nonisolated)访问 170 | 171 | 了解 Actor 内部的隔离概念很重要。上面的例子已经展示了如何通过要求使用 `await` 从外部参与者实例同步访问。但是,如果您仔细观察,您可能已经注意到我们的 `notifyObservers` 方法不需要使用 `await` 来访问我们的可变属性 `numberOfEatingChickens`。 172 | 173 | 当访问 Actor 中的隔离方法时,你基本上可以访问任何其他需要同步访问的属性或方法。因此,你基本上是在重复使用你给定的访问,以获得最大的收益。 174 | 175 | 然而,在有些情况下,你知道不需要有隔离的访问。actor 中的方法默认是隔离的。下面的方法只访问我们的不可变的属性`food`,但仍然需要`await`访问它: 176 | 177 | ```swift 178 | let feeder = ChickenFeeder() 179 | await feeder.printWhatChickensAreEating() 180 | ``` 181 | 182 | 这很奇怪,因为我们知道,我们不访问任何需要同步访问的东西。[SE-0313](https://github.com/apple/swift-evolution/blob/main/proposals/0313-actor-isolation-control.md)的引入正是为了解决这个问题。我们可以用`nonisolated`关键字标记我们的方法,告诉 Swift编 译器我们的方法没有访问任何隔离数据: 183 | 184 | ```swift 185 | extension ChickenFeeder { 186 | nonisolated func printWhatChickensAreEating() { 187 | print("Chickens are eating \(food)") 188 | } 189 | } 190 | 191 | let feeder = ChickenFeeder() 192 | feeder.printWhatChickensAreEating() 193 | ``` 194 | 195 | 注意,你也可以对计算的属性使用`nonisolated`的关键字,这对实现`CustomStringConvertible`等协议很有帮助: 196 | 197 | ```swift 198 | extension ChickenFeeder: CustomStringConvertible { 199 | nonisolated var description: String { 200 | "A chicken feeder feeding \(food)" 201 | } 202 | } 203 | ``` 204 | 205 | 然而,在不可变的属性上定义它们是不需要的,因为编译器会告诉你: 206 | 207 | ![Marking immutable properties nonisolated is redundant.](https://www.avanderlee.com/wp-content/uploads/2021/06/nonisolated_properties.png) 208 | 209 | 将不可变的属性标记为 nonisolated 是多余的。 210 | 211 | ## 为什么在使用 Actors 时仍会出现数据竞争? 212 | 213 | 当在你的代码中持续使用 Actors 时,你肯定会降低遇到数据竞争的风险。创建同步访问可以防止与数据竞争有关的奇怪崩溃。然而,你显然需要持续地使用它们来防止你的应用程序中出现数据竞争。 214 | 215 | 在你的代码中仍然可能出现竞争条件,但可能不再导致异常。认识到这一点很重要,因为Actors 毕竟被宣扬为可以解决一切问题的工具。例如,想象一下两个线程使用 `await`正确地访问我们的 Actor 的数据: 216 | 217 | ```swift 218 | queueOne.async { 219 | await feeder.chickenStartsEating() 220 | } 221 | queueTwo.async { 222 | print(await feeder.numberOfEatingChickens) 223 | } 224 | ``` 225 | 226 | 这里的竞争条件定义为:“哪个线程将首先开始隔离访问?”。所以基本上有两种结果: 227 | 228 | - 队列一在先,增加吃食的鸡的数量。队列二将打印:1 229 | - 队列二在先,打印出吃食的鸡的数量,该数量仍为:0 230 | 231 | 232 | 这里的不同之处在于我们在修改数据时不再访问数据。如果没有同步访问,在某些情况下这可能会导致无法预料的行为。 233 | 234 | ## 继续你的Swift并发之旅 235 | 236 | 并发更改不仅仅是 async-await,还包括许多您可以在代码中受益的新功能。所以当你在使用它的时候,为什么不深入研究其他并发特性呢? 237 | 238 | - [Sendable 和 @Sendable 闭包代码实例详解](https://mp.weixin.qq.com/s/IA9CgMjZf63_RFwNBB9QqQ) 239 | 240 | 241 | - [Swift AsyncSequence — 代码实例详解](https://mp.weixin.qq.com/s/7HuYcMFCjqEhRHWlPc3ydA) 242 | 243 | 244 | - [Swift AsyncThrowingStream 和 AsyncStream 代码实例详解](https://mp.weixin.qq.com/s/j42yzmsOMNAsq3bRmUzUEA) 245 | 246 | 247 | - [Swift 中的 async/await ——代码实例详解](https://mp.weixin.qq.com/s/TL1LPvVdhSFxrYCFWQ2LfQ) 248 | 249 | 250 | ## 结论 251 | 252 | Swift Actors 解决了用 Swift 编写的应用程序中常见的数据竞争问题。可变数据是同步访问的,这确保了它是安全的。我们还没有介绍 `MainActor` 实例,它本身就是一个主题。我将确保在以后的文章中介绍这一点。希望您能够跟随并知道如何在您的应用程序中使用 Actor。 253 | 254 | > 来自:[Actors in Swift: how to use and prevent data races](https://www.avanderlee.com/swift/actors/) -------------------------------------------------------------------------------- /resource/39 Swift 单元测试入门.md: -------------------------------------------------------------------------------- 1 | 编程语言中的单元测试是为了确保编写的代码按预期工作。给定一个特定的输入,您希望代码带有一个特定的输出。通过测试您的代码,能够给您当前的重构和发布建立信心,因为您将能够确保代码在成功运行您的测试套件后按预期工作。 2 | 3 | 4 | 5 | 许多开发人员不编写单元测试,因为他们认为这会花费太多时间,有可能错过最后期限。在我看来,单元测试会让你在最后期限前完成更多工作,因为你会花更少的时间解决错误或为关键问题打补丁。 6 | 7 | **这篇文章内不会涵盖 内存泄漏测试 或 为共享扩展编写 UI 测试,而是主要关注编写更好的单元测试。我还将分享帮助我开发更好、更稳定的应用程序的最佳实践。** 8 | 9 | 10 | 11 | ## 什么是单元测试 12 | 13 | 单元测试是运行和验证一段代码(称为“单元”)以确保其按预期运行并符合其设计的自动化测试。 14 | 15 | 16 | 17 | 单元测试在 Xcode 中有它们的 target,并使用 [XCTest 框架](https://developer.apple.com/documentation/xctest)编写。 `XCTestCase` 的子类包含要运行的测试方法,其中只有以 "test" 开头的方法才会被 Xcode 解析并允许运行。 18 | 19 | 20 | 21 | 例如,假设有一个字符串扩展方法将第一个字母大写: 22 | 23 | ```swift 24 | extension String { 25 | func uppercasedFirst() -> String { 26 | let firstCharacter = prefix(1).capitalized 27 | let remainingCharacters = dropFirst().lowercased() 28 | return firstCharacter + remainingCharacters 29 | } 30 | } 31 | ``` 32 | 33 | 我们要确保 `uppercasedFirst() `方法按预期工作。如果我们给它一个输入 `antoine`,我们期望它输出 `Antoine`。我们可以使用` XCTAssertEqual` 方法为此方法编写单元测试: 34 | 35 | ```swift 36 | final class StringExtensionsTests: XCTestCase { 37 | func testUppercaseFirst() { 38 | let input = "antoine" 39 | let expectedOutput = "Antoine" 40 | XCTAssertEqual(input.uppercasedFirst(), expectedOutput, "The String is not correctly capitalized.") 41 | } 42 | } 43 | ``` 44 | 45 | 46 | 47 | 如果我们的方法不再按预期工作(比如上面的扩展代码不小心被修改了),Xcode 将使用我们提供的描述显示失败: 48 | 49 | ![单元测试失败,因为输入与预期输出不匹配。](https://upload-images.jianshu.io/upload_images/2955252-e2ea09af17709336.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 50 | 51 | 52 | ## 在 Swift 中编写单元测试 53 | 54 | 有多种方法可以测试相同的结果,但是当测试失败时它并不总是给出相同的反馈。以下提示可帮助您编写测试,通过从详细的失败消息中获益,帮助您更快地解决失败的测试。 55 | 56 | 57 | 58 | ### 命名测试用例和方法 59 | 60 | 描述你的单元测试是很重要的,这样你就会明白测试试图验证什么。如果你不能想出一个简短的名字,那你可能测试了太多东西。一个好名字还可以帮助您更快地解决失败的测试。 61 | 62 | 63 | 64 | 要快速找到特定类的测试用例,建议使用相同的命名并结合 “test”。就像上面的例子一样,我们根据我们正在测试一组字符串扩展的事实命名了 `StringExtensionTests`。如果您正在测试`ContentViewModel` 实例,另一个示例可能是 `ContentViewModelTests`。 65 | 66 | 67 | 68 | ### 不要所有测试都使用 XCTAssert 69 | 70 | 许多场景都可以使用 `XCTAssert`,但当测试失败时会导致不同的结果。以下代码行都测试了完全相同的结果: 71 | 72 | ```swift 73 | func testEmptyListOfUsers() { 74 | let viewModel = UsersViewModel(users: ["Ed", "Edd", "Eddy"]) 75 | XCTAssert(viewModel.users.count == 0) 76 | XCTAssertTrue(viewModel.users.count == 0) 77 | XCTAssertEqual(viewModel.users.count, 0) 78 | } 79 | ``` 80 | 81 | 正如你所看到的,该方法使用了一个描述性的名字,告诉人们要测试一个空的用户列表。然而,我们定义的视图模型不是空的,因此,所有的断言都失败了。 82 | 83 | 84 | ![使用正确的断言可以帮助您更快地解决故障。](https://upload-images.jianshu.io/upload_images/2955252-99021cc17e03493c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 85 | 86 | 87 | 结果显示了为什么必须对验证类型使用正确的断言。 `XCTAssertEqual` 方法为我们提供了有关断言失败原因的更多上下文。这显示在红色错误和控制台日志中,可帮助您快速识别失败的测试。 88 | 89 | 90 | 91 | ### Setup and Teardown 92 | 93 | 多个测试方法中使用的参数可以定义为测试用例类中的属性。您可以使用 `setUp()` 方法为每个测试方法设置初始状态,并使用 `tearDown()` 方法进行清理。有多种设置和拆卸方法的变体供您选择,例如支持并发的变体或抛出变体,如果设置失败,您可以在其中提前使测试失败。 94 | 95 | 96 | 97 | 一个可以生成用户默认实例以用于单元测试的示例: 98 | 99 | ```swift 100 | struct SearchQueryCache { 101 | var userDefaults: UserDefaults = .standard 102 | 103 | func storeQuery(_ query: String) { 104 | /// ... 105 | } 106 | } 107 | 108 | final class SearchQueryCacheTests: XCTestCase { 109 | 110 | private var userDefaults: UserDefaults! 111 | private var userDefaultsSuiteName: String! 112 | 113 | override func setUpWithError() throws { 114 | try super.setUpWithError() 115 | userDefaultsSuiteName = UUID().uuidString 116 | userDefaults = UserDefaults(suiteName: userDefaultsSuiteName) 117 | } 118 | 119 | override func tearDownWithError() throws { 120 | try super.tearDownWithError() 121 | userDefaults.removeSuite(named: userDefaultsSuiteName) 122 | userDefaults = nil 123 | } 124 | 125 | func testSearchQueryStoring() { 126 | /// 使用生成的用户默认值作为输入。 127 | let cache = SearchQueryCache(userDefaults: userDefaults) 128 | 129 | /// ... write the test 130 | } 131 | } 132 | ``` 133 | 134 | 这样做可以确保您不会操纵在模拟器上测试期间使用的标准用户默认值。其次,您将确保在测试开始时处于干净状态。我们使用了拆卸方法来删除用户默认套件并进行相应的清理。 135 | 136 | 137 | 138 | ### 抛出方法 139 | 140 | 和编写应用程序代码时一样,您也可以定义一个可抛出测试的方法。这允许您在测试中的方法抛出错误时使测试失败。例如,在测试 JSON 响应的解码时: 141 | 142 | ```swift 143 | func testDecoding() throws { 144 | /// 当数据初始值设定项抛出错误时,测试将失败。 145 | let jsonData = try Data(contentsOf: URL(string: "user.json")!) 146 | 147 | /// `XCTAssertNoThrow` 可用于获取有关抛出的额外上下文 148 | XCTAssertNoThrow(try JSONDecoder().decode(User.self, from: jsonData)) 149 | } 150 | ``` 151 | 152 | 当在任何进一步的测试执行中不需要 throwing 方法的结果时,可以使用 `XCTAssertNoThrow` 方法。您应该使用 `XCTAssertThrowsError` 方法来匹配预期的错误类型。例如,您可以为证书密钥验证程序编写测试: 153 | 154 | ```swift 155 | struct LicenseValidator { 156 | enum Error: Swift.Error { 157 | case emptyLicenseKey 158 | } 159 | 160 | func validate(licenseKey: String) throws { 161 | guard !licenseKey.isEmpty else { 162 | throw Error.emptyLicenseKey 163 | } 164 | } 165 | } 166 | 167 | class LicenseValidatorTests: XCTestCase { 168 | let validator = LicenseValidator() 169 | 170 | func testThrowingEmptyLicenseKeyError() { 171 | XCTAssertThrowsError(try validator.validate(licenseKey: ""), "An empty license key error should be thrown") { error in 172 | /// 我们确保预期的错误被抛出。 173 | XCTAssertEqual(error as? LicenseValidator.Error, .emptyLicenseKey) 174 | } 175 | } 176 | 177 | func testNotThrowingLicenseErrorForNonEmptyKey() { 178 | XCTAssertNoThrow(try validator.validate(licenseKey: "XXXX-XXXX-XXXX-XXXX"), "Non-empty license key should pass") 179 | } 180 | } 181 | ``` 182 | 183 | 184 | 185 | ### 可选值解包 186 | 187 | `XCTUnwrap` 方法最适合用于抛出测试,因为它是一个抛出断言: 188 | 189 | ```swift 190 | func testFirstNameNotEmpty() throws { 191 | let viewModel = UsersViewModel(users: ["Antoine", "Maaike", "Jaap"]) 192 | 193 | let firstName = try XCTUnwrap(viewModel.users.first) 194 | XCTAssertFalse(firstName.isEmpty) 195 | } 196 | ``` 197 | 198 | `XCTUnwrap` 断言可选变量的值不为 `nil`,如果断言成功则返回它的值。它会阻止您编写 `XCTAssertNotNil` 并结合解包或处理其余测试代码的条件链接。我鼓励您阅读我的文章 [《如何使用 XCTest 在 Swift 中测试可选值》](https://www.avanderlee.com/swift/test-optionals-xctest/)以了解更多详细信息。 199 | 200 | 201 | 202 | ## 在 Xcode 中运行单元测试 203 | 204 | 编写测试后,就该运行它们了。通过以下提示,这将变得更有效率。 205 | 206 | 207 | 208 | ### 使用测试三角形 209 | 210 | 您可以使用前导三角形运行单个测试或一组测试: 211 | 212 | 213 | ![前导三角形可用于运行单个或一组测试。](https://upload-images.jianshu.io/upload_images/2955252-8c88cdde322fe065.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 214 | 215 | 216 | 根据最新的测试运行结果,同一方块显示红色或绿色。 217 | 218 | 219 | 220 | ### 重新运行最新的测试 221 | 222 | 使用以下命令重新运行上次运行测试: 223 | 224 | `⌃ Control + ⌥ Option + ⌘ Command + G`. 225 | 226 | 上面的快捷方式可能是我最常用的快捷方式之一,因为它可以帮助我在对失败测试实施修复后快速重新运行测试。 227 | 228 | 229 | 230 | ### 运行测试组合 231 | 232 | 使用 CTRL 或 SHIFT 选择要运行的测试,右键单击并选择“Run X Test Methods”。 233 | 234 | ![运行测试组合](https://upload-images.jianshu.io/upload_images/2955252-a5e5035bc45e90d9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 235 | 236 | 237 | ### 在测试导航器中应用过滤器 238 | 239 | 测试导航器底部的过滤栏允许您缩小测试概览范围。 240 | 241 | ![测试导航器过滤栏](https://upload-images.jianshu.io/upload_images/2955252-dae6d592b08d509b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 242 | 243 | - 使用搜索字段根据名称搜索特定测试 244 | - 仅显示当前所选方案的测试。如果您有多个测试方案,这将很有用。 245 | - 只显示失败的测试。这将帮助您快速找到失败的测试。 246 | 247 | ### 在侧边栏中启用覆盖 248 | 249 | ![在编辑器中启用代码覆盖](https://upload-images.jianshu.io/upload_images/2955252-50a9a28c44816dd4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 250 | 251 | 测试迭代计数向您显示在上次运行测试期间是否命中了特定代码段。 252 | 253 | 254 | ![命中提示](https://upload-images.jianshu.io/upload_images/2955252-41e0dc1c79fc0fc9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 255 | 256 | 257 | 它显示了迭代次数(在上面的示例中为 3),一段代码在到达时变为绿色。当一段代码是红色时,这意味着它在上次运行的测试中没有被覆盖。 258 | 259 | 260 | 261 | ## 编写单元测试时的心态 262 | 263 | 你的心态是编写高质量单元测试的一个很好的起点。通过一些基本原则,您可以确保工作效率、保持专注并编写您的应用程序最需要的测试。 264 | 265 | 266 | 267 | ### 您的测试代码与您的应用程序代码一样重要 268 | 269 | 在深入探讨实用技巧之后,我想介绍一种必要的心态。就像编写应用程序代码一样,您应该尽最大努力编写高质量的测试代码。 270 | 271 | 272 | 273 | 考虑重用代码、使用协议、在多个测试中使用时定义属性,并确保您的测试清理所有创建的数据。这将使您的单元测试更易于维护,并防止不稳定和奇怪的测试失败。如果您不熟悉片状的测试,我鼓励您阅读我的文章 [Flaky tests resolving using Test Repetitions in Xcode](https://www.avanderlee.com/debugging/flaky-tests-test-repetitions/)。 274 | 275 | 276 | 277 | ### 100% 的代码覆盖率不应该是你的目标 278 | 279 | 尽管它是很多人的目标,但 100% 的覆盖率不应该是您编写测试时的主要目标。一个很好的开始是确保至少测试您最关键的业务逻辑。覆盖率达到 100% 可能会很耗时,而收益并不总是那么显著。并且达到100%,也意味着可能需要付出很大的努力。 280 | 281 | 282 | 283 | 最重要的是,100% 的覆盖率可能会产生误导。上面的单元测试示例覆盖了所有方法,覆盖率为 100%。但是,它并没有测试所有场景,因为它只测试了一个非空数组。同时,也可能存在空数组的情况,其中 `hasUsers` 属性应该返回 false。 284 | 285 | ![可以通过编辑 Scheme 来启用单元测试代码覆盖率](https://upload-images.jianshu.io/upload_images/2955252-63940dc7afd0e48d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 286 | 287 | 288 | 您可以从 Scheme 设置窗口启用测试覆盖率。这个窗口可以通过`Product ➞ Scheme ➞ Edit Scheme`打开。 289 | 290 | 291 | 292 | ### 在修复错误之前编写测试 293 | 294 | 跳到一个错误上并尽快修复它是很诱人的。虽然这很好,但如果您可以防止将来再次出现相同的错误,那就更好了。通过在修复 bug 之前编写单元测试,可以确保相同的 bug 不会再次发生。将其视为“测试驱动的错误修复”,从现在开始也称为 TDBF 。 295 | 296 | 297 | 298 | 其次,您可以开始编写修复程序并运行新的单元测试来验证修复程序是否有效。此技术比运行模拟器来验证您的修复是否有效要快。 299 | 300 | 301 | 302 | ## 结论 303 | 304 | 编写定性的单元测试是开发人员的基本技能。将能够对您的代码库建立信心,确保您在新版本发布之前没有破坏任何东西。使用正确的断言,您可以更快地解决失败的测试。确保至少测试关键业务代码并避免达到 100% 的代码覆盖率。 305 | 306 | > 来自:[Getting started with Unit Tests in Swift](https://www.avanderlee.com/swift/unit-tests-best-practices/) 307 | -------------------------------------------------------------------------------- /resource/41 当今Swift包中的二进制目标.md: -------------------------------------------------------------------------------- 1 | # 现今 Swift 包中的二进制目标 2 | 3 | ## 文章目录 4 | 1. 理解二进制在 Swift 中的演变 5 | 2. 命令行工具相关 6 | 3. 结论 7 | 8 | 在 **iOS** 和 **macOS** 开发中, Swift 包现在变得越来越重要。Apple 已经努力推动桥接那些缝隙,并且修复那些阻碍开发者的问题,例如阻碍开发者将他们的库和依赖由其他诸如 **[Carthage](https://github.com/Carthage/Carthage "Carthage")** 或 **[CocoaPods](https://github.com/CocoaPods/CocoaPods "CocoaPods")** 依赖管理工具迁移到 Swift 包依赖管理工具的问题,例如没有能力添加构建步骤的问题。这对任何依赖一些代码生成的库来说都是破坏者,比如,协议和 Swift 生成。 9 | 10 | ## 理解二进制在 Swift 中的演变 11 | 为了充分理解 Apple 的 Swift 团队在二进制目标和他们引入的一些新 API 方面采取的一些步骤,我们需要理解它们从何而来。在后续的部分中,我们将调研 Apple 架构的演变,以及为什么二进制目标的 API 在过去几年中逐渐形成的,特别是自 Apple 发布了自己的硅芯片之后。 12 | 13 | ### 胖二进制和 Frameworks 框架 14 | 如果你曾必须处理二进制依赖,或者你曾创建一个属于你自己的可执行文件,你将会对**胖二进制**这个术语感到熟悉。这些被扩展(或增大)的可执行文件,是包含了为多个不同架构原生构建的切片。这允许库的所有者分发一个运行在所有预期的目标架构上的单独的二进制。 15 | 16 | 当源码不能被暴露或当处理非常庞大的代码仓库时,预编译库成为可执行文件非常有意义,因为预编译源码以及以二进制文件分发他们,将节省构建程序在他们的应用上的构建时间。 17 | 18 | **[Pods](https://cocoapods.org/ "Pods")** 是一个非常好的例子,当开发者发现他们自己没必要构建那些非常少改动的依赖。这是一个很共通的问题,它激发了诸如 **[cocoapods-binary](https://github.com/leavez/cocoapods-binary "cocoapods-binary")** 之类的项目,该项目预编译了 pod 依赖项以减少客户端的构建时间。 19 | 20 | #### Frameworks 框架 21 | 嵌入静态二进制文件可能对应用程序来说已经足够了,但如果需要某些资源(如 assets 或头文件),则需要将这些资源与包含所有切片的**胖二进制文件**捆绑在一起,形成所谓的 **\`frameworks\`** 文件。 22 | 23 | 这就是诸如 **[Google Cast](https://developers.google.com/cast/docs/ios_sender#manual_setup "Google")** 之类的预编译库在过渡到使用 **\`xcframework\`** 进行分发之前所做的事情 —— 下一节将详细介绍这种过渡的原因。 24 | 25 | 到目前为止,一切都很好。 如果我们要为分发预编译一个库,那么胖二进制文件听起来很理想,对吧?并且,如果我们需要捆绑一些其他资源,我们可以只使用一个 **\`frameworks\`**。 一个二进制来统治他们所有! 26 | 27 | #### XCFrameworks 框架 28 | 好吧,不完全是。胖二进制文件有一个大问题,那就是你不能有两个架构相同但命令/指令不同的切片。 这曾经很好,因为设备和模拟器的架构总是不同的,但是随着 Apple Silicon 计算机 (M1) 的推出,模拟器和设备共享相同的架构 (arm64),但具有不同的加载器命令。 这与面向未来的二进制目标相结合,正是 Apple 引入 **[XCFrameworks](https://developer.apple.com/videos/play/wwdc2019/416/ "XCFrameworks")** 的原因。 29 | 30 | > 你可以在 **[Bogo Giertler 撰写的这篇精彩文章](https://twitter.com/giertler)**中详细了解为 iOS 设备构建的 arm64 切片和为 M1 mac 的 iOS 模拟器构建的 arm64 切片之间的区别。 31 | 32 | **[XCFrameworks](https://help.apple.com/xcode/mac/11.4/#/dev6f6ac218b "XCFrameworks")** 现在允许将多个二进制文件捆绑在一起,解决了 M1 Mac 引入的设备和模拟器冲突架构问题,因为我们现在可以为每个用例提供包含相关切片的二进制文件。 事实上,如果我们需要,我们可以走得更远,例如,在同一个 xcframework 中捆绑一个包含 iOS 目标的 **\`UIKit\`** 接口的二进制文件和一个包含 macOS 的 **\`AppKit\`** 接口的二进制文件,然后让 Xcode 基于期望的目标架构决定使用哪一个。 33 | 34 | 在 Swift 包中,那先能够以 **[binaryTarget](https://developer.apple.com/documentation/swift_packages/distributing_binary_frameworks_as_swift_packages "binaryTarget")** 被包含进项目的,能够在包中被引入任意其他目标。这相同的操作同样适用于 `frameworks`。 35 | 36 | ## 命令行工具相关 37 | 由于 Swift 5.6 版本中引入了用于 Swift 包管理器的 **[可扩展构建工具](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md "Extensible Build Tools")** ,因此可以在构建过程中的不同时间执行命令。 38 | 39 | 这是 iOS 社区长期以来一直强烈要求的事情,例如格式化源代码、代码生成甚至收集公制代码库的指标。 Swift 5.6 中所有这些所谓的 **[插件](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-exte""nsible-build-tools.md#plugin-api "Plugins")** 最终都需要调用可执行文件来执行特定任务。 这是二进制文件再次在 Swift 包中参与的地方。 40 | 41 | 在大多数情况下,对于我们 iOS 开发人员来说,这些工具将来自同时支持 macOS 的不同架构切片 —— Apple Silicon 的 arm64 架构和 Intel Mac 的 x86_64 架构。开发者工具如, **[SwiftLint](https://github.com/realm/SwiftLint "SwiftLint")** 或 **[SwiftGen](https://github.com/SwiftGen/SwiftGen "SwiftGen")** 正是这种案例。 在这种情况下,可以使用包含可执行文件(本地或远程)的 **.zip** 文件的路径创建新的二进制目标。 42 | 43 | > 注意可执行文件必须在.zip文件的根目录下,否则找不到。 44 | 45 | ### Artifact Bundles 46 | 到目前为止,命令行工具所采用的方法仅适用于 macOS 架构。但我们不能忘记,Linux 机器也支持 Swift 包。 这意味着如果要同时支持 M1 macs (**\`arm64\`**) 和 Linux **\`arm64\`** 机器,上面的胖二进制方法将不起作用 —— 请记住,二进制不能包含具有相同架构的多个切片。 在这个阶段可能有人会想,我们可以不只使用 **\`xcframeworks\`** 吗? 不,因为它们在 Linux 操作系统上不受支持! 47 | 48 | Apple 已经考虑到这一点,除了引入 **[可扩展构建工具](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md "Extensible Build Tools")** 之外,**[Artifact Bundles](https://github.com/apple/swift-evolution/blob/main/proposals/0305-swiftpm-binary-target-improvements.md "Artifact Bundles")** 和对二进制目标的其他改进也作为 Swift 5.6 的一部分发布。 49 | 50 | 工件包(Artifact Bundles) 是包含*工件*的目录。 这些工件需要包含支持架构的所有不同二进制文件。 二进制文件和支持的架构的路径是使用清单文件 (**\`info.json\`**) 指定的,该文件位于 Artifact Bundle 目录的根目录中。 你可以将此清单文件视为一个地图或指南,以帮助 Swift 确定哪些可执行文件可用于哪种架构以及可以在哪里找到它们。 51 | 52 | #### 以 SwiftLint 为例 53 | **[SwiftLint](https://github.com/realm/SwiftLint "SwiftLint")** 在整个社区中被广泛用作 Swift 代码的静态代码分析工具。 由于很多人都非常渴望让这个插件在他们的 SwiftPM 项目中运行,我认为这将是一个很好的例子来展示我们如何将分发的可执行文件从他们的发布页面变成一个与 macOS 架构和 Linux arm64兼容的工件包。 54 | 55 | 让我们从下载两个可执行文件(**[macOS](https://github.com/realm/SwiftLint/releases/download/0.47.0/portable_swiftlint.zip macOS)** 和 **[Linux](https://github.com/realm/SwiftLint/releases/download/0.47.0/swiftlint_linux.zip "Linux")**)开始。 56 | 57 | 至此,bundle的结构就可以创建好了。 为此,创建一个名为 **\`swiftlint.artifactbundle\`** 的目录并在其根目录添加一个空的 **\`info.json\`**: 58 | 59 | ```shell 60 | mkdir swiftlint.artifactbundle 61 | touch swiftlint.artifactbundle/info.json 62 | ``` 63 | 64 | 现在可以使用 **\`schemaVersion\`** 填充清单文件,这可能会在未来版本的工件包和具有两个变体的工件中发生变化,这将很快定义: 65 | 66 | ```json 67 | { 68 | "schemaVersion": "1.0", 69 | "artifacts": { 70 | "swiftlint": { 71 | "version": "0.47.0", # The version of SwiftLint being used 72 | "type": "executable", 73 | "variants": [ 74 | ] 75 | }, 76 | } 77 | } 78 | ``` 79 | 80 | 需要做的最后一件事是将二进制文件添加到包中,然后将它们作为变体添加到 **\`info.json\`** 文件中。 让我们首先创建目录并将二进制文件放入其中(macOS 的一个在 **\`swiftlint-macos/swiftlint\`**,Linux 的一个在 **\`swiftlint-linux/swiftlint\`**)。 81 | 82 | 添加这些之后,可以在清单文件中变量: 83 | 84 | ```json 85 | { 86 | "schemaVersion": "1.0", 87 | "artifacts": { 88 | "swiftlint": { 89 | "version": "0.47.0", # The version of SwiftLint being used 90 | "type": "executable", 91 | "variants": [ 92 | { 93 | "path": "swiftlint-macos/swiftlint", 94 | "supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"] 95 | }, 96 | { 97 | "path": "swiftlint-linux/swiftlint", 98 | "supportedTriples": ["x86_64-unknown-linux-gnu"] 99 | }, 100 | ] 101 | }, 102 | } 103 | } 104 | ``` 105 | 106 | 为此,需要为每个变量指定二进制文件的相对路径(从工件包目录的根目录)和支持的三元组。 如果您不熟悉 **[目标三元组](https://clang.llvm.org/docs/CrossCompilation.html#target-triples "target-triples")**,它们是一种选择构建二进制文件的架构的方法。 请注意,这不是**主机**(构建可执行文件的机器)的体系结构,而是**目标**机器(应该运行所述可执行文件的机器)。 107 | 108 | 这些三元组具有以下格式: **\`----\`** 并非所有字段都是必需的,如果其中一个字段未知并且要使用默认值,则可以省略或替换为 **\`unknown\`** 关键字。 109 | 110 | 可执行文件的架构切片可以通过运行 **\`file\`** 找到,这将打印捆绑的任何切片的供应商、系统和架构。 在这种情况下,为这两个命令运行它会显示: 111 | 112 | **swiftlint-macos/swiftlint** 113 | 114 | ``` 115 | swiftlint: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64] 116 | swiftlint (for architecture x86_64): Mach-O 64-bit executable x86_64 117 | swiftlint (for architecture arm64): Mach-O 64-bit executable arm64 118 | ``` 119 | 120 | **swiftlint-linux/swiftlint** 121 | 122 | ``` 123 | -> file swiftlint 124 | swiftlint: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped 125 | ``` 126 | 这带来了上面显示的 macOS 支持的两个三元组(**\`x86_64-apple-macosx‌\`**、**\`arm64-apple-macosx\`**)和 Linux 支持的一个三元组(**\`x86_64-unknown-linux-gnu\`**)。 127 | 128 | 与 **\`XCFrameworks\`** 类似,工件包也可以通过使用 **[binaryTarget](https://developer.apple.com/documentation/swift_packages/distributing_binary_frameworks_as_swift_packages)** 包含在 Swift 包中。 129 | 130 | ## 结论 131 | 简而言之,我们可以总结 2022 年如何在 Swift 包中使用二进制文件的最佳实践,如下所示: 132 | 133 | 1. 如果你需要为你的 iOS/macOS 项目添加预编译库或可执行文件,您应该使用 **\`XCFramework\`**,并为每个用例(iOS 设备、macOS 设备和 iOS 模拟器)包含单独的二进制文件。 134 | 2. 如果你需要创建一个插件并运行一个可执行文件,你应该将其嵌入为一个工件包,其中包含适用于不同支持架构的二进制文件。 -------------------------------------------------------------------------------- /resource/42 如何使用 SwiftUI 中新地图框架 MapKit.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 了解 iOS 17 中的 MapKit 后,我们会发现 Apple 引入了更适合 SwiftUI 的 API。 4 | 5 | ## MapKit 弃用项 6 | 7 | 一旦将你的 App 目标更新到 iOS 17,Xcode 会将任何使用旧的 Map 初始化器的用法标记为已弃用: 8 | 9 | ![](https://files.mdnice.com/user/17787/e99b3a8b-50af-474d-9cc5-5b5b2bbdfb46.png) 10 | 11 | 会有警告提示:init coordinate region 已在 iOS 17 中弃用。请改用带有 MapContentBuilder 参数的地图初始化器。 12 | 13 | 在 iOS 17 中,MapKit 为 SwiftUI 引入了需要 `MapContentBuilder` 参数的地图初始化器。下面为大家介绍一下MapKit 相关的基础知识。 14 | 15 | ## MapContentBuilder(iOS 17) 16 | 17 | 在 iOS 17 中,用于地图视图的各种初始化器都需要一个名为 `MapContentBuilder` 的 content 参数。MapContentBuilder 是一个结果构建器,允许在闭包中添加地图内容,例如标记、注释和自定义内容。 18 | 19 | 下面让我们看看是如何使用的,这里是一些伦敦地标的坐标: 20 | 21 | ```swift 22 | extension CLLocationCoordinate2D { 23 | static let towerBridge = CLLocationCoordinate2D(latitude: 51.5055, longitude: -0.075406) 24 | static let boe = CLLocationCoordinate2D(latitude: 51.5142, longitude: -0.0885) 25 | static let hydepark = CLLocationCoordinate2D(latitude: 51.508611, longitude: -0.163611) 26 | static let kingsCross = CLLocationCoordinate2D(latitude: 51.5309, longitude: -0.1233) 27 | } 28 | ``` 29 | 30 | 要创建一个带有标记和注释的地图视图,详细代码如下: 31 | 32 | ```swift 33 | struct ContentView: View { 34 | var body: some View { 35 | Map { 36 | Marker("Tower Bridge", coordinate: .towerBridge) 37 | Marker("Hyde Park", coordinate: .hydepark) 38 | Marker("Bank of England", 39 | systemImage: "sterlingsign", coordinate: .boe) 40 | .tint(.green) 41 | 42 | Annotation("Kings Cross", 43 | coordinate: .kingsCross, anchor: .bottom) { 44 | VStack { 45 | Text("在此搭乘火车!") 46 | Image(systemName: "train.side.front.car") 47 | } 48 | .foregroundColor(.blue) 49 | .padding() 50 | .background(in: .capsule) 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | 在没有其他选项的情况下,地图视图的边界将包围地图内容。 58 | 59 | ## 地图交互 60 | 61 | 为了控制用户与地图的交互方式,可以传递一组允许的模式。默认情况下允许所有模式(平移、缩放、倾斜、旋转),代码如下: 62 | 63 | ```swift 64 | Map(interactionModes: [.pan,.pitch]) { ... } 65 | ``` 66 | 67 | ## 地图样式 68 | 69 | 使用 Map Style 视图修饰符可以在标准、卫星或混合样式之间切换,控制高度、显示兴趣点和显示交通情况,代码如下: 70 | 71 | ```swift 72 | Map { ... 73 | } 74 | .mapStyle(.hybrid(elevation: .realistic, 75 | pointsOfInterest: .including([.publicTransport]), 76 | showsTraffic: true)) 77 | ``` 78 | 79 | ## 地图控件 80 | 81 | 标准的地图控件,如指南针、用户位置、倾斜、比例尺和缩放控件都实现为 SwiftUI 视图。这意味着可以将它们放置在视图的任何位置,不过需要定义一个地图范围命名空间,以将它们与它们控制的地图关联起来,代码如下: 82 | 83 | ```swift 84 | struct ContentView: View { 85 | @Namespace var mapScope 86 | 87 | var body: some View { 88 | VStack { 89 | Map(scope: mapScope) { ... } 90 | MapCompass(scope: mapScope) 91 | } 92 | .mapScope(mapScope) 93 | } 94 | } 95 | ``` 96 | 97 | 要将它们放置在标准位置,使用地图控件视图修饰符,代码如下: 98 | 99 | ```swift 100 | Map { ... 101 | } 102 | .mapControls { 103 | MapPitchToggle() 104 | MapUserLocationButton() 105 | MapCompass() 106 | } 107 | ``` 108 | 109 | ## 地图相机位置 110 | 111 | 地图相机位置定义了从地图表面上方查看地图的虚拟位置。可以使用现有的地图项、地图边界、区域或用户位置来创建地图相机位置并设置初始地图位置,代码如下: 112 | 113 | ```swift 114 | Map(initialPosition: position) 115 | ``` 116 | 117 | 将 `MapCameraPosition` 的绑定传递给地图,使其在用户在地图上移动时跟踪相机位置,代码如下: 118 | 119 | ```swift 120 | struct ContentView: View { 121 | @State private var position: MapCameraPosition = .region(.uk) 122 | 123 | var body: some View { 124 | Map(position: $position) { 125 | Marker("Tower Bridge", coordinate: .towerBridge) 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | 设置位置会导致地图更改其相机位置以匹配。例如,在用户移动位置后,要在 toolbar 中添加一个按钮,以将地图重置为初始位置,代码如下: 132 | 133 | ```swift 134 | Map(position: $position) { ... 135 | } 136 | .toolbar { 137 | ToolbarItem { 138 | Button("重置") { 139 | position = .region(.uk) 140 | } 141 | } 142 | } 143 | ``` 144 | 145 | 将位置设置为 `.automatic` 可以使地图框架内容。 146 | 147 | ## 总结 148 | 149 | 这就是在 iOS 17 中使用 SwiftUI 中的 MapKit 所需要了解的内容。通过引入 MapContentBuilder 和其他新的初始化器,可以更方便地创建交互式地图视图,添加标记、注释和自定义内容,并在用户移动地图相机时自动更新位置。 150 | 151 | 此外,还可以使用 Map Style 修饰符和 Map 控件来自定义地图的样式和控件。这些改进使得在 SwiftUI 中使用 MapKit 变得更加强大和灵活。 -------------------------------------------------------------------------------- /resource/43 实现模块化应用的本地化.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 我已经有一段时间没有从头开始一个需要支持多种语言的新项目了。当然不是从头开始,而是在代码库中通过使用 Swift 包将代码分成不同模块。 4 | 5 | 我想提醒自己记住许多在本地化实行中的过程,所以我认为最好写一篇文章,以便下次开始同类型项目时可以参考。 6 | 7 | ## 开始吧! 8 | 9 | 让我们看看代码库的一个简化版本。它包含一个 Xcode 项目,一个单独的 app target(即将运行的那个)和一个名为 `Features` 的 Swift 包。后者将包含 app 中所有页面的代码,每一页将被定义为自己的产品: 10 | 11 | **Package.swift** 12 | 13 | ```swift 14 | // swift-tools-version: 5.6 15 | import PackageDescription 16 | 17 | let package = Package( 18 | name: "Features", 19 | products: [ 20 | .library( 21 | name: "Home", 22 | targets: ["Home"]), 23 | .library( 24 | name: "Detail", 25 | targets: ["Detail"] 26 | ) 27 | ], 28 | dependencies: [ 29 | ], 30 | targets: [ 31 | .target( 32 | name: "Home" 33 | ), 34 | .target( 35 | name: "Detail" 36 | ) 37 | ] 38 | ) 39 | ``` 40 | 41 | 这个 app target 将会作为 app 的组合层,其唯一的目的是导入每个功能,实例化它们并协调导航。所有的 UI ,演示和业务逻辑将留在各自的 "模块" 中(` Features Swift Package` 中的一个 target)。这将允许每个功能独立开发并完全的与其他功能隔离。 42 | 43 | 为了简单起见,这个例子里仅有两个功能:主页和详情,他们代表 app 中仅有的两个页面。 44 | 45 | 主页有一个按钮允许用户导航到详情页面,还有一个标签展示用户当前所在区域的语言代码。详情页只展示一个标签,和主页标签展示的信息一致: 46 | 47 | ![Images of both screens with english selected as the language](https://www.polpiella.dev/assets/posts/modularised-app-localisation/detail-home-en.png) 48 | 49 | ## 添加字符串! 50 | 51 | 看起来不错,但是现在展示的信息是用英文通过硬编码编写的字符串。app 需要内容被翻译成另外两种语言:加泰罗尼亚语和西班牙语。 52 | 53 | 虽然有多种实现方式,我更倾向每个功能(或页面)只包含它所需要的本地化字符串,这样可以增加功能的可移植性和可重用性。 54 | 55 | 这可以在 Swift 包中完成,通过将所有必需的 `.lproj` 文件和所有需要本地化的内容(当前例子中只有 `Localizable.strings` 文件)放在目标文件夹下 - 我的习惯是放在父 `Resources/` 文件夹下,并将这些资源定义为 `Package.swift` 的特定 target。 56 | 57 | 添加文件之后构建该功能将导致编译器抛出如下错误: 58 | 59 | ![Error thrown by Xcode when no default localisation is set](https://www.polpiella.dev/assets/posts/modularised-app-localisation/default-localisation-error.png) 60 | 61 | 这是因为 `defaultLocalization` 必须由 `Package.swift` 提供。所有功能的 target 来自一个包,所以只能有一个 `defaultLocalization` 。以下是 `Package.swift` 添加本地化内容之后的样子: 62 | 63 | **Package.swift** 64 | 65 | ```swift 66 | // swift-tools-version: 5.6 67 | import PackageDescription 68 | 69 | let package = Package( 70 | name: "Features", 71 | defaultLocalization: "en", 72 | platforms: [.iOS(.v15)], 73 | products: [ 74 | .library( 75 | name: "Home", 76 | targets: ["Home"]), 77 | .library( 78 | name: "Detail", 79 | targets: ["Detail"] 80 | ) 81 | ], 82 | dependencies: [ 83 | ], 84 | targets: [ 85 | .target( 86 | name: "Home", 87 | dependencies: [], 88 | resources: [.process("Resources/")] 89 | ), 90 | .target( 91 | name: "Detail", 92 | resources: [.process("Resources/")] 93 | ) 94 | ] 95 | ) 96 | ``` 97 | 98 | > 注意:如果没有为默认的本地化代码提供本地化的内容,编译器会显示警告。这对于确保你不会发布包含基本本地化内容的软件包版本非常有帮助。 99 | > 100 | > ![Xcode warning shown when default localisation is missing.](https://www.polpiella.dev/assets/posts/modularised-app-localisation/missing-default-localisation.png) 101 | 102 | 103 | ## 支持本地化 104 | 105 | 可能与你的想法正好相反,把设备系统语言设置为加泰罗尼亚语或西班牙语并且运行 app 内容仍然用英文展示。原因是 Swift 包需要额外的信息去决定使用哪些本地化的内容,就目前来看,如果包里有目标内容,它们将只使用目标的基本本地化,否则使用包的默认本地化。 106 | 107 | 现在有两种方式我们可以实现本地化:使新的本地化在 app target 中可用或启用混合本地化。 108 | 109 | ### 在 app target 中添加新的本地化内容 110 | 111 | 在 `Features` Swift 包中启用新的本地化的一种方式是将它们添加到导入功能的 Xcode 项目中。这可以通过进入 Xcode 项目,在项目设置中的 "Info" 一栏,添加本地化支持: 112 | 113 | 116 | 117 | > 需要注意的是,本地化需要至少一个文件(例如一个空的 `Localizable.strings` 文件)。在本例中,因为 app target 是用 UIKit 构建的,并且在添加新的本地化时选择了启动 storyboard 进行本地化(如上视频所示),所以已经有一个本地化文件。 118 | 119 | 现在这将允许包从主包中获取支持的本地化,并选择相应的要使用的资源。 120 | 121 | 值得注意的是,如果设备有被 app 支持但是包不支持的语言,则后者将会回退到 `Package.swift` 中提供的 `defaultLocalization` . 122 | 123 | 同样的,如果 app 不支持该语言,同样会回退到相同的值。这也是为什么将 `defaultLocalization` 设置为与主目标基础语言相同,以确保所有页面上的一致性是非常重要的。这也是我更倾向于所有功能分组在一个 Swift 包之下的原因,这样所有页面上的 `defaultLocalization` 就有了单一真正的来源。 124 | 125 | ### 允许混合本地化 126 | 127 | 虽然采用 app target 的本地化是首选方法,因为他确保了所有页面的一致性,并且只允许少数受支持的地方使用,但还有另一种方法允许包内容被本地化,而不必在主项目之外。 128 | 129 | 可以通过将 app 的 `Info.plist` 文件中的 `CFBundleAllowMixedLocalizations` 值设置为 `YES` 来实现。 130 | 131 | 这个设置将会告诉 app target 在不同的 target 或功能使用不同本地化是可以的,当添加新的本地化资源时, app 本地化会自动工作。 132 | 133 | ![Enabling mixed localisations in the app target](https://www.polpiella.dev/assets/posts/modularised-app-localisation/enable-mixed-localizations.png) 134 | 135 | 使用这种方法需要注意以下几点: 136 | 137 | 1.不再需要将本地化添加到 app target,添加带有本地化内容的 `lproj` 到包资源就可以了。当用户修改区域时,如果你的资源包存在该语言包或默认提供 `Package.swift` ,软件包也会展示该区域的语言内容。 138 | 139 | 2.支持多少个区域就会有多少个本地化资源。这意味着没有一个单一的真实来源来确定整个 app 支持哪些本地化。这可能会导致一些问题,例如,某个功能有本地化资源内容,而该内容的本地化资源还未被应用。在本例中,除了删除资源,没有办法隐藏它。 140 | 141 | 144 | 145 | 第二点如上面的视频中所示,当用户把设备语言设置为法语。混合来源导致了不一致,因为主屏幕没有 `fr.lproj` --因此它又回到了默认本地化资源,英语。另一方面,在详情页面,有可用的本地化内容,这是正确翻译字符串的原因,正是这个原因,我喜欢将 app target 作为所有支持本地化的真实来源。 146 | 147 | ## 额外提示 - 自动化 148 | 149 | 我一直鼓励尽可能地自动化检索特定包的本地化字符串的流程。如果你的 app 有很多页面,希望使添加本地化字符串的过程尽可能简单和简便。 150 | 151 | 我一直在使用的一款工具 [SwiftGen](https://github.com/SwiftGen/SwiftGen),它可以为各种资源生成 Swift 接口,例如 `Localizable.strings` 文件。 152 | 153 | 创建一个利用这个可执行文件的构建工具插件,可以使支持新本地化过程变得容易一点,并在各功能之间保持一致。 154 | 155 | > 译自:[Localising a modularised application](https://www.polpiella.dev/modularised-app-localisation) 156 | -------------------------------------------------------------------------------- /resource/44 使用 Swift 的并发系统并行运行多个任务.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | Swift 内置并发系统的好处之一是它可以更轻松地并行执行多个异步任务,这反过来又可以使我们显着加快可以分解为单独部分的操作。 4 | 5 | 在本文中,让我们看一下几种不同的方法,以及这些技术中的每一种何时特别有用。 6 | 7 | ## 从异步到并发 8 | 9 | 首先,假设我们正在开发某种形式的购物应用程序来显示各种产品,并且我们已经实现了一个`ProductLoader`允许我们使用一系列异步 API 加载不同产品集合的应用程序,如下所示: 10 | 11 | ```swift 12 | class ProductLoader { 13 | ... 14 | 15 | func loadFeatured() async throws -> [Product] { 16 | ... 17 | } 18 | 19 | func loadFavorites() async throws -> [Product] { 20 | ... 21 | } 22 | 23 | func loadLatest() async throws -> [Product] { 24 | ... 25 | } 26 | } 27 | ``` 28 | 29 | 尽管大多数情况下上述每个方法都可能会被单独调用,但假设在我们应用程序的某些部分中,我们还希望形成一个`Recommendations`包含这三个`ProductLoader`方法的所有结果的组合模型: 30 | 31 | ```swift 32 | extension Product { 33 | struct Recommendations { 34 | var featured: [Product] 35 | var favorites: [Product] 36 | var latest: [Product] 37 | } 38 | } 39 | ``` 40 | 41 | 一种方法是使用`await`关键字调用每个加载方法,然后使用这些调用的结果来创建我们`Recommendations`模型的实例——如下所示: 42 | 43 | ```swift 44 | extension ProductLoader { 45 | func loadRecommendations() async throws -> Product.Recommendations { 46 | let featured = try await loadFeatured() 47 | let favorites = try await loadFavorites() 48 | let latest = try await loadLatest() 49 | 50 | return Product.Recommendations( 51 | featured: featured, 52 | favorites: favorites, 53 | latest: latest 54 | ) 55 | } 56 | } 57 | ``` 58 | 59 | 上面的实现确实有效——然而,即使我们的三个加载操作都是完全异步的,它们目前正在*按顺序*执行,一个接一个。因此,尽管我们的顶级`loadRecommendations`方法相对于我们应用程序的其他代码正在并发执行,但实际上它还没有利用并发来执行其内部操作集。 60 | 61 | 由于我们的产品加载方法不以任何方式相互依赖,因此实际上没有理由按顺序执行它们,所以让我们看看如何让它们完全同时执行。 62 | 63 | 关于如何做到这一点的初步想法可能是将上述代码简化为单个表达式,这将使我们能够使用单个`await`关键字来等待我们的每个操作完成: 64 | 65 | ```swift 66 | extension ProductLoader { 67 | func loadRecommendations() async throws -> Product.Recommendations { 68 | try await Product.Recommendations( 69 | featured: loadFeatured(), 70 | favorites: loadFavorites(), 71 | latest: loadLatest() 72 | ) 73 | } 74 | } 75 | ``` 76 | 77 | 然而,即使我们的代码现在*看起来是*并发的,它实际上仍会像以前一样完全按顺序执行。 78 | 79 | 相反,我们需要利用 Swift 的`async let`绑定来告诉并发系统并行执行我们的每个加载操作。使用该语法使我们能够在后台启动异步操作,而无需我们立即等待它完成。 80 | 81 | `await`如果我们在实际*使用*加载的数据时(即形成模型时)将其与单个关键字组合`Recommendations`,那么我们将获得并行执行加载操作的所有好处,而无需担心状态管理或数据竞争之类的事情: 82 | 83 | ```swift 84 | extension ProductLoader { 85 | func loadRecommendations() async throws -> Product.Recommendations { 86 | async let featured = loadFeatured() 87 | async let favorites = loadFavorites() 88 | async let latest = loadLatest() 89 | 90 | return try await Product.Recommendations( 91 | featured: featured, 92 | favorites: favorites, 93 | latest: latest 94 | ) 95 | } 96 | } 97 | ``` 98 | 99 | 很整齐!因此`async let`,当我们有一组已知的、有限的任务要执行时,它提供了一种同时运行多个操作的内置方法。但如果不是这样呢? 100 | 101 | ## 任务组 102 | 103 | 现在假设我们正在开发一个`ImageLoader`可以让我们通过网络加载图像的工具。要从给定的 加载单个图像`URL`,我们可以使用如下所示的方法: 104 | 105 | ```swift 106 | class ImageLoader { 107 | ... 108 | 109 | func loadImage(from url: URL) async throws -> UIImage { 110 | ... 111 | } 112 | } 113 | ``` 114 | 115 | 为了使一次加载一系列图像变得简单,我们还创建了一个方便的 API,它接受一个 URL 数组并异步返回一个图像字典,该字典由下载图像的 URL 键控: 116 | 117 | ```swift 118 | extension ImageLoader { 119 | func loadImages(from urls: [URL]) async throws -> [URL: UIImage] { 120 | var images = [URL: UIImage]() 121 | 122 | for url in urls { 123 | images[url] = try await loadImage(from: url) 124 | } 125 | 126 | return images 127 | } 128 | } 129 | ``` 130 | 131 | 现在让我们说,就像我们`ProductLoader`之前的工作一样,我们想让上面的`loadImages`方法并发执行,而不是按顺序下载每个图像(目前是这种情况,因为我们`await`在调用时直接使用`loadImage`我们的`for`环形)。 132 | 133 | 但是,这次我们将无法使用`async let`,因为我们需要执行的任务数量在编译时是未知的。值得庆幸的是,Swift 并发工具箱中还有一个工具可以让我们并行执行动态数量的任务——*任务组*。 134 | 135 | 要形成一个任务组,我们可以调用`withTaskGroup`或`withThrowingTaskGroup`,这取决于我们是否希望可以选择在我们的任务中抛出错误。在这种情况下,我们将选择后者,因为我们的底层`loadImage`方法是用`throws`关键字标记的。 136 | 137 | 然后我们将遍历每个 URL,就像以前一样,只是这次我们将每个图像加载任务添加到我们的组中,而不是直接等待它完成。相反,我们将`await`在添加每个任务之后单独分组结果,这将允许我们的图像加载操作完全并发执行: 138 | 139 | ```swift 140 | extension ImageLoader { 141 | func loadImages(from urls: [URL]) async throws -> [URL: UIImage] { 142 | try await withThrowingTaskGroup(of: (URL, UIImage).self) { group in 143 | for url in urls { 144 | group.addTask{ 145 | let image = try await self.loadImage(from: url) 146 | return (url, image) 147 | } 148 | } 149 | 150 | var images = [URL: UIImage]() 151 | 152 | for try await (url, image) in group { 153 | images[url] = image 154 | } 155 | 156 | return images 157 | } 158 | } 159 | } 160 | ``` 161 | 162 | 要了解有关上述`for try await`语法和一般异步序列的更多信息,请查看[“异步序列、流和组合”](https://www.swiftbysundell.com/articles/async-sequences-streams-and-combine)。 163 | 164 | 就像使用 时一样`async let`,以我们的操作不会直接改变任何状态的方式编写并发代码的一个巨大好处是,这样做可以让我们完全避免任何类型的数据竞争问题,同时也不需要我们引入任何锁定或序列化代码混合在一起。 165 | 166 | `await`因此,在可能的情况下,让我们的每个并发操作返回一个完全独立的结果,然后依次返回这些结果以形成我们的最终数据集,这通常是一种很好的方法。 167 | 168 | 在以后的文章中,我们将更仔细地研究避免数据竞争的其他方法(例如通过使用 Swift 的新`actor`类型)。 169 | 170 | ## 结论 171 | 172 | 重要的是要记住,仅仅因为给定的函数被标记为`async`并不一定意味着它同时执行它的工作。相反,如果这是我们想要做的,我们必须故意让我们的任务并行运行,这只有在执行一组可以独立运行的操作时才有意义。 173 | 174 | -------------------------------------------------------------------------------- /resource/47 项目中第三方库并不是必须的.md: -------------------------------------------------------------------------------- 1 | 2 | ## 前言 3 | 4 | 我在Lyft的八年间,很多产品经理以及工程师经常想往我们 app 里添加第三方库。有时候集成一个特定的库(比如 **PayPal**)是必须的,有时候是避免去开发一些非常复杂的功能,有时候仅仅只是避免c重复造轮子。 5 | 6 | 虽然这些都是合理的考量,但使用第三方库的风险和相关成本往往被忽视或误解。在某些情况下,风险是值得的,但是在决定冒险之前,首先要能够明确的定义风险。为了使风险评估更加的透明和一致,我们制定了一个流程来衡量我们将其集成到app有多大的风险。 7 | 8 | ## 风险 9 | 10 | 大多数大型组织,包括我们,都有某种形式的代码审查,作为开发实践的一部分。对这些团队来说,添加一个第三方库就相当于添加了一堆由不属于团队成员开发,未经审查的代码。这破坏了团队一直坚持的代码审查原则,交付了质量未知的代码。这给app的运行方式以及长期开发带来了风险,对于大型团队而言,更是对整体业务带来了风险。 11 | 12 | ### 运行时风险 13 | 14 | 库代码通常来说,对于系统资源,和app拥有相同级别的访问权限,但它们不一定应用团队为管理这些资源而制定的最佳实践。这意味着它们可以在没有限制的情况下访问磁盘,网络,内存,CPU等等,因此,它们可以(过度)将文件写入磁盘,使用未优化的代码占用内存或CPU,导致死锁或主线程延迟,下载(和上传!)大量数据等等。更糟糕的是他们会导致崩溃,甚至[崩溃循环](https://www.theverge.com/2020/5/7/21250689/facebook-sdk-bug-ios-app-crash-apple-spotify-venmo-tiktok-tinder)。[两次](https://github.com/facebook/facebook-ios-sdk/issues/1427)。 15 | 16 | 其中许多情况直到 app 已经上架才被发现,在这种情况下,修复它需要创建一个新版本,并通过审核,这通常需要大量时间和成本。这种风险可以通过一个变量控制是否调用来进行一定程度的控制,但是这种方法也并非万无一失(看下文)。 17 | 18 | ### 开发风险 19 | 20 | 引用一个同事的话:“每一行代码都是一种负担”,对不是你自己写的代码而言,这句话更甚。库在适配新技术或API时可能很慢,这阻碍了代码开发,或者太快,导致开发的版本过高。 21 | 22 | 库在采用新技术或API时可能很慢,阻碍了代码库,或者太快,导致部署目标太高。每当 Apple 和 Google 每年发布一个新 OS 版本时,他们通常要求开发人员根据SDK的变化更新代码,库开发人员也必须这样做。这需要协调一致的努力、优先事项的一致性以及及时完成工作的能力。 23 | 24 | 随着移动平台的不断变化,以及团队(成员)也不是一成不变,这将会成为一个持续不断的风险。当被集成的库不存在了,而库又需要更新时,会花很多时间来决定谁来做。事实证明一旦一个库存在,就很少也很难被移除,因此我们将其视为长期维护成本。 25 | 26 | ### 商业风险 27 | 28 | 如同我上面所说,现代的操作系统并没有对 app 代码和库代码进行区分,因此除了系统资源之外,它们还可以访问用户信息。作为 app 的开发者,我们负责恰当的使用这部分信息,也需要为任何第三方库负责。 29 | 30 | 如果用户给了 Lyft app 地理位置授权,任何第三方库也将自动得获得授权。他们可以将那些(地理位置)数据上传到自己服务器,竞对服务器,或者谁知道还有什么地方。当一个库需要我们没有的权限时,那问题就更大了。 31 | 32 | 同样,一个系统的安全取决于其最薄弱的环节,但如果其中包含未经审核的代码,那么你就不知道它到底有多安全。你精心设计的安全编码实践可能会被一个行为不当的库所破坏。苹果和谷歌实施的任何政策都是如此,例如“你不得对用户追踪”。 33 | 34 | ## 减少风险 35 | 36 | 当对一个库(是否)进行使用评估时,我们首先要问几个问题,以了解对库的需求。 37 | 38 | ### 我们内部能做么? 39 | 40 | 有时候我们只需要简单的粘贴复制真正需要的部分。在更复杂的场景中,库与自定义后端通信,我们对该API进行了逆向,并自己构建了一个迷你SDK(同样,只构建了我们需要的部分)。在90%的情况下,这是首选,但在与非常特定的供应商或需求集成时并不总是可行。 41 | 42 | ### 有多少用户从该库中受益? 43 | 44 | 在一种情况下,我们正在考虑添加一个风险很大的库(根据下面的标准),旨在为一小部分用户提供服务,同时将我们的所有用户都暴露在该库中。 对于我们认为会从中受益的一小部分客户,我们冒了为我们所有用户带来问题的风险。 45 | 46 | ### 这个库有什么传递依赖? 47 | 48 | 我们还需要评估库的所有依赖项的以下标准。 49 | 50 | ### 退出标准是什么? 51 | 52 | 如果集成成功,是否有办法将其转移到内部? 如果不成功,是否有办法删除? 53 | 54 | ## 评价标准 55 | 56 | 如果此时团队仍然希望集成库,我们要求他们根据一组标准对库进行“评分”。下面的列表并不全面,但应该能很好地说明我们希望看到的。 57 | 58 | ### 阻断标准 59 | 60 | 这些标准将阻止我们从技术上或者公司政策上集成此库,在进行下一步之前,我们必须解决: 61 | 62 | **过高的** **deployment target/target SDKs。** 我们支持过去4年主流的操作系统(版本),所以第三方库至少也需要支持一样多。 63 | 64 | **许可证不正确/缺失。** 我们将许可文件与应用捆绑在一起,以确保我们可以合法使用代码并将其归属于许可持有人。 65 | 66 | **没有冲突的传递依赖关系。** 一个库不能有一个我们已经包含但版本不同的传递依赖项。 67 | 68 | **不显示它自己的 UI 。** 我们非常小心地使我们的产品看起来尽可能统一,定制用户界面对此不利。 69 | 70 | **它不使用私有 API 。** 我们不愿意冒 app 因使用私有 API 而被拒绝的风险。 71 | 72 | ### 主要关注点 73 | 74 | **闭源。** 访问源代码意味着我们可以选择我们想要包含的库的哪些部分,以及如何将该源代码与应用程序的其余部分捆绑在一起。 对于我们来说,一个封闭源代码的二进制发行版更难集成。 75 | 76 | **编译时有警告。** 我们启用了“警告视为错误”,具有编译警告的库是库整体质量(下降)的良好指示。 77 | 78 | **糟糕的文档。** 我们希望有高质量的内联文档,外部”如何使用“文档,以及有意义的更新日志。 79 | 80 | **二进制体积。** 这个库有多大?一些库提供了很多功能,而我们只需要其中的一小部分。尤其是在没有访问源码权限的情况下,这通常是一个全有或全无的情况。 81 | 82 | **外部的网络流量。** 与我们无法控制的上游服务器/端点通信的库可能会在服务器关闭、错误数据被发回等时关闭整个应用程序。这也与我上面提到的隐私问题相同。 83 | 84 | **技术支持。** 当事情不能正常工作时,我们需要能够报告/上报问题,并在合理的时间内解决问题。开源项目通常由志愿者维护,也很难有一个时间线,但至少我们可以自己进行修改。这在闭源项目是不可能的。 85 | 86 | **无法禁用。** 虽然大多数库特别要求我们初始化它,但有些库在实例化时更“主动”,并且在我们不调用它的情况下可以自己执行工作。这意味着当库导致问题时,我们无法通过功能变量或其他机制将其关闭。 87 | 88 | 我们为所有这些(和其他一些)标准分配了点数,并要求工程师为他们想要集成的库汇总这些点数。虽然默认情况下,低分数并不难被拒绝,但我们通常会要求更多的理由来继续前进。 89 | 90 | ## 最后 91 | 92 | 虽然这个过程看起来非常严格,在许多情况下,潜在风险是假设的,但我们有我在这篇博文中描述的每个场景的实际例子。将评估记录下来并公开,也有助于将相对风险传达给不熟悉移动平台工作方式的人,并证明我们没有随意评估风险。 93 | 94 | 此外,我不想声称每一个第三方库本质上都是坏的。事实上,我们在Lyft使用了很多:`RxSwift`和`RxJava`、`Bugsnag`的`SDK`、`Google Maps`、`Tensorflow`,以及一些较小的用于非常特定的用例。但所有这些要么都经过了充分审查,要么我们已经决定风险值得收益,同时对这些风险和收益的真正含义有了清晰的认识。 95 | 96 | 最后,作为一个专业开发人员提示:始终在库的`API`之上创建自己的抽象,不要直接调用它们的`API`。这使得将来替换(或删除)底层库更加容易,再次减轻了与长期开发相关的一些风险。 97 | 98 | > [译自:Third-party libraries are no party at all](https://scottberrevoets.com/2022/07/15/third-party-libraries-are-no-party-at-all/?utm_source=swiftlee&utm_medium=swiftlee_weekly&utm_campaign=issue_124) -------------------------------------------------------------------------------- /resource/48 在Swift中编写脚本:Git Hooks.md: -------------------------------------------------------------------------------- 1 | # 在Swift中编写脚本:Git Hooks 2 | 3 | > [原文地址](https://www.polpiella.dev/scripting-in-swift-git-hooks#retrieving-the-ticket-number) 4 | 5 | 这周,我决定完成因为工作而推迟了一周的TODO事项来改进我的Git工作流程。 6 | 7 | 为了在提交的时候尽可能多的携带上下文信息,我们让提交信息包含了正在处理的JIRA编号。这样,将来如果有人回到我们现在正在提交的源代码,输入`git blame`,就能很容易的找出JIRA的编号。 8 | 9 | 每次提交都包含这些信息可能会有点乏味(如果你使用了类似[TDD](https://en.wikipedia.org/wiki/Test-driven_development)之类的方法,您会提交的更加频繁),而且,尽管像[Tower](https://www.git-tower.com/mac)这样的git客户端会让此变得容易一些,但是您仍然需要手动将问题编号复制粘贴到提交消息中,并且记住这样做,这是我最难以解决的问题😅。 10 | 11 | 出于这个原因,我开始寻求了解git hooks,试图自动化这项任务。我的想法是能够从git分支获取JIRA编号(我们有一个分支命名约定,形如:story/ISSUE-1234_branch-name),然后将提交消息更改为以JIRA编号为前缀,从而生成最终结果消息:ISSUE-1234-其他原本的提交信息。 12 | 13 | 14 | 15 | # 用git hooks自动生成提交信息 16 | 17 | **[Git Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)**提供了一种在运行某些重要的git命令时触发自定义操作的方法,例如在一次commit或者push之前执行一些操作。 18 | 19 | 在本例中,我使用了**`commit-msg`**钩子,它能够在当前提交信息生效前修改此信息。钩子由一个参数调用,该参数是指向包含用户输入的提交消息的文件的路径。这意味着,为了改变提交消息,我们只需要从文件中读取、修改其内容,然后写回调用挂钩的文件。 20 | 21 | 要创建git钩子,我们需要在**`.git/hooks`**路经下提供一个可执行脚本。我的钩子放在了**`.git/hooks/commit-msg`**路经之下。 22 | 23 | 24 | 25 | # 为什么我使用Swift? 26 | 27 | Git hooks可以使用任何你熟悉的,并且在主机上安装了解释器(通过`shebang`来指定)的脚本语言来编写。 28 | 29 | 虽然有很多更受欢迎的选项,比如`bash`、`ruby`等等,但我还是决定使用Swift。因为我对Swift更熟悉,因为我每天都在使用它,而且我真的非常喜欢它强大的类型语法以及低内存占用。 30 | 31 | 32 | 33 | ## 让我们开始吧 34 | 35 | 你可以使用任何你喜欢的IDE编写Swift脚本。但是如果你想要有适当的代码补全以及调试能力,你可以为其创建一个Xcode项目。为此,在**`macOS`**下选择**`Command Line Tool`**创建一个新的项目。 36 | 37 | ![在Xcode中创建项目](Assets/xcode-new-project.png "在Xcode中创建项目") 38 | 39 | 40 | 41 | 在创建的文件顶部加上Swift shebang,引入`Foundation`库。 42 | 43 | ``` swift 44 | #!/usr/bin/swift 45 | import Foundation 46 | ``` 47 | 48 | 这样当git执行文件时,shebang将确保使用文件作为输入数据调用/usr/bin/swift二进制文件。 49 | 50 | 51 | 52 | ## 编写git钩子 53 | 54 | 项目已经全部设置好,所以现在可以编写git挂钩了。让我们走完所有的步骤。 55 | 56 | 57 | 58 | ### 检索提交消息 59 | 60 | 要做的第一件事就是从脚本传进来的参数检索临时提交文件的路径然后读取文件内容。 61 | 62 | ```swift 63 | let commitMessageFile = CommandLine.arguments[1] 64 | 65 | guard let data = FileManager.default.contents(atPath: commitMessageFile), 66 | let commitMessage = String(data: data, encoding: .utf8) else { 67 | exit(1) 68 | } 69 | ``` 70 | 71 | 在上面的代码片段中,我们首先拿到了提交文件的路径(`git`传递给脚本),然后通过`FileManagerAPI`读取了文件内容。如果因为一些原因检索失败了,我们退出(`exit`)脚本同时返回状态码`1`,这将告诉git终止此次提交。 72 | 73 | --- 74 | **注意:** 75 | 76 | 根据[git hooks文档](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks),如果任何钩子脚本返回的状态码大于`0`,它都将终止即将要要发生的操作。这将在本文后面的部分中使用,以便在不需要做任何修改而优雅地退出。 77 | 78 | --- 79 | 80 | ### 检索问题编号 81 | 82 | 既然提交信息的字符串已经可用,接下来就需要找到当前分支并从中检索到问题编号。正如本文前面提到的,这只可能是因为团队对分支命名的严格格式,在其名称中始终包含JIRA编号(例如,**`story/ISSUE-1234_some-awesome-feature-work`**)。 83 | 84 | 为了实现这一点,我们必须检索当前的工作分支,然后用[正则表达式](https://nshipster.com/swift-regular-expressions/)从中检索问题编号。 85 | 86 | 让我们从添加脚本调用`zsh shell`命令的能力开始。通过使用`Process`api,脚本可以与`git`命令行界面交互。 87 | 88 | ```swift 89 | func shell(_ command: String) -> String { 90 | let task = Process() 91 | let outputPipe = Pipe() 92 | let errorPipe = Pipe() 93 | 94 | task.standardOutput = outputPipe 95 | task.standardError = errorPipe 96 | task.arguments = ["-c", command] 97 | task.executableURL = URL(fileURLWithPath: "/bin/zsh") 98 | 99 | do { 100 | try task.run() 101 | task.waitUntilExit() 102 | } catch { 103 | print("There was an error running the command: \(command)") 104 | print(error.localizedDescription) 105 | exit(1) 106 | } 107 | 108 | guard let outputData = try? outputPipe.fileHandleForReading.readToEnd(), 109 | let outputString = String(data: outputData, encoding: .utf8) else { 110 | // Print error if needed 111 | if let errorData = try? errorPipe.fileHandleForReading.readToEnd(), 112 | let errorString = String(data: errorData, encoding: .utf8) { 113 | print("Encountered the following error running the command:") 114 | print(errorString) 115 | } 116 | exit(1) 117 | } 118 | 119 | return outputString 120 | } 121 | ``` 122 | 123 | 现在实现了`shell`命令,那么就可以使用它询问`git`当前分支是什么,然后尽可能的从中提取出问题编号。 124 | 125 | ```swift 126 | let gitBranchName = shell("git rev-parse --abbrev-ref HEAD") 127 | .trimmingCharacters(in: .newlines) 128 | 129 | let stringRange = NSRange(location: 0, length: gitBranchName.utf16.count) 130 | 131 | guard let regex = try? NSRegularExpression(pattern: #"(\w*-\d*)"#, options: .anchorsMatchLines), 132 | let match = regex.firstMatch(in: gitBranchName, range: stringRange) else { 133 | exit(0) 134 | } 135 | 136 | let range = match.range(at: 1) 137 | 138 | let ticketNumber = (gitBranchName as NSString) 139 | .substring(with: range) 140 | .trimmingCharacters(in: .newlines) 141 | ``` 142 | 143 | 请注意,如果没有匹配项(即分支名称中不包含JIRA问题编号),脚本将以0的状态退出,允许提交继续进行,而不进行任何更改。这是为了不破坏诸如main或其他测试/调查分支中的工作流。 144 | 145 | 146 | 147 | ### 修改提交信息 148 | 149 | 为了更改提交消息,必须将脚本开头读取的文件内容(包含提交消息)写回同一路径。 150 | 151 | 在这种情况下,只需要做一个更改,即在提交信息的前面加上JIRA编号和(-),以将其与提交信息的其余部分很好地分开。还必须确保检查了提交信息字符串,仅在编号不存在时才添加编号: 152 | 153 | ```swift 154 | if !commitMessage.contains(ticketNumber) { 155 | do { 156 | try "\(ticketNumber) - \(commitMessage.trimmingCharacters(in: .newlines))" 157 | .write(toFile: commitMessageFile, atomically: true, encoding: .utf8) 158 | } catch { 159 | print("Could not write to file \(commitMessageFile)") 160 | exit(1) 161 | } 162 | } 163 | ``` 164 | 165 | 166 | 167 | ### 设置git钩子 168 | 169 | 现在脚本已经准备好了,是时候把它放在git可以找到它的位置了。Git钩子可以全局设置,也可以基于单个repo设置。 170 | 171 | 我个人对这类脚本的偏好是基于单个repo设置,因为这样可以在出现问题时为您提供更多的控制和可见性,并且如果钩子开始失败,它会在它设置的repo中失败,而不是全局都失败。 172 | 173 | 要设置它们,我们只需要使文件可执行,重命名并将其复制到所要设置repo的**`.git/hooks/`**路径之下: 174 | 175 | ```shell 176 | chmod +x main.swift 177 | mv main.swift /.git/hooks/commit-msg 178 | ``` 179 | 180 | 181 | 182 | # 测试结果 183 | 184 | 现在repo已经全部设置好了,剩下的就是对部署的脚本进行测试。在下面的截屏中,创建了两个分支,一个带有问题编号,一个没有,它们有着相同的提交信息。可以看出脚本运行正常,并且只在需要时才更改提交消息! 185 | 186 | ![测试结果](Assets/git-hook-output.png "测试结果") -------------------------------------------------------------------------------- /resource/49 逐步实现基于源码的 Swift 代码覆盖率.md: -------------------------------------------------------------------------------- 1 | # 逐步实现基于源码的 Swift 代码覆盖率 2 | 3 | ## 介绍 4 | 5 | 最近,正在为我司的项目研究基于 Swift 的代码覆盖率检测方案的解决方案,我已经努力尝试并且找到了最佳实践。 6 | 7 | 在这篇短文中,我将会给你介绍: 8 | 9 | - **如何生成 \*.profraw 文件并通过命令行测量代码覆盖率** 10 | 11 | - **如何在 Swift App 项目里调用 C/C++ 方法** 12 | 13 | - **如何在 Xcode 中测量完整 Swift App 项目的代码覆盖率** 14 | 15 | ## 使用命令行练习 16 | 17 | 在我们测量完整 App 项目的代码覆盖率之前,需要创建一个简单的 Swift 源代码文件,并且用命令行生成一个 `*.profraw` 文件,以便我们学习生成覆盖配置文件的基本工作流程。 18 | 19 | 创建一个 Swift 文件并包含以下代码: 20 | 21 | ```swift 22 | test() 23 | print("hello") 24 | func test() { 25 | print("test") 26 | } 27 | func add(_ x: Double, _ y: Double) -> Double { 28 | return x + y 29 | } 30 | test() 31 | ``` 32 | 33 | 在终端运行以下命令: 34 | 35 | ```shell 36 | swiftc -profile-generate -profile-coverage-mapping hello.swift 37 | ``` 38 | 39 | 传递给编译器的选项 `-profile-generate` 和 `-profile-coverage-mapping` 将在编译源码时启用覆盖特性。基于源码的代码覆盖功能直接对 AST 和预处理器信息进行操作。 40 | 41 | 然后运行输出的二进制文件: 42 | 43 | ```shell 44 | ./hello 45 | ``` 46 | 47 | 运行完成之后,在当前目录下执行 `ls` ,我们会看到这里生成了一个名为 `default.profraw` 的新文件。该文件由 llvm 生成,为了衡量代码覆盖率,我们必须使用另一个工具 llvm-profdata 来组合多个原始配置文件并同时对其进行索引。 48 | 49 | ```shell 50 | xcrun llvm-profdata merge -sparse default.profraw -o hello.profdata 51 | ``` 52 | 53 | 在终端运行上面的命令行,我们会得到一个名为 `hello.profdata` 的新文件,它可以显示我们想要的覆盖率报告。我们可以使用 llvm-cov 来显示或生成 JSON 报告。 54 | 55 | ```shell 56 | xcrun llvm-cov show ./hello -instr-profile=hello.profdata 57 | xcrun llvm-cov export ./hello -instr-profile=hello.profdata 58 | ``` 59 | 60 | 现在,我们已经了解了生成快速代码覆盖率报告的基本工作流程。似乎 Swift 基于源码的代码覆盖并没有那么困难。但是,Xcode 中完整的 Swift App 项目的配置与命令行有很大的不同。那我们接着往下看吧! 61 | 62 | ## 在 Xcode 中测量 Swift App 项目的代码覆盖率 63 | 64 | ### 创建 Swift 项目 65 | 66 | ![img](https://miro.medium.com/max/1400/1*WI8GsF-tic-7ouDE0K93aQ.png) 67 | 68 | 69 | 70 | 选择 `SwiftCovApp target -> Build Settings -> Swift Compiler — Custom Flags`。 71 | 72 | 在 Other Swift Flags 添加 `-profile-generate` 和 `-profile-coverage-mapping` 选项: 73 | 74 | ![img](https://miro.medium.com/max/1400/1*mBc3LKpo3mq-tLen4xjc6g.png) 75 | 76 | 77 | 78 | 如果现在尝试编译,我们将会得到以下错误报告: 79 | 80 | ![img](https://miro.medium.com/max/1400/1*ZuxbmSFKnWGQ-ySkFeQwUw.png) 81 | 82 | 83 | 84 | 为了解决这个问题,我们必须为所有目标启用代码覆盖率: 85 | 86 | ![img](https://miro.medium.com/max/1400/1*SuLyYnOqgvEGru0eH--FLA.png) 87 | 88 | 89 | 90 | 在启用代码覆盖率之后再次运行,项目将会构建成功。 91 | 92 | 我们了解到,当程序退出时,编译器会将原始配置文件写入 `LLVM_PROFILE_FILE` 环境变量指定的路径。所以我们应该杀掉 Application 的进程来实现 `*.profraw` 文件。但是,当我们结束应用程序时,它会在控制台中报错: 93 | 94 | ![img](https://miro.medium.com/max/1400/1*VgfzkI0fWSHMuHECqDJ7CQ.png) 95 | 96 | 虽然我在 Build Settings 中设置了相同的配置,但 Xcode 中的默认环境路径为空。为了解决这个问题,我们必须新建一个头文件,并声明一些 llvm C api 函数供 Swift 调用。 97 | 98 | ### 在 Swift 中调用 C/C++ 方法 99 | 100 | Swift 是一种基于 C/C++ 的强大语言,它可以直接调用 C/C++ 方法。但是,在我们调用 llvm C/C++ api 之前,我们必须将我们需要的方法导出为一个模块。 101 | 102 | 首先,创建一个头文件: 103 | 104 | ![img](https://miro.medium.com/max/1400/1*j_nSIjeJ3Tx64yzCCwA9kQ.png) 105 | 106 | 然后,将以下代码复制粘贴到该文件中: 107 | 108 | ```swift 109 | #ifndef PROFILE_INSTRPROFILING_H_ 110 | #define PROFILE_INSTRPROFILING_H_int __llvm_profile_runtime = 0;void __llvm_profile_initialize_file(void); 111 | const char *__llvm_profile_get_filename(); 112 | void __llvm_profile_set_filename(const char *); 113 | int __llvm_profile_write_file(); 114 | int __llvm_profile_register_write_file_atexit(void); 115 | const char *__llvm_profile_get_path_prefix();#endif /* PROFILE_INSTRPROFILING_H_ */ 116 | ``` 117 | 118 | 创建一个 `module.modulemap` 文件并将所有内容导出为一个模块。 119 | 120 | ```swift 121 | // 122 | // module.modulemap 123 | // 124 | // Created by yao on 2020/10/15. 125 | //module InstrProfiling { 126 | header "InstrProfiling.h" 127 | export * 128 | } 129 | ``` 130 | 131 | 事实上我们不能直接创建 `module.modulemap`,首先创建一个 `module.c` 文件然后重命名为 `module.modulemap`,它还可以帮助我创建一个 `SwiftCovApp-Bridging-Header` 文件。 132 | 133 | 构建项目,然后,我们可以在 Swift 代码中调用 llvm apis。 134 | 135 | ``` swift 136 | import UIKit 137 | import InstrProfiling 138 | class ViewController: UIViewController { 139 | override func viewDidLoad() { 140 | super.viewDidLoad() 141 | // Do any additional setup after loading the view. 142 | print("File Path Prefix: \(String(cString: __llvm_profile_get_path_prefix()) )") 143 | print("File Name: \(String(cString: __llvm_profile_get_filename()) )") 144 | let name = "test.profraw" 145 | let fileManager = FileManager.default 146 | 147 | do { 148 | let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false) 149 | let filePath: NSString = documentDirectory.appendingPathComponent(name).path as NSString 150 | __llvm_profile_set_filename(filePath.utf8String) 151 | print("File Name: \(String(cString: __llvm_profile_get_filename()))") 152 | __llvm_profile_write_file() 153 | } catch { 154 | print(error) 155 | } 156 | } 157 | } 158 | ``` 159 | 160 | 构建并启动 App,我们将在控制台中看到原始配置文件路径。 161 | 162 | ![img](https://miro.medium.com/max/1400/1*mtmsCiJIZOfGlR6Ya5YlAQ.png) 163 | 164 | 最后,我们得到了需要的原始配置文件! 🎉 165 | 166 | 我们可以复制这个文件和 Swift App 项目中的 Mach-O(二进制文件)到 temp 目录下,这样我们就可以检查配置文件是否可以生成正确的报告。 167 | 168 | 创建一个新的 Swift 文件: 169 | 170 | ```swift 171 | import Foundation 172 | struct BasicMath { 173 | static func add(_ a: Double, _ b: Double) -> Double { 174 | return a + b 175 | } 176 | 177 | var x: Double = 0 178 | var y: Double = 0 179 | 180 | func sqrt(_ x: Double, _ min: Double, _ max: Double) -> Double { 181 | let q = 1e-10 182 | let mid = (max + min) / 2.0 183 | 184 | if fabs(mid * mid - x) > q { 185 | if mid * mid < x { 186 | return sqrt(x, mid, max) 187 | } else if mid * mid > x { 188 | return sqrt(x, min, mid) 189 | } else { 190 | return mid 191 | } 192 | } 193 | 194 | return mid 195 | } 196 | 197 | func sqrt(_ x: Double) -> Double { 198 | sqrt(x, 0, x) 199 | } 200 | } 201 | ``` 202 | 203 | 在 ViewController.swift 中调用 __llvm_profile_write_file 之前调用 sqrt。然后,构建并运行。 204 | 205 | ```swift 206 | print("√2=\(BasicMath().sqrt(2))") 207 | __llvm_profile_write_file() 208 | ``` 209 | 210 | 在命令行中运行以下命令: 211 | 212 | ```shell 213 | mkdir TestCoverage 214 | cd TestCoverage 215 | cp /Users/yao/Library/Developer/CoreSimulator/Devices/4545834C-8D1F-4D2C-B243-F9E617F6C52D/data/Containers/Data/Application/6AEFAB1B-DA52-4FAF-9B27-3D47A898E55C/Documents/test.profraw . 216 | cp /Users/yao/Library/Developer/Xcode/DerivedData/SwiftCovApp-bohvioqnvkjxnnesyhlznzvmmgcg/Build/Products/Debug-iphonesimulator/SwiftCovApp.app/SwiftCovApp . 217 | ls 218 | xcrun llvm-profdata merge -sparse test.profraw -o test.profdata 219 | xcrun llvm-cov show ./SwiftCovApp -instr-profile=test.profdata 220 | ``` 221 | 222 | 我们就能看到最后的报告啦~👏🎉 223 | 224 | ![img](https://miro.medium.com/max/1400/1*mYGP_PXVGym-6IqekxSFTQ.png) 225 | 226 | ## 参考 227 | 228 | - [Clang 12 Documentation](https://clang.llvm.org/docs/SourceBasedCodeCoverage.html) 229 | - [Objective-C 与 Swift 混编工程精准测试探索](https://mp.weixin.qq.com/s/14hmLWNXAh1FKZT5NI5QsQ) -------------------------------------------------------------------------------- /resource/51 Swift 中的动态成员查找.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 我最喜欢 Swift 语言的一个特性是动态成员查找(dynamic member lookup)。虽然我们并不经常使用它,但它通过改进我们访问特定类型数据的方式,显著改善了所提供类型的 API。 4 | 5 | > Glassfy:简化构建、管理和推广应用内购买。从订阅管理 SDK 到付费墙等完整的货币化工具。立即免费构建。 6 | 7 | ## 基础知识 8 | 9 | 假设我们正在开发一个提供缓存功能的类型,并将其建模为名为 Cache 的结构体。 10 | 11 | ```Swift 12 | struct Cache { 13 | var storage: [String: Data] = [:] 14 | } 15 | ``` 16 | 17 | 为了访问缓存的数据,我们调用存储属性的下标,该存储属性是 Dictionary 类型提供的。 18 | 19 | ```Swift 20 | var cache = Cache() 21 | let profile = cache.storage["profile"] 22 | ``` 23 | 24 | 在这里没有什么特别之处。我们像以前一样通过 Dictionary 类型的下标访问字典。让我们看看如何使用 `@dynamicMemberLookup` 属性改进 Cache 类型的 API。 25 | 26 | ```Swift 27 | @dynamicMemberLookup 28 | struct Cache { 29 | private var storage: [String: Data] = [:] 30 | 31 | subscript(dynamicMember key: String) -> Data? { 32 | storage[key] 33 | } 34 | } 35 | ``` 36 | 37 | 如上例所示,我们使用 `@dynamicMemberLookup` 属性标记了 Cache 类型。我们必须实现具有 `dynamicMember` 参数并返回我们需要的任何内容的下标。 38 | 39 | ```Swift 40 | var cache = Cache() 41 | let profile = cache.profile 42 | ``` 43 | 44 | 现在,我们可以更方便地访问 Cache 类型的配置文件数据。我们的 API 的使用者可能会认为配置文件是 Cache 类型的属性,但事实并非如此。 45 | 46 | 此特性完全在运行时工作,并利用了在点符号后键入的任何属性名称来访问 Cache 类型的下标,该下标具有 dynamicMember 参数。 47 | 48 | 整个逻辑在运行时运行,编译期间的结果是不确定的。在运行时,您完全可以决定应该从下标返回哪些数据以及如何处理 dynamicMember 参数。 49 | 50 | ## 使用 KeyPath 的编译时安全性 51 | 52 | 我们唯一能找到的缺点是缺乏编译时安全性。我们可以将 Cache 类型视为代码中键入的任何属性名称。幸运的是,`@dynamicMemberLookup` 下标的参数不仅可以是 String 类型,还可以是 KeyPath 类型。 53 | 54 | ```Swift 55 | @dynamicMemberLookup 56 | final class Store\: ObservableObject { 57 | typealias ReduceFunction = (State, Action) -> State 58 | 59 | @Published private var state: State 60 | private let reduce: ReduceFunction 61 | 62 | init( 63 | initialState state: State, 64 | reduce: @escaping ReduceFunction 65 | ) { 66 | self.state = state 67 | self.reduce = reduce 68 | } 69 | 70 | subscript(dynamicMember keyPath: KeyPath) -> T { 71 | state[keyPath: keyPath] 72 | } 73 | 74 | func send(_ action: Action) { 75 | state = reduce(state, action) 76 | } 77 | 78 | } 79 | ``` 80 | 81 | 如上例所示,我们定义了接受强类型 KeyPath 实例的 dynamicMember 参数下标。在这种情况下,我们允许 State 类型的 KeyPath,这有助于我们获得编译时安全性。因为每当我们传递与 State 类型无关的错误 KeyPath 时,编译器都会显示错误。 82 | 83 | ```Swift 84 | struct State { 85 | var products: \[String] = \[] 86 | var isLoading = false 87 | } 88 | 89 | enum Action { 90 | case fetch 91 | } 92 | 93 | let store: Store\ = .init(initialState: .init()) { state, action in 94 | var state = state 95 | switch action { 96 | case .fetch: 97 | state.isLoading = true 98 | } 99 | return state 100 | } 101 | 102 | print(store.isLoading) 103 | print(store.products) 104 | print(store.favorites) // Compiler error 105 | ``` 106 | 107 | 在上例中,我们通过接受 KeyPath 的下标访问 Store 的私有 state 属性。这看起来与前面的例子类似,但在这种情况下,只要您尝试访问 State 类型的不可用属性,编译器就会显示错误。 108 | 109 | ## 总结 110 | 111 | 今天我们学习了如何使用 `@dynamicMemberLookup` 属性改进特定类型的 API。虽然并不是每个类型都需要它,但您可以谨慎使用它来改善 API。 112 | -------------------------------------------------------------------------------- /resource/52 Swift 中的热重载.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 我们最新的 MacBook M30X 处理器可以感知到瞬间编译大型 Swift 项目,除此之外,编译代码库只是我们迭代周期的一部分。包括: 4 | 5 | - 重新启动它(或将其部署到设备) 6 | - 导航到您在应用程序中的先前位置 7 | - 重新生成您需要的数据。 8 | 9 | 如果您只需要做一次的话,听起来还不错。但是如果您和我一样,在特别的一天中,对代码库进行 200 - 500 次迭代,该怎么办呢?它增加了。 10 | 11 | 有一种更好的方法,被其他平台所接受,并且可以在 Swift/iOS 生态系统中实现。我已经用了十多年了。 12 | 13 | 从今天开始,您想每周节省多达 10 小时的工作时间吗? 14 | 15 | ## 热重载 16 | 17 | 热重载是关于摆脱编译整个应用程序并尽可能避免部署/重新启动周期,同时允许您编辑正在运行的应用程序代码并且能立即看到更改。 18 | 19 | 这种流程改进可以每天为您节省数小时的开发时间。我跟踪我的工作一个多月,对我来说,每天节省了 1-2 小时。 20 | 21 | 坦白地说,如果每周节省10个小时的开发时间都不能说服您去尝试,那么我认为任何方法都不能说服你。 22 | 23 | ## 其他平台在做什么? 24 | 25 | 如果您只使用 Apple 平台,您会惊讶地发现有好多平台几十年前已经采用了热重载。无论您是编写 Node 还是任何其他 JS 框架,都有一个使用热重载的设置。 Go 也提供了热重载(本博客使用了该特性) 26 | 27 | 另一个例子是谷歌的 Flutter 架构,从一开始就设计用于热重载。如果您与从事 Flutter 工作的工程师交谈,你会发现他们最喜欢 Flutter 开发者体验的一点就是能够实时编写他们的应用程序。当我为《纽约时报》写了一个拼字游戏时,我很喜欢它。 28 | 29 | 微软最近推出了 [Visual Studio 2022](https://devblogs.microsoft.com/visualstudio/visual-studio-2022-now-available/),并为 .NET 和 标准 C++ 应用程序提供热重载,在过去的十年中,微软在开发工具和经验方面一直在大杀四方,所以这并不令人惊讶。 30 | 31 | ## 苹果生态系统怎么样? 32 | 33 | 早在 2014 年推出时,很多人都对 Swift Playgrounds 感到敬畏,因为它们允许我们快速迭代并查看代码的结果,但它们并不能很好地工作,因为它存在崩溃、挂起等问题。不能支持整个iPad环境。 34 | 35 | 在它们发布后不久,我启动了一个名为 Objective-C Playgrounds 的开源项目,它比官方 Playgrounds 运行得更快、更可靠。我的想法是设计一个架构/工作流程,利用我已经使用了几年的 [DyCI](https://github.com/DyCI/dyci-main) 代码注入工具,该工具已经由 Paul 制作。 36 | 37 | 自从 Swift Playgrounds 存在以来,已经过去了八年,而且它们变得更好了,但它们可靠吗?人们是否在使用它们来推动开发? 38 | 39 | >*以我的经验:并非如此。Playgrounds 在大型项目中往往不太可靠或适用。* 40 | 41 | SwiftUI 出现了,它是一项了不起的技术(尽管仍然存在错误),它引入了与 Playgrounds 非常相似的 Swift Previews 的想法,它们有什么好处吗? 42 | 43 | 类似的故事,当它工作的时候是很好的,但是在更大的项目中,它的工作是不可靠的,而且往往中断的次数比它们工作的次数多。如果你有任何错误,他们不会为你提供调试代码的能力,因此,采用的情况有限。 44 | 45 | ## 我们需要等待 Apple 吗? 46 | 47 | 如果你关注我一段时间,你就已经知道答案了,绝对不要。毕竟,我的职业生涯是构建普通 Apple 解决方案无法解决的问题:从像 [Sourcery](https://github.com/krzysztofzablocki/Sourcery) 这样的语言扩展、像 [Sourcery Pro](http://merowing.info/sourcery-pro/) 这样的 Xcode 改进,再到 [LifetimeTracker](https://github.com/krzysztofzablocki/LifetimeTracker) 以及许多其他开源工具。 48 | 49 | 我们可以利用我最初在 2014 Playgrounds 中使用的相同方法。我已经使用它十多年了,并且在数十个 Swift 项目中使用它并取得了巨大的成功! 50 | 51 | 许多年前,我从使用 [DyCI](https://github.com/DyCI/dyci-main "DyCI") 切换到 **InjectionForXcode**,通过利用 LLVM 互操作而不是任何 swizzling ,它的效果更好。它是一个完全免费的开源工具,您可以在菜单栏中运行,它是由多产的工程师 John Holdsworth 创建的。你应该看看他的书 [Swift Secrets](http://books.apple.com/us/book/id1551005489 "Swift Secrets")。 52 | 53 | 我意识到 [Playgrounds](https://github.com/krzysztofzablocki/Playgrounds) 的方法可能过于笨重,所以今天,我开源了。一个非常专注的名为 [Inject](https://github.com/krzysztofzablocki/Inject) 的微型库,与 [InjectionForXcode](https://github.com/johnno1962/InjectionIII) 搭配使用时,将使您的 Apple 开发更加高效和愉快! 54 | 55 | 但不要只相信我的话。看看 Alexandra 和 Nate 的反馈,在我将这个工作流程引入 [The Browser Company](https://thebrowser.company/) 设置之前,他们已经非常精通了,这使得它更加令人印象深刻。 56 | 57 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8c732bf44986441199f6d24eb84b64a5~tplv-k3u1fbpfcp-zoom-1.image) 58 | 59 | ## Inject 60 | 61 | 这个小型库是完全通用的,无论您使用 `UIKit`、 `AppKit` 还是 `SwiftUI`,您都可以使用它。 62 | 63 | 您无需为生产应用程序添加条件或删除 `Inject` 代码。它变成了无操作内联代码,将在非调试版本中被编译过程剥离。您可以在每个视图中集成一次,并持续使用数年。 64 | 65 | 请参考 [GitHub repo](https://github.com/krzysztofzablocki/Inject "GitHub repo") 中关于配置项目的说明。现在让我们来看看您有哪些工作流程选项。 66 | 67 | ## 工作流 68 | 69 | ### SwiftUI 70 | 71 | 只需要两行字就可以使任何 SwiftUI 启用实时编程,而当您这样做时,您将拥有比使用 Swift Previews 更快的工作流程,同时能够使用实际的生产数据。 72 | 73 | 这是我的 [Sourcery Pro](http://merowing.info/sourcery-pro/ "Sourcery Pro") 应用程序的示例,其中加载了我所有的实际数据和逻辑,使我能够即时快速迭代整个应用程序设计,而无需任何重新启动、重新加载或类似的事情。 74 | 75 | 看看这个开发工作流程有多快吧,告诉我你宁愿在我每次接触代码时等待Xcode的重新构建和重新部署。 76 | 77 | ### UIKit / AppKit 78 | 79 | 我们需要一种方法来清理标准命令式UI框架的代码注入阶段之间的状态。 80 | 81 | 我创建了 `Host` 的概念并且在这种情况下工作的很好。有两个: 82 | 83 | ```shell 84 | - Inject.ViewHost 85 | - Inject.ViewControllerHost 86 | ``` 87 | 88 | 我们如何集成它?我们把我们想迭代的类包装在父级,因此我们不修改要注入的类型,而是改变父级的调用站点。 89 | 90 | 例如,如果你有一个 SplitViewController ,它创建了 PaneA 和 PaneB ,而你想在PaneA 中迭代布局/逻辑代码,你就修改 SplitViewController 中的调用站点。 91 | 92 | ```swift 93 | paneA = Inject.ViewHost( 94 | PaneAView(whatever: arguments, you: want) 95 | ) 96 | ``` 97 | 98 | 这就是你需要做的所有改变。注入现在允许你更改 PaneAView 中的任何东西,除了它的初始化API。这些变化将立即反映在你的应用程序中。 99 | 100 | *** 101 | 102 | 一个更具体的例子? 103 | 104 | - 我下载了 [Covid19 App](https://github.com/dkhamsing/covid19.swift) 105 | 106 | - 添加 `-Xlinker -interposable` 到 `Other Linker Flags` 107 | 108 | - 交换了一行 `Covid19TabController.swift:L63` 行 109 | 110 | 从这句: 111 | 112 | ```swift 113 | let vc = TwitterViewController(title: Tab.twitter.name, usernames: Twitter.content) 114 | ``` 115 | 116 | 替换为: 117 | 118 | ```swift 119 | let vc = Inject.ViewControllerHost(TwitterViewController(title: Tab.twitter.name, usernames: Twitter.content)) 120 | ``` 121 | 122 | 现在,我可以在不重新启动应用程序的情况下迭代控制器设计。 123 | 124 | ### 这是如何运作的呢? 125 | 126 | Hosts 利用了自动闭包,因此每次您注入代码时,我们都会使用与最初相同的参数创建您类型的新实例,从而允许您迭代任何代码、内存布局和其他所有内容。你唯一不能改变的是你的初始化 API。 127 | 128 | > Host 的变化不能完全内联,所以这些类在 Release 构建中被删除。最简单的方法是做一个单独的提交,交换此单行代码,然后在工作流程的最后删除它。 129 | 130 | ### 逻辑注入如何呢? 131 | 132 | 像 MVVM / MVC 这样的标准架构可以获得免费的逻辑注入,重新编译你的类,当方法重新执行时,你已经在使用新代码了。 133 | 134 | 如果像我一样,你喜欢 [PointFree Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture "PointFree Composable Architecture"),你可能想要注入 reducer 代码。 Vanilla TCA 不允许这样做,因为 reducer 代码是一个免费功能,不能直接用注入替换,但我们在 The Browser Company 的分支 支持它。 135 | 136 | 当我最初开始咨询 TBC 时,我想要的第一件事是将 `Inject` 和 `XcodeInjection` 集成到我们的工作流程中。公司管理层非常支持。 137 | 138 | 如果您切换到我们的 [TCA 分支](https://github.com/thebrowsercompany/swift-composable-architecture/tree/develop)(我们保持最新),你可以在 UI 和 TCA 层上使用 `Inject` 。 139 | 140 | ### 它有多可靠? 141 | 142 | 没有什么是完美的,但我已经使用它十多年了。它比 Apple 技术(Playgrounds / Previews)可靠得多。 143 | 144 | 如果您投入时间学习它,它将为您和您的团队节省数千小时! 145 | 146 | ### Demo 源码 147 | 148 | [在 GitHub 上获取项目](https://github.com/krzysztofzablocki/Inject "Demo 源码") -------------------------------------------------------------------------------- /resource/53 Swift中的幻象类型.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 模糊的数据可以说是一般应用程序中最常见的错误和问题的来源之一。虽然 Swift 通过其强大的类型系统和完善的编译器帮助我们避免了许多含糊不清的来源——但只要我们无法在编译时保证某个数据总是符合我们的要求,就总是有风险,我们最终会处于含糊不清或不可预测的状态。 4 | 5 | 本周,让我们来看看一种技术,它可以让我们利用 Swift 的类型系统在编译时执行更多种类的数据验证——消除更多潜在的歧义来源,并帮助我们在整个代码库中保持类型安全——通过使用**幻象类型**(*phantom types*)。 6 | 7 | ## 定义良好,但仍然含糊不清 8 | 9 | 举个例子,假设我们正在开发一个文本编辑器,虽然它最初只支持纯文本文件——随着时间的推移,我们还增加了对编辑HTML文档的支持,以及PDF预览。 10 | 11 | 为了能够尽可能多地重复使用我们原来的文档处理代码,我们继续使用与开始时相同的`Document`模型——只是现在它获得了一个`Format`属性,告诉我们正在处理什么样的文档: 12 | 13 | ```swift 14 | struct Document { 15 | enum Format { 16 | case text 17 | case html 18 | case pdf 19 | } 20 | 21 | var format: Format 22 | var data: Data 23 | var modificationDate: Date 24 | var author: Author 25 | } 26 | ``` 27 | 28 | 能够避免代码重复当然是件好事,而且枚举是当我们在处理一个模型的不同格式或变体时[一般情况下建模](https://www.swiftbysundell.com/articles/modelling-state-in-swift) 的好方法,但是上述那种设置实际上最终会造成相当多的模糊性。 29 | 30 | 例如,我们可能有一些API,只有在调用给定格式的文档时才有意义——比如这个打开文本编辑器的函数,它假定任何传入它的`Document`都是文本文档: 31 | 32 | ```swift 33 | func openTextEditor(for document: Document) { 34 | let text = String(decoding: document.data, as: UTF8.self) 35 | let editor = TextEditor(text: text) 36 | ... 37 | } 38 | ``` 39 | 40 | 虽然如果我们不小心将一个HTML文档传递给上述函数并不是世界末日(HTML毕竟只是文本),但试图以这种方式打开一个PDF,很可能会导致呈现出完全无法理解的东西,我们的文本编辑功能将无法工作,我们的应用程序甚至可能最终崩溃。 41 | 42 | 我们在编写任何其他特定格式的代码时都会不断遇到同样的问题,例如,如果我们想通过实现一个解析器和一个专门的编辑器来改善编辑HTML文档的用户体验: 43 | 44 | ```swift 45 | func openHTMLEditor(for document: Document) { 46 | // 就像我们上面用于文本编辑的函数一样, 47 | // 这个函数假设它总是被传递给HTML文档。 48 | let parser = HTMLParser() 49 | let html = parser.parse(document.data) 50 | let editor = HTMLEditor(html: html) 51 | ... 52 | } 53 | ``` 54 | 55 | 一个关于如何解决上述问题的初步想法可能是编写一个包装函数,切换到所传递文档的格式,然后为每种情况打开正确的编辑器。然而,虽然这对文本和HTML文档很有效,但由于PDF文档在我们的应用程序中是不可编辑的——当遇到PDF时,我们将被迫抛出一个错误,触发一个断言,或以[其他方式失败](https://www.swiftbysundell.com/articles/picking-the-right-way-of-failing-in-swift): 56 | 57 | ```swift 58 | func openEditor(for document: Document) { 59 | switch document.format { 60 | case .text: 61 | openTextEditor(for: document) 62 | case .html: 63 | openHTMLEditor(for: document) 64 | case .pdf: 65 | assertionFailure("Cannot edit PDF documents") 66 | } 67 | } 68 | ``` 69 | 70 | 71 | 72 | 上述情况不是很好,因为它要求我们作为开发者始终跟踪我们在任何给定的代码路径中所处理的文件类型,而我们可能犯的任何错误只能在运行时被发现——编译器根本没有足够的信息可以在编译时进行这种检查。 73 | 74 | 因此,尽管我们的 "Document "模型乍一看可能非常优雅和完善,但事实证明,它并不完全是手头情况的正确解决方案。 75 | 76 | ## 看起来我们需要一个协议! 77 | 78 | 解决上述问题的一个方法是把`Document`变成一个协议,而不是作为一个具体的类型,把它的所有属性(除了`format`)都作为要求: 79 | 80 | ```swift 81 | protocol Document { 82 | var data: Data { get } 83 | var modificationDate: Date { get } 84 | var author: Author { get } 85 | } 86 | ``` 87 | 88 | 有了上述变化,我们现在可以为我们的三种文档格式中的每一种实现专门的类型,并让这些类型都符合我们新的文档协议——比如这样: 89 | 90 | ```swift 91 | struct TextDocument: Document { 92 | var data: Data 93 | var modificationDate: Date 94 | var author: Author 95 | } 96 | ``` 97 | 98 | 上述方法的好处是,它使我们既能实现可以对任何`Document`进行操作的通用功能,又能实现只接受某种具体类型的特定API: 99 | 100 | ```swift 101 | // 这个函数可以保存任何文件, 102 | // 所以它接受任何符合我们的新文档协议。 103 | func save(_ document: Document) { 104 | ... 105 | } 106 | 107 | // 我们现在只能向我们的函数传递文本文件, 108 | // 即打开一个文本编辑器。 109 | func openTextEditor(for document: TextDocument) { 110 | ... 111 | } 112 | ``` 113 | 114 | 我们在上面所做的基本上是将以前在运行时进行的检查转为在编译时进行验证——因为编译器现在能够检查我们是否总是向我们的每个API传递正确格式的文件,这是一个很大的进步。 115 | 116 | 然而,通过执行上述改变,我们也**失去了我们最初实现的优点——代码重用**。由于我们现在使用一个协议来表示所有的文档格式,我们将需要为我们的三种文档类型中的每一种编写完全重复的模型实现,以及为我们将来可能增加的任何其他格式提供支持。 117 | 118 | ## 引入幻象类型 119 | 120 | 如果我们能找到一种方法,既能为所有格式重用相同的`Document`模型,又能在编译时验证我们特定格式的代码,岂不妙哉?事实证明,我们之前的一行代码实际上可以给我们一个实现这一目标的提示: 121 | 122 | ```swift 123 | let text = String(decoding: document.data, as: UTF8.self) 124 | ``` 125 | 126 | 当把`Data`转换为`String`时,就像我们上面做的那样,我们通过传递对该类型本身的引用来传递我们希望字符串被解码的编码——在本例中是UTF8。这真的很有趣。如果我们再深入一点,就会发现 Swift 标准库将我们上面提到的UTF8类型定义为另一个类似命名空间的枚举中的一个无大小写枚举,称为`Unicode`。 127 | 128 | ```swift 129 | enum Unicode { 130 | enum UTF8 {} 131 | ... 132 | } 133 | 134 | typealias UTF8 = Unicode.UTF8 135 | ``` 136 | 137 | > 请注意,如果你看一下`UTF8`类型的实际实现,它确实包含一个私有case,只是为了向后兼容 Swift 3 而存在。 138 | 139 | 我们在这里看到的是一种被称为**幻象类型的技术——当类型被用作标记,而不是被实例化来表示值或对象时**。事实上,由于上述枚举都没有任何公开的情况,它们甚至不能被实例化! 140 | 141 | 让我们看看是否可以用同样的技术来解决我们的`Document`困境。我们首先将`Document`还原成一个结构体,只是这次我们将删除它的`format`属性(以及相关的枚举),而将它变成一个覆盖任何`Format`类型的泛型——比如这样: 142 | 143 | ```swift 144 | struct Document { 145 | var data: Data 146 | var modificationDate: Date 147 | var author: Author 148 | } 149 | ``` 150 | 151 | 受标准库的`Unicode`枚举及其各种编码的启发,我们将定义一个类似的枚举——`DocumentFormat`——作为三个无大小写的枚举的命名空间,每种格式都有一个: 152 | 153 | ```swift 154 | enum DocumentFormat { 155 | enum Text {} 156 | enum HTML {} 157 | enum PDF {} 158 | } 159 | ``` 160 | 161 | 请注意,这里不涉及任何协议——任何类型都可以被用作格式,因为就像`String`和它的各种编码一样,我们将只使用文档的`Format`类型作为编译时的标记。这将使我们能够像这样写出我们特定格式的API: 162 | 163 | ```swift 164 | func openTextEditor(for document: Document) { 165 | ... 166 | } 167 | 168 | func openHTMLEditor(for document: Document) { 169 | ... 170 | } 171 | 172 | func openPreview(for document: Document) { 173 | ... 174 | } 175 | ``` 176 | 177 | 当然,我们仍然可以编写不需要任何特定格式的通用代码。例如,这里我们可以把之前的`save`API变成一个完全通用的函数: 178 | 179 | ```swift 180 | func save(_ document: Document) { 181 | ... 182 | } 183 | ``` 184 | 185 | 然而,总是输入`Document`来引用一个文本文档是相当乏味的,所以让我们也使用类型别名为每种格式定义速记。这将给我们提供漂亮的、有语义的名字,而不需要任何重复的代码: 186 | 187 | ```swift 188 | typealias TextDocument = Document 189 | typealias HTMLDocument = Document 190 | typealias PDFDocument = Document 191 | ``` 192 | 193 | 在涉及到特定格式的扩展时,幻象类型也确实大放异彩,现在可以直接使用 Swift 强大的泛型系统和[泛型型约束](https://www.swiftbysundell.com/articles/using-generic-type-constraints-in-swift-4/)来实现。例如,我们可以用一个生成`NSAttributedString`的方法来扩展所有文本文档: 194 | 195 | ```swift 196 | extension Document where Format == DocumentFormat.Text { 197 | func makeAttributedString(withFont font: UIFont) -> NSAttributedString { 198 | let string = String(decoding: data, as: UTF8.self) 199 | 200 | return NSAttributedString(string: string, attributes: [ 201 | .font: font 202 | ]) 203 | } 204 | } 205 | ``` 206 | 207 | 由于我们的幻象类型在最后只是普通的类型——我们也可以让它们遵守协议,并使用这些协议作为泛型约束。例如,我们可以让我们的一些`DocumentFormat`类型遵守`Printable`协议,然后我们可以在打印代码中使用这些协议作为约束条件。这里有大量的可能性。 208 | 209 | ## 一个标准的模式 210 | 211 | 起初,幻象类型在 Swift 中可能看起来有点 "格格不入"。然而,虽然 Swift 并没有像更多的纯函数式语言(如Haskell)那样为幻象类型提供一流的支持,但在标准库和苹果平台SDK的许多不同地方都可以找到这种模式。 212 | 213 | 例如,`Foundation`的`Measurement` API使用幻象类型来确保在传递各种测量值时的类型安全——例如度数、长度和重量: 214 | 215 | ```swift 216 | let meters = Measurement(value: 5, unit: .meters) 217 | let degrees = Measurement(value: 90, unit: .degrees) 218 | ``` 219 | 220 | 通过使用幻影类型,上述两个测量值不能被混合,因为每个值是哪种单位,都被编码到该值的类型中。这可以防止我们不小心将一个长度传递给一个接受角度的函数,反之亦然——就像我们之前防止文档格式被混淆一样。 221 | 222 | ## 结论 223 | 224 | 使用幻象类型是一种非常强大的技术,它可以让我们利用类型系统来验证一个特定值的不同变体。虽然使用幻象类型通常会使API更加冗长,而且确实伴随着泛型的复杂性——当处理不同的格式和变体时,它可以让我们减少对运行时检查的依赖,而让编译器来执行这些检查。 225 | 226 | 就像一般的泛型一样,我认为在部署幻象类型之前,首先要仔细评估当前的情况,这很重要。就像我们最初的`Document`模型并不是手头任务的正确选择,尽管它的结构很好,但如果部署在错误的情况下,幻象类型会使简单的设置变得更加复杂。像往常一样,它归结为为工作选择正确的工具。 227 | 228 | 谢谢你的阅读! 🚀 -------------------------------------------------------------------------------- /resource/54 Swift中的类型占位符 .md: -------------------------------------------------------------------------------- 1 | Swift 的类型推断能力从一开始就是语言的核心部分,它极大地减少了我们在声明有默认值的变量和属性时手动指定类型的工作。例如,表达式`var number = 7`不需要包含任何类型注释,因为编译器能够推断出值`7`是一个`Int`,我们的`number`变量应该被相应的类型化。 2 | 3 | 作为 Xcode 13.3 的一部分而一起发布的 Swift 5.6,通过引入 "类型占位符(type placeholders) "的概念,继续扩展这些类型推理能力,这在处理集合和其他通用类型时非常有用。 4 | 5 | 例如,假设我们想创建一个`Combine`里面具有默认整数值的 `CurrentValueSubject`的实例。关于如何做到这一点的初步想法可能是简单地将我们的默认值传递给该主体的初始化器,然后将结果存储在本地的一个`let`声明的属性中(就像创建一个普通的`Int`值时一样)。然而,这样做会给我们带来以下编译器错误: 6 | 7 | ```swift 8 | // Error: "Generic parameter 'Failure' could not be inferred" 9 | // Error: “无法被推断出泛型的`Failure`参数 ” 10 | let counterSubject = CurrentValueSubject(0) 11 | ``` 12 | 13 | 这是因为`CurrentValueSubject`是一个泛型类型,实例化时不仅需要`Output`类型,还需要`Failure`类型——这是该主体能够抛出的错误类型。 14 | 15 | 因为我们不希望我们的主体在这种情况下抛出任何错误,所以我们会给它一个`Failure`类型的值`Never`(这是在 Swift 中使用 `Combine` 的一个常见惯例)。但为了做到这一点,在 Swift 5.6 之前,我们需要明确地指定我们的`Int`输出类型——像这样: 16 | 17 | ```swift 18 | let counterSubject = CurrentValueSubject(0) 19 | ``` 20 | 21 | 不过从 Swift 5.6 开始,这种情况就不存在了——因为我们现在可以使用一个类型占位符来表示我们主体的`Output`类型,这让我们再次利用编译器为我们自动推断出该类型,就像在声明一个普通的`Int`值一样: 22 | 23 | ```swift 24 | let counterSubject = CurrentValueSubject<_, Never>(0) 25 | ``` 26 | 27 | 这很好,但可以说这并不是 swift 里面很大的改进。毕竟,我们用`_`代替`Int`只是节省了两个字符,而且手动指定像`Int`这样的简单类型也不是一开始就有问题的。 28 | 29 | **但现在让我们看看这个功能如何扩展到更复杂的类型,这是它真正开始发光的地方。**例如,假设我们的项目包含以下函数,让我们加载一个用户注解的PDF文件: 30 | 31 | ```swift 32 | func loadAnnotatedPDF(named: String) -> Resource> { 33 | ... 34 | } 35 | ``` 36 | 37 | 上面的函数使用了一个相当复杂的泛型作为它的返回类型,这可能是因为我们需要在多个地方中重复使用我们的`Resource`类型,也因为我们选择了使用*[幻象类型](https://www.swiftbysundell.com/articles/phantom-types-in-swift)*来指定我们当前处理的是哪种PDF。 38 | 39 | 现在让我们看看,如果我们在创建主体时调用上述函数,而不是仅仅使用一个简单的整数,那么我们之前基于`CurrentValueSubject`的代码会是什么样子: 40 | 41 | ```swift 42 | // Before Swift 5.6: 43 | let pdfSubject = CurrentValueSubject>, Never>( 44 | loadAnnotatedPDF(named: name) 45 | ) 46 | 47 | // Swift 5.6: 48 | let pdfSubject = CurrentValueSubject<_, Never>( 49 | loadAnnotatedPDF(named: name) 50 | ) 51 | ``` 52 | 53 | 这是一个相当大的改进啊 基于 Swift 5.6 的版本不仅为我们节省了一些输入,而且由于 `pdfSubject` 的类型现在完全来自 `loadAnnotatedPDF` 函数,这可能会使该函数(及其相关代码)的迭代更加容易——因为如果我们改变该函数的返回类型,需要更新的手动类型注释将减少。 54 | 55 | 不过,值得指出的是,在上述情况下,还有另一种方法可以利用Swift的类型推理能力——那就是使用**类型别名**,而不是**类型占位符**。例如,我们可以在这里定义一个`UnfailingValueSubject`类型别名,我们可以用它来轻松地创建不会产生任何错误的主体: 56 | 57 | ```swift 58 | typealias UnfailingValueSubject = CurrentValueSubject 59 | ``` 60 | 61 | 有了上述内容,我们现在就可以在没有任何泛型注解的情况下创建我们的`pdfSubject`了——因为编译器能够推断出`T`指的是什么类型,而且失败类型`Never`已经被硬编码到我们的新类型别名中: 62 | 63 | ```swift 64 | let pdfSubject = UnfailingValueSubject(loadAnnotatedPDF(named: name)) 65 | ``` 66 | 67 | 但这并不意味着类型别名在通常情况下都比类型占位符好,因为如果我们要为每种特定情况定义新的类型别名,那么这也会使我们的代码库变得更加复杂。有时,在内联中指定所有的东西(比如使用类型占位符时)绝对是个好办法,因为这可以让我们定义完全独立的表达式。 68 | 69 | 在我们总结之前,让我们也来看看类型占位符是如何与集合字面量(literals)一起使用的——例如在创建一个字典时。在这里,我们选择手动指定我们的字典的 `Key` 类型(为了能够使用点语法来指代枚举的各种情况),同时为该字典的值使用一个类型占位符: 70 | 71 | ```swift 72 | enum UserRole { 73 | case local 74 | case remote 75 | } 76 | 77 | let latestMessages: [UserRole: _] = [ 78 | .local: "", 79 | .remote: "" 80 | ] 81 | ``` 82 | 83 | 这就是类型占位符——Swift 5.6 中引入的一个新功能,在处理稍微复杂的通用类型时,它可能真的很有用。但值得指出的是,这些占位符只能在调用站点使用,而不是在指定函数或计算属性的返回类型时使用。 -------------------------------------------------------------------------------- /resource/55 SwiftUI 之 HStack 和 VStack 的切换.md: -------------------------------------------------------------------------------- 1 | # SwiftUI 之 HStack 和 VStack 的切换 2 | 3 | ## 前言 4 | 5 | `SwiftUI` 的各种堆栈是许多框架中最基本的布局工具,能够让我们定义组视图,这些组视图可以按照水平、垂直或覆盖视图对齐。 6 | 7 | 当涉及到水平和垂直的变体时( `HStack` 和 `VStack` ),我们需要在这两者之间动态的切换。举个例子,假如我们正在构建一个 `app` 其中包含 `LoginActionsView` ,一个让用户登录时在列表中选择操作的类: 8 | 9 | ```swift 10 | struct LoginActionsView: View { 11 | ... 12 | 13 | var body: some View { 14 | VStack { 15 | Button("Login") { ... } 16 | Button("Reset password") { ... } 17 | Button("Create account") { ... } 18 | } 19 | .buttonStyle(ActionButtonStyle()) 20 | } 21 | } 22 | 23 | struct ActionButtonStyle: ButtonStyle { 24 | func makeBody(configuration: Configuration) -> some View { 25 | configuration.label 26 | .fixedSize() 27 | .frame(maxWidth: .infinity) 28 | .padding() 29 | .foregroundColor(.white) 30 | .background(Color.blue) 31 | .cornerRadius(10) 32 | } 33 | } 34 | 35 | ``` 36 | 37 | >以上代码中,我们用到了 `fixedSize` 防止按钮文本被截断,这仅是在我们确信给定的内容视图不会比视图本身更大的情况。想了解更多信息,可以查看我的文章 - [SwiftUI 布局系统第三章](https://www.swiftbysundell.com/articles/swiftui-layout-system-guide-part-3/#fixed-dimensions) 38 | 39 | 目前,我们的按钮是垂直排列的,并且填满了水平线上的可用空间(你可以用以上示例代码预览按钮的样子),虽然这在竖向的 iPhone 上看起来很好,但假设我们现在想要在横向模式下让 `UI` 横向排列。 40 | 41 | ## GeometryReader 能实现吗? 42 | 43 | 一种方式是用 `GeometryReader` 测量当前可用空间,并根据宽度是否大于其高度,可以选择使用 `HStack` 或 `VStack` 来渲染内容。 44 | 45 | 虽然可以在 `LoginActionsView` 中放入该逻辑,但我们希望以后能复用代码,因此需要重新创建一个专门的视图,作为一个独立的组件来实现动态堆栈的切换逻辑。 46 | 47 | 为了使代码可用性更高,我们不会硬编码让两个堆栈变体使用对齐或间距什么的。相反,让我们像 `SwiftUI` 一样,对这些属性参数化,同时设定框架所使用的默认值 — 就像这样: 48 | 49 | ```swift 50 | struct DynamicStack: View { 51 | var horizontalAlignment = HorizontalAlignment.center 52 | var verticalAlignment = VerticalAlignment.center 53 | var spacing: CGFloat? 54 | @ViewBuilder var content: () -> Content 55 | 56 | var body: some View { 57 | GeometryReader { proxy in 58 | Group { 59 | if proxy.size.width > proxy.size.height { 60 | HStack( 61 | alignment: verticalAlignment, 62 | spacing: spacing, 63 | content: content 64 | ) 65 | } else { 66 | VStack( 67 | alignment: horizontalAlignment, 68 | spacing: spacing, 69 | content: content 70 | ) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | 由于我们使新的 `DynamicStack` 使用了与 `HStack` 和 `VStack` 相同的 `API` ,现在可以在 `LoginActionsView` 中直接将以前的 `VStack` 换成新的自定义的实例: 79 | 80 | ```swift 81 | struct LoginActionsView: View { 82 | ... 83 | 84 | var body: some View { 85 | DynamicStack { 86 | Button("Login") { ... } 87 | Button("Reset password") { ... } 88 | Button("Create account") { ... } 89 | } 90 | .buttonStyle(ActionButtonStyle()) 91 | } 92 | } 93 | ``` 94 | 95 | 优秀!然而,就像上面的代码展示的那样,使用 `GeometeryReader` 来展示动态切换有一个相当明显的缺点,在几何图形阅读器中总是会填充水平和垂直方向的所有可用空间(以便测量实际空间)。在我们的例子中,`LoginActionsView` 不再只是水平方向的排列,它现在也能移动到屏幕的顶部。 96 | 97 | 虽然我们也有很多方法能解决这些问题(例如使用类似在[这篇 Q&A ](https://swiftbysundell.com/questions/syncing-the-width-or-height-of-two-swiftui-views/)中用来使多个视图具有相同宽度和高度的技术),但真正的问题是当我们要动态的确定方向时,测量可用空间是否是一个好的方法。 98 | 99 | ## 一个使用尺寸类的例子 100 | 101 | 相反,让我们使用 `Apple` 的尺寸类系统来决定 `DynamicStack` 应该在底层使用 `HStack` 还是 `VStack` 。这样做的好处不仅仅是在引入 `GeometeryReader` 之前保留同样紧凑的布局,并且会使 `DynamicStack` 在开始的时候以一种和系统组件类似的方式在所有设备和方向上构建。 102 | 103 | 为了观察当前水平方向的尺寸,我们需要用到 [SwiftUI 环境系统](https://swiftbysundell.com/articles/swiftui-state-management-guide/#observing-and-modifying-the-environment) — 通过在 `DynamicStack` 中声明 `@Environment` - 标记属性(带有 `horizontalSizeClass` [关键路径](https://swiftbysundell.com/articles/the-power-of-key-paths-in-swift/)),将会使我们在视图内容中切换到当前 `sizeClass` 的值: 104 | 105 | ```swift 106 | struct DynamicStack: View { 107 | ... 108 | @Environment(\.horizontalSizeClass) private var sizeClass 109 | 110 | var body: some View { 111 | switch sizeClass { 112 | case .regular: 113 | hStack 114 | case .compact, .none: 115 | vStack 116 | @unknown default: 117 | vStack 118 | } 119 | } 120 | } 121 | 122 | private extension DynamicStack { 123 | var hStack: some View { 124 | HStack( 125 | alignment: verticalAlignment, 126 | spacing: spacing, 127 | content: content 128 | ) 129 | } 130 | 131 | var vStack: some View { 132 | VStack( 133 | alignment: horizontalAlignment, 134 | spacing: spacing, 135 | content: content 136 | ) 137 | } 138 | } 139 | ``` 140 | 141 | 经过以上操作,`LoginActionsView` 将可以在常规的尺寸渲染时动态切换成水平布局(例如在大尺寸的 `iPhone` 使用横屏,或者全屏 `iPad` 上的任一方向),而其它所有尺寸的配置使用垂直布局。所有这些仍然使用紧凑垂直布局,它使用的空间不超过渲染其内容所需的空间。 142 | 143 | ## 使用布局协议 144 | 145 | 虽然我们最后已经用了非常棒的解决方案,可以在所有支持 `SwiftUI ` 的 `iOS` 版本中使用,但也让我们来探索一下在 `iOS 16` 中引入的一些新的布局工具(在写这篇文章时,它作为 `Xcode 14` 的一部分仍在测试阶段) 146 | 147 | 其中一个工具是新的 `Layout` 协议,它既能让我们创建完整的自定义布局,直接集成到 `SwiftUI ` 的布局系统中,同时也提供给我们一种更丝滑更动画的方式在各种布局之间动态切换 。 148 | 149 | 这都是因为事实证明 `Layout` 不仅仅是我们第三方开发者的 `API` ,`Apple` 也让 `SwiftUI ` 自己的布局容器使用这个新协议 。所以,与其直接使用 `HStack ` 和 `VStack ` 作为容器视图,不如将它们作为符合 `Layout` 的实例,使用 `AnyLayout` 类型进行包装 — 就像这样: 150 | 151 | ```swift 152 | private extension DynamicStack { 153 | var currentLayout: AnyLayout { 154 | switch sizeClass { 155 | case .regular, .none: 156 | return horizontalLayout 157 | case .compact: 158 | return verticalLayout 159 | @unknown default: 160 | return verticalLayout 161 | } 162 | } 163 | 164 | var horizontalLayout: AnyLayout { 165 | AnyLayout(HStack( 166 | alignment: verticalAlignment, 167 | spacing: spacing 168 | )) 169 | } 170 | 171 | var verticalLayout: AnyLayout { 172 | AnyLayout(VStack( 173 | alignment: horizontalAlignment, 174 | spacing: spacing 175 | )) 176 | } 177 | } 178 | ``` 179 | 180 | 以上的操作是可行的,因为当 `HStack` 和 `VStack` 的内容类型是 `EmptyView` 时,它们都符合新的 `Layout` 协议(当内容为空时就是这种情况),让我们来看一下`SwiftUI ` 的 公共接口 181 | 182 | ```swift 183 | struct DynamicStack: View { 184 | ... 185 | 186 | var body: some View { 187 | currentLayout(content) 188 | } 189 | } 190 | ``` 191 | 192 | > 注意:由于回归, `Xcode 14 beta 3` 中省略了以上条件的一致性,根据 `SwiftUI ` 团队的 [Matt Ricketson 的说法](https://twitter.com/ricketson_/status/1544784314453282817),可以直接使用底层的 `_HStackLayout` 和 `_VStackLayout` 类型作为临时的解决方法。并希望能在未来测试版本中修复。 193 | 194 | 现在我们能通过使用新的 `currentLayout` 解决使用什么布局,现在我们来更新 `body` 的实现,简单调用从该属性返回的 `AnyLayout` ,就像函数一样 — 像这样: 195 | 196 | ```swift 197 | struct DynamicStack: View { 198 | ... 199 | 200 | var body: some View { 201 | currentLayout(content) 202 | } 203 | } 204 | ``` 205 | 206 | > 我们之所以能像一个函数一样调用布局方法(尽管它实际上是一个结构)是因为 `Layout` 协议使用了 `Swift` [”像函数一样调用“ 的特性](https://swiftbysundell.com/articles/exploring-swift-5-2s-new-functional-features/#calling-types-as-functions) 207 | 208 | 那么我们之前的方案和上面基于布局的方案有什么区别呢?关键的区别在于(除了后者需要 `iOS 16` )切换布局可以保留正在渲染的底层视图的标识,而在 `HStack` 和 `VStack` 之间切换就不会这样。这样做会令动画更流畅,例如在切换设备方向时,我们也有可能在执行此类更改时获得小幅的性能提升(因为 `SwiftUI` 总是在其视图层次结构为静态时尽可能表现最佳) 209 | 210 | ## 选择合适的视图 211 | 212 | 但我们还没有结束,因为 `iOS 16` 也给了我们其他有趣的新的布局工具,它有可能也能用于实现 `DynamicStack` — 一种全新的视图类型,名字叫做 `ViewThatFits` 。就像字面意思一样,这种新的容器将会在我们初始化时传递的候选列表中,基于当前上下文挑选出最优视图。 213 | 214 | 在我们的例子中,这意味着我们能同时把 `HStack` 和 `VStack` 传递给它,并且代表我们在它们中间自动切换。 215 | 216 | ```swift 217 | struct DynamicStack: View { 218 | ... 219 | 220 | var body: some View { 221 | ViewThatFits { 222 | HStack( 223 | alignment: verticalAlignment, 224 | spacing: spacing, 225 | content: content 226 | ) 227 | 228 | VStack( 229 | alignment: horizontalAlignment, 230 | spacing: spacing, 231 | content: content 232 | ) 233 | } 234 | } 235 | } 236 | ``` 237 | 238 | 注意:在这种情况下,我们首先放置 `HStack` 是很重要的,因为 `VStack` 可能总是合适的,即使在我们希望布局是横向的情况下(例如 `iPad` 的全屏模式)。同样重要的是要指出,上述基于 `ViewThatFits` 的技术将会始终尝试 `HStack` ,即使在用紧凑尺寸渲染布局时也是如此,只有在 `HStack` 不适合时才会选择基于`VStack` 的布局。 239 | 240 | ## 结语 241 | 242 | 以上就是通过四种不同的方式实现 `DynamicStack` 视图,它可以根据当前内容在 `HStack` 和 `VStack` 之间动态切换。 243 | 244 | >译自:https://www.swiftbysundell.com/articles/switching-between-swiftui-hstack-vstack/ -------------------------------------------------------------------------------- /resource/56 SwiftUI 中的自定义导航.md: -------------------------------------------------------------------------------- 1 | # SwiftUI 中的自定义导航 2 | 3 | 默认情况下,SwiftUI提供的各种导航API在很大程度上是以用户直接输入为中心的——也就是说,导航是在系统响应例如按钮的点击和标签切换等事件时由系统本身处理的。 4 | 5 | 然而,有时我们可能想更直接地控制应用程序的导航执行方式,尽管SwiftUI在这方面仍然不如UIKit或AppKit灵活,但它确实提供了相当多的方法,让我们在构建的视图中执行完全自定义的导航。 6 | 7 | ### 切换标签(tabs) 8 | 9 | 让我们先来看看我们如何能控制当前在`TabView`中显示的标签。通常情况下,当用户手动点击每个标签栏中的一个项目时,标签就会被切换,但是通过在一个给定的`TabView`中注入一个选择(`selection`)绑定,我们可以观察并控制当前显示的标签。在这里,我们要做的就是在两个标签之间切换,这两个标签是用整数`0`和`1`标记的: 10 | 11 | ```swift 12 | struct RootView: View { 13 | @State private var activeTabIndex = 0 14 | 15 | var body: some View { 16 | TabView(selection: $activeTabIndex) { 17 | Button("Switch to tab B") { 18 | activeTabIndex = 1 19 | } 20 | .tag(0) 21 | .tabItem { Label("Tab A", systemImage: "a.circle") } 22 | 23 | Button("Switch to tab A") { 24 | activeTabIndex = 0 25 | } 26 | .tag(1) 27 | .tabItem { Label("Tab B", systemImage: "b.circle") } 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | 但真正好的地方是,在识别和切换标签时,我们并不仅仅局限于使用整数。相反,我们可以自由地使用任何`Hashable`值来表示每个标签——例如通过使用一个枚举,其中包含我们想要显示的每个标签的情况。然后我们可以将这部分状态封装在一个`ObservableObject`中,这样我们就可以很容易地注入到我们的视图层次环境中: 34 | 35 | ```swift 36 | enum Tab { 37 | case home 38 | case search 39 | case settings 40 | } 41 | 42 | class TabController: ObservableObject { 43 | @Published var activeTab = Tab.home 44 | 45 | func open(_ tab: Tab) { 46 | activeTab = tab 47 | } 48 | } 49 | ``` 50 | 51 | 有了上述内容,我们现在可以用新的`Tab`类型来标记`TabView`中的每个视图,如果我们再把`TabController`注入到视图层次结构的环境中,那么其中的任何视图都可以随时切换显示的Tab。 52 | 53 | ```swift 54 | struct RootView: View { 55 | @StateObject private var tabController = TabController() 56 | 57 | var body: some View { 58 | TabView(selection: $tabController.activeTab) { 59 | HomeView() 60 | .tag(Tab.home) 61 | .tabItem { Label("Home", systemImage: "house") } 62 | 63 | SearchView() 64 | .tag(Tab.search) 65 | .tabItem { Label("Search", systemImage: "magnifyingglass") } 66 | 67 | SettingsView() 68 | .tag(Tab.settings) 69 | .tabItem { Label("Settings", systemImage: "gearshape") } 70 | } 71 | .environmentObject(tabController) 72 | } 73 | } 74 | ``` 75 | 76 | 例如,现在我们的`HomeView`可以使用一个完全自定义的按钮切换到设置标签——它只需要从环境中获取我们的`TabController`,然后它可以调用`open`方法来执行标签切换,像这样: 77 | 78 | ```swift 79 | struct HomeView: View { 80 | @EnvironmentObject private var tabController: TabController 81 | 82 | var body: some View { 83 | ScrollView { 84 | ... 85 | Button("Open settings") { 86 | tabController.open(.settings) 87 | } 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | 很好! 另外,由于`TabController`是一个完全由我们控制的对象,我们也可以用它来切换主视图层次结构以外的标签。例如,我们可能想根据推送通知或其他类型的服务器事件来切换标签,现在可以通过调用上述视图代码中的相同的`open`方法来完成。 94 | 95 | > 要了解更多关于环境对象以及SwiftUI状态管理系统的其余部分,[请查看本指南](https://www.swiftbysundell.com/articles/swiftui-state-management-guide)。 96 | 97 | ### 控制导航堆栈 98 | 99 | 就像标签视图一样,SwiftUI的`NavigationView`也可以被编程自定义控制。例如,假设我们正在开发一个应用程序,在其主导航堆栈中显示一个日历视图作为根视图,然后用户可以通过点击位于该应用程序导航栏中的编辑按钮来打开一个日历编辑视图。为了连接这两个视图,我们使用了一个`NavigationLink`,每当点击一个给定的视图时,它就会自动将其压入到导航栈中: 100 | 101 | ```swift 102 | struct RootView: View { 103 | @ObservedObject var calendarController: CalendarController 104 | 105 | var body: some View { 106 | NavigationView { 107 | CalendarView( 108 | calendar: calendarController.calendar 109 | ) 110 | .toolbar { 111 | ToolbarItem(placement: .navigationBarTrailing) { 112 | NavigationLink("Edit") { 113 | CalendarEditView( 114 | calendar: $calendarController.calendar 115 | ) 116 | .navigationTitle("Edit your calendar") 117 | } 118 | } 119 | } 120 | .navigationTitle("Your calendar") 121 | } 122 | .navigationViewStyle(.stack) 123 | } 124 | } 125 | ``` 126 | 127 | > 在这种情况下,我们在所有设备上使用堆栈式导航风格,甚至是iPad,而不是让系统选择使用哪种导航风格。 128 | 129 | 现在我们假设,我们想让我们的`CalendarView`以自定义方式显示其编辑视图,而不需要构建一个单独的实例。要做到这一点,我们可以在编辑按钮的`NavigationLink`中注入一个`isActive`绑定,然后将其传递给我们的`CalendarView`: 130 | 131 | ```swift 132 | struct RootView: View { 133 | @ObservedObject var calendarController: CalendarController 134 | @State private var isEditViewShown = false 135 | 136 | var body: some View { 137 | NavigationView { 138 | CalendarView( 139 | calendar: calendarController.calendar, 140 | isEditViewShown: $isEditViewShown 141 | ) 142 | .toolbar { 143 | ToolbarItem(placement: .navigationBarTrailing) { 144 | NavigationLink("Edit", isActive: $isEditViewShown) { 145 | CalendarEditView( 146 | calendar: $calendarController.calendar 147 | ) 148 | .navigationTitle("Edit your calendar") 149 | } 150 | } 151 | } 152 | .navigationTitle("Your calendar") 153 | } 154 | .navigationViewStyle(.stack) 155 | } 156 | } 157 | ``` 158 | 159 | 如果我们现在也更新`CalendarView`,使其使用`@Binding`绑定属性接受上述值,那么现在只要我们想显示我们的编辑视图,就可以简单地将该属性设置为`true`,我们的根视图的`NavigationLink`将自动被触发: 160 | 161 | ```swift 162 | struct CalendarView: View { 163 | var calendar: Calendar 164 | @Binding var isEditViewShown: Bool 165 | 166 | var body: some View { 167 | ScrollView { 168 | ... 169 | Button("Edit calendar settings") { 170 | isEditViewShown = true 171 | } 172 | } 173 | } 174 | } 175 | ``` 176 | 177 | > 当然,我们也可以选择将`isEditViewShown`属性封装在某种形式的`ObservableObject`中,例如`NavigationController`,就像我们之前处理`TabView`时那样。 178 | 179 | 这就是我们如何以自定义编程方式触发显示在我们的用户界面中的`NavigationLink`——但如果我们想在不给用户任何直接控制的情况下执行这种导航呢? 180 | 181 | 例如,我们现在假设我们正在开发一个包括导出功能的视频编辑应用程序。当用户进入导出流程时,一个`VideoExportView`被显示为模态,一旦导出操作完成,我们想把`VideoExportFinishedView`推送到该模态的导航栈中。 182 | 183 | 最初,这可能看起来非常棘手,因为(由于SwiftUI是一个声明式的UI框架)没有`push`方法,当我们想在导航栈中添加一个新视图时,我们可以调用该方法。事实上,在`NavigationView`中显示一个新视图的唯一内置方法是使用`NavigationLink`,它需要成为我们视图层次结构本身的一部分。 184 | 185 | 也就是说,这些`NavigationLink`实际上不一定是可见的——所以在这种情况下,实现我们目标的一个方法是在我们的视图中添加一个隐藏的导航链接,然后我们可以在视频导出操作完成后以编程方式触发该链接。如果我们也在我们的目标视图中隐藏系统提供的返回按钮,那么我们就可以完全锁定用户能够在这两个视图之间手动导航: 186 | 187 | ```swift 188 | struct VideoExportView: View { 189 | @ObservedObject var exporter: VideoExporter 190 | @State private var didFinish = false 191 | @Environment(\.presentationMode) private var presentationMode 192 | 193 | var body: some View { 194 | NavigationView { 195 | VStack { 196 | ... 197 | Button("Export") { 198 | exporter.export { 199 | didFinish = true 200 | } 201 | } 202 | .disabled(exporter.isExporting) 203 | 204 | NavigationLink("Hidden finish link", isActive: $didFinish) { 205 | VideoExportFinishedView(doneAction: { 206 | presentationMode.wrappedValue.dismiss() 207 | }) 208 | .navigationTitle("Export completed") 209 | .navigationBarBackButtonHidden(true) 210 | } 211 | .hidden() 212 | } 213 | .navigationTitle("Export this video") 214 | } 215 | .navigationViewStyle(.stack) 216 | } 217 | } 218 | 219 | struct VideoExportFinishedView: View { 220 | var doneAction: () -> Void 221 | 222 | var body: some View { 223 | VStack { 224 | Label("Your video was exported", systemImage: "checkmark.circle") 225 | ... 226 | Button("Done", action: doneAction) 227 | } 228 | } 229 | } 230 | ``` 231 | 232 | > 我们在`VideoExportFinishedView`中注入一个`doedAction`闭包,而不是让它检索当前的`presentationMode`本身,是因为我们希望解耦整个模态流程,而不仅仅是那个特定的视图。要了解更多信息,请查看 "[解耦SwiftUI模态或详细视图](https://www.swiftbysundell.com/articles/dismissing-swiftui-modal-and-detail-views)"。 233 | 234 | 使用这样一个隐藏的`NavigationLink`绝对可以被认为是一个有点 "黑 "的解决方案,但它的效果非常好,如果我们把一个导航链接看成是导航堆栈中两个视图之间的连接(而不仅仅是一个按钮),那么上述设置可以说是有意义的。 235 | 236 | ### 小结 237 | 238 | 尽管SwiftUI的导航系统仍然不如UIKit和AppKit提供的系统灵活,但它已经足够强大,可以满足很多不同的使用情——-特别是当与SwiftUI非常全面的状态管理系统相结合时。 239 | 240 | 当然,我们也可以选择将我们的SwiftUI视图层次包裹在[托管控制器](https://www.swiftbysundell.com/articles/swiftui-and-uikit-interoperability-part-2)中,只使用UIKit/AppKit来实现我们的导航代码。哪种解决方案是最合适的,可能取决于我们在每个项目中实际想要执行多少自定义和程序化的导航。 241 | 242 | 感谢您的阅读! 243 | 244 | > https://www.swiftbysundell.com/articles/swiftui-programmatic-navigation/ 245 | -------------------------------------------------------------------------------- /resource/58 WWDC 23 之后的 SwiftUI 有哪些新功能.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | WWDC 23 已经到来,SwiftUI 框架中有很多改变和新增的功能。在本文中将主要介绍 SwiftUI 中**数据流**、**动画**、**ScrollView**、**搜索**、**新手势**等功能的新变化。 4 | 5 | ## 数据流 6 | 7 | Swift 5.9 引入了**宏功能**,成为 SwiftUI 数据流的核心。SwiftUI 不再使用 `Combine`,而是使用新的 `Observation` 框架。Observation 框架为我们提供了 `Observable` 协议,必须使用它来允许 SwiftUI 订阅更改并更新视图。 8 | 9 | ```Swift 10 | @Observable 11 | final class Store { 12 | var products: [String] = [] 13 | var favorites: [String] = [] 14 | 15 | func fetch() async { 16 | try? await Task.sleep(nanoseconds: 1_000_000_000) 17 | // load products 18 | products = [ 19 | "Product 1", 20 | "Product 2" 21 | ] 22 | } 23 | } 24 | ``` 25 | 26 | 不需要在代码中遵循 Observable 协议。相反,可以使用 `@Observable` 宏来标记你的类型,它会自动为符合 Observable 协议。也不再需要 `@Published` 属性包装器,因为 SwiftUI 视图会自动跟踪任何可观察类型的可用属性的更改。 27 | 28 | ```Swift 29 | struct ProductsView: View { 30 | @State private var store = Store() 31 | 32 | var body: some View { 33 | List(store.products, id: \.self) { product in 34 | Text(verbatim: product) 35 | } 36 | .task { 37 | if store.products.isEmpty { 38 | await store.fetch() 39 | } 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | 以前,有一系列的属性包装器,如 `State`、`StateObject`、`ObservedObject` 和 `EnvironmentObject`,你应该了解何时以及为何使用它们。 46 | 47 | 现在,状态管理变得更加简单。对于值类型(如字符串和整数)和符合 Observable 协议的引用类型,只需使用 State 属性包装器。 48 | 49 | ```Swift 50 | struct FavoriteProductsView: View { 51 | let store: Store 52 | 53 | var body: some View { 54 | List(store.favorites, id: \.self) { product in 55 | Text(verbatim: product) 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | 在上面的示例中,有一个接受 Store 类型的视图。在之前的 SwiftUI 框架版本中,应该使用 `@ObservedObject` 属性包装器来订阅更改。现在不需要了,因为 SwiftUI 视图会自动跟踪符合 Observable 协议的类型的更改。 62 | 63 | ```Swift 64 | struct EnvironmentViewExample: View { 65 | @Environment(Store.self) private var store 66 | 67 | var body: some View { 68 | Button("Fetch") { 69 | Task { 70 | await store.fetch() 71 | } 72 | } 73 | } 74 | } 75 | 76 | struct ProductsView: View { 77 | @State private var store = Store() 78 | 79 | var body: some View { 80 | List(store.products, id: \.self) { product in 81 | Text(verbatim: product) 82 | } 83 | .task { 84 | if store.products.isEmpty { 85 | await store.fetch() 86 | } 87 | } 88 | .toolbar { 89 | NavigationLink { 90 | EnvironmentViewExample() 91 | } label: { 92 | Text(verbatim: "Environment") 93 | } 94 | } 95 | .environment(store) 96 | } 97 | } 98 | ``` 99 | 100 | 还可以使用 Environment 属性包装器与 environment 视图修饰符配对,将可观察类型放入 SwiftUI 环境中。不需要使用 `@EnvironmentObject` 属性包装器或 environmentObject 视图修饰符。同样的 Environment 属性包装器现在适用于可观察类型。 101 | 102 | ```Swift 103 | struct BindanbleViewExample: View { 104 | @Bindable var store: Store 105 | 106 | var body: some View { 107 | List($store.products, id: \.self) { $product in 108 | TextField(text: $product) { 109 | Text(verbatim: product) 110 | } 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | 每当需要从可观察类型中提取绑定时,可以使用新的 Bindable 属性包装器。 117 | 118 | ## 动画 119 | 120 | 动画始终是 SwiftUI 框架中最重要的部分。在 SwiftUI 中轻松实现任何动画,但之前的框架版本缺少一些现在具有的功能。 121 | 122 | ```Swift 123 | struct AnimationExample: View { 124 | @State private var value = false 125 | 126 | var body: some View { 127 | Text(verbatim: "Hello") 128 | .scaleEffect(value ? 2 : 1) 129 | .onTapGesture { 130 | withAnimation { 131 | value.toggle() 132 | } completion: { 133 | print("Animation have finished") 134 | } 135 | } 136 | } 137 | } 138 | ``` 139 | 140 | 如上例所示,我们有了新版本的 `withAnimation` 函数,允许提供动画完成处理程序。这是一个很好的补充,现在您可以构建阶段性动画。 141 | 142 | ```Swift 143 | enum Phase: CaseIterable { 144 | case start 145 | case loading 146 | case finish 147 | 148 | var offset: CGFloat { 149 | // Calculate offset for the particular phase 150 | switch self { 151 | case start: 100.0 152 | case loading: 0.0 153 | case finish: 50.0 154 | } 155 | } 156 | } 157 | 158 | struct PhasedAnimationExample: View { 159 | @State private var value = false 160 | 161 | var body: some View { 162 | PhaseAnimator(Phase.allCases, trigger: value) { phase in 163 | LoadingView() 164 | .offset(x: phase.offset) 165 | } animation: { phase in 166 | switch phase { 167 | case .start: .easeIn(duration: 0.3) 168 | case .loading: .easeInOut(duration: 0.5) 169 | case .finish: .easeOut(duration: 0.1) 170 | } 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | SwiftUI 框架引入了新的 `PhaseAnimator` 视图,它遍历阶段序列,允许为每个阶段提供不同的动画,并在阶段更改时更新内容。还有 `KeyframeAnimator` 视图,可以使用关键帧来实现动画。 177 | 178 | ## ScrollView 179 | 180 | 今年 ScrollView 有了很多优秀的新增功能。首先,可以使用 `scrollPosition` 视图修饰符来观察内容偏移量。 181 | 182 | ```Swift 183 | struct ContentView: View { 184 | @State private var scrollPosition: Int? = 0 185 | 186 | var body: some View { 187 | ScrollView { 188 | Button("Scroll") { 189 | scrollPosition = 80 190 | } 191 | 192 | ForEach(1..<100, id: \.self) { number in 193 | Text(verbatim: number.formatted()) 194 | } 195 | .scrollTargetLayout() 196 | } 197 | .scrollPosition(id: $scrollPosition) 198 | } 199 | } 200 | ``` 201 | 202 | 如上例所示,使用 `scrollPosition` 视图修饰符将内容偏移量绑定到一个状态属性上。每当用户滚动视图时,它会通过设置第一个可见视图的标识来更新绑定。还可以通过编程方式滚动到任何视图,但是,应该使用 `scrollTargetLayout` 视图修饰符来告诉 SwiftUI 框架在哪里查找标识以更新绑定。 203 | 204 | ```Swift 205 | struct ContentView: View { 206 | var body: some View { 207 | ScrollView { 208 | ForEach(1..<100, id: \.self) { number in 209 | Text(verbatim: number.formatted()) 210 | } 211 | .scrollTargetLayout() 212 | } 213 | .scrollTargetBehavior(.paging) 214 | } 215 | } 216 | ``` 217 | 218 | 可以通过使用 `scrollTargetBehavior` 视图修饰符来更改滚动行为。它允许在滚动视图中启用分页。 219 | 220 | ## 搜索 221 | 222 | 与搜索相关的视图修饰符也有一些很好的新增功能。例如,可以通过编程方式聚焦到搜索字段。 223 | 224 | ```Swift 225 | struct ProductsView: View { 226 | @State private var store = Store() 227 | @State private var query = "" 228 | @State private var scope: Scope = .default 229 | 230 | var body: some View { 231 | List(store.products, id: \.self) { product in 232 | Text(verbatim: product) 233 | } 234 | .task { 235 | if store.products.isEmpty { 236 | await store.fetch() 237 | } 238 | } 239 | .searchable(text: $query, isPresented: .constant(true), prompt: "Query") 240 | .searchScopes($scope, activation: .onTextEntry) { 241 | Text(verbatim: scope.rawValue) 242 | } 243 | } 244 | } 245 | ``` 246 | 247 | 如上例所示,可以使用可搜索视图修饰符的 `isPresented` 参数来显示/隐藏搜索字段。还可以使用 `searchScopes` 视图修饰符的 `activation` 参数来定义范围的可见性逻辑。 248 | 249 | ## 新手势 250 | 251 | 新增的 `RotateGesture` 和 `MagnifyGesture` 使我们能够跟踪视图的旋转和放大。 252 | 253 | ```Swift 254 | struct RotateGestureView: View { 255 | @State private var angle = Angle(degrees: 0.0) 256 | 257 | var rotation: some Gesture { 258 | RotateGesture() 259 | .onChanged { value in 260 | angle = value.rotation 261 | } 262 | } 263 | 264 | var body: some View { 265 | Rectangle() 266 | .frame(width: 200, height: 200, alignment: .center) 267 | .rotationEffect(angle) 268 | .gesture(rotation) 269 | } 270 | } 271 | ``` 272 | 273 | ## 新增的小功能 274 | 275 | 增加了全新的 `ContentUnavailableView` 类型,当需要显示空视图时可以使用它。示例如下: 276 | 277 | ```Swift 278 | struct ProductsView: View { 279 | @State private var store = Store() 280 | 281 | var body: some View { 282 | List(store.products, id: \.self) { product in 283 | Text(verbatim: product) 284 | } 285 | .background { 286 | if store.products.isEmpty { 287 | ContentUnavailableView("Products list is empty", systemImage: "list.dash") 288 | } 289 | } 290 | .task { 291 | if store.products.isEmpty { 292 | await store.fetch() 293 | } 294 | } 295 | } 296 | } 297 | ``` 298 | 299 | 还有新增了新的视图修饰符,允许调整列表中的间距。可以使用 `listRowSpacing` 和 `listSectionSpacing` 视图修饰符来设置列表中所需的间距。`EnvironmentValues` 结构体包含了一系列与最新平台更新相关的新属性,例如 `isActivityFullscreen` 和 `showsWidgetContainerBackground`。Swift Charts 也具有可滚动和可动画的功能。 300 | 301 | ```Swift 302 | #Preview { 303 | ContentView() 304 | } 305 | ``` 306 | 307 | 还有一个新的 Preview 宏,可以让我们轻松地为 UIKit 和 SwiftUI 构建预览,只需几行代码。 308 | 309 | ## 总结 310 | 311 | SwiftUI 框架中有许多小的新增功能,我们将会继续分享。希望能帮到你。 312 | 313 | 特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。 -------------------------------------------------------------------------------- /resource/59 使用 Swift 6 语言模式构建 Swift 包.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 我最近了解到,Swift 6 的一些重大变更(如完整的数据隔离和数据竞争安全检查)将成为 Swift 6 语言模式的一部分,该模式将在 Swift 6 编译器中作为可选功能启用。 4 | 5 | 这意味着,当你更新 Xcode 版本或使用 Swift 6 编译器的 Swift 工具链时,除非你明确启用 Swift 6 语言模式,否则你的代码将使用 Swift 5 语言模式进行编译。 6 | 7 | 在本文中,我将向你展示如何下载和安装 Swift 6 工具链的开发快照,并在构建 Swift 包时启用 Swift 6 语言模式。 8 | 9 | ## 下载 Swift 6 工具链 10 | 11 | 使用 Swift 6 编译器和语言模式构建代码的第一步是下载 Swift 6 开发工具链。 12 | 13 | Apple 在 swift.org 网站上提供了从 release/6.0 分支构建的 Swift 编译器版本,适用于多个平台,你可以下载并安装到系统中。 14 | 15 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f7790360ae094574ae6d944e69d3097d~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1758\&h=1488\&s=480181\&e=png\&b=fefefe) 16 | 17 | 你可以手动执行此操作,但我建议使用像 Swiftenv(用于 macOS)或 Swiftly(用于 Linux)这样的工具来管理你的 Swift 工具链,就像本文中所示的那样。 18 | 19 | ### Swiftenv - macOS 20 | 21 | Swiftenv 是一个受 pyenv 启发的 Swift 版本管理器,它允许你轻松安装和管理多个版本的 Swift。 22 | 23 | 使用 Swiftenv,安装最新的 Swift 6 开发快照只需运行以下命令: 24 | 25 | ```bash 26 | # 安装最新的 Swift 6 开发工具链 27 | swiftenv install 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a 28 | 29 | # 进入你的 Swift 包目录 30 | cd your-swift-package 31 | 32 | # 将 Swift 6 工具链设置为此目录的默认工具链 33 | swiftenv local 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a 34 | ``` 35 | 36 | ### Swiftly - Linux 37 | 38 | 如果你在 Linux 机器上构建代码,可以使用 Swift Server Workgroup 的 Swiftly 命令行工具来安装和管理 Swift 工具链,运行以下命令: 39 | 40 | ```bash 41 | # 安装最新的 Swift 6 开发工具链 42 | swiftly install 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a 43 | 44 | # 将 Swift 6 工具链设置为活动工具链 45 | swiftly use 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a 46 | ``` 47 | 48 | ## 在 SPM 中启用语言模式 49 | 50 | 让我们考虑一个 Swift 包目标,其代码在使用 Swift 6 编译器和 Swift 6 语言模式编译时会产生错误: 51 | 52 | ```swift 53 | class NonIsolated { 54 | func callee() async {} 55 | } 56 | 57 | actor Isolated { 58 | let isolated = NonIsolated() 59 | 60 | func callee() async { 61 | await isolated.callee() 62 | } 63 | } 64 | ``` 65 | 66 | 让我们使用我们之前下载的 Swift 6 工具链并启用 StrictConcurrency 实验功能进行构建: 67 | 68 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05b9001a6b0446f58278184d10ae80c8~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=600\&h=407\&s=2089769\&e=gif\&f=138\&b=262835) 69 | 70 | 如你所见,构建结果是警告而不是错误。这是因为默认情况下,Swift 6 编译器使用的是 Swift 5 语言模式,而 Swift 6 语言模式是可选的。 71 | 72 | 有两种方法可以启用 Swift 6 语言模式:直接从命令行通过将 `-swift-version` 标志传递给 swift 编译器,或者在包清单文件中指定它。 73 | 74 | ### 命令行 75 | 76 | 要启用 Swift 6 语言模式编译代码,可以使用以下命令: 77 | 78 | ```bash 79 | swift build -Xswiftc -swift-version -Xswiftc 6 80 | ``` 81 | 82 | ### 包清单文件 83 | 84 | 你可以通过更新 tools-version 到 6.0 并在包清单文件中添加 `swiftLanguageVersions` 键来为你的 Swift 包启用 Swift 6 语言模式: 85 | 86 | ```swift 87 | // swift-tools-version: 6.0 88 | import PackageDescription 89 | 90 | let package = Package( 91 | name: "Swift6Examples", 92 | platforms: [.macOS(.v10_15), .iOS(.v13)], 93 | products: [ 94 | .library( 95 | name: "Swift6Examples", 96 | targets: ["Swift6Examples"] 97 | ) 98 | ], 99 | targets: [ 100 | .target(name: "Swift6Examples") 101 | ], 102 | swiftLanguageVersions: [.version("6")] 103 | ) 104 | ``` 105 | 106 | ## 输出 107 | 108 | 正如你所见,当启用了 Swift 6 语言模式后,编译器报告了与数据隔离相关的错误。这些错误表明我们在代码中存在需要修复的并发问题。 109 | 110 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/52dfa20affad46808de2281bcc02d5c3~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=600\&h=407\&s=1666523\&e=gif\&f=75\&b=262835) 111 | 112 | ### 结论 113 | 114 | Swift 6 带来了许多重要的新特性,如数据隔离和数据竞争安全检查,这些特性有助于编写更安全、更高效的代码。然而,这些新特性并不会自动启用,需要通过 Swift 6 语言模式显式开启。通过下载和安装 Swift 6 工具链,并在命令行或包清单文件中启用 Swift 6 语言模式,我们可以提前体验和适应这些变化。尽管新特性带来了一些学习和调整成本,但它们最终会使我们的代码更加健壮。 115 | -------------------------------------------------------------------------------- /resource/60 如何使用 Swift 中的 GraphQL.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 我一直在分享关于类型安全和在 Swift 中构建健壮 API 的更多内容。今天,我想继续探讨类型安全的话题,介绍 GraphQL。GraphQL 是一种用于 API 的查询语言。本周,我们将讨论 GraphQL 的好处,并学习如何在 Swift 中使用它。 4 | 5 | ## 基础知识 6 | 7 | 首先介绍一下 GraphQL。GraphQL 是一种用于 API 的查询语言。通常,后端开发人员或网络服务会为你提供一个模式文件和一个 GraphQL 端点。模式文件包含所有你可以使用该端点进行的类型和查询。让我们来看一个模式文件的例子。 8 | 9 | ```graphql 10 | schema { 11 | query: Query 12 | mutation: Mutation 13 | } 14 | 15 | type Query { 16 | film(id: ID, filmID: ID): Film 17 | allFilms(after: String, first: Int, before: String, last: Int): FilmsConnection 18 | """更多代码""" 19 | } 20 | ``` 21 | 22 | 模式文件应包含 `Query` 和 `Mutation` 类型。这些类型定义了当前 GraphQL 端点支持的所有查询和变更操作。模式文件还描述了你可以在查询中使用的所有类型的列表。 23 | 24 | ```graphql 25 | type Film implements Node { 26 | title: String! 27 | episodeID: Int 28 | openingCrawl: String 29 | director: String! 30 | } 31 | ``` 32 | 33 | GraphQL 是一种强类型语言。GraphQL 自定义类型中的每个字段都必须声明其类型。默认情况下,每个字段都可以为 nil。带有感叹号的字段不能为 nil。 34 | 35 | 我使用星球大战 API 来向你展示本文中的示例。让我们继续进行一些查询。你可以通过 GraphiQL 应用轻松玩转 GraphQL API,使用以下端点。 36 | 37 | ```swift 38 | query AllFilms { 39 | allFilms { 40 | films { 41 | title 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | **响应:** 48 | 49 | ```json 50 | { 51 | "data": { 52 | "allFilms": { 53 | "films": [ 54 | { 55 | "title": "A New Hope" 56 | }, 57 | { 58 | "title": "The Empire Strikes Back" 59 | }, 60 | { 61 | "title": "Return of the Jedi" 62 | }, 63 | { 64 | "title": "The Phantom Menace" 65 | }, 66 | { 67 | "title": "Attack of the Clones" 68 | }, 69 | { 70 | "title": "Revenge of the Sith" 71 | } 72 | ] 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | 如你所见,我们使用模式文件中的数据类型构建我们的查询。我喜欢GraphQL的一点是响应格式。请求格式直接映射到响应格式。你可以在请求中添加更多字段,响应也会包含它们。 79 | 80 | ```swift 81 | query AllFilms { 82 | allFilms { 83 | films { 84 | title 85 | director 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | **响应:** 92 | 93 | ```json 94 | { 95 | "data": { 96 | "allFilms": { 97 | "films": [ 98 | { 99 | "title": "A New Hope", 100 | "director": "George Lucas" 101 | }, 102 | { 103 | "title": "The Empire Strikes Back", 104 | "director": "Irvin Kershner" 105 | }, 106 | { 107 | "title": "Return of the Jedi", 108 | "director": "Richard Marquand" 109 | }, 110 | { 111 | "title": "The Phantom Menace", 112 | "director": "George Lucas" 113 | }, 114 | { 115 | "title": "Attack of the Clones", 116 | "director": "George Lucas" 117 | }, 118 | { 119 | "title": "Revenge of the Sith", 120 | "director": "George Lucas" 121 | } 122 | ] 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | 使用 GraphQL,我们只获取我们请求的数据,绝不会多余。 129 | 130 | ## ApolloGraphQL 131 | 132 | ApolloGraphQL 是一个很棒的框架,它可以让你轻松进行 GraphQL 查询和变更。ApolloGraphQL iOS 框架负责缓存和代码生成。ApolloGraphQL 为你在项目中定义的查询和变更生成 Swift 类型。它通过自动生成所有样板代码来节省你的时间。 133 | 134 | 以下是将 `ApolloGraphQL` 设置到项目中的一些步骤: 135 | 136 | 1. 你应该使用SPM或其他包管理器将 `ApolloGraphQL` 嵌入到你的项目中。 137 | 2. 在编译源代码部分上方的构建阶段添加运行脚本。这个脚本下载模式并为你的查询生成 Swift 类型。你可以在这个脚本中轻松更改 GraphQL 端点以连接到你的 GraphQL 后端。 138 | 139 | 我们已准备好使用 `ApolloGraphQL` 的项目。现在我们可以向项目添加第一个查询。我们应该在项目中创建一个带有 `.graphql` 扩展名的文件,并将这些行放入文件中。 140 | 141 | ```graphql 142 | query AllFilms { 143 | allFilms { 144 | films { 145 | title 146 | director 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | 让我们现在构建项目。`ApolloGraphQL` 生成一个 `API.swift` 文件,你应该将其添加到项目中。所有需要的类型都在这里,可以非常类型安全地进行 `GraphQL` 查询。每个请求类型都定义了其响应类型。`ApolloGraphQL` 生成了 `AllFilmsQuery` 和 Data 类型,描述了请求和响应。现在我们可以使用生成的代码进行 GraphQL 请求。 153 | 154 | ```swift 155 | let url = URL(string: "https://swapi-graphql.netlify.app/.netlify/functions/index")! 156 | let client = ApolloClient(url: url) 157 | 158 | client.fetch(query: AllFilmsQuery()) { result in 159 | switch result { 160 | case .success(let response): 161 | print(response.data?.allFilms?.films ?? []) 162 | case .failure(let error): 163 | print(error) 164 | } 165 | } 166 | ``` 167 | 168 | ## 结论 169 | 170 | GraphQL 为 API 开发带来了诸多优势,尤其是在类型安全和数据查询方面。通过定义明确的模式文件,GraphQL 确保了请求和响应的一致性,使得开发者能够精准获取所需数据,避免多余信息的传输。此外,GraphQL 强类型的特性进一步提升了代码的可靠性和可维护性。 171 | 172 | 在 Swift 中,ApolloGraphQL 框架极大地简化了 GraphQL 查询和变更的实现过程,自动生成的 Swift 类型和缓存机制不仅提高了开发效率,还减少了样板代码的编写。总之,GraphQL 是一种高效、灵活且类型安全的API解决方案,适用于构建现代化应用程序。尽管 GraphQL 也有其挑战,但其带来的优势使其成为 REST API 的有力竞争者。通过不断探索和优化,GraphQL 将在更多项目中得到广泛应用。 -------------------------------------------------------------------------------- /resource/61 SwiftUI 在 WWDC 24 之后的新变化.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | WWDC 24 已经到来,我们有很多内容要讨论。每年,SwiftUI 都会通过引入更多功能来赶上 UIKit。今年也不例外。让我们深入了解 SwiftUI 框架引入的新功能。 4 | 5 | 我首先要提到的主要变化是 App、Scene 和 View 协议的 `@MainActor` 隔离。这可能会破坏你的代码,所以请记住这一点。 6 | 7 | ## 视图集合 8 | 9 | SwiftUI 为 Group 和 ForEach 视图引入了新的重载,允许我们创建自定义容器,如 List 或 TabView。 10 | 11 | ```swift 12 | struct AppStoreView: View { 13 | @ViewBuilder var content: Content 14 | 15 | var body: some View { 16 | VStack { 17 | Group(subviewsOf: content) { subviews in 18 | HStack { 19 | if !subviews.isEmpty { 20 | subviews[0] 21 | } 22 | 23 | if subviews.count > 1 { 24 | subviews[1] 25 | } 26 | } 27 | 28 | if subviews.count > 2 { 29 | VStack { 30 | subviews[2...] 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | 如上例所示,我们使用带有新初始化器的 Group 视图,允许我们访问通过 `@ViewBuilder` 闭包传递的内容视图的子视图。SwiftUI 引入了新的 `Subview` 和 `SubviewsCollection` 类型,提供了对真实视图的代理访问。 40 | 41 | ## 新的标签栏体验 42 | 43 | 使用新的 Tab 类型,SwiftUI 提供了新的可定制标签栏体验,带有流畅过渡到侧边栏。 44 | 45 | ```swift 46 | enum Destination: Hashable { 47 | case home 48 | case search 49 | case settings 50 | case trends 51 | } 52 | 53 | struct RootView: View { 54 | @State private var selection: Destination = .home 55 | 56 | var body: some View { 57 | TabView { 58 | Tab("home", systemImage: "home", value: .home) { 59 | HomeView() 60 | } 61 | 62 | Tab("search", systemImage: "search", value: .search) { 63 | SearchView() 64 | } 65 | 66 | TabSection("Other") { 67 | Tab("trends", systemImage: "trends", value: .trends) { 68 | TrendsView() 69 | } 70 | Tab("settings", systemImage: "settings", value: .settings) { 71 | SettingsView() 72 | } 73 | } 74 | .tabViewStyle(.sidebarAdaptable) 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | 如上例所示,我们使用新的 Tab 类型来定义标签。我们还在 `TabSection` 实例上使用 `tabViewStyle` 视图修饰符,将特定的标签部分分组并移动到侧边栏。 81 | 82 | ## 英雄动画 83 | 84 | SwiftUI 引入了 `matchedTransitionSource` 和 `navigationTransition`,我们可以在任何 `NavigationLink` 实例中配对使用。 85 | 86 | ```swift 87 | struct HeroAnimationView: View { 88 | @Namespace var hero 89 | 90 | var body: some View { 91 | NavigationStack { 92 | NavigationLink { 93 | DetailView() 94 | .navigationTransition(.zoom(sourceID: "myId", in: hero)) 95 | } label: { 96 | ThumbnailView() 97 | } 98 | .matchedTransitionSource(id: "myId", in: hero) 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | 这使我们能够在 NavigationStack 内从一个视图导航到另一个视图时,使用相同的标识符和命名空间创建平滑的过渡。 105 | 106 | ## 滚动位置 107 | 108 | 新的 ScrollPosition 类型与 scrollPosition 视图修饰符配对,允许我们读取 ScrollView 实例的精确位置。我们还可以使用它编程地滚动到滚动内容的特定点。 109 | 110 | ```swift 111 | struct ScrollPositionExample: View { 112 | @State private var position: ScrollPosition = .init(point: .zero) 113 | 114 | var body: some View { 115 | ScrollView { 116 | ForEach(1..<1000) { item in 117 | Text(item.formatted()) 118 | } 119 | 120 | Button("jump to top") { 121 | position = ScrollPosition(point: .zero) 122 | } 123 | } 124 | .scrollPosition($position) 125 | } 126 | } 127 | ``` 128 | 129 | ## Entry 宏 130 | 131 | 新的 Entry 宏允许我们快速引入环境值、聚焦值、容器值等,无需样板代码。让我们看看在 Entry 宏之前我们如何定义环境值。 132 | 133 | ```swift 134 | struct ItemsPerPageKey: EnvironmentKey { 135 | static var defaultValue: Int = 10 136 | } 137 | 138 | extension EnvironmentValues { 139 | var itemsPerPage: Int { 140 | get { self[ItemsPerPageKey.self] } 141 | set { self[ItemsPerPageKey.self] = newValue } 142 | } 143 | } 144 | ``` 145 | 146 | 现在,我们可以通过使用 Entry 宏来简化代码。 147 | 148 | ```swift 149 | extension EnvironmentValues { 150 | @Entry var itemsPerPage: Int = 10 151 | } 152 | ``` 153 | 154 | ## 预览 155 | 156 | 新的 Previewable 宏允许我们在预览中引入状态,而无需将其包装到额外的包装视图中。 157 | 158 | ```swift 159 | #Preview("toggle") { 160 | @Previewable @State var toggled = true 161 | return Toggle("Loud Noises", isOn: $toggled) 162 | } 163 | ``` 164 | 165 | ## 其他 166 | 167 | SwiftUI 框架的下一版本包括许多新 API,如窗口推送、TextField 和 TextEditor 视图中的文本选择观察、搜索焦点监控、自定义文本渲染、新的 MeshGradient 类型等等,我无法在一篇文章中涵盖所有内容。 168 | 169 | ## 总结 170 | 171 | 在 WWDC 24 上,SwiftUI 再次通过引入更多新功能来提升其成熟度,以赶上 UIKit。今年的主要变化包括 @MainActor 隔离、视图集合的新重载、新的可定制标签栏体验、英雄动画、滚动位置的新功能以及新的 Entry 和 Previewable 宏。这些改进使开发者能够创建更灵活和高效的用户界面。SwiftUI还引入了许多新的API,如窗口推送、文本选择观察、搜索焦点监控等,使开发更加便捷和强大。 -------------------------------------------------------------------------------- /resource/62 在 SwiftUI 中 accessibilityChildren 视图修饰符的作用.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | SwiftUI 为我们提供了一系列丰富的视图修饰符,用于操作视图的可访问性树。我已经介绍了其中许多,你可以在博客中找到它们。本文我们将讨论 `accessibilityChildren` 视图修饰符以及我们如何从中受益。 4 | 5 | `accessibilityChildren` 视图修饰符允许我们为视图创建一个可访问性容器,并使用 `ViewBuilder` 闭包提供的视图元素进行填充。 6 | 7 | ## 示例 8 | 9 | 让我们来看一个简单的示例。 10 | 11 | ```swift 12 | struct BarChartShape: Shape { 13 | let dataPoints: [DataPoint] 14 | 15 | func path(in rect: CGRect) -> Path { 16 | Path { p in 17 | let spacing: CGFloat = 10 // 间距为 10 像素 18 | let totalSpacing = CGFloat(dataPoints.count - 1) * spacing 19 | let availableWidth = rect.size.width - totalSpacing 20 | let width: CGFloat = availableWidth / CGFloat(dataPoints.count) 21 | var x: CGFloat = 0 22 | 23 | for point in dataPoints { 24 | let pointRect = CGRect( 25 | x: x, 26 | y: rect.size.height - point.value, 27 | width: width, 28 | height: point.value 29 | ) 30 | let pointPath = RoundedRectangle(cornerRadius: 8).path(in: pointRect) 31 | p.addPath(pointPath) 32 | x += width + spacing // 添加间距 33 | } 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | 如你在上面的示例中所见,我们有一个绘制数据点的形状类型。我们无法为每个数据点提供可访问性值,因为在描边或填充形状后,该形状将成为一个单一视图。 40 | 41 | ## accessibilityChildren 使用 42 | 43 | 不过,SwiftUI 为这种情况专门提供了 `accessibilityChildren` 视图修饰符。 44 | 45 | ```swift 46 | struct ContentView: View { 47 | @State private var dataPoints: [DataPoint] = [ 48 | .init(value: 200), 49 | .init(value: 300), 50 | .init(value: 50), 51 | .init(value: 600), 52 | .init(value: 500) 53 | ] 54 | 55 | var body: some View { 56 | BarChartShape(dataPoints: dataPoints) 57 | .fill(.red) 58 | .accessibilityLabel("Chart") 59 | .accessibilityChildren { 60 | HStack(alignment: .bottom, spacing: 0) { 61 | ForEach(dataPoints) { point in 62 | RoundedRectangle(cornerRadius: 8) 63 | .accessibilityValue(Text(point.value.formatted())) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | 通过应用 `accessibilityChildren` 视图修饰符,我们创建了一个可访问性容器,并使用 ViewBuilder 闭包中提供的视图元素进行填充。SwiftUI 不会渲染我们通过 ViewBuilder 闭包传递的视图,它仅用于填充可访问性树的子元素。 72 | 73 | `accessibilityChildren` 和 `accessibilityRepresentation` 视图修饰符之间的主要区别在于前者不会影响视图本身。它仅为子元素创建一个可访问性容器,而 `accessibilityRepresentation` 视图修饰符会完全替换当前视图的可访问性树。 74 | 75 | ## 完整代码 76 | 77 | 首先,你需要定义 `DataPoint` 结构体,然后可以在 `ContentView` 中初始化 `dataPoints` 数组。以下是完善后的代码: 78 | 79 | ```swift 80 | import SwiftUI 81 | 82 | struct DataPoint: Identifiable { 83 | var id = UUID() 84 | var value: CGFloat 85 | } 86 | 87 | struct BarChartShape: Shape { 88 | let dataPoints: [DataPoint] 89 | 90 | func path(in rect: CGRect) -> Path { 91 | Path { p in 92 | let width: CGFloat = rect.size.width / CGFloat(dataPoints.count) 93 | var x: CGFloat = 0 94 | 95 | for point in dataPoints { 96 | let pointRect = CGRect( 97 | x: x, 98 | y: rect.size.height - point.value, 99 | width: width, 100 | height: point.value 101 | ) 102 | let pointPath = RoundedRectangle(cornerRadius: 8).path(in: pointRect) 103 | p.addPath(pointPath) 104 | x += width 105 | } 106 | } 107 | } 108 | } 109 | 110 | struct ContentView: View { 111 | @State private var dataPoints: [DataPoint] = [ 112 | .init(value: 20), 113 | .init(value: 30), 114 | .init(value: 5), 115 | .init(value: 100), 116 | .init(value: 80) 117 | ] 118 | 119 | var body: some View { 120 | BarChartShape(dataPoints: dataPoints) 121 | .fill(Color.red) 122 | .accessibilityLabel("Chart") 123 | .accessibilityChildren { 124 | HStack(alignment: .bottom, spacing: 0) { 125 | ForEach(dataPoints) { point in 126 | RoundedRectangle(cornerRadius: 8) 127 | .fill(Color.blue) // You can change the color here 128 | .frame(width: 20, height: point.value) 129 | .accessibilityValue(Text(String(Int(point.value))) 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | ``` 137 | 138 | 上述代码已添加了所需的 `DataPoint` 结构体和 `ContentView`,并通过 `BarChartShape` 创建了柱状图。此代码将以红色柱状图的形式显示数据点,每个数据点的值决定柱状的高度,同时也包括辅助功能信息以提供无障碍体验。 139 | 140 | 请注意,柱状图的颜色可以通过 `.fill(Color.red)` 进行自定义。在上述代码中,将柱状图填充颜色设为红色。您可以根据需要自行更改填充颜色。 141 | 142 | 运行截图: 143 | 144 | ![](https://files.mdnice.com/user/17787/c7bc4e90-32b0-4cc4-969c-dec3e2a8fa13.png) 145 | 146 | ## 总结 147 | 148 | 今天,我们了解了 SwiftUI 为我们提供的又一个强大的可访问性视图修饰符。SwiftUI 凭借提供如此多友好的 API,简化了我们为了使我们的应用对每个人都具有可访问性而必须做的工作,做得非常出色。 -------------------------------------------------------------------------------- /resource/63.Swift 并发中的任务让步Yielding和Debouncing.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 本篇文章的主题是 **任务让步(Task Yielding)** 和 **防抖(Debouncing)**。Swift 并发为我们提供了两个简单但非常强大的函数:`yield` 和 `sleep`。今天我们就来看看它们的用法,以及在什么场景下应该使用它们。 4 | 5 | ## 什么是任务防抖(Debouncing)? 6 | 7 | 想象一下,你正在开发一个搜索功能,用户每输入一个字符,程序就会去一个庞大的数据集里查找匹配的结果。如果不加控制,每次键入都会触发新的搜索任务,可能会导致多个任务同时执行,影响性能甚至引发竞争条件(Race Condition)。 8 | 9 | 比如,下面这个 SwiftUI 代码: 10 | 11 | ```swift 12 | @MainActor @Observable final class Store { 13 | private(set) var results: [HKCorrelation] = [] 14 | private let store = HKHealthStore() 15 | 16 | func search(matching query: String) async { 17 | // 执行复杂的搜索任务 18 | } 19 | } 20 | 21 | struct ContentView: View { 22 | @State private var store = Store() 23 | @State private var query = "" 24 | 25 | var body: some View { 26 | NavigationStack { 27 | List(store.results, id: \.uuid) { result in 28 | Text(verbatim: result.endDate.formatted()) 29 | } 30 | .searchable(text: $query) 31 | .task(id: query) { 32 | await store.search(matching: query) 33 | } 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | 在这个代码里,每次用户输入新字符,都会创建一个新的任务去执行搜索。而 Swift 并发采用 **协作式取消机制(Cooperative Cancellation)**,也就是说,它不会直接强行终止任务,而是提供一个“取消标记”,任务需要自己检查并响应取消请求。因此,这种写法可能会导致多个搜索任务并行运行,消耗不必要的计算资源。 40 | 41 | 为了解决这个问题,我们可以用 **防抖(Debouncing)** 技术。 42 | 43 | ## 如何用 sleep 实现防抖? 44 | 45 | 防抖的思路很简单: 46 | 47 | - 用户输入时,我们 **等待一小段时间**,看看用户是否继续输入。 48 | - 如果输入仍然在变化,我们就 **继续等待**,而不是立即启动搜索。 49 | - 只有当输入 **稳定** 一定时间后,才触发搜索任务。 50 | 51 | 换句话说,如果用户输入 `"apple"`,我们希望忽略 `"a"`, `"ap"`, `"app"`, `"appl"`,只在最终输入 `"apple"` 后再进行搜索。 52 | 53 | 在 Swift 并发中,我们可以用 `Task.sleep` 来实现这个效果: 54 | 55 | ```swift 56 | struct ContentView: View { 57 | @State private var store = Store() 58 | @State private var query = "" 59 | 60 | var body: some View { 61 | NavigationStack { 62 | List(store.results, id: \.uuid) { result in 63 | Text(verbatim: result.endDate.formatted()) 64 | } 65 | .searchable(text: $query) 66 | .task(id: query) { 67 | do { 68 | try await Task.sleep(for: .seconds(1)) // 等待 1 秒 69 | await store.search(matching: query) // 执行搜索 70 | } catch { 71 | // 任务可能因为新输入被取消 72 | } 73 | } 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | **为什么这样就能实现防抖?** 80 | - `Task.sleep(for: .seconds(1))` 让当前任务暂停 1 秒。 81 | - 如果用户在 1 秒内继续输入,之前的任务会被取消,新任务重新计时。 82 | - 只有 **用户停止输入超过 1 秒**,才会触发真正的搜索任务。 83 | 84 | **效果:** 这样可以避免在输入过程中反复触发搜索,减少不必要的计算量。 85 | 86 | ## 什么是任务让步(Task Yielding)? 87 | 88 | 除了防抖,Swift 并发还提供了 **任务让步(Task Yielding)**,让你在执行长时间任务时,主动把线程让出来,让其他任务有机会运行。 89 | 90 | 想象一个场景: 91 | 92 | 你需要解析一批巨大的 JSON 文件,并将数据保存到磁盘。这个过程可能会运行很久,占用线程资源。如果你在主线程或并发线程池(Cooperative Thread Pool)上运行这种任务,会 **阻塞其他任务的执行**,导致性能问题。 93 | 94 | 比如,下面是一个解析 JSON 文件的代码: 95 | 96 | ```swift 97 | struct Item: Decodable { 98 | // 解析 JSON 的结构 99 | } 100 | 101 | struct DataHandler { 102 | func process(json files: [Data]) async throws -> [Item] { 103 | let decoder = JSONDecoder() 104 | var result: [Item] = [] 105 | 106 | for file in files { 107 | let items = try decoder.decode([Item].self, from: file) 108 | result.append(contentsOf: items) 109 | } 110 | 111 | return result 112 | } 113 | } 114 | ``` 115 | 116 | 这个 `process` 函数会遍历所有 JSON 文件,并解析它们。但问题是: 117 | - 解析 JSON 是一个 **同步操作**,它不会自动释放 CPU 资源。 118 | - 如果 JSON 文件很大,整个解析过程可能会 **占用线程很长时间**,导致其他任务被阻塞。 119 | 120 | ## 如何用 yield 让出线程? 121 | 122 | 为了解决这个问题,我们可以 **在每次解析完一个 JSON 文件后,让出线程**,让其他任务有机会执行: 123 | 124 | ```swift 125 | struct DataHandler { 126 | func process(json files: [Data]) async throws -> [Item] { 127 | let decoder = JSONDecoder() 128 | var result: [Item] = [] 129 | 130 | for file in files { 131 | let items = try decoder.decode([Item].self, from: file) 132 | result.append(contentsOf: items) 133 | 134 | await Task.yield() // 让出线程,让其他任务有机会执行 135 | } 136 | 137 | return result 138 | } 139 | } 140 | ``` 141 | 142 | **任务让步的好处:** 143 | - `await Task.yield()` 会让当前任务 **暂停一下**,让其他等待中的任务有机会执行。 144 | - 之后,系统会恢复这个任务的执行,继续处理下一个 JSON 文件。 145 | - 这样可以 **更公平地分配 CPU 资源**,防止某个任务独占线程。 146 | 147 | ## 什么时候需要 yield? 148 | 149 | 通常来说,如果你的代码已经是 **异步的(async/await)**,系统会自动在 `await` 语句处让出线程。所以 **大部分情况下,你不需要手动 `yield`**。 150 | 151 | 但是,当你处理 **非异步 API**(比如 JSON 解析、图片处理、大量计算等)时,手动 `yield` 可能会提升性能。 152 | 153 | ## 总结 154 | 155 | 1. **防抖(Debouncing)** 156 | - 适用于 **用户频繁输入的场景**,如搜索框、按钮点击等。 157 | - 通过 `Task.sleep(for:)` 实现,等输入稳定后再执行任务。 158 | - 避免频繁创建任务,提高性能。 159 | 160 | 2. **任务让步(Task Yielding)** 161 | - 适用于 **长时间运行的计算密集型任务**,如解析 JSON、图片处理等。 162 | - 通过 `Task.yield()` 让出 CPU,避免线程被长时间占用。 163 | - 让其他任务有机会执行,提高系统响应速度。 164 | 165 | 这两个技巧虽然简单,但在实际开发中非常有用,可以帮助你更高效地利用 Swift 并发,让你的应用运行得更流畅! 166 | 167 | > 来自:[Yielding and debouncing in Swift Concurrency](https://swiftwithmajid.com/2025/02/18/yielding-and-debouncing-in-swift-concurrency/) -------------------------------------------------------------------------------- /resource/64.「实战指南 」Swift 并发中的任务取消机制.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | Swift 并发提供了一种**协作式取消(cooperative cancellation)** 机制,来让任务在需要时自己退出。简单来说,**Swift 不会强行终止你的任务**,但它会告诉你任务已经被标记为取消,至于你要不要停下来,那是你自己的决定。 4 | 5 | 这篇文章会讲清楚**任务取消的原理、如何正确使用它,以及如何写出高效又优雅的代码**。 6 | 7 | ## 什么是协作式取消? 8 | 9 | 协作式取消的核心思想是: 10 | 11 | 1. **调用方(比如 SwiftUI)没法直接终止任务,只能“标记”为取消**。 12 | 2. **任务本身需要定期检查这个标记,并决定要不要提前终止**。 13 | 3. **你可以选择直接返回、提供部分结果,或者继续执行**,全看你的业务逻辑。 14 | 15 | 简单来说,Swift 只是给你一个“信号”——“嘿,这个任务已经没用了,看看你要不要停下来”。 16 | 17 | ## 如何用 Task API 处理任务取消 18 | 19 | 来看个例子,这是一个 SwiftUI 界面,用户输入搜索内容时,会触发异步搜索。 20 | 21 | ```swift 22 | struct ContentView: View { 23 | @State private var store = Store() 24 | @State private var query = "" 25 | 26 | var body: some View { 27 | NavigationStack { 28 | List(store.results, id: \.self) { result in 29 | Text(verbatim: result) 30 | } 31 | .searchable(text: $query) 32 | .task(id: query) { 33 | await store.search(matching: query) 34 | } 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | 这里最关键的是 `task(id: query)` 这行代码: 41 | 42 | - **当 `query` 变化时,SwiftUI 会启动一个新的搜索任务**。 43 | - **同时,它会标记上一个任务为“已取消”**,但不会立刻终止它。 44 | - **如果旧任务里没有检查取消状态,它可能仍然会跑完所有逻辑**。 45 | 46 | 这意味着,如果用户输入了很多字符,可能会同时存在多个搜索任务,这就是为什么我们要手动处理取消逻辑。 47 | 48 | ## 在异步方法中正确处理取消 49 | 50 | 假设 `Store` 负责查询数据,我们的 `search(matching:)` 方法如下: 51 | 52 | ```swift 53 | import HealthKit 54 | 55 | @MainActor @Observable final class Store { 56 | private(set) var results: [HKCorrelation] = [] 57 | private let store = HKHealthStore() 58 | 59 | func search(matching query: String) async { 60 | let foodQuery = HKSampleQueryDescriptor( 61 | predicates: [.correlation(type: .init(.food))], 62 | sortDescriptors: [] 63 | ) 64 | 65 | do { 66 | let food = try await foodQuery.result(for: store) 67 | 68 | try Task.checkCancellation() // 检查任务是否已取消 69 | 70 | results = food.filter { food in 71 | let title = food.metadata?["title"] as? String ?? "" 72 | return title.localizedStandardContains(query) 73 | } 74 | } catch { 75 | results = [] 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | 这里有个关键点:**`Task.checkCancellation()`**。 82 | 83 | - 这个方法会**抛出一个错误**,如果任务已经被取消,它就会立刻停止执行,后续的代码不会再运行。 84 | - 这样可以**避免执行一些不必要的逻辑**,比如过滤数据、更新 UI 等。 85 | - **如果任务被取消,我们直接把 `results` 置空**,这样用户不会看到过时的搜索结果。 86 | 87 | ## 在多个步骤中检查取消状态 88 | 89 | 如果你的异步代码有多个步骤,比如先获取数据、然后再做一些处理,那你可能需要**在多个关键点检查任务是否已取消**,否则即使任务已经无效了,它可能还会跑完整个流程。 90 | 91 | ```swift 92 | import HealthKit 93 | 94 | @MainActor @Observable final class Store { 95 | private(set) var results: [HKCorrelation] = [] 96 | private let store = HKHealthStore() 97 | 98 | func search(matching query: String) async { 99 | let foodQuery = HKSampleQueryDescriptor( 100 | predicates: [.correlation(type: .init(.food))], 101 | sortDescriptors: [] 102 | ) 103 | 104 | do { 105 | let food = try await foodQuery.result(for: store) 106 | 107 | try Task.checkCancellation() // 第一次取消检查 108 | 109 | // 假设这里有额外的数据处理 110 | try Task.checkCancellation() // 第二次取消检查 111 | 112 | results = food.filter { food in 113 | let title = food.metadata?["title"] as? String ?? "" 114 | return title.localizedStandardContains(query) 115 | } 116 | } catch { 117 | results = [] 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | **为什么要多次检查?** 124 | 125 | 1. 如果 `foodQuery` 运行了一段时间,任务被取消了,**我们希望尽早停下来**,而不是等所有代码都跑完。 126 | 2. **某些任务可能是分步执行的**,比如先获取原始数据,再处理数据。**如果第一步完成了,但任务已经取消了,我们就没必要继续处理数据**。 127 | 128 | ## 用 isCancelled 进行检查 129 | 130 | 除了 `Task.checkCancellation()` 之外,Swift 还提供了 `Task.isCancelled` 这个属性,它是一个布尔值,你可以用它更灵活地处理任务取消: 131 | 132 | ```swift 133 | actor SearchService { 134 | private var cachedResults: [HKCorrelation] = [] 135 | private let store = HKHealthStore() 136 | 137 | func search(matching query: String) async throws -> [HKCorrelation] { 138 | guard !Task.isCancelled else { 139 | return cachedResults // 任务取消了,直接返回缓存 140 | } 141 | 142 | let foodQuery = HKSampleQueryDescriptor( 143 | predicates: [.correlation(type: .init(.food))], 144 | sortDescriptors: [] 145 | ) 146 | 147 | let food = try await foodQuery.result(for: store) 148 | 149 | guard !Task.isCancelled else { 150 | return cachedResults // 任务取消了,避免不必要的计算 151 | } 152 | 153 | cachedResults = food.filter { food in 154 | let title = food.metadata?["title"] as? String ?? "" 155 | return title.localizedStandardContains(query) 156 | } 157 | 158 | return cachedResults 159 | } 160 | } 161 | ``` 162 | 163 | **两种方式的区别**: 164 | - `Task.checkCancellation()`:**如果任务已取消,直接抛出错误,代码不再继续执行**。 165 | - `Task.isCancelled`:**任务是否继续执行,由你自己决定**,比如可以提前返回缓存数据,而不是直接终止。 166 | 167 | ## 手动取消任务 168 | 169 | 通常情况下,Swift 会帮你管理任务的取消,但如果你想手动创建和取消任务,也可以用 `Task`: 170 | 171 | ```swift 172 | struct ExampleView: View { 173 | @State private var store = Store() 174 | @State private var task: Task? 175 | 176 | var body: some View { 177 | VStack { 178 | Button("开始任务") { 179 | task = Task { 180 | await store.fetch() 181 | } 182 | } 183 | 184 | Button("取消任务") { 185 | task?.cancel() 186 | } 187 | } 188 | } 189 | } 190 | ``` 191 | 192 | 这里 `task?.cancel()` 只会**标记任务为取消**,但不会真的终止它,所以你仍然需要在 `fetch()` 里检查 `Task.isCancelled` 或 `Task.checkCancellation()`。 193 | 194 | ## 总结 195 | 196 | 1. **Swift 不会自动终止任务,只会标记它为取消**。 197 | 2. **用 `Task.checkCancellation()` 可以立即终止任务,防止执行不必要的逻辑**。 198 | 3. **用 `Task.isCancelled` 可以更灵活地决定如何处理取消**。 199 | 4. **如果任务有多个异步步骤,应该在关键点多次检查取消状态**。 200 | 5. **手动创建的任务可以用 `.cancel()` 取消,但仍然需要手动检查取消状态**。 201 | 202 | 学会这些,你的 Swift 并发代码就能更高效、更优雅地处理任务取消,让用户体验更流畅! 203 | 204 | >来自:[Task Cancellation in Swift Concurrency](https://swiftwithmajid.com/2025/02/11/task-cancellation-in-swift-concurrency/) -------------------------------------------------------------------------------- /resource/7个大型iOS项目的Xcode快捷方式.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/7个大型iOS项目的Xcode快捷方式.pdf -------------------------------------------------------------------------------- /resource/8 SwiftUI 锁屏小组件.md: -------------------------------------------------------------------------------- 1 | # SwiftUI 锁屏小组件 2 | 3 | 4 | iOS呼声最高的功能之一是可定制的锁屏。终于,在最新发布的iOS 16得以实现。我们可以用可浏览的小组件填充锁屏。实现锁屏小组件很简单,因为它的API与主屏小组件共享相同的代码。本周我们将学习如何为我们的pp实现锁屏小组件。 5 | 6 | 让我们从你可能早就有的App主屏小组件代码开始。 7 | 8 | ```swift 9 | struct WidgetView: View { 10 | let entry: Entry 11 | 12 | @Environment(\.widgetFamily) private var family 13 | 14 | var body: some View { 15 | switch family { 16 | case .systemSmall: 17 | SmallWidgetView(entry: entry) 18 | case .systemMedium: 19 | MediumWidgetView(entry: entry) 20 | case .systemLarge, .systemExtraLarge: 21 | LargeWidgetView(entry: entry) 22 | default: 23 | EmptyView() 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | 在上面的示例中,我们有一个定义小组件的典型视图。我们使用`Environment`来知道`widget family`并显示适当的大小。我们需要做的就是删除默认语句,并实现定义锁屏小组件的所有新用例。 30 | 31 | ```swift 32 | struct WidgetView: View { 33 | let entry: Entry 34 | 35 | @Environment(\.widgetFamily) private var family 36 | 37 | var body: some View { 38 | switch family { 39 | case .systemSmall: 40 | SmallWidgetView(entry: entry) 41 | case .systemMedium: 42 | MediumWidgetView(entry: entry) 43 | case .systemLarge, .systemExtraLarge: 44 | LargeWidgetView(entry: entry) 45 | case .accessoryCircular: 46 | Gauge(value: entry.goal) { 47 | Text(verbatim: entry.label) 48 | } 49 | .gaugeStyle(.accessoryCircularCapacity) 50 | case .accessoryInline: 51 | Text(verbatim: entry.label) 52 | case .accessoryRectangular: 53 | VStack(alignment: .leading) { 54 | Text(verbatim: entry.label) 55 | Text(entry.date, format: .dateTime) 56 | } 57 | default: 58 | EmptyView() 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | 最好记住,系统对锁屏和主屏小组件使用不同的渲染模式。系统为我们提供了三种不同的渲染模式。 65 | 66 | 1. 主屏小组件和 Watch OS支持颜色的[全色模式](https://www.interaction-design.org/literature/topics/color-modes)。是的,从 watchOS 9 开始,你还可以用 WidgetKit 去实现 watchOS 的复杂性。 67 | 2. 振动模式(vibrant mode)是指系统将文本、图像和仪表还原为单色,并为锁屏背景正确着色。 68 | 3. 重音模式(accented mode)仅在 watchOS 上使用,系统将小部件分为两组,默认和重音。 系统使用用户在表盘设置中选择的色调颜色为小部件的重音部分着色。 69 | 70 | 渲染模式可通过SwiftUI `Environment`变量使用,因此你可以始终检查哪个渲染模式处于活动状态,并将其反映在设计中。例如,可以使用具有不同渲染模式的不同图片。 71 | 72 | ```swift 73 | struct WidgetView: View { 74 | let entry: Entry 75 | 76 | @Environment(\.widgetRenderingMode) private var renderingMode 77 | 78 | var body: some View { 79 | switch renderingMode { 80 | case .accented: 81 | AccentedWidgetView(entry: entry) 82 | case .fullColor: 83 | FullColorWidgetView(entry: entry) 84 | case .vibrant: 85 | VibrantWidgetView(entry: entry) 86 | default: 87 | EmptyView() 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | 如上所示,我们使用`widgetRenderingMode`环境值来获得实际的渲染模式,并表现出不同的行为。像之前讲到的,在重音模式(accented mode)下,系统将小部件分为两部分,并对它们进行特殊着色。可以使用`widgetAccentable`视图修改器标记视图层次的一部分。在这种情况下,系统将知道哪些视图应用着色颜色。 94 | 95 | ```swift 96 | struct AccentedWidgetView: View { 97 | let entry: Entry 98 | var body: some View { 99 | HStack { 100 | Image(systemName: "moon") 101 | .widgetAccentable() 102 | Text(verbatim: entry.label) 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | 最后,我们需要为小组件配置支持类型。 109 | 110 | ```swift 111 | @main 112 | struct MyAppWidget: Widget { 113 | let kind: String = "Widget" 114 | 115 | var body: some WidgetConfiguration { 116 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 117 | WidgetView(entry: entry) 118 | } 119 | .configurationDisplayName("My app widget") 120 | .supportedFamilies( 121 | [ 122 | .systemSmall, 123 | .systemMedium, 124 | .systemLarge, 125 | .systemExtraLarge, 126 | .accessoryInline, 127 | .accessoryCircular, 128 | .accessoryRectangular 129 | ] 130 | ) 131 | } 132 | } 133 | ``` 134 | 135 | 如果你仍然支持iOS 15,可以检查新锁屏小组件的可用性。 136 | 137 | ```swift 138 | @main 139 | struct MyAppWidget: Widget { 140 | let kind: String = "Widget" 141 | 142 | private var supportedFamilies: [WidgetFamily] { 143 | if #available(iOSApplicationExtension 16.0, *) { 144 | return [ 145 | .systemSmall, 146 | .systemMedium, 147 | .systemLarge, 148 | .accessoryCircular, 149 | .accessoryRectangular, 150 | .accessoryInline 151 | ] 152 | } else { 153 | return [ 154 | .systemSmall, 155 | .systemMedium, 156 | .systemLarge 157 | ] 158 | } 159 | } 160 | 161 | var body: some WidgetConfiguration { 162 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 163 | WidgetView(entry: entry) 164 | } 165 | .configurationDisplayName("My app widget") 166 | .supportedFamilies(supportedFamilies) 167 | } 168 | } 169 | ``` 170 | 171 | > 来源:[Lock screen widgets in SwiftUI](https://swiftwithmajid.com/2022/08/30/lock-screen-widgets-in-swiftui/) -------------------------------------------------------------------------------- /resource/Sourcery的Swift Package命令行插件/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/Sourcery的Swift Package命令行插件/.DS_Store -------------------------------------------------------------------------------- /resource/Sourcery的Swift Package命令行插件/assets/sourcery-command-cli.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/Sourcery的Swift Package命令行插件/assets/sourcery-command-cli.mp4 -------------------------------------------------------------------------------- /resource/Sourcery的Swift Package命令行插件/assets/sourcery-command-xcode.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/Sourcery的Swift Package命令行插件/assets/sourcery-command-xcode.mp4 -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/.DS_Store -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/.DS_Store -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/disable-swiftlint-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/disable-swiftlint-code.png -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/extra-blank-space-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/extra-blank-space-warning.png -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/fix-before-exclude-codable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/fix-before-exclude-codable.png -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/identifier-name-violations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/identifier-name-violations.png -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/identifier-names-with-underscores.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/identifier-names-with-underscores.png -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/integrate-swiftlint-xcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/integrate-swiftlint-xcode.png -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/swiftlint-fix-automatically-xcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/swiftlint-fix-automatically-xcode.png -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/todo-causes-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/todo-causes-warning.png -------------------------------------------------------------------------------- /resource/用SwiftLint保持Swift风格一致/assets/todo-no-warning-yml-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftCommunityRes/article-ios/7c8aef27c970ae9bac3dd73ea6d5e83b018236c4/resource/用SwiftLint保持Swift风格一致/assets/todo-no-warning-yml-file.png --------------------------------------------------------------------------------