如何在 SwiftUI 中使用 Identifiable 以及注意事项

前言

SwiftUI 中使用 Identifiable 协议可以让你自定义的实例对象具有唯一性。该协议需要实现一个任何可哈希类型的单个 ID 属性,这使其成为适用于所有类型实例的灵活协议。

虽然它是一个相对简单的协议,但一些可能的边缘情况可能会导致 SwiftUI 代码中出现意想不到的错误。因此,了解如何正确使用协议非常重要。

自定义对象实现 Identifiable 协议

假设我们有一个需求:实现一个展示博客标题的列表。首先,我们需要定义一个 Blog 的数据结构来表示博客:

swift 复制代码
class Blog {
    var title: String
    var blogURL: URL
    
    init(title: String, blogURL: URL) {
        self.title = title
        self.blogURL = blogURL
    }
}

定义完数据结构之后,我们需要创建数据源,最后将数据源在页面中展示出来:

scss 复制代码
struct ContentView: View {
    let blogs = [Blog(title: "blog1", blogURL: URL(string: "http://xxx.com")!)]
    
    var body: some View {
        VStack {
            Text("Blogs")
            ForEach(blogs) { blog in
                Text(blog.title)
            }
        }
    }
}

写完上述代码你会发现代码是运行不了的,编译报错如下: Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Blog' conform to 'Identifiable'

当你在 ForEach 元素内使用不可唯一标识的对象集合时,就会出现这个构建错误。SwiftUI 需要每个对象都有一个唯一标识,否则它将无法确定在集合更改后是否需要重绘视图。换句话说,添加对象的唯一标识会影响你的视图更新行为。 我们可以通过给 Blog 遵守 Identifiable 来扩展一个 id 属性来改正这个错误:

dart 复制代码
extension Blog: Identifiable {
    var id: String {
        return blogURL.absoluteString
    }
}

乍一看,这段代码似乎还可以,因为我们可以成功编译并运行我们的项目。但是,如果我们遇到两篇具有相同 URL 的文章,结果输出就会变得出乎意料:

php 复制代码
struct ContentView: View {
    let blogs = [Blog(title: "blog1", blogURL: URL(string: "http://xxx.com")!), Blog(title: "blog2", blogURL: URL(string: "http://xxx.com")!)]
    
    var body: some View {
        VStack {
            Text("Blogs")
            ForEach(blogs) { blog in
                Text(blog.title)
            }
        }
    }
}

代码运行结果如下:

虽然两篇文章标题不同,一个为 blog1 一个为 blog2,但两篇文章的 URL 是相同的。结果视图显示相同的标题两次,因为系统认为两篇文章的唯一标识是相同的。这说明了为什么应该小心地向对象添加唯一标识,并考虑在所有情况下使用的 ID 是否唯一。

我们可以通过给 Blog 再添加一个属性来解决这个问题:

swift 复制代码
class Blog {
    var title: String
    var blogURL: URL
    var postID = UUID()
    
    
    init(title: String, blogURL: URL) {
        self.title = title
        self.blogURL = blogURL
    }
}

extension Blog: Identifiable {
    var id: UUID {
        return postID
    }
}

借助类的默认实现

当然,你可能已经注意到,你不需要向遵守 Identifiable 的类添加任何 id 属性代码也是可以正常运行的,因为 Identifiable 为类提供了默认实现来保证对象的唯一性,但该保证仅存在对象的生命周期内。

默认的 ID 类型将是 ObjectIdentifier,这是类实例或元类型的唯一标识符。示例代码如下:

swift 复制代码
class Blog: Identifiable {
    var title: String
    var blogURL: URL
    
    init(title: String, blogURL: URL) {
        self.title = title
        self.blogURL = blogURL
    }
}

打印出默认实现如下所示:

scss 复制代码
print(blog.id)
// ObjectIdentifier(0x0000600000c74d50)

这个 ID 的值是对象指针,所以如上文所说:它只保证在对象的生命周期内是唯一的。

相关推荐
东坡肘子1 天前
谁是 AI-relay App 的真正威胁 -- 肘子的 Swift 周报 #158
人工智能·swiftui·swift
东坡肘子8 天前
当 Build in Public 重新转入地下 -- 肘子的 Swift 周报 #157
人工智能·swiftui·swift
OliverQiu9 天前
开源一个 macOS 小工具:集中管理 ChatGPT 的网络启动配置
macos·chatgpt·swiftui·mac·proxy·codex·dmg
传奇开心果编程12 天前
【SwiftUI提高练中学】第9课 安全与隐私
swiftui·移动开发·swift·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程13 天前
【声明式UI开发实用技术学与练】第8课 状态提升与下放
学习·flutter·react native·ui·swiftui·composer
东坡肘子13 天前
Swift Server,又多了一个 Google -- 肘子的 Swift 周报 #156
人工智能·swiftui·swift
传奇开心果编程14 天前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程15 天前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui
传奇开心果编程15 天前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
tink15 天前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode