我们开始使用SwiftUI构建一个Swift Talk应用程序。
我们通过构建一个简单的货币转换器来试验SwiftUI。今天,我们想进一步采用SwiftUI并开始研究Swift Talk应用程序。我们将通过将我们在过去几集中看到的各种组件(例如小型网络库和Resource
包装器)整合在一起来获得基本设置和运行。
小编这里推荐一个群:691040931 里面有大量的书籍和面试资料哦有技术的来闲聊 没技术的来学习
添加包
我们创建一个新的Xcode项目,通过Xcode的File菜单,我们通过输入存储库URL并选择master分支来添加微型网络包。
我们还添加了一个包,它提供了两个库,代码可以由我们的后端和前端共享。第一个,Model
包含两个 Codable
结构。
另一个库ViewHelpers
包含日期格式化程序。在服务器和客户端之间共享此代码非常有用,因为它可确保在网站和应用程序中呈现日期和持续时间的一致方式。
在ContentView
文件中,我们现在可以导入三个模块TinyNetworking
, Model
和ViewHelpers
。
除了Swift软件包之外,还有一些我们需要的东西。我们将Resource
上周的文件复制到项目中,以及一些包含剧集和剧集集合的静态JSON数据。
收藏清单
我们现在要做的第一件事就是加载集合。我们创建一个文件Model.swift
,在那里我们可以组织我们的数据。在那里,我们导入Model
模块,它给我们两种类型:CollectionView
和EpisodeView
。
这些类型的名称并不完美,它们在服务器上下文中更有意义,因为它们实际上是对我们数据的JSON视图,而不是UI视图。也许我们可以在将来重命名它们,但现在让我们继续它。
我们使用微型网络库来定义Endpoint
可以加载剧集的方法:
import Foundation
import SwiftUI
import TinyNetworking
import Model
let allCollections = Endpoint<[CollectionView]>(json: .get, url: URL(string: "https://talk.objc.io/collections.json")!)
现在ContentView
,我们可以创建一个Resource
收集集合端点。我们必须创建资源ObjectBinding
- 如果我们忘记这样做,我们将不会在资源更改时获得任何视图更新。
在正文视图中,如果已加载,我们会在列表中显示集合,否则我们会显示描述加载状态的文本:
import SwiftUI
import TinyNetworking
import Model
import ViewHelpers
struct ContentView : View {
@ObjectBinding var collections = Resource(endpoint: allCollections)
var body: some View {
Group {
if collections.value == nil {
Text("Loading...")
} else {
List {
ForEach(collections.value!) { coll in
Text(coll.title)
}
}
}
}
}
}
这还没有编译,因为我们collections
尚未确定。上一次,我们通过提供数组元素上的标识属性的关键路径来实现。但是,我们实际上可以遵循 CollectionView
的Identifiable
协议,它,因为它有一个可哈希箱的id
性能:
extension CollectionView: Identifiable {}
我们运行应用程序,我们看到一个收集标题列表。
下一步是将列表视图拉出到一个单独的文件中。我们添加一个包含集合数组的简单属性:
import SwiftUI
import Model
import ViewHelpers
struct CollectionsList : View {
let collections: [CollectionView]
var body: some View {
List {
ForEach(collections) { coll in
Text(coll.title)
}
}
}
}
在ContentView
,我们就可以通过加载收藏到CollectionsList
:
struct ContentView : View {
@ObjectBinding var collections = Resource(endpoint: allCollections)
var body: some View {
Group {
if collections.value == nil {
Text("Loading...")
} else {
CollectionsList(collections: collections.value!)
}
}
}
}
我们收到编译器错误,因为CollectionsList
视图的预览 缺少collections参数。如果我们对预览进行评论,我们会看到其余的代码都进行了编译,并且CollectionsList
工作也是如此。为了使预览工作,我们需要传递一些静态数据。
为此,我们将JSON文件添加到项目中。我们编写了一个简单的函数,它从bundle中加载一个具有给定名称的JSON文件,并将其内容解码为泛型类型。我们可以强制解包任何可选项,因为此函数仅用于加载示例数据,在这种情况下崩溃无关紧要:
func sample<A: Codable>(name: String) -> A {
let url = Bundle.main.url(forResource: name, withExtension: "json")!
let data = try! Data(contentsOf: url)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
return try! decoder.decode(A.self, from: data)
}
然后我们加载样本集以进行预览 CollectionsList
:
#if DEBUG
struct CollectionsList_Previews : PreviewProvider {
static var previews: some View {
CollectionsList(collections: sample(name: "collections"))
}
}
#endif
我们打开画布,看到样本集被加载到列表视图中。
预览工作非常方便快速设置列表视图的样式。我们将标题包装在a中VStack
,然后我们Text
为标题添加另一个标题,以显示集合中的剧集数量。然后我们将其VStack
与前沿对齐,然后 设置文本标签的样式。
最后,我们将集合的总持续时间添加到标题中,使用共享格式ViewHelpers
:
struct CollectionsList : View {
let collections: [CollectionView]
var body: some View {
List {
ForEach(collections) { coll in
VStack(alignment: .leading) {
Text(coll.title)
Text("\(coll.episodes_count) episodes ᐧ \(TimeInterval(coll.total_duration).hoursAndMinutes)")
.font(.caption)
.color(.gray)
}
}
}
}
}
在某些时候,我们想要从列表中显示集合的详细视图,但在我们开始之前,我们必须设置导航视图。我们在列表中添加了一个导航标题修饰符,我们将列表的预览包装在一个NavigationView
中,以便查看导航栏和标题:
struct CollectionsList : View {
let collections: [CollectionView]
var body: some View {
List {
// ...
}.navigationBarTitle(Text("Collections"))
}
}
#if DEBUG
struct CollectionsList_Previews : PreviewProvider {
static var previews: some View {
NavigationView {
CollectionsList(collections: sample("collections"))
}
}
}
#endif
我们还需要在导航视图中包含集合列表ContentView
,以便在运行应用程序时查看标题:
struct ContentView : View {
@ObjectBinding var collections = Resource(endpoint: allCollections)
var body: some View {
Group {
if collections.value == nil {
Text("Loading...")
} else {
NavigationView {
CollectionsList(collections: collections.value!)
}
}
}
}
}
收集细节
在导航视图到位后,我们可以轻松地为单个集合添加详细视图。在那里,我们可以通过创建另一个Endpoint
包装的艺术品来加载该集合的艺术作品Resource
。
我们为我们创建一个新的SwiftUI视图文件,CollectionDetails
然后导入我们需要的模块:
import SwiftUI
import TinyNetworking
import Model
我们为CollectionView
这个视图中的呈现定义了一个属性,并且我们添加了一个VStack
包含大字体集合标题的标签。我们还调用lineLimit
修饰符 nil
以获得多行标签:
struct CollectionDetails : View {
let collection: CollectionView
var body: some View {
VStack {
Text(collection.title)
.font(.largeTitle)
.lineLimit(nil)
Text(collection.description)
}
}
}
在集合列表中,我们将每个列表项包装在一个以CollectionsDetails
视图为目标的导航按钮中:
struct CollectionsList : View {
let collections: [CollectionView]
var body: some View {
List {
ForEach(collections) { coll in
NavigationButton(destination: CollectionDetails(collection: coll)) {
VStack(alignment: .leading) {
Text(coll.title)
Text("\(coll.episodes_count) episodes ᐧ \(TimeInterval(coll.total_duration).hoursAndMinutes)")
.font(.caption)
.color(.gray)
}
}
}
}.navigationBarTitle(Text("Collections"))
}
}
和之前的集合列表一样,我们现在需要提供样本数据以预览集合详细信息视图。将sample
函数移动到我们的模型文件中,并将样本集合拉出到全局变量,并在列表和详细信息视图的预览中使用此变量是有意义的:
let allCollections = Endpoint<[CollectionView]>(json: .get, url: URL(string: "https://talk.objc.io/collections.json")!)
let sampleCollections: [CollectionView] = sample(name: "collections")
func sample<A: Codable>(name: String) -> A {
// ... }
#if DEBUG
struct CollectionsList_Previews : PreviewProvider {
static var previews: some View {
NavigationView {
CollectionsList(collections: sampleCollections)
}
}
}
#endif
#if DEBUG
struct CollectionDetails_Previews : PreviewProvider {
static var previews: some View {
CollectionDetails(collection: sampleCollections[0])
}
}
#endif
我们在模拟器中运行应用程序,我们尝试通过选择其中一个集合来打开详细视图。然后我们注意到集合的描述标签仍然仅限于显示单行文本。我们添加lineLimit(nil)
修饰符来修复:
struct CollectionDetails : View {
let collection: CollectionView
var body: some View {
VStack {
Text(collection.title)
.font(.largeTitle)
.lineLimit(nil)
Text(collection.description)
.lineLimit(nil)
}
}
}
但这对预览没有影响。这似乎是一个错误,因为在模拟器中,标签现在正确地跨越所需的行数。
加载艺术品
要加载集合的图稿,我们将图像资源添加到集合详细信息视图中。
struct CollectionDetails : View {
let collection: CollectionView
let image: Resource<UIImage>
// ... }
然后我们定义一个初始化器,我们可以在其中从collection参数构造该资源。我们首先创建一个Endpoint
带有集合的图片URL,以及一个解析闭包,它UIImage
从加载的数据构造一个:
struct ImageError: Error {}
struct CollectionDetails : View {
let collection: CollectionView
let image: Resource<UIImage>
init(collection: CollectionView) {
self.collection = collection
let endpoint = Endpoint<UIImage>(.get, url: collection.artwork.png, expectedStatusCode: expected200to300) { data in
guard let d = data, let i = UIImage(data: d) else {
return .failure(ImageError())
}
return .success(i)
}
self.image = Resource(endpoint: endpoint)
}
// ... }
这些代码大部分是可重用的,所以我们稍后会把它拉出来以便能够加载其他图像。
现在我们可以包含图像,如果它已加载。我们必须制作图像resizable
并将其纵横比设置为.fit
,因为我们希望图像可以缩放并适合视图。如果我们不使用任何修饰符,那么SwiftUI将始终使用图像的原始大小。
我们现在记得图像资源需要 ObjectBinding
。没有它,代码仍然可以编译,一切似乎都有效,但是在加载图像时,视图永远不会被重新渲染:
@ObjectBinding var image: Resource<UIImage>
加载图像在预览中不起作用,因此我们必须在模拟器中运行应用程序来测试它。在那里,我们看到该系列的艺术作品出现在详细视图中。
网友评论