普通视图
苹果、华为“撞档”上新 | Swift 周报 issue 62
Apple 新品发布会亮点有哪些 | Swift 周报 issue 61
苹果仍在研发更大尺寸的 iMac | Swift 周报 issue 60
提升代码调试技巧:从思维到实践
如何以编程方式解析 XCResult 包的内容
Swift 中的函数式核心与命令式外壳:单向数据流
如何在 CI/CD 过程中实施高效的自动化测试和部署
自定义 SwiftUI 中符号图像的外观
SwiftUI 中掌握 ScrollView 的使用:滚动可见性
苹果将为 Apple Watch X 铺路 | Swift 周报 issue 45
讨论在 Swift 中引入函数体宏
掌握 SwiftUI 中的 ScrollView:滚动几何
如何使用 SwiftUI 中 ScrollView 的滚动偏移
SwiftUI 在 WWDC 24 之后的新变化
使用 Swift 6 语言模式构建 Swift 包
如何使用 Swift 中的 GraphQL
如何在 SwiftUI 视图中显示应用图标和版本
使用 SwiftUI 为 macOS 创建类似于 App Store Connect 的选择器
SwiftUI 中的内容边距
Swift 6:导入语句上的访问级别
使用 Swift 递归搜索目录中文件的内容,同时支持 Glob 模式和正则表达式
掌握 Swift 中的 reduce 操作符,使你的代码更高效
Swift 中的 StoreKit 测试
如何使用 SPM 插件从 Pkl 配置文件生成 Swift 接口
AnyView 对 SwiftUI 性能的影响
前言
AnyView 是一种类型擦除的视图,对于 SwiftUI 容器中包含的异构视图非常方便。在这些情况下,你不需要指定视图层次结构中所有视图的具体类型。通过这种方式,你可以避免使用泛型,从而简化你的代码。
然而,这可能会带来性能损失。如果是 AnyView(基本上是一个包装类型),SwiftUI 将很难确定视图的身份和结构,并且它将重新绘制整个视图,这并不是真正高效的。你可以在这个出色的 WWDC 演讲中找到有关 SwiftUI 差异机制的更多细节。
Apple 也多次提到,我们应该避免在 ForEach 中使用 AnyView,称其可能会导致性能问题。一个可能发生的情况是无尽的不同视图列表,呈现不同类型的数据(例如聊天、活动动态等)。在本文中,我将使用 Stream 的 SwiftUI 聊天 SDK 进行一些测量,使用其默认的基于泛型的实现,并将其与使用 AnyView 的修改后的实现进行比较。
测试设置
关于测试设置的几点说明:
- 所有测试和测量都在 iPhone 11 Pro Max 上进行。
- 为保持一致性,在所有测试中都使用相同的数据集和用户。
- 测试会执行多次。
- 正在测试的列表具有不同类型的数据(例如图像、视频、GIF、文本等)。
- 在测试不同实现时执行相同的操作(例如,在内容上滚动三次)。
- 数据以每页 25 个项目的形式获取。
- 我们将使用动画卡顿仪器配置文件以及这个开源 FPS 计数器。
动画卡顿
苹果建议使用动画卡顿作为衡量应用性能的指标。卡顿基本上是指在屏幕上显示的帧比预期晚的帧。卡顿时间越长,出现的故障和挂起就越明显,从而造成用户体验不佳。例如,如果你有 100 毫秒的卡顿,这意味着此帧显示晚于预期的 100 毫秒,从而使用户可以看到挂起。卡顿可以出现在提交阶段或渲染阶段。
为了提高我们应用的性能,我们需要将这些动画卡顿降到最低(或者更好地摆脱它们)。
我还将展示与 FPS(每秒帧数)的比较,因为它通常是开发人员更熟悉的度量标准之一。当使用 FPS 作为度量标准时,重要的是指定最大帧速率(在这种情况下为 60),并在应用程序没有活动时丢弃值。
浏览数据
首先,让我们看看在浏览内容时不同的实现会表现如何。在这个测试中,我们将通过整个消息列表三次滚动。
没有 AnyView
下面是没有泛型实现的动画卡顿记录。
如你所见,有几个动画卡顿,其中 2 个是橙色的,这意味着卡顿持续时间超过了可接受的延迟时间 33 毫秒。因此,在这 2 种情况下,将会丢失一帧。这 2 个卡顿发生在加载新消息并将其附加到消息列表时。在加载消息时进行任何后续滚动,不会影响性能。
在此测试期间,FPS 值的平均值约为每秒 59 帧。滚动是流畅且响应迅速的。
有 AnyView
接下来,让我们做同样的测试,同时使用 AnyView 包装器。以下是动画卡顿仪器配置文件中的结果。
你可以在此示例中看到一些更多的橙色。有更多的动画卡顿超过了可接受的延迟时间 33 毫秒。这导致在执行测试时在仪器和视觉上都出现一些可见的卡顿。
此外,当你再次浏览列表时,性能不会改善(甚至变得更糟)。这是有道理的,因为 SwiftUI 不知道它已经显示过此视图一次(因为它隐藏在 AnyView 下)。因此,它会再次绘制它,同时还可能缓存(但不使用)该视图的旧版本。
此测试中的平均 FPS 约为每秒 55 帧,你可能会注意到在滚动时出现一些可见的故障,尽管情况并不那么糟糕。
在浏览数据时修改
我们可以进行的另一个测试是性能测试 - 向列表发送大量内容并强制更新视图(例如,响应消息),同时我们也浏览数据。这将在较短的时间间隔内触发视图的多次重绘。
没有 AnyView
在没有 AnyView 包装器的情况下进行测试产生了与常规滚动测试相似的结果(58-59 FPS)。这也是预期的,因为 SwiftUI 知道视图的标识和结构。当需要更新视图时,仅对其进行更改(例如,向视图添加另一个反应)。
有 AnyView
当我们在这种情况下使用 AnyView 时,事情就变得有趣了 - 在短时间内对屏幕上的视图进行频繁更新。
在此场景中,有几个可见的卡顿和挂起,当我们频繁响应消息时,FPS 降至 50 以下。由于在几秒钟内强制重绘视图多次,帧丢失在这里更加明显。由于 SwiftUI 不知道这个视图是什么,我假设它每次都会从头开始重绘。其中一些视图相当昂贵(例如 GIF),因此重新绘制可能是一项相当昂贵的操作。
通过使用 AnyView,效果类似于将 id 修饰符的值设置为 UUID() - 这将在发生更改时始终更新视图项目。
分析结果
测试/实现 | 没有 AnyView(FPS) | 有 AnyView(FPS) | 性能退化 |
---|---|---|---|
浏览数据 | 59 | 55 | 10% |
在浏览数据时修改 | 59 | 50 | 16.5% |
这些数字相当依赖于设置,因此不应该被视为铁板钉钉的结果,而只是一个指示。
仅浏览数据时,如果你将视图包装在 AnyView 中,则会比不包装时慢大约 10%。如果你在浏览数据时更改数据,则此差异将增加到约 17%,而且这些故障在这里更加明显。
为了更好地理解结果,我们需要深入了解 SwiftUI 的工作原理。在这个关于 SwiftUI 性能的 WWDC 会话中,来自 SwiftUI 团队的 Raj 讨论了列表或表需要提前知道所有标识符。只有在内容解析为恒定数量的行时,才能高效地收集它们而无需访问所有内容。如果使用条件检查或 AnyView,将无法确定行数,并且必须提前创建所有视图,这会影响性能。
因此,请尽量避免这样的代码:
ForEach(someData) { someElement in
if someCondition {
SomeView(data: someElement)
}
}
以及像这样的代码:
ForEach(someData) { someElement in
AnyView(SomeView(data: someElement))
}
最后一段代码类似于我们使用 AnyView 进行测试的方式。这意味着,当列表发生更改时,我们实际上重新创建了整个列表。这也解释了为什么 AnyView 实现随着时间的推移变慢 - 每次重绘时都需要从头开始创建更多内容。
总结
总而言之,在这些情景中(包含异构视图的可滚动列表),最好为容器中的不同视图使用具体类型。这可能听起来更复杂一些,但实际上你可以使其更简单,而不必过多地处理泛型。
然而,这并不意味着使用 AnyView 总是会以这种方式影响性能。例如,如果你有一个菜单,作为几个异构元素的列表,在点击时显示不同的导航目标,并且决定将这些视图包装为 AnyView,我的测量结果表明与使用其他方法相比,性能没有区别。
在这篇文章中,使用 AnyView 与使用 if-else 语句的不同类型的测试显示出没有显着差异。使用 if-else 导致视图标识丢失,就像 AnyView 一样,因此在这里没有性能差异是可以预期的。
这也取决于实现的方式 - 你的数据模型,将状态传递到哪里,哪些更新可能会导致视图重绘等等。
使用 App Store Connect API 生成和读取分析报告
Swift Core Data 分阶段迁移
前言
在这之前,我发布了一篇文章,在其中解释了如何使用映射模型和自定义迁移策略执行复杂的 Core Data 迁移。虽然这种方法性能良好且运行良好,但很难维护,不适用于应用程序扩展,并且存在高度的错误风险。
例如,对于每个需要自定义迁移的新模型,你需要定义一个映射模型,以定义如何将每个模型的现有版本迁移到新版本。与你可能认为的相反(以及我所认为的),Core Data 在跨多个版本进行迁移时并不会按顺序迭代映射模型,相反,它需要从当前版本到新版本的精确模型。
除此之外,你需要使用 Xcode 的 UI 和映射模型来定义所有这些内容,这使得 PR 难以审查,错误难以发现。出于这些原因,我最近重新设计了我们的迁移流程,改用分阶段迁移,对开发者体验产生了巨大的影响!
什么是分阶段迁移?
正如在 WWDC23 中宣布的那样,与在 Swift 数据模型之间执行迁移的方式非常相似,你现在可以使用 NSStagedMigrationManager
实例以编程方式定义 Core Data 迁移。
该方法通过定义一系列迁移步骤(称为阶段),描述了如何在模型的不同版本之间进行迁移。
例如,假设你的应用程序当前正在使用数据模型的第 1 版,你想要迁移到第 3 版。迁移管理器将顺序应用所有必要的阶段,以从第 1 版迁移到第 2 版,然后从第 2 版迁移到第 3 版。
提供一些背景信息
为了演示 Core Data 分阶段迁移的工作原理,我将使用我之前在有关使用映射模型进行自定义 Core Data 迁移的文章中使用的相同示例。
与之前的文章一样,我们想要将 Track
模型中的 json
属性转换为一个单独的实体,该实体将为每个曲目保存所有相关的艺术家信息。将此属性转换也将使模型更灵活、更易于维护,因为我们将能够删除 json
属性本身和 artistName
,而使用新的关系。
让我们比较一下我们的 Track
模型之前和之后的情况,CoreData.swift 文件代码如下:
Copy code
CoreData.swift
// Before
import Foundation
import CoreData
@objc(Track)
public class Track: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
return NSFetchRequest<Track>(entityName: "Track")
}
@NSManaged public var imageURL: String?
@NSManaged public var json: String?
@NSManaged public var lastPlayedAt: Date?
@NSManaged public var title: String?
@NSManaged public var artistName: String?
}
// After
@objc(Track)
public class Track: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
return NSFetchRequest<Track>(entityName: "Track")
}
@NSManaged public var imageURL: String?
@NSManaged public var lastPlayedAt: Date?
@NSManaged public var title: String?
@NSManaged public var artists: NSSet?
@objc(addArtistsObject:)
@NSManaged public func addToArtists(_ value: Artist)
@objc(removeArtistsObject:)
@NSManaged public func removeFromArtists(_ value: Artist)
@objc(addArtists:)
@NSManaged public func addToArtists(_ values: NSSet)
@objc(removeArtists:)
@NSManaged public func removeFromArtists(_ values: NSSet)
}
@objc(Artist)
public class Artist: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Artist> {
return NSFetchRequest<Artist>(entityName: "Artist")
}
@NSManaged public var name: String?
@NSManaged public var id: String?
@NSManaged public var imageURL: String?
@NSManaged public var tracks: NSSet?
@objc(addTracksObject:)
@NSManaged public func addToTracks(_ value: Track)
@objc(removeTracksObject:)
@NSManaged public func removeFromTracks(_ value: Track)
@objc(addTracks:)
@NSManaged public func addToTracks(_ values: NSSet)
@objc(removeTracks:)
@NSManaged public func removeFromTracks(_ values: NSSet)
}
从上面的代码中可以看出,迁移并不是微不足道的,而且,对我们来说,Core Data 不能自动推断它。让我们看看如何使用分阶段迁移以代码形式定义迁移步骤。
创建迁移管理器
要定义我们的阶段,我们需要将我们的模型拆分为三个不同的模型版本和迁移:
- 保持原始模型版本不变。
- 第二个模型版本包含所有属性,并添加
Artist
实体和关系。这将是一个自定义阶段。 - 第三个模型版本删除了
json
和artistName
属性。这将是一个轻量级的阶段。
我们需要将迁移分解为三个阶段的原因是,就目前而言,我们不能在同一个阶段中使用并删除属性。
让我们从创建一个负责创建 NSStagedMigrationManager
实例并定义所有阶段的工厂类开始。StagedMigrationFactory.swift 文件代码如下:
import Foundation
import CoreData
import OSLog
// 1
extension Logger {
private static var subsystem = "dev.polpiella.CustomMigration"
static let storage = Logger(subsystem: subsystem, category: "Storage")
}
// 2
extension NSManagedObjectModelReference {
convenience init(in database: URL, modelName: String) {
let modelURL = database.appending(component: "\(modelName).mom")
guard let model = NSManagedObjectModel(contentsOf: modelURL) else { fatalError() }
self.init(model: model, versionChecksum: model.versionChecksum)
}
}
// 3
final class StagedMigrationFactory {
private let databaseURL: URL
private let jsonDecoder: JSONDecoder
private let logger: Logger
init?(
bundle: Bundle = .main,
jsonDecoder: JSONDecoder = JSONDecoder(),
logger: Logger = .storage
) {
// 4
guard let databaseURL = bundle.url(forResource: "CustomMigration", withExtension: "momd") else { return nil }
self.databaseURL = databaseURL
self.jsonDecoder = jsonDecoder
self.logger = logger
}
// 5
func create() -> NSStagedMigrationManager {
let allStages = [
v1toV2(),
v2toV3()
]
return NSStagedMigrationManager(allStages)
}
// 6
private func v1toV2() -> NSCustomMigrationStage {
struct Song: Decodable {
let artists: [Artist]
struct Artist: Decodable {
let id: String
let name: String
let imageURL: String
}
}
// 7
let customMigrationStage = NSCustomMigrationStage(
migratingFrom: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration"),
to: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 2")
)
// 8
customMigrationStage.didMigrateHandler = { migrationManager, currentStage in
guard let container = migrationManager.container else {
return
}
// 9
let context = container.newBackgroundContext()
context.performAndWait {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Track")
fetchRequest.predicate = NSPredicate(format: "json != nil")
do {
let allTracks = try context.fetch(fetchRequest)
let addedArtists = [String: NSManagedObject]()
for track in allTracks {
if let jsonString = track.value(forKey: "json") as? String {
let jsonData = Data(jsonString.utf8)
let object = try? self.jsonDecoder.decode(Song.self, from: jsonData)
let artists: [NSManagedObject] = object?.artists.map { jsonArtist in
if let matchedArtist = addedArtists[jsonArtist.id] {
return matchedArtist
}
let artist = NSEntityDescription
.insertNewObject(
forEntityName: "Artist",
into: context
)
artist.setValue(jsonArtist.name, forKey: "name")
artist.setValue(jsonArtist.imageURL, forKey: "imageURL")
artist.setValue(jsonArtist.id, forKey: "id")
return artist
} ?? []
track.setValue(Set<NSManagedObject>(artists), forKey: "artists")
}
}
try context.save()
} catch {
logger.error("\(error.localizedDescription)")
}
}
}
return customMigrationStage
}
// 10
private func v2toV3() -> NSCustomMigrationStage {
NSCustomMigrationStage(
migratingFrom: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 2"),
to: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 3")
)
}
}
回到上面的代码,让我们逐步分解:
- 我们定义了一个自定义记录器,以将迁移过程中发生的任何错误报告到控制台。
- 我们扩展了
NSManagedObjectModelReference
,创建了一个方便的初始化方法,它接受数据库 URL 和模型名称,并返回一个新的NSManagedObjectModelReference
实例。 - 我们定义了一个工厂类,负责创建
NSStagedMigrationManager
实例并定义所有阶段。 - 我们使用 bundle 初始化工厂,并检索数据库的 URL、JSON 解码器和记录器。
- 我们创建了
NSStagedMigrationManager
实例,并定义了所有阶段。 - 我们定义了一个方法,该方法将返回从我们模型的第 1 版迁移到第 2 版的迁移阶段。
- 我们创建了一个
NSCustomMigrationStage
实例,并传递我们要从何处迁移和迁移到的对象模型引用。文件名需要与包中的.mom
文件的名称匹配。 - 我们定义了
didMigrateHandler
闭包,在模型迁移后调用。此时,新的模型版本可在上下文中使用,你可以填充其属性。你必须知道,还有一个在先前模型版本上执行的单独处理程序,称为willMigrateHandler
,但我们在这种情况下不会使用它。 - 我们创建了一个新的后台上下文,并获取所有具有
json
属性的曲目。然后,我们将 JSON 字符串解码为Song
对象,并为 JSON 中的每个艺术家创建一个新的Artist
实体。然后,我们将Track
实体的artists
关系设置为新的Artist
实体。 - 我们定义了一个方法,该方法将返回从我们模型的第 2 版迁移到第 3 版的迁移阶段。这个迁移非常简单,事实上,它应该是一个轻量级的迁移。然而,我找不到一个能够在所有情况下使用的
NSLightweightMigrationStage
实例的方法。如果你知道如何做,请告诉我!
设置使用 Core Data 栈。
设置使用分阶段迁移的 Core Data 栈。
现在我们有了创建 NSStagedMigrationManager
实例的方法,我们需要设置我们的 Core Data 栈以使用它。PersistenceController.swift 文件代码如下:
PersistenceController.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CustomMigration")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.viewContext.automaticallyMergesChangesFromParent = true
if let description = container.persistentStoreDescriptions.first {
if let migrationFactory = StagedMigrationFactory() {
description.setOption(migrationFactory.create(), forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
}
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
这部分非常简单,你只需要将 NSStagedMigrationManager
实例设置为持久化存储描述的选项。
总结
这篇文章介绍了使用分阶段迁移来改进 Core Data 迁移流程的重要性和方法。传统的迁移方法使用映射模型,但这种方法不易维护,扩展性差且容易出错。分阶段迁移通过定义一系列迁移步骤,使得在不同模型版本之间进行迁移变得更加简单和可控。文章以一个示例来说明分阶段迁移的工作原理,以及如何以代码形式定义迁移步骤。最后,文章展示了如何设置使用分阶段迁移的 Core Data 栈。通过使用分阶段迁移,可以显著提高开发者体验,简化迁移流程,并降低错误风险。
Swift 定制 Core Data 迁移
前言
随着应用程序和用户群的增长,你需要添加新功能,删除其他功能,并改变应用程序的工作方式。这是软件开发生命周期的自然结果,我们应该接受。
随着应用程序的发展,你的数据模型也会发生变化。你需要更改数据结构的方式,以适应新功能,同时确保用户不会在不同版本之间丢失任何数据。如果你使用 Core Data 在应用程序中持久化信息,那么 Core Data 迁移就会发挥作用。
什么是 Core Data 迁移?
Core Data 迁移是将数据模型从一个版本更新到另一个版本的过程,因为数据的形状发生了变化(例如,添加或删除新属性)。
在大多数情况下,Core Data 将自动处理迁移过程。但是,有些情况下,你需要通过提供一个映射模型来自定义迁移过程,告诉 Core Data 究竟如何从源模型迁移到目标模型中的每个属性和实体。
甚至有些情况下,映射模型是不够的,你需要编写自定义迁移策略来处理特定情况。这是本文要重点讨论的情况。
示例
让我们考虑一个应用程序,在 Core Data 栈中存储表示音乐曲目的对象。模型非常简单,只包含一个实体:Track
,Track.swift 代码如下:
Copy code
Track.swift
import Foundation
import CoreData
@objc(Track)
public class Track: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
return NSFetchRequest<Track>(entityName: "Track")
}
@NSManaged public var imageURL: String?
@NSManaged public var json: String?
@NSManaged public var lastPlayedAt: Date?
@NSManaged public var title: String?
@NSManaged public var artistName: String?
}
上面的 Track
实体有五个属性:
-
imageURL
:表示曲目封面图像的 URL 的字符串。 -
json
:表示来自服务器的原始 JSON 数据响应的字符串。 -
lastPlayedAt
:表示上次播放曲目的日期。 -
title
:表示曲目的标题的字符串。 -
artistName
:表示艺术家的名称的字符串。
Core Data 栈不会与 iCloud 同步,并具有以下设置,CoreDataStack.swift 文件代码如下:
Copy code
CoreDataStack.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CustomMigration")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.viewContext.automaticallyMergesChangesFromParent = true
if let description = container.persistentStoreDescriptions.first {
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = false
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
如果你仔细观察上面的示例,你会注意到我们告诉 Core Data 自动迁移存储,因为我们不想做渐进式迁移,这种迁移速度慢得多且更复杂,并且我们还告诉 Core Data 不要自动推断映射模型,这意味着我们将不得不为每个迁移提供一个映射模型文件,并且可以允许我们自定义这个过程。
持久化了一首歌曲后,使用 Core Data Lab 检查数据库,我们可以看到属性被相应保存:
更新模型
当前版本的模型存在一些可扩展性问题:
- 模型仅允许每个曲目有一个艺术家,而实际上,一个曲目可以有多个艺术家。
- 模型存储一个表示曲目数据的原始 JSON 字符串,这不太高效,当应用程序需要解析 JSON 字符串以显示曲目数据以获取艺术家列表时,可能会导致性能问题。
为了解决这些问题,让我们删除 artistName
和 json
属性,采用一个新的 Artist
实体,该实体将与 Track
实体建立一对多的关系。
Artist
实体将具有一个表示艺术家名称的 name
属性,以及 id
和 imageURL
属性,我们将从原始 JSON 字符串中获取它们。
创建一个新的模型版本
首先,让我们通过选择 .xcdatamodeld
文件,然后从菜单栏中选择 Editor > Add Model Version...
来创建一个新的模型版本。
给它起一个名称,并以第一个模型版本为基础:
现在,让我们创建 Artist
实体并添加所有字段:
也让我们为新的 Artist
实体创建 NSManagedObject
子类,Artist.swift 代码如下:
Copy code
import Foundation
import CoreData
@objc(Artist)
public class Artist: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Artist> {
return NSFetchRequest<Artist>(entityName: "Artist")
}
@NSManaged public var name: String?
@NSManaged public var id: String?
@NSManaged public var imageURL: String?
@NSManaged public var tracks: NSSet?
@objc(addTracksObject:)
@NSManaged public func addToTracks(_ value: Track)
@objc(removeTracksObject:)
@NSManaged public func removeFromTracks(_ value: Track)
@objc(addTracks:)
@NSManaged public func addToTracks(_ values: NSSet)
@objc(removeTracks:)
@NSManaged public func removeFromTracks(_ values: NSSet)
}
正如你在上面的示例中看到的那样,我们将向 Track
实体添加一个对多的 artists
关系,还将向 Artist
实体添加一个对多的 tracks
关系。
现在,让我们为 Track
实体添加缺失的关系,并删除 artistName
和 json
属性:
并更新 NSManagedObject
子类以反映更改,Track.swift 文件代码如下:
import Foundation
import CoreData
@objc(Track)
public class Track: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
return NSFetchRequest<Track>(entityName: "Track")
}
@NSManaged public var imageURL: String?
@NSManaged public var lastPlayedAt: Date?
@NSManaged public var title: String?
@NSManaged public var artists: NSSet?
@objc(addArtistsObject:)
@NSManaged public func addToArtists(_ value: Artist)
@objc(removeArtistsObject:)
@NSManaged public func removeFromArtists(_ value: Artist)
@objc(addArtists:)
@NSManaged public func addToArtists(_ values: NSSet)
@objc(removeArtists:)
@NSManaged public func removeFromArtists(_ values: NSSet)
}
最后但并非最不重要的,让我们将新的模型设置为 .xcdatamodeld
文件的当前模型:
创建映射模型
由于我们告诉 Core Data 不要自动推断映射模型,所以我们将不得不创建一个映射模型文件来在两个版本之间建立桥梁。
从菜单栏中选择 File > New > File...
,然后选择 Mapping Model
。
然后,选择源模型:
最后,选择目标模型:
编写自定义迁移策略
默认情况下,Core Data 将尽力映射属性,并且大部分工作都将由它自动完成(包括已删除的属性)。
然而,由于我们创建了一个新的实体,并且我们希望保留现有数据,因此我们需要告诉 Core Data 如何迁移。
我们将创建一个新的类,该类继承自 NSEntityMigrationPolicy
,并在旧的 Track
实体上创建并链接一个新的关系到 Artist
实体,V2MigrationPolicy.swift 文件代码如下:
Copy code
import CoreData
struct Song: Decodable {
let artists: [Artist]
struct Artist: Decodable {
let id: String
let name: String
let imageURL: String
}
}
class V2MigrationPolicy: NSEntityMigrationPolicy {
private let decoder = JSONDecoder()
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
// 1
let sourceKeys = sInstance.entity.attributesByName.keys
let sourceValues = sInstance.dictionaryWithValues(forKeys: sourceKeys.map { $0 as String })
// 2
let destinationInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext)
let destinationKeys = destinationInstance.entity.attributesByName.keys.map { $0 as String }
// 3
for key in destinationKeys {
if let value = sourceValues[key] {
destinationInstance.setValue(value, forKey: key)
}
}
if let jsonString = sInstance.value(forKey: "json") as? String {
// 3
let jsonData = Data(jsonString.utf8)
let object = try? decoder.decode(Song.self, from: jsonData)
// 4
let artists: [NSManagedObject] = object?.artists.map { jsonArtist in
// 5
let request = Artist.fetchRequest()
request.fetchLimit = 1
request.predicate = NSPredicate(format: "name == %@", jsonArtist.name)
// Do not add duplicates to the list...
if let matchedArtists = try? manager.destinationContext.fetch(request), let matchedArtist = matchedArtists.first {
return matchedArtist
}
// 6
let artist = NSEntityDescription.insertNewObject(forEntityName: "Artist", into: manager.destinationContext)
artist.setValue(jsonArtist.name, forKey: "name")
artist.setValue(jsonArtist.imageURL, forKey: "imageURL")
artist.setValue(jsonArtist.id, forKey: "id")
return artist
} ?? []
// 7
destinationInstance.setValue(Set<NSManagedObject>(artists), forKey: "artists")
}
// 8
manager.associate(sourceInstance: sInstance, withDestinationInstance: destinationInstance, for: mapping)
}
}
让我们逐步解释上面的代码:
- 获取源实体的属性名称和值。
- 创建与源实体相同类型的全新目标实体。
- 将源实体的属性值复制到目标实体。
- 如果源实体具有
json
属性,则将其解析为Song
对象。 - 为避免重复项,请检查艺术家是否已经存在于目标上下文中。
- 如果艺术家不存在,则创建一个新的 Artist 实体,将其插入到上下文中,并设置其属性。
- 设置目标实体上的新艺术家关系。
- 将源和目标实例关联起来。
最后,让我们将此自定义策略添加到映射模型中:
现在,如果我们再次运行应用程序并使用 Core Data Lab 检查数据库,我们可以看到一个新的实体已经填充了正确的数据。
总结
文章介绍了在应用程序发展过程中,数据模型可能需要进行更改的情况下,如何使用 Core Data 迁移来保持数据的一致性和完整性。首先,它解释了什么是 Core Data 迁移,以及为什么需要进行迁移。接着,通过一个示例应用程序,详细介绍了如何更新数据模型,添加新实体和关系,以解决现有模型的可扩展性问题。然后,文章介绍了如何创建映射模型来定义不同模型版本之间的映射关系,并演示了如何编写自定义迁移策略来处理特定情况,例如将旧模型数据迁移到新模型的新关系中。最后,通过将自定义迁移策略添加到映射模型中,完成了整个迁移过程。
如何使用 SwiftUI 构建 visionOS 应用
前言
Apple Vision Pro 即将推出,现在是看看 SwiftUI API 的完美时机,这使我们能够将我们的应用程序适应 visionOS 提供的沉浸式世界。苹果表示,构建应用程序的最佳方式是使用 Swift 和 SwiftUI。下面,我们将学习如何使用 SwiftUI 构建 visionOS 应用程序。
Windows
我喜欢 SwiftUI 的一点是它如何自动适应平台。你无需执行任何操作即可在 visionOS 上运行使用 SwiftUI 编写的应用程序。它可以即插即用。但是,你始终可以通过向前移动并适应平台功能来改进用户体验。
struct ContentView: View {
var body: some View {
NavigationSplitView {
List {
// 列表内容
}
.navigationTitle("Models")
.toolbar {
ToolbarItem(placement: .bottomOrnament) {
Button("open", systemImage: "doc.badge.plus") {
}
}
ToolbarItem(placement: .bottomOrnament) {
Button("open", systemImage: "link.badge.plus") {
}
}
}
} detail: {
Text("Choose something from the sidebar")
}
}
}
在上面的示例中,我们使用了称为 bottomOrnament
的新工具栏放置。 visionOS 中的装饰是位于窗口外部的位置,用于呈现与窗口连接的控件。你还可以通过使用新的 ornament 视图修改器手动创建它们。
struct ContentView: View {
var body: some View {
NavigationSplitView {
List {
// 列表内容
}
.navigationTitle("Models")
.ornament(attachmentAnchor: .scene(.leading)) {
// 在此处放置你的视图
}
} detail: {
Text("Choose something from the sidebar")
}
}
}
新的 ornament 视图修改器允许我们为其连接的窗口创建一个具有特定锚点的装饰。将你的应用内容适应 visionOS 提供的沉浸式体验的另一种方法是使用 transform3DEffect
和 rotation3DEffect
视图修改器来加入深度效果。如下图:
Volumes
你的应用程序可以在 visionOS 上的同一场景中并排显示 2D 和 3D 内容。在这种情况下,我们可以使用 RealityKit
框架来呈现 3D 内容。例如,RealityKit 为我们提供了 Model3D SwiftUI 视图,允许我们从 USDZ 或实际文件中显示 3D 模型。
struct ContentView: View {
var body: some View {
NavigationSplitView {
List(Model.all) { model in
NavigationLink {
Model3D(named: model.name)
} label: {
Text(verbatim: model.name)
}
}
.navigationTitle("Models")
} detail: {
Model3D(named: "robot")
}
}
}
Model3D
视图的工作方式类似于 AsyncImage
视图,并异步加载模型。你还可以使用 Model3D 初始化器的另一种变体,它允许你自定义模型配置并添加占位视图。
struct ContentView: View {
var body: some View {
NavigationSplitView {
List(Model.all) { model in
NavigationLink {
Model3D(
url: Bundle.main.url(
forResource: model.name,
withExtension: "usdz"
)!
) { resolved in
resolved
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
ProgressView()
}
} label: {
Text(verbatim: model.name)
}
}
.navigationTitle("Models")
} detail: {
Model3D(named: "robot")
}
}
}
在你的应用程序中呈现 3D 内容时,你可以使用 windowStyle
修饰符来启用内容的体积显示。体积样式允许你的内容在第三维中增长,以匹配模型的大小。
对于更复杂的 3D 场景,我们可以使用 RealityView 并填充它以 3D 内容。
struct ContentView: View {
var body: some View {
NavigationSplitView {
List(Model.all) { model in
NavigationLink {
RealityView { content in
// load the content and add to the scene
}
} label: {
Text(verbatim: model.name)
}
}
.navigationTitle("Models")
} detail: {
Text("Choose something from the sidebar")
}
}
}
沉浸式空间
visionOS 的第三个选项是完全沉浸式体验,允许我们通过隐藏周围的所有内容来专注于你的场景。
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
ImmersiveSpace(id: "solar-system") {
SolarSystemView()
}
}
}
正如你在上面的示例中所看到的,我们通过使用 ImmersiveSpace
类型来定义场景。它允许我们通过使用 openImmersiveSpace
环境值来启用它。
struct MyMenuView: View {
@Environment(\.openImmersiveSpace) private var openImmersiveSpace
var body: some View {
Button("Enjoy immersive space") {
Task {
await openImmersiveSpace(id: "solar-system")
}
}
}
}
我们还可以使用 dismissImmersiveSpace
环境值来关闭沉浸式空间。请记住,你一次只能显示一个沉浸式空间。
struct SolarSystemView: View {
@Environment(\.dismissImmersiveSpace) private var dismiss
var body: some View {
// Immersive experience
Button("Dismiss") {
Task {
await dismiss()
}
}
}
}
结论
在介绍了 SwiftUI 在 visionOS 上的应用之后,我们了解到 SwiftUI 可以帮助我们轻松构建适应 visionOS 的应用程序。不仅如此,SwiftUI 还提供了许多方便的工具和修饰符,例如 windowStyle 修饰符,可用于在应用程序中呈现 3D 内容,并使内容根据模型的大小自动适应。通过引入沉浸式空间,我们可以将用户带入全新的体验,让他们沉浸在应用程序的世界中。总的来说,SwiftUI 为构建 visionOS 应用程序提供了强大而灵活的工具,我们可以期待在这个全新的平台上开发出令人惊叹的应用体验。
Swift 周报 第四十三期
前言
本期是 Swift 编辑组整理周报的第四十三期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。
Swift 周报在 GitHub 开源,欢迎提交 issue,投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。
命是弱者的借口,运是强者的谦辞,辉煌与否?且看Swift社区强中意!
周报精选
新闻和社区:iPhone 破发促销、印度市场寻增量,苹果再攀 3 万亿美元高点
提案:函数体 Macros 提案正在审查中
Swift 论坛:Swift 中引入函数体宏
推荐博文:在 SwiftUI 中实战使用 MapKit API
话题讨论:
你是更能接受同性上司还是更能接受异性上司?
上期话题结果
投票结果反映,大多数开发者还是比较担心自己的头发,另外就是身体变胖。久坐缺乏运动会导致一系列的身体健康问题。建议大家抽时间多运动,避免久坐。
新闻和社区
关于 App Store 提交的隐私更新
2023 年 12 月 7 日,第三方 SDK 隐私清单和签名。 第三方软件开发工具包 (SDK) 能够为 App 提供强大的功能,同时也可能会影响用户隐私,而这些影响可能对开发者和用户来说并不明显。请注意,当你将第三方 SDK 与你的 App 搭配使用时,你需要对 App 中使用的相应 SDK 包含的所有代码负责,并且需要了解 SDK 的数据收集和使用实践。
在 WWDC23 (简体中文字幕) 上,我们宣布了新的 SDK 隐私清单和签名,以帮助 App 开发者更好地了解第三方 SDK 如何使用数据、保护软件依赖项并为用户提供额外的隐私保护。从 2024 年春季开始,如果你提交的新 App 或 App 更新添加了 App Store 上的 App 中常用的第三方 SDK,那么你需要包含相应 SDK 的隐私清单。将 SDK 用作二进制文件依赖项时,也需要包含签名。此功能对于所有 App 来说都是向前迈出的重要一步,我们鼓励所有 SDK 采用这项功能,以更好地支持依赖于相应 SDK 的 App。
需要声明原因的 API 的新用例。 如果你上传到 App Store Connect 的新 App 或 App 更新使用了需要声明原因的 API (包括第三方 SDK 使用的 API),而你没有在 App 的隐私清单中提供批准的原因,那么你会收到通知。根据我们收到的开发者反馈,批准的原因列表已扩展到包含更多用例。如果你的用例可让用户直接受益,但未在现有批准原因列表中,请提交请求 (英文) 以便我们添加新的原因。
从 2024 年春季开始,若要将新 App 或 App 更新上传到 App Store Connect,你需要在 App 的隐私清单中注明批准的原因,以准确反映你的 App 如何使用相应 API。
iPhone 破发促销、印度市场寻增量,苹果再攀 3 万亿美元高点
作为全球科技公司的标杆,苹果公司的市值在今年 8 月初创下了新高,突破了 3 万亿美元的大关。不过,由于手机、PC 等市场的波动,加上外部多种因素的影响,近几月的时间,苹果公司的股价起起伏伏,市场也都在等待苹果何时能够再次站上 3 万亿美元的高点。
四个月的等待后,这一节点被定格在了美国当地时间 12 月 5 日。截至美股当日收盘,苹果公司股价报收于 193.42 美元,上涨 2.11% ,总市值达 3.01 万亿美元。
临近 2023 年年底,苹果公司股价 3 万亿美元的再次冲关,也算是给过去起伏的一年收了个尾。回望过去的一年,作为贡献出近一半收入的产品,iPhone 也未能幸免于整个大环境的下滑。为了提振销量,渠道商不得不降价促销,而新品开售破发加速、华为 5G 的回归更是给了苹果重重一击。
不过,依然需要指出的是,在绝大部分手机品牌亏本做买卖的同时,苹果公司则是赚走了全球超 8 成的利润。另外,印度市场的崛起,也让苹果公司找到了新增量。
Counterpoint 高级分析师 Ivan Lam 对钛媒体 App 表示,“印度俨然已经成为了人口第一大国,而且出生率还不错,年轻群体庞大且消费活跃。对于苹果来说,是未来十年的一个重要潜力市场。”
假日将至,请为你的 App 做好准备
App Store 最繁忙的季节即将到来!确保及时更新你的 App 和游戏,并在岁末假日到来之前做好准备。整个假日季期间同样会开放 App 提交,我们非常期待看到你提交的 App。平均而言,90% 的提交内容会在 24 小时内得到审核。但请注意,在 12 月 22 日至 27 日,完成审核所需的时间可能略长一些。
提案
通过的提案
SE-0411 隔离的默认值表达式 提案通过审查。该提案已在 四十期周报 正在审查的提案模块做了详细介绍。
正在审查的提案
SE-0414 基于区域的隔离 提案正在审查。
Swift Concurrency 将值分配给由 actor 和任务边界确定的隔离域。在不同隔离域中运行的代码可以并发执行,并且通过 Sendable 检查,防止非 Sendable 值跨越隔离边界传递,从而彻底消除对共享可变状态的并发访问。在实践中,这是一个重要的语义限制,因为它禁止了无数据竞争的自然编程模式。
在本文档中,我们提出通过引入一种新的控制流敏感的诊断来放宽这些规则,该诊断确定非 Sendable 值是否可以安全地在隔离边界上传递。通过引入隔离区域的概念,编译器可以保守地推断两个值是否可能相互影响。通过使用隔离区域,语言可以证明在将非 Sendable 值传递过隔离边界后,该值(以及可能引用它的任何其他值)在调用者中不会被使用,从而防止竞争的发生。
SE-0415 函数体 Macros 提案正在审查。
宏通过附加代码来增强 Swift 程序,其中包括新的声明、表达式和语句。目前,宏系统不支持可能希望增强代码的关键方式之一,即合成或更新函数的主体。可以创建具有自己函数主体的新函数,但不能为用户声明的函数提供、增强或替换函数主体。
该提案引入了函数体宏,确切地说:允许根据声明全面合成函数主体,以及通过更多功能增强现有函数主体。这为宏提供了许多新的用例,包括:
- 根据函数声明和一些元数据(例如自动合成传递提供的参数的远程过程调用)全面合成函数主体。
- 通过执行日志/跟踪、检查前置条件或建立不变量来增强函数主体。
- 根据提供的实现替换函数主体。例如,将主体移入在其他地方执行的闭包中,或将主体视为宏“降低”为可执行代码的特定领域语言。
Swift论坛
内容概括
SE-0415 提议在 Swift 中引入函数体宏。 该提案的审核期截至 2023 年 12 月 20 日。该提案建议启用一项可通过带有 -enable-experimental-feature BodyMacros 标志的主干开发快照访问的功能。
审核过程鼓励反馈以改进提案。 它要求审阅者评估所解决问题的重要性、提案是否符合 Swift 的方向,并将其与其他语言或库中的类似功能(如果适用)进行比较。 Tony Allevato 作为审核经理负责监督此审核流程。
讨论的序言中提出的一个具体问题提到,所提议的宏可能无法有效地处理从函数体抛出的错误。 有人建议使用一种新的延迟块来捕获抛出的错误,从而允许访问块内的这些错误以进行处理。
所提出的语法示例演示了一个概念,其中 defer 块可能会捕获从函数体抛出的错误并执行报告错误和重新抛出错误等操作。
内容概括
讨论围绕获取具有关联值的枚举情况的字符串表示,特别是寻求一种为每种情况生成类型化签名或插值的方法。 一个示例枚举了具有关联值及其预期输出签名的各种情况。
当前的方法涉及使用反射,但由于反射元数据对应用程序二进制大小的影响,因此存在可能删除反射元数据的担忧。 另一种考虑的方法是使用宏,但这些可能不适用于较旧的操作系统版本,从而带来兼容性挑战。
该对话强调了与处理重复的枚举案例名称相关的编译器错误,该错误使枚举实例的唯一性变得复杂。
核心需求是为某些枚举案例的所有实例导出一个稳定的 hashValue,无论它们的关联值如何,旨在将具有相同案例名称但不同关联值的实例视为相同的存储目的。 然而,传统的 Hashable 实现不足以实现此目的。
一个探索的想法涉及利用 String(describing:) 生成枚举案例的字符串表示形式,但如果客户端为枚举实现 CustomStringConvertible,则这种方法可能会失败。 人们有兴趣了解如何调用枚举描述的默认 Swift 标准库实现,以解决 CustomStringConvertible 的客户端实现所产生的问题。
内容概括
讨论围绕着 ~Copyable 类型中不存在不可用的 deinit 以及它对程序员构建代码以与本地数据流分析保持一致的依赖展开。
不可破坏类型的概念旨在增强本地数据流分析并提供编译时保证。 它类似于函数的想法,从技术上讲,函数承诺返回一些东西,但实际上却没有,而编译器静态地证明了理论上的不可能。
该提案引入了不可破坏类型(Destructible)作为取代Copyable 的新根类型。 它设想了类型不需要显式反初始化的场景,依赖编译器的静态分析来强制执行预期的清理例程。
讨论对比了使用和不使用此功能时 API 使用的难度,强调了需要显式清理时面临的潜在挑战。 对 API 文档、运行时检查和潜在风险的仔细研究与用于防止错误使用的编译时诊断进行了比较。
对话承认实现此功能的潜在复杂性以及收益是否值得付出努力的不确定性。 它引用了 Scott Meyers 关于使接口易于正确使用且难以错误使用的名言,强调了编程接口简单性和清晰性的重要性。
内容概括
Swift 社区受邀参加“Advent of Code”,这是一项从 12 月 1 日到 12 月 25 日举办的年度编码挑战赛。 这项挑战由 Eric Wastl 组织,涉及日常编码练习,开始时很简单,逐渐变得更具挑战性。
参与者可以使用任何编程语言,但有一个 Swift 团队模板可供那些喜欢 Swift 的人使用。 该模板提供了解决挑战的起点。
加入:
- 克隆 Swift 团队的入门模板(可选)。
- 在 Advent of Code 网站上创建一个帐户(参与排行榜所需)。
- 使用ID 3315857-f51d8ce5加入Swift社区排行榜。
我们鼓励参与者每天使用 Swift 尝试挑战。 排行榜跟踪完成时间,但分数只是为了好玩,可以忽略。
排行榜允许那些想要分享解决方案的人链接到 GitHub 帐户,为参与者提供了互相学习方法的机会。
这是一个社区活动,并不正式隶属于 Swift 项目,旨在整个 12 月享受乐趣、提高 Swift 技能并享受一些编码挑战。 参与者被警告,随着挑战变得更加严峻,挫败感可能会出现!
内容概括
Swift OpenAPI Generator 团队已发布版本 1.0.0-alpha.1,该版本作为即将发布的 1.0 版本的候选版本,预计将在大约两周内发布。 1.0 的主要重点是增强文档和示例,没有计划更改代码。
自 2023 年 5 月以 0.1.0 版本首次开源以来,Swift OpenAPI Generator 已经取得了实质性进展。 合并了 200 多个拉取请求,产生了 24 项更新并引入了重要的新功能。
主要亮点:
- 新功能包括对 Base64 编码数据的支持、文档过滤、递归类型支持、服务器 URL 模板变量支持以及具有类型安全和流式传输的多部分内容类型支持。
- 此外,生成代码的可自定义访问修饰符允许在公共、包(默认)和内部可见性之间进行选择。
- 该版本还包含各种改进和错误修复,例如将 Swift 5.9 更新为最低支持版本、错误处理增强以及生成的代码注释的细化。
重大变更和迁移:
- 该版本包括 API 反馈所必需的重大更改。 提供了将代码从版本 0.3.x 迁移到 1.0.0-alpha.1 的指南,详细说明了潜在的构建错误及其解决方案。
下一步是什么:
- 版本 1.0.0-alpha.1 作为候选版本,邀请反馈意见被考虑用于计划在两周内发布的最终 1.0.0 版本。 鼓励用户测试 alpha 版本以确保与其项目的兼容性。
该团队对贡献者表示感谢,并邀请通过 Swift OpenAPI Generator GitHub 存储库进一步参与。
内容概括
该对话探讨了 Swift Codable 协议在处理存在类型时的细微差别,特别是涉及 URL、Decimal 和 AnyEncodable 的可编码行为。
讨论解决了使用存在类型时期望与实际行为之间的差异。 值得注意的是,当抽象具有预期行为(例如,meow())的 Cat 等类型的实例时,预期 Cat 的所有实例都将统一表现出该行为。 当使用encode()时,内部表示(例如Decimal类型)会出现在最终的JSON字符串中,这会让人感到惊讶,从而导致方法分派和类型编码的混乱。
该演讲深入探讨了 Codable 的基础知识以及存储类型信息以进行解码的必要性。 出于安全性和互操作性原因,可编码省略了编码数据中的类型信息,因此需要在代码中预定义以进行解码。 这种方法允许解码不明确的值,但对类型擦除的值(如 AnyEncodable)带来了挑战,使得在解码期间难以对类型进行逆向工程。 如果解码时不知道类型,则不可能重建原始数据。
所讨论的警告方面围绕着未来可能需要解码的场景。 如果在不考虑未来解码要求的情况下做出编码决策,则可能会使数据检索变得复杂。
最后,讨论暗示了从枚举案例数组中收集枚举时的挑战和注意事项,强调了编码和解码策略的复杂性以及在设计导出或序列化工具时深思熟虑的重要性。
内容概括
本讨论围绕自动验证值更改的概念展开,旨在消除 CRUD 方法中出现的显式验证调用。 对话的重点是在 Swift 构造中实现自动验证的挑战。
该示例使用 Name 结构来探索拦截值访问以进行实时验证的潜在方法。 然而,诸如计算属性或属性观察器之类的现有机制缺乏对在验证过程中抛出错误的直接支持。 这一限制对在 Swift 结构中无缝实现自动验证造成了重大障碍。
这次对话强调了手动验证的必要性,即使是基本类型,因为从这些基本类型构建的复杂类型会产生复杂性。 例如,讨论介绍了 Employee 结构体,并说明了对其 addr1 和 addr2 属性的手动验证规则的需求,强调尽管基本类型具有验证机制,但手动验证在复杂类型级别至关重要。
尽管函数体宏被认为是另一种潜在的方法,但讨论主要集中在计算变量或动态查找功能是否可以支持自动验证,最终表达了对在 Swift 现有机制中实现它的可行性的怀疑。
提出了两种建议的“手动”方法:
- 使用 let 代替 var 字段,并在构造函数中加入验证逻辑,使其失败。
- 在外部执行验证,如果验证失败,则利用 didSet 恢复到之前的值。
这些手动方法旨在在更改期间同步强制验证,确保值保持一致。 但是,后一种方法可能会暂时使不变量无效,但可能适用于可接受同步验证的场景,例如避免由于暂时不正确的值导致的 UI 闪烁。
推荐博文
从预编译的角度理解 Swift 与 Objective-C 及混编机制
摘要: 这篇博客讨论了 Objective-C 的预编译工作机制和与 Xcode 相关的技术细节。Clang Module 提升了编译的健壮性和扩展性,而使用 hmap 技术可以提高编译效率。
Xcode Phases 构建系统中的不同类型代表不同的文件。使用 cocoapods-hmap-built 插件可以节省大型项目的编译时间。Clang Module 和 Swift Module 有相似的概念,而 Swift 与 Objective-C 混编有几种方法可选。利用 VFS 机制可以提升编译效率。
摘要: 这篇 Swift 博客介绍了在 SwiftUI 中使用 MapKit 的基础知识。最新版本的 SwiftUI 引入了新的 MapKit 集成 API ,提供了更全面的功能。
文章示例了如何使用 Marker 和 Annotation 类型在地图上放置标记和自定义视图。还介绍了控制地图初始位置和交互类型的方法。
该博客将在接下来的几周继续深入讨论相机操作、地图样式和用户位置跟踪等主题。
摘要: 这篇 Swift 博客介绍了计算机编程语言原理与源码实例中的 Swift 函数和闭包。文章首先介绍了 Swift 作为一种强类型、编译型、面向对象的编程语言的背景。
然后,详细讲解了函数和闭包的核心概念和联系,包括函数的定义、调用和返回值,以及闭包的定义、调用和返回值。
接下来,文章深入探讨了函数和闭包的算法原理,包括函数的接收输入参数、执行操作和返回输出结果的过程,以及闭包的类似过程。
最后,文章通过具体的代码实例展示了函数和闭包的使用方法,并讨论了它们未来的发展趋势和可能面临的挑战。附录部分回答了一些常见问题,帮助读者更好地理解 Swift 函数和闭包的概念和用法。
话题讨论
你是更能接受同性上司还是更能接受异性上司?
- 性别无关:不在意上司的性别,更关注他们的能力和领导风格。
- 同性上司:同性上司更容易理解自己的处境和需求。
- 异性上司:异性上司会带来不同的观点和经验。
- 不确定:没有明确的偏好,根据情况判断是否接受同性或异性上司。
欢迎在文末留言参与讨论。
关于我们
Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战、SwiftUl、Swift基础为核心的技术内容,也整理收集优秀的学习资料。
特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。
如何在 SwiftUI 中熟练使用 sensoryFeedback 修饰符
前言
SwiftUI 引入了新的 sensoryFeedback
视图修饰符,使我们能够在所有 Apple 平台上播放触觉反馈。下面我们将学习如何使用 sensoryFeedback
修饰符在应用程序中的不同操作中提供触觉反馈。
背景介绍
在 iOS 17 之前,如果你想要从 SwiftUI 视图中向用户提供触觉反馈,你会使用其中一个 UIKit(或 AppKit)的反馈生成器。例如,使用选择反馈生成器:
struct ListView: View {
@Binding var store: Store
let generator = UISelectionFeedbackGenerator()
var body: some View {
List(store.items, selection: $store.selection) { ... }
.onChange(of: store.selection) { oldValue, newValue in
if newValue != nil {
generator.selectionChanged()
}
}
}
}
在 iOS 17 中,Apple 直接向 SwiftUI 中添加了一系列感觉反馈的视图修饰符,以播放触觉和/或音频反馈。
平台支持
并非所有平台都支持所有反馈选项。以下是我所知道的每个平台上可用的内容列表。请注意,iPad不支持触觉反馈。
仅支持watchOS
- start:活动开始
- stop:活动停止
支持watchOS和iOS
- decrease:重要值减少到显着阈值以下
- increase:重要值增加到显着阈值以上
- selection:UI元素的值正在更改
- success:任务成功完成
- warning:任务产生警告
- error:任务产生错误
- impact:UI元素碰撞时的物理冲击
请注意,impact反馈有两个变体,让您指定元素碰撞的重量(轻,中,重)或灵活性(刚性,柔软,实心)。在这两种情况下,您还可以更改强度(默认为1.0):
// 默认impact反馈
.impact()
// 具有柔韧性并增加强度的impact
.impact(flexibility: .rigid, intensity: 2.0)
// 具有重量并增加强度的impact
.impact(weight: .heavy, intensity: 2.0)
基本用法
要在 SwiftUI 视图中播放触觉反馈,我们只需要使用 sensoryFeedback
视图修饰符,带有两个参数。第一个定义了反馈样式,第二个是触发器值。
struct ContentView: View {
@State private var store = Store()
var body: some View {
NavigationStack {
List(store.results, id: \.self) { result in
Text(result)
}
.searchable(text: $store.query)
.sensoryFeedback(.success, trigger: store.results)
}
}
}
在上面的示例中,我们使用 sensoryFeedback
视图修饰符,带有成功样式。我们还将存储的 results
属性定义为触发器。这意味着 SwiftUI 将在存储的结果更改时播放成功样式的触觉反馈。
预定义样式
SwiftUI 提供了许多预定义的反馈样式,如 success
、warning
、error
、selection
、increase
、decrease
、start
、stop
、alignment
、levelChange
、impact
等。
struct ContentView: View {
@State private var trigger = false
var body: some View {
NavigationStack {
Button("Action") {
// 进行某些操作
trigger.toggle()
}
.sensoryFeedback(
.impact(weight: .heavy, intensity: 0.9),
trigger: trigger
)
}
}
}
如上所示,impact
样式允许我们调整反馈的权重和强度。请记住,最好使用预定义的样式,并在超级自定义的情况下自定义触觉反馈。
根据触发器值选择样式
sensoryFeedback
视图修饰符的另一种变体允许我们根据触发器值选择特定的反馈样式。在这里,我们在存储包含结果时播放成功反馈,并在结果为空时播放错误反馈。
struct ContentView: View {
@State private var store = Store()
var body: some View {
NavigationStack {
List(store.results, id: \.self) { result in
Text(result)
}
.searchable(text: $store.query)
.sensoryFeedback(trigger: store.results) { oldValue, newValue in
return newValue.isEmpty ? .error : .success
}
}
}
}
SwiftUI 还提供了在触发器值上定义条件的选项,决定是否播放预定义的反馈样式。
使用场景
这些感觉反馈修饰符都是基于触发器的。触发器需要是可等同的类型。有三种感觉反馈视图修饰符的变体:
当值更改时触发
struct ListView: View {
@Binding var store: Store
var body: some View {
List(store.items, selection: $store.selection) { ... }
.sensoryFeedback(.selection, trigger: store.selection)
}
}
视图修饰符的第一个参数是 SensoryFeedback
类型。并非所有反馈类型都适用于所有平台。当触发器值更改时,反馈会播放。
使用条件闭包触发
如果要更灵活地控制何时触发反馈,请使用带有条件闭包版本的视图修饰符。例如,仅在选择更改为非空值时播放选择反馈:
.sensoryFeedback(.selection, trigger: store.selection) {
oldValue, newValue in
newValue != nil
}
条件闭包接收监视的触发器值的旧值和新值。在闭包中,返回一个布尔值,指示是否应播放反馈。
使用反馈闭包触发
要控制播放何种反馈,请使用视图修饰符的反馈闭包版本。例如,基于错误代码提供警告或错误反馈:
// @State private var errorCode: Int = 0
.sensoryFeedback(trigger: errorCode) { oldValue, newValue in
switch newValue {
case 1: .warning
case 2: .error
default: nil
}
}
在这种情况下,在闭包中返回所需的反馈,如果不想要任何反馈,则返回nil。
可以运行 Demo
提供一个可以运行的 Demo,完整代码如下:
import SwiftUI
struct ContentView: View {
@State private var store = Store()
var body: some View {
NavigationView {
List(store.results, id: \.self) { result in
Text(result)
}
.searchable(text: $store.query)
.sensoryFeedback(.success, trigger: store.results)
.navigationTitle("Sensory Feedback Demo")
}
}
}
struct Store {
@State var query: String = ""
@State var results: [String] = ["Result 1", "Result 2", "Result 3"]
}
Demo 包括一个带有触觉反馈的 SwiftUI 列表。你可以根据需要进行进一步的调整和扩展。
总结
SwiftUI引入了新的sensoryFeedback
视图修饰符,为所有Apple平台提供触觉反馈。通过简单的附加,我们可以定义反馈样式和触发器值,实现了在应用程序中不同操作产生的触觉效果。支持多种预定义样式,如success、warning、error,以及个性化的impact样式。对于触发器值的处理也非常灵活,可以根据其条件选择不同的反馈样式。
总体而言,这个新的视图修饰符为提高应用的可访问性和用户体验提供了简便的方式。在使用时需谨慎,避免过多干扰用户。希望通过学习这个新特性,开发者能更好地运用触觉反馈功能,提升应用的交互性。
在 SwiftUI 中实战应用 ContentUnavailableView
前言
SwiftUI 引入了新的 ContentUnavailableView
类型,允许我们在应用程序中展示空状态、错误状态或任何其他内容不可用的状态。本周,我们将学习如何使用 ContentUnavailableView
引导用户浏览应用程序中的空状态。
基本用法
让我们从展示 ContentUnavailableView
视图的基本用法开始。
struct ContentView: View {
let store: Store
var body: some View {
NavigationStack {
List(store.products, id: \.self) { product in
Text(verbatim: product)
}
.navigationTitle("Products")
.overlay {
if store.products.isEmpty {
ContentUnavailableView(
"Connection issue",
systemImage: "circle"
)
}
}
}
}
}
在上面的示例中,我们将 ContentUnavailableView
定义为产品列表的叠加层。每当产品列表为空时,我们使用带有标题和图像的 ContentUnavailableView
显示。ContentUnavailableView
的另一种变体还允许我们定义当前状态的描述文本。
自定义视图
struct ContentView: View {
let store: Store
var body: some View {
NavigationStack {
List(store.products, id: \.self) { product in
Text(verbatim: product)
}
.navigationTitle("Products")
.overlay {
if store.products.isEmpty {
ContentUnavailableView {
Label("Connection issue", systemImage: "wifi.slash")
} description: {
Text("Check your internet connection")
} actions: {
Button("Refresh") {
store.fetch()
}
}
}
}
}
}
}
ContentUnavailableView
还允许我们在描述文本下方显示操作按钮。因此,ContentUnavailableView
初始化程序的另一种变体允许我们使用 ViewBuilder
闭包定义视图的每个部分,从而完全自定义其外观和感觉。
搜索屏幕使用
struct ContentView: View {
@Bindable var store: Store
var body: some View {
NavigationStack {
List(store.products, id: \.self) { product in
Text(verbatim: product)
}
.navigationTitle("Products")
.overlay {
if store.products.isEmpty {
ContentUnavailableView.search
}
}
.searchable(text: $store.query)
}
}
}
在搜索屏幕显示搜索结果时,可以使用 ContentUnavailableView
类型的搜索功能。它由框架本地化,并遍历视图层次结构以找到搜索栏并提取其文本以显示在视图内。
手动提供查询
struct ContentView: View {
@Bindable var store: Store
var body: some View {
NavigationStack {
List(store.products, id: \.self) { product in
Text(verbatim: product)
}
.navigationTitle("Products")
.overlay {
if store.products.isEmpty {
ContentUnavailableView.search(text: store.query)
}
}
.searchable(text: $store.query)
}
}
}
你还可以通过使用 ContentUnavailableView
类型的搜索功能并提供单个参数来手动将查询输入描述中。
可运行 Demo
完整可以运行的 Demo 需要有相关的环境和依赖项,而代码片段中涉及到了一些 Store
和其他可能的模型或服务。由于代码片段中的 Store
类型未提供,我将使用一个简化版本的示例代码来创建一个简单的 SwiftUI Demo,以展示 ContentUnavailableView
的基本使用。
import SwiftUI
struct Product: Identifiable {
let id: UUID
let name: String
}
class ProductStore: ObservableObject {
@Published var products: [Product] = []
func fetchProducts() {
// Simulating product fetching
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.products = [Product(id: UUID(), name: "iPhone"), Product(id: UUID(), name: "iPad")]
}
}
}
struct ContentView: View {
@StateObject var store = ProductStore()
var body: some View {
NavigationView {
List(store.products) { product in
Text(product.name)
}
.navigationTitle("Products")
.overlay {
if store.products.isEmpty {
ContentUnavailableView(
"No Products",
systemImage: "exclamationmark.triangle"
)
}
}
.onAppear {
store.fetchProducts()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
上述代码中,我们创建了一个简单的 Product
结构体表示产品,以及一个 ProductStore
类作为存储产品的模拟服务。在 ContentView
中,我们使用 ContentUnavailableView
来处理产品为空的情况。
请确保在 Xcode 中创建一个新的 SwiftUI 项目,并将上述代码替换到主 ContentView 中,然后运行该项目。在项目的初始加载时,ContentUnavailableView
将显示“No Products”消息,几秒后模拟产品加载,之后产品列表将显示在主视图中。
总结
今天,我们学习了如何在 SwiftUI 中使用 ContentUnavailableView
类型以用户友好的方式显示空状态。通过这些简单而强大的功能,我们能够更好地引导用户,使他们能够理解应用程序的当前状态。 ContentUnavailableView
的灵活性和易用性为我们处理应用程序中的不可用状态提供了有力的工具。
如何在 SwiftUI 中熟练使用 visualEffect 修饰符
前言
在 WWDC 23 中,SwiftUI 引入了一个名为 visualEffect 的新视图修饰符。此修饰符允许我们通过访问特定视图的布局信息来附加一组可动画化的视觉效果。下面我们将学习如何在 SwiftUI 中使用新的 visualEffect 视图修饰符。
介绍 visualEffect
让我们从使用 visualEffect 视图修饰符的最简单示例开始。
struct ContentView: View {
var body: some View {
Text("Hello World!")
.visualEffect { initial, geometry in
initial.offset(geometry.size)
}
}
}
正如你在上面的示例中所看到的,我们定义了一个文本视图并附加了 visualEffect 视图修饰符。每当你附加 visualEffect 视图修饰符时,你应该指定效果闭包。这是你应用所有需要的效果的地方。
效果闭包为你提供了两个参数。第一个是附加到视图的效果集合的初始状态。它是 EmptyVisualEffect
类型的实例。我们使用此实例来附加额外的效果。第二个参数是包含视图的所有布局信息的 GeometryProxy
类型的实例,比如 frame、安全区域等。
什么是视觉效果?
视觉效果是可以改变视图的视觉外观但不影响其布局的任何东西。在 SwiftUI 框架的先前版本中,我们有视图修饰符,如缩放、偏移、模糊、对比度、饱和度、不透明度、旋转等。它们全部都是视觉效果,并且现在符合 VisualEffect 协议。你可以在 visualEffect 闭包中使用其中任何一个。
struct ContentView: View {
var body: some View {
Text("Hello World!")
.visualEffect { initial, geometry in
initial
.blur(radius: 8)
.opacity(0.9)
.scaleEffect(.init(width: 2, height: 2))
}
}
}
像 frame 和 padding 这样的东西不是视觉效果,你不能在 visualEffect 闭包中使用它们,因为它们修改了视图层次结构的布局。
visualEffect 修饰符视觉效果
visualEffect 视图修饰符是完成旧事物的新方法。我们可以使用旧视图修饰符修改视图的不透明度和偏移。如果你不需要布局信息,你可以继续使用它们。新方法的唯一区别是我们通过从 GeometryProxy 提供的布局信息计算视图的视觉效果的方式来限定视图的视觉效果。
visualEffect 视图修饰符支持可动画化的值。因此,你可以继续使用它根据视图在视图层次结构中的框架和边界来动画化视图的视觉外观。
struct ContentView: View {
@State private var isScaled = false
var body: some View {
VStack {
Button("Scale") {
isScaled.toggle()
}
Text("Hello World!")
.visualEffect { initial, geometry in
initial.scaleEffect(
CGSize(
width: isScaled ? 2 : 1,
height: isScaled ? 2 : 1
)
)
}
.animation(.smooth, value: isScaled)
}
}
}
完整的代码
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello World!")
.visualEffect { initial, geometry in
initial.offset(geometry.size)
}
}
}
struct ContentViewWithEffects: View {
var body: some View {
Text("Hello World!")
.visualEffect { initial, geometry in
initial
.blur(radius: 8)
.opacity(0.9)
.scaleEffect(.init(width: 2, height: 2))
}
}
}
struct ContentViewWithAnimation: View {
@State private var isScaled = false
var body: some View {
VStack {
Button("Scale") {
isScaled.toggle()
}
Text("Hello World!")
.visualEffect { initial, geometry in
initial.scaleEffect(
CGSize(
width: isScaled ? 2 : 1,
height: isScaled ? 2 : 1
)
)
}
.animation(.smooth, value: isScaled)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
ContentViewWithEffects()
ContentViewWithAnimation()
}
}
将上述代码放入 Swift 文件中,然后在 Xcode 中打开并运行,选择合适的模拟器。请注意,由于视觉效果和动画效果,最好在模拟器上查看效果。
总结
本文章介绍了在 SwiftUI 中引入的新视图修饰符 visualEffect
。该修饰符允许我们通过访问特定视图的布局信息来附加一组可动画的视觉效果。给出了一些使用 visualEffect
的简单示例,包括如何使用效果闭包以及如何应用一些常见的视觉效果(例如模糊、透明度、缩放)。
此外,还提到了 GeometryProxy
类型的使用,以及 visualEffect
对可动画值的支持,使得可以根据视图的帧和边界来动态调整视觉外观。
最后,指出了 visualEffect
修饰符在向后兼容性方面的注意事项,并建议在不需要布局信息的情况下继续使用传统的视图修饰符。
如何在 SwiftUI 中开发定制 MapKit 功能
介绍
在上一篇文章中,我们探讨了 SwiftUI 中新的 MapKit API 的基础知识。现在,让我们深入 MapKit API 的定制点,以便根据我们的需求定制地图呈现。
地图样式
新的 MapKit API 引入了 mapStyle
视图修饰符,使我们能够自定义地图上呈现的数据样式。
struct ContentView: View {
var body: some View {
Map {
// ...
}
.mapStyle(.imagery(elevation: .realistic))
}
}
在上面的示例中,我们使用了 mapStyle
视图修饰符,并使用了 imagery 样式和逼真的高程。imagery 样式的高程参数的另一个选项是 flat。
imagery-map
SwiftUI 为我们提供了一套预定义且可配置的地图样式。在前面的示例中,我们使用了一个称为 imagery 的样式。默认情况下,SwiftUI 框架使用标准样式。标准样式允许我们配置地图的高程、要包括或排除的兴趣点,以及是否需要显示交通信息。
struct ContentView: View {
var body: some View {
Map {
// ...
}
.mapStyle(
.standard(
elevation: .flat,
pointsOfInterest: .excluding([.store]),
showsTraffic: false
)
)
}
}
另一个选项是混合样式,允许在地图上显示影像、道路和道路名称。混合样式还配置了高程、交通和兴趣点。
struct ContentView: View {
var body: some View {
Map {
// ...
}
.mapStyle(
.hybrid(
elevation: .flat,
pointsOfInterest: .including([.airport]),
showsTraffic: true
)
)
}
}
地图交互
MapKit 支持与地图的不同类型交互,包括缩放、平移、倾斜和旋转地图上的内容。默认情况下,SwiftUI 激活所有可用手势,但你可以轻松将可用交互限制为首选交互的列表。
struct ContentView: View {
var body: some View {
Map(interactionModes: [.pan, .pitch]) {
// ...
}
}
}
地图控件
每当将 MapKit 与 SwiftUI 一起导入时,你将获得可用作地图控件的特定 SwiftUI 视图。这些视图包括 MapScaleView
、MapCompass
、MapPitchToggle
、MapUserLocationButton
和 MapZoomStepper
视图。
struct ContentView: View {
var body: some View {
Map {
// ...
}
.mapControls {
MapScaleView()
MapCompass()
}
}
}
你可以将这些视图与 mapControls
视图修饰符一起使用,为在 SwiftUI 视图层次结构中共享相同环境的任何地图实例指定控件。当你将 MapScaleView
或 MapCompass
视图放在 mapControls
视图修饰符内时,SwiftUI 会处理控件的放置,具体取决于运行应用的平台。
这些地图控件是简单的 SwiftUI 视图,这意味着你可以在 mapControls
视图修饰符之外的任何位置使用它们。在这种情况下,要将地图控件绑定到特定的地图实例,你应该使用 mapScope
视图修饰符。
struct MapScopeExample: View {
@Namespace private var favoritesMap
var body: some View {
VStack {
Map(scope: favoritesMap) {
// 收藏的标记
}
HStack {
MapScaleView(scope: favoritesMap)
MapCompass(scope: favoritesMap)
}
}
.mapScope(favoritesMap)
}
}
如上例所示,我们使用 Namespace
属性包装器生成一个地图标识符,将控件绑定到地图实例。当你需要更改自动可见性配置为始终可见或隐藏时,还可以使用 mapControlVisibility
视图修饰符。
struct MapScopeExample: View {
@Namespace private var favoritesMap
var body: some View {
VStack {
Map(scope: favoritesMap) {
// 收藏的标记
}
HStack {
MapScaleView(scope: favoritesMap)
MapCompass(scope: favoritesMap)
.mapControlVisibility(.hidden)
}
}
.mapScope(favoritesMap)
}
}
总结
本文介绍了 SwiftUI 中 MapKit API 的定制功能。首先,通过 mapStyle 视图修饰符,我们学习了如何定制地图的呈现样式,包括 imagery 样式的高程设置。其次,我们了解了预定义和可配置的地图样式,例如 standard 样式允许配置地图的高程、感兴趣点和是否显示交通信息,而 hybrid 样式则允许同时显示影像、道路和道路名称。
我们深入了解了 SwiftUI 中 MapKit 的强大功能,包括定制地图样式、交互方式和控件,为开发者提供了更多灵活性和可定制性的选择。
在 SwiftUI 中的作用域动画
前言
从一开始,动画就是 SwiftUI 最强大的功能之一。你可以在 SwiftUI 中快速构建流畅的动画。唯一的缺点是每当我们需要运行多步动画或将动画范围限定到视图层次结构的特定部分时,我们如何控制动画。
简单示例
让我们从一个简单的示例开始,展示我们旧方法的一些缺点,这些方法用于在 SwiftUI 中驱动动画。
struct ContentView: View {
@State private var isHidden = false
var body: some View {
VStack {
Button("Animate") {
isHidden.toggle()
}
HugeView()
.opacity(isHidden ? 0.0 : 1.0)
AnotherHugeView()
}
.animation(.default)
}
}
如上例所示,我们有一个包含按钮和两个视图的视图层次结构,这些视图放置在垂直堆栈中。我们将动画视图修饰符附加到整个堆栈,以动画堆栈内的任何更改。
当我们按下按钮时,堆栈会动画显示内部的任何更改。但是,动画视图修饰符不连接到 isHidden 属性,这意味着它将动画显示可能发生的任何更改。其中一些更改可能是意外的,比如环境值的变化。
动画视图修饰符
我们可以通过使用动画视图修饰符的另一个版本来消除意外动画,在这个版本中,我们可以绑定到特定值,并且仅在值更改时进行动画处理。
struct ContentView: View {
@State private var isHidden = false
var body: some View {
VStack {
Button("Animate") {
isHidden.toggle()
}
HugeView()
.opacity(isHidden ? 0.0 : 1.0)
AnotherHugeView()
}
.animation(.default, value: isHidden)
}
}
在上面的示例中,我们使用了带有 value 参数的动画视图修饰符。它允许我们将动画范围限定为单个值,并仅在与特定值相关的更改时执行动画。在这种情况下,我们没有任何意外的动画。
使用多个可动画属性
如果我们有多个可动画属性怎么办?
在这种情况下,我们必须为每个可动画属性附加一个动画修饰符。这个解决方案非常有效,但在人体工程学方面有一个缺点。
struct ContentView: View {
@State private var firstStep = false
@State private var secondStep = false
var body: some View {
VStack {
Button("Animate") {
Task {
firstStep.toggle()
try? await Task.sleep(nanoseconds: 3_000_000_000)
secondStep.toggle()
}
}
// 其他视图在这里
SomeView()
.opacity(firstStep ? 1.0 : 0.0)
.blur(radius: secondStep ? 0 : 20.0)
}
.animation(.default, value: firstStep)
.animation(.default, value: secondStep)
}
}
幸运的是,SwiftUI 引入了动画视图修饰符的一个新变体,允许我们使用 ViewBuilder 闭包来限定动画的范围。
struct ContentView: View {
@State private var firstStep = false
@State private var secondStep = false
var body: some View {
VStack {
Button("Animate") {
Task {
firstStep.toggle()
try? await Task.sleep(nanoseconds: 1_000_000_000)
secondStep.toggle()
}
}
// 其他视图在这里
SomeView()
.animation(.default) { content in
content
.opacity(firstStep ? 1.0 : 0.0)
.blur(radius: secondStep ? 0 : 20.0)
}
}
}
}
如上例所示,我们使用动画视图修饰符,提供我们需要的动画类型和一个 ViewBuilder 闭包,在这个动画中应用。动画仅在提供的 ViewBuilder 闭包的上下文中工作,不会扩展到其他任何地方。
使用 ViewBuilder
作为起点,ViewBuilder 闭包提供一个参数,用于占位视图,在其中应用了动画视图修饰符。在 ViewBuilder 闭包内部,可以安全地对视图应用任何视图修饰符,并期望仅对此代码块进行动画处理。
struct ContentView: View {
@State private var firstStep = false
@State private var secondStep = false
var body: some View {
VStack {
Button("Animate") {
Task {
firstStep.toggle()
try? await Task.sleep(nanoseconds: 1_000_000_000)
secondStep.toggle()
}
}
// 其他视图在这里
SomeView()
.transaction { t in
t.animation = t.animation?.speed(2)
} body: { content in
content
.opacity(firstStep ? 1.0 : 0.0)
.blur(radius: secondStep ? 0 : 20.0)
}
}
}
}
正如你所看到的,SwiftUI 提供了一种类似的方法,以在视图层次结构中维护有作用域的事务。
总结
这篇文章介绍了在SwiftUI中构建动画的新方法,重点解决了在多步动画或特定视图层次结构中控制动画的挑战。通过引入带有value
参数的动画修饰符,以及使用ViewBuilder
闭包限定动画范围,作者展示了更精确和灵活的动画控制方式。
这种方法在处理多个可动画属性时尤其强大。文章还提到了SwiftUI引入的一项新变体,使用ViewBuilder
闭包可在动画中应用视图修饰符,有效地将动画范围限定在特定的上下文中。
最后,介绍了在 SwiftUI 中构建有作用域的事务的新方法,以维护更具精确性和可控性的动画。这些新功能在最新的平台上可用,为SwiftUI开发者提供了更强大的动画工具。
在 SwiftUI 中实战使用 MapKit API
前言
SwiftUI 与 MapKit 的集成在今年发生了重大变化。在之前的 SwiftUI 版本中,我们将 MKMapView 的基本功能封装到名为 Map 的 SwiftUI 视图中。幸运的是,事情发生了变化,SwiftUI 引入了与 MapKit 集成的新 API。本篇文章我们将学习如何在 SwiftUI 的最新版本中使用可用的新功能丰富的 API 与 MapKit 集成。
正如我之前所说,在 SwiftUI 框架的早期版本中,我们有一个 Map 视图,为我们提供了 MapKit 的基本功能,该功能现在已被弃用。在面向较早 Apple 平台版本的情况下,仍然使用已弃用的 Map 视图是有意义的。
新 MapKit API 的引入
新的 MapKit API 引入了 MapContentBuilder 结果构建器,它看起来类似于 ViewBuilder,但是使用符合 MapContent 协议的类型。让我们从使用 SwiftUI 中最新迭代中提供的新 MapKit API 集成的基本示例开始。
import MapKit
import SwiftUI
extension CLLocationCoordinate2D {
static let newYork: Self = .init(
latitude: 40.730610,
longitude: -73.935242
)
static let seattle: Self = .init(
latitude: 47.608013,
longitude: -122.335167
)
static let sanFrancisco: Self = .init(
latitude: 37.733795,
longitude: -122.446747
)
}
struct ContentView: View {
var body: some View {
Map {
Annotation("Seattle", coordinate: .seattle) {
Image(systemName: "mappin")
.foregroundStyle(.black)
.padding()
.background(.red)
.clipShape(Circle())
}
Marker(coordinate: .newYork) {
Label("New York", systemImage: "mappin")
}
Marker("San Francisco", monogram: Text("SF"), coordinate: .sanFrancisco)
}
}
}
正如你在上面的示例中看到的,我们通过使用 MapContentBuilder 闭包定义地图,并在其上放置内容。MapContentBuilder 类型与符合 MapContent 协议的任何类型一起使用。
在我们的示例中,我们使用了 Marker 和 Annotation 类型。Marker 是一个基本项,允许我们在地图上放置预定义的标记。Annotation 类型更先进,将使我们能够使用纬度和经度在地图上放置 SwiftUI 视图。
SwiftUI 为我们提供了许多符合 MapContent 协议的类型。我们已经使用了其中的两个:Marker 和 Annotation。其中许多包括 MapCircle、MapPolygon、MapPolyline、UserAnnotation 等。
struct ContentView: View {
var body: some View {
Map {
Annotation("Seattle", coordinate: .seattle) {
Image(systemName: "mappin")
.foregroundStyle(.black)
.padding()
.background(.red)
.clipShape(Circle())
}
Marker(coordinate: .newYork) {
Label("New York", systemImage: "mappin")
}
UserAnnotation()
}
}
}
控制初始地图位置
你可以通过使用 Map 初始化器的另一个重载来控制地图的初始位置,该初始化器提供 initialPosition 参数。
struct ContentView: View {
let initialPosition: MapCameraPosition = .userLocation(
fallback: .camera(
MapCamera(centerCoordinate: .newYork, distance: 0)
)
)
var body: some View {
Map(initialPosition: initialPosition) {
Annotation("Seattle", coordinate: .seattle) {
Image(systemName: "mappin")
.foregroundStyle(.black)
.padding()
.background(.red)
.clipShape(Circle())
}
Marker(coordinate: .newYork) {
Label("New York", systemImage: "mappin")
}
Marker("San Francisco", monogram: Text("SF"), coordinate: .sanFrancisco)
}
}
}
initialPosition 参数接受 MapCameraPosition 类型的实例。MapCameraPosition 允许我们以几种方式定义地图位置。它可以是我们在示例中使用的用户位置,或者你可以使用 camera、region、rect 或 item 等静态函数将其指向地图上的任何区域。默认情况下,它使用 MapCameraPosition 类型的自动实例,该类型适合地图内容。
相机位置的双向绑定
每当你需要对相机位置有恒定的控制时,你可以使用 Map 初始化器的另一个重载,允许你提供与地图相机位置的双向绑定。
struct ContentView: View {
@State private var position: MapCameraPosition = .userLocation(
fallback: .camera(
MapCamera(centerCoordinate: .newYork, distance: 0)
)
)
var body: some View {
Map(position: $position) {
// ...
}
}
}
SwiftUI 在用户拖动地图时更新位置绑定。它还在你以编程方式更新 position 属性时立即更新地图相机位置。
struct ContentView: View {
@State private var position: MapCameraPosition = .userLocation(
fallback: .camera(
MapCamera(centerCoordinate: .newYork, distance: 0)
)
)
var body: some View {
Map(position: $position, interactionModes: .pitch) {
// ...
}
}
}
通过使用 interactionModes 参数,你可以控制与地图允许的交互类型。MapInteractionModes 类型定义了一组交互,如平移、俯仰、旋转和缩放。默认情况下,它启用所有可用的交互类型。
总结
今天,我们学习了在 SwiftUI 中集成 MapKit 的基础知识。在接下来的几周里,我们将继续讨论相机操作、地图控件和其他高级主题。希望你喜欢这篇文章。
Swift 周报 第四十二期
前言
本期是 Swift 编辑组整理周报的第四十二期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。
Swift 周报在 GitHub 开源,欢迎提交 issue,投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。
最热烈的火焰,封锁在最沉默的火山深处。最朴实纯真的智慧,就浅藏在Swift社区里!
周报精选
新闻和社区:苹果 CEO 库克透露接班计划,希望继任者来自公司内部
提案:Typed throws 提案正在审查
Swift 论坛:讨论 MainActor 上的上下文切换和线程数
推荐博文:SwiftUI 中的作用域动画
话题讨论:
那个活在记忆中的帅气少年,已慢慢变成了大叔模样。岁月无情呀,那么各位程序猿和攻城狮们,你们心中最担心的容貌变化是哪一个呢?
上期话题结果
这个结果反映了员工在工作和生活平衡方面的个体差异。一些人更注重通勤时间的利用效率,而另一些人则更注重在自己的房子中获得更大的舒适感和生活空间。这对公司提供灵活的工作安排和住房福利可能有一定的启示。
新闻和社区
苹果 CEO 库克透露接班计划,希望继任者来自公司内部
11 月 21 日消息,63 岁的苹果公司首席执行官蒂姆库克近日透露,苹果已经为他的继任者做好了 " 非常详细 " 的接班计划,但他也表示,他目前还没有离开苹果的打算。
在 BBC Sounds 播客《Dua Lipa: At Your Service》的一次 45 分钟的采访中,库克向歌手 Dua Lipa 坦承,他不知道自己还会在苹果待多久。" 我爱这里," 他说,回顾了自己在苹果的 25 年," 我无法想象没有苹果的生活,所以我还会在这里一段时间。" 但是,当被问及苹果是否有任何 CEO 接班计划时,库克称:" 我们是一家相信制定接班计划的公司,所以我们有非常详细的接班计划。因为总会发生一些不可预测的事情。我明天可能会走错路边,希望不会发生这种事,我祈祷不会。"
Dua Lipa 问道:" 你能说出谁是接班人吗?" 库克回答称," 我不能说,但我想说的是,我的工作是找到几个有能力接班的人,我真的希望下一任首席执行官是来自苹果内部的人。所以这是我的角色:让董事会有几个人可以选择。"
在这次采访中,库克讲述了自己作为苹果 CEO 的一天,分享了他在阿拉巴马州一个蓝领家庭长大的经历,以及最终成为苹果 CEO。(文章来源:IT 之家)
消息称苹果自研 5G 调制解调器开发再“难产”,将推迟到 2026 年
IT之家 11 月 17 日消息,彭博社的马克・古尔曼(Mark Gurman)发布最新一期 Power On 时事通讯,表示苹果的自研 5G 调制解调器计划遇到麻烦。
IT之家注:苹果公司于 2019 年收购了英特尔大部分智能手机业务,并开始认真开发自己的调制解调器硬件,但开发过程并不顺利。
苹果公司原本计划 2024 年推出自研 5G 调制解调器芯片,并率先装备在 iPhone SE 机型上,但随后有消息称延后到 2025 年。
古尔曼在最新时事通讯中表示,苹果计划再次延后推出自研 5G 调制解调器芯片时间,目前已经推迟到 2025 年年底或者 2026 年年初。
古尔曼在文章中透露,苹果的自研 5G 调制解调器芯片目前还处于早期阶段,可能落后竞争对手“数年”时间。
消息称苹果目前自研的 5G 调制解调器芯片并不支持 mmWave 技术,目前主要存在 2 个难题:第一是英特尔遗留代码,需要苹果重写,而添加新功能可能会中断现有功能;第二是开发芯片过程中,要小心绕过不侵犯高通的专利。
一位苹果员工表示:“我们接手了英特尔的一个失败项目,我们盲目自信地认为可以成功”。据说苹果的硬件技术部门在众多项目中“捉襟见肘”,各项资源没有向其倾斜,导致难以解决错误。
提案
正在审查的提案
SE-0413 Typed throws 提案正在审查。
Swift 的错误处理模型允许标记为 throws 的函数和闭包指示它们可以通过引发错误来退出。错误值本身始终被类型擦除为 any Error
。这种方法鼓励以通用方式处理错误,并且对于大多数代码来说仍然是一个很好的默认选项。然而,有一些情况下类型擦除是不幸的,因为它不允许在可能且有必要处理所有错误的狭窄位置进行更精确的错误类型化,或者在类型擦除的成本很高的情况下。
该提案引入了指定函数和闭包只能引发特定具体类型错误的能力。
Swift论坛
内容概括
该提案基于 SE-0380,引入了“then”关键字来处理 if 或 switch 表达式中的多个语句,从而促进更清晰的语法并提高可读性。 “then”关键字允许这些表达式每个分支有多个语句,从而简化了以前需要立即执行闭包或显式键入的场景。 此外,它还引入了“do”表达式,使代码结构更加清晰,并处理 API 需要价值创建和后续突变的情况。
该提案概述了详细设计,引入“then”作为上下文关键字,指定其在 if、switch 和 do 表达式中的用法。 它强调了解析歧义和可能的替代方案,探索诸如在 Swift 中使用最后一个表达式或受 Rust 启发的分号终止等变体,同时讨论它们对代码可读性和语言设计的影响。
总体而言,该提案旨在增强 Swift 的表达能力而不影响 ABI 稳定性,并邀请讨论引入的“then”关键字的替代方案和潜在的解析复杂性。
介绍
该提案引入了 then 关键字,用于确定单个分支中包含多个语句的 if 或 switch 表达式的值。 它还介绍了 do 表达式。
动机
SE-0380 引入了使用 if 和 switch 语句作为表达式的功能。 正如该提案所述,这可以大大改进语法,例如在初始化变量时:
let width = switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default: 4
}
否则需要诸如立即执行闭包或显式类型确定初始化之类的技术。 然而,该提案将让 switch 分支包含多个语句的能力作为未来的方向:
let width = switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default:
log("this is unexpected, investigate this")
4 // error: Non-expression branch of 'switch' expression may only end with a 'throw'
}
当需要这样的分支时,当前用户必须退回到旧技术。 该提案引入了一个新的上下文关键字,它允许 switch 保留为表达式:
let width = switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default:
log("this is unexpected, investigate this")
then 4
}
then 可以类似地用于允许 if 表达式中的多语句分支。 该关键字的引入还使得独立的 do 表达式更加可行。 它们有两个用例:
- 要从 do/catch 块的成功路径和失败路径生成值:
let foo: String = do {
try bar()
} catch {
"Error \(error)"
}
- 当使用单个表达式无法轻松完成变量初始化时,能够初始化变量:
let icon: IconImage = do {
let image = NSImage(
systemSymbolName: "something",
accessibilityDescription: nil)!
let preferredColor = NSColor(named: "AccentColor")!
then IconImage(
image,
isSymbol: true,
isBackgroundSupressed: true,
preferredColor: preferredColor.cgColor)
}
虽然上面的内容可以组成一个表达式,但声明单独的变量然后使用它们会更清晰。
在其他情况下,这是无法完成的,因为 API 的结构要求您首先创建一个值,然后更改其中的一部分:
let motionManager: CMMotionManager = {
let manager = CMMotionManager()
manager.deviceMotionUpdateInterval = 0.05
return manager
}()
这种立即执行的闭包模式在 Swift 代码中很常见。 以至于在某些情况下,用户认为即使是单个表达式也必须包含在闭包中。 do 表达式将提供更清晰的习惯用法来对这些进行分组。
内容概括
讨论围绕着通过启用借用和输入输出模式匹配来增强 Swift 的模式匹配、允许在不复制或消耗值的情况下进行值匹配以及在模式匹配期间启用枚举的就地突变来增强 Swift 的模式匹配。 主要设计问题包括:
- 新的绑定模式:引入“借用 x”和“inout x”分别作为借用和变异模式绑定的语法。 这些将允许借用或改变部分匹配值而不消耗它。
- 模式的所有权行为:分析 Swift 中的各种模式类型以了解其所有权含义。 诸如绑定、通配符、元组、枚举、可选展开、布尔值、动态转换和表达式模式之类的模式将根据其所有权行为进行评估。
- 确定模式匹配所有权:探索确定模式匹配的所有权行为的方法。 聚合模式(元组和枚举)遵循其组件之间最严格的所有权行为:借用、变异或消费。
- 确定开关的效果:讨论如何通过句法标记或从应用模式推断所有权来确定开关对其主题的总体效果。 有人建议使用“&”标记来改变模式匹配。
- 条件中的所有权控制:考虑“if let”和“if case”构造中借用和 inout 模式绑定的含义。 这些新的绑定形式可用于可选展开,并且其行为类似于根据其所有权要求切换主题。
总体而言,我们的目标是在 Swift 中引入更细致的模式匹配,允许在不消耗值的情况下进行借用和变异,并探索这些增强功能在各种语言结构(如 switch 语句和条件)中的含义。
问题
理论上,SPM 是一个普通的 swift 包,您可以将其(使用工具链附带的 SPM)构建为普通的 swift 包。但 swift-package-manager 存储库没有最新的 semver 标签,它使用“工具链”标记方案(swift-5.9.1-RELEASE)。 如何依赖 SPM 作为library?
回答
截至目前,libSwiftPM 尚未维护可以遵循语义版本控制的稳定 API。 您使用自己的 libSwiftPM 构建的软件包将从当前的 Swift 安装中提取 PackageDescription 模块,这可能与您使用的 libSwiftPM 版本不兼容。 这种不兼容性将表现为用于传递包清单和插件信息的不同序列化格式(本身是私有 API),这将导致模糊且难以诊断的错误。
作为以前维护过基于 libSwiftPM 构建的 CLI 工具,现在维护 SwiftPM 本身的人,我建议不要将其添加为依赖项。 它不适合在一起版本化并随 Swift 工具链分发的工具集之外使用。
如果您需要一个可以在包上操作的 CLI 界面,请改用 SwiftPM 命令插件,它们确实提供了稳定的 API。
提问
我正在观看 Swift 并发:幕后我了解到,作为使用 Swift 并发的开发人员,我们不应该违反不阻塞线程的运行时契约。 看来 Swift 的目标是运行与设备中 CPU 核心数量一样多的线程。 然而,会议结束时提出的一个观点引起了一些混乱。 演讲者提到,当我们调用 MainActor 的方法时,会发生上下文切换,因为主线程与协作池中的线程是分开的。 这引发了几个问题:
1、协作池中有多少个线程?
2、如果不包括主线程,这是否意味着实际的协作线程数是 numberOfCoresInDevice - 1?
3、为什么主线程不是协作池的一部分?
我的假设是,这可能是出于优化目的,允许主线程专注于 UI 任务; 否则,任何线程的任何继续都可以在挂起后在主线程上恢复。
4、这里是否违反了运行时契约:当我们将上下文切换到主线程时,我们当前的线程应该被阻塞?
5、或者这个合约只针对我们,开发者,系统可以随意违反吗?
无论如何,看起来在这种情况下我们有一个线程被阻塞。
也许,这个问题将作为前三个问题的答案得到回答,但无论如何:为什么主线程不能像协作池中的线程一样工作? 只是接收必须在主线程上执行的延续? 这将解决上下文切换问题。
回答
主线程主要通过 NSRunLoop 进行管理,因为它的存在时间比 Swift 存在的时间要长得多,更不用说 Swift 并发了。 当在默认模式下不可重入运行时,主调度队列由主运行循环提供服务。 在 Swift Concurrency 中,主要参与者的执行者负责将工作分派到该队列上,就像常规参与者的执行者(默认执行者)将工作分派到协作队列上一样,如您链接的文章中所述
但并非所有进程都有主线程; 它主要是一个与 UI 相关的概念,像守护进程这样的非 UI 进程不需要它。
内容概述
讨论围绕使用 Swift 宏增强对枚举的关键路径支持,特别是引入“案例关键路径”以更好地处理枚举案例。
-
@CasePathable
宏:该宏为枚举案例生成实际的关键路径,称为“案例关键路径”。 这些关键路径提供动态案例查找功能,并且可以与常规关键路径类似地使用。 - 使用示例:
@CasePathable
宏允许实现各种功能:
- 通过下标访问枚举案例。
- 使用
callAsFunction
嵌入新的有效负载。 - 简化枚举案例检查和有效负载提取。
- 利用 SwiftUI 绑定的大小写键路径,启用基于枚举大小写的导航和表单控件使用。
- 使用大小写键路径组合应用程序功能,在构建和组合不同的应用程序功能时特别有用。
- 对库的影响:
SwiftUINavigation
和Composable Architecture
等库已更新,以合并案例键路径,使用 Swift 键路径语法增强其功能、结构和可组合性。
提供的示例和案例研究旨在展示案例关键路径的多功能性和实用性,强调它们在简化代码、增强 SwiftUI 绑定、组合应用程序功能等方面的潜力。 希望展示这些用例将鼓励将案例关键路径纳入语言中,并激发进一步的创新应用程序。
案例研究:SwiftUI Bindings
大小写键路径使从枚举而不是一堆独立选项驱动 SwiftUI 导航成为可能。 例如,如果一个视图可以导航到两个不同的、互斥的功能,那么最好像这样建模:
struct FeatureView: View {
@State var destination: Destination?
enum Destination {
case activity(ActivityModel)
case settings(SettingsModel)
}
…
}
但构建对 Destination 枚举的每种情况的绑定可能很困难,以便您可以使用 sheet(item:)
、popover(item:)
(以及更多)视图修饰符。
但是如果你的枚举用 @CasePathable
注释
@CasePathable
enum Destination {
// ...
}
然后我们可以利用绑定上的“动态大小写查找”,允许它们通过点链语法转换为 SwiftUI 现有视图修饰符所期望的形状:
.sheet(item: self.$destination.activity) { model in
ActivityView(model: model)
}
.popover(item: self.$destination.settings) { model in
SettingsView(model: model)
}
还可以使用 String 或 Bool 来驱动表单控件,例如 TextFields 和 Toggles,否则这些控件将被困在枚举案例中:
@CasePathable
enum Status {
case inStock(quantity: Int)
case outOfStock(isOnBackOrder: Bool)
}
@Binding var status: Status
switch self.item.status {
case .inStock:
$status.inStock.map { $quantity in
Section {
Stepper("Quantity: \(quantity)", value: $quantity)
Button("Mark as sold out") {
status = .outOfStock(isOnBackOrder: false)
}
} header: { Text("In stock") }
}
case .outOfStock:
$status.outOfStock.map { $isOnBackOrder in
Section {
Toggle("Is on back order?", isOn: $isOnBackOrder)
Button("Is back in stock!") {
status = .inStock(quantity: 1)
}
} header: { Text("Out of stock") }
}
}
如果您想尝试其中任何一个,我们的 SwiftUINavigation
库已更新,可以在使用 CaseKeyPath
进行绑定时定义动态成员查找。
案例研究:Composing App Features
近 4 年前我们开发案例路径的主要推动力是我们的可组合架构库,它提供了一种定义功能并将它们组合在一起的结构化方法。 功能使用枚举来枚举应用程序中所有可能的用户操作,并且这些枚举嵌套在父/子域层中,并且需要案例路径来编写可以将这些功能抽象地粘合在一起的代码。
我们还更新了该库以使用案例键路径,这允许人们通过使用简单且熟悉的键路径语法隔离子状态和操作来将功能组合在一起:
Reduce { state, action in
// ...
}
-.ifLet(\.child, action: /Action.child) {
+.ifLet(\.child, action: \.child) {
ChildFeature()
}
这使我们能够利用本机键路径给我们带来的所有好处,例如 Xcode 自动完成和类型推断。
推荐博文
摘要: 本文介绍了利用页面多模态信息在UI测试领域的探索与实践经验。针对意图信息识别问题,我们利用图像+文本+渲染布局属性信息探索出了一种交互意图簇识别模型,验证了基于自注意力的多模态方向可行性。
此模型可以识别出渲染树元素多维度的意图属性信息,同时利用聚类算法将节点聚成交互意图簇,可以为后续的任务提供结构化决策信息。在标注数据较少的情况下仍体现了较好的准确率以及泛化能力。后续计划通过扩大数据集、加强预训练等方式继续提升模型识别的精度。
摘要: 文章介绍了在 SwiftUI 中使用作用域动画的新方法。首先,我们回顾了以前在 SwiftUI 中处理动画的方式,并指出了其中的一些缺点。随后,我们展示了如何使用带有 value 参数的 animation 视图修饰符来限定动画范围,以及如何处理多个可动画属性的情况。
接着,我们介绍了 SwiftUI 中引入的 animation 视图修饰符的新变体,允许我们使用 ViewBuilder 闭包来限定动画范围。最后,我们还提到了在视图层次结构中维护作用域事务的方法。这些新方法为我们在 SwiftUI 中创建精确且有限范围的动画提供了更灵活的选择。
摘要: 本文讨论了在 Swift 中使用线程调度和 Actors 时的执行机制。Actors 可以确保代码在特定线程上执行,如主线程或后台线程,并帮助同步访问可变状态以防止数据竞争。
然而,开发人员常常误解 Actors 在非异步上下文中的线程调度,这是为了避免意外崩溃而至关重要的。作者建议在深入研究调度的具体细节之前,先阅读他的两篇文章:《Actors in Swift: how to use and prevent data races》和《MainActor usage in Swift explained to dispatch to the main thread》,因为它们会向您介绍 Actors 的概念。在本文中,探讨了调用带有任何 actor 属性标记的方法的影响。
在异步上下文中,文章讨论了使用 Actors 时的线程调度。通常情况下,您可能会在异步环境中使用 Actors 。如果您的调用代码访问带有 actor 属性的方法,您必须使用任务(task)或采用相同的全局 actor 。文章提供了相关的示例代码,并说明了编译器如何防止在非异步上下文中调度到 actor 线程。
话题讨论
那个活在记忆中的帅气少年,已慢慢变成了大叔模样。岁月无情呀,那么各位程序猿和攻城狮们,你们心中最担心的容貌变化是哪一个呢?
- 最担心越吃越肥胖,横向发展。
- 最担心逐渐变厚的高度镜片。
- 最担心青丝若雪,白发横生。
- 最担心秀发稀疏,日渐秃然。
欢迎在文末留言参与讨论。
关于我们
Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战、SwiftUl、Swift基础为核心的技术内容,也整理收集优秀的学习资料。
特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。