使用SwiftUI开发一个APP - 详情页
前面已经完成了从首页跳转到搜索页面,接下来要做的是进入详情页。既然我们要使用swiftUI,那么就不能不使用其预览的功能,所以我在写详情页的时候,首先先通过预览来完成页面的布局。
1. 详情页数据结构
首先我们要看一下详情页用到的数据结构
@Environment(\.colorScheme) var colorScheme
var resource: Resource // 1
@StateObject var resourceDetail: ResourceDetailViewModel = ResourceDetailViewModel() // 2
- Resource模型是我们一开始就定义过的,且需要从ResourceListView跳转过来时带过来的。这里我们也可以看一下,如何在Navigation页面跳转的时候带参数过去。
NavigationLink(destination:ResourceDetailView(resource: resource))
其实,很简单,只要在NavigationLink中destination参数的对象初始化的时候,初始化该参数就可以了。
- 因为详情页中还有其他需要的数据,所以这里新定义一个数据模型ResourceDetailViewModel,用来通过接口获取到资源相关的下载链接。
// ResourceDetailViewModel.swift
// FoloPro
//
// Created by GUNNER on 2021/8/24.
//
import Foundation
import Foundation
import Combine
class ResourceDetailViewModel: ObservableObject {
@Published var resourceDetail: ResourceDetail!
var cancellationToken: AnyCancellable?
init(_ resourceDetail: ResourceDetail? = nil) {
if resourceDetail == nil {
getResourceDetail("")
} else {
self.resourceDetail = resourceDetail
}
} // 2.1
}
extension ResourceDetailViewModel {
// Subscriber implementation
func getResourceDetail(_ resourceId: String) {
if (resourceId == "") {
return
}
let queryItems = []
cancellationToken = Folo.detailRequest(.resourceDetail, queryItems, resourceId) // 2.2
.mapError({ (error) -> Error in
print(error)
return error
})
.sink(receiveCompletion: { _ in },
receiveValue: {
self.resourceDetail = $0.data
})
}
}
2.1. 这里的处理主要是为了后续预览提供方便,当构造函数包含了resourceDetail就不通过网络请求获取,而是直接将resourceDetail参数进行赋值
2.2. 因为资源详情的接口路由格式与之前不同,所以这里新加了一个方法,用来针对这种请求。这里发现自己实现的网络请求已经日益无法满足需求,后续将整体更换为三方网络请求库。
2. 准备预览数据
数据模型定义完,就要看一下预览的时候如何模拟这些数据了
struct ResourceDetailView_Previews: PreviewProvider {
static var resource = Resource(
resourceId: "123", area: "美国", category: "动作", channel: "tv", cnName: "迷失", content: "123", enName: "Lost", playStatus: "测试数据", poster: "https://cdn.bagli.me/cdn/yy_41556.jpg-small", posterA: "", posterB: "", posterM: "", posterS: "", premiere: "", remark: "", createTime: 1628414182, updateTime: 1628414182, season: 2, episode: 5, score: 9.9, views: 8899)
static var resourceEpisodes = [
ResourceEpisode(episode: 1, format: "MP4", link: "magnet:?xt=urn:btih:EA776AF5BCDA63875F536246166E4CEDAEDC2D8A&dn=Mare.of.Easttown.S01E07.720p.FIX%e5%ad%97%e5%b9%95%e4%be%a0&tr=http%3a%2f%2ft.nyaatracker.com%2fannounce&tr=http%3a%2f%2fshare.camoe.cn%3a8080%2fannounce&tr=http%3a%2f%2ftracker.kamigami.org%3a2710%2fannounce"),
ResourceEpisode(episode: 2, format: "MP4", link: "magnet:?xt=urn:btih:EA776AF5BCDA63875F536246166E4CEDAEDC2D8A&dn=Mare.of.Easttown.S01E07.720p.FIX%e5%ad%97%e5%b9%95%e4%be%a0&tr=http%3a%2f%2ft.nyaatracker.com%2fannounce&tr=http%3a%2f%2fshare.camoe.cn%3a8080%2fannounce&tr=http%3a%2f%2ftracker.kamigami.org%3a2710%2fannounce"),
]
static var resourceSeasons = [
ResourceSeason(season: 2, episodeList: resourceEpisodes),
ResourceSeason(season: 1, episodeList: resourceEpisodes)
]
static var resourceDetail = ResourceDetail(
resource: resource, resourceSeasonList: resourceSeasons
)
static var previews: some View {
ResourceDetailView(resource: resource, resourceDetail: ResourceDetailViewModel(resourceDetail))
}
}
通过预览数据,我们可以通过预览直接看到页面结构

这里会有一个图片预览的问题,因为图片是用的URL方式加载的,所以静态预览是无法展示的,需要进行preview。之前查资料还是看到过某位大佬的文章,讲到如何通过mock来实现预览和测试。
https://objectpartners.com/2020/07/09/structuring-swiftui-previews-for-api-calls/
3 详情页布局
前面说了那么多,还是要看一下详情页的布局代码
struct EpisodeView: View {
var episodeList: [ResourceEpisode]
var body: some View {
List(episodeList) { resourceEpisode in
HStack {
Text(String(resourceEpisode.episode))
Text(resourceEpisode.link)
.lineLimit(1)
.frame(maxWidth: 300)
}
}.frame(height: 100) // 5
}
}
struct ResourceDetailView: View {
@Environment(\.colorScheme) var colorScheme
var resource: Resource
@StateObject var resourceDetail: ResourceDetailViewModel = ResourceDetailViewModel() // 1
var body: some View {
VStack {
Spacer()
HStack(alignment:.top) {
AsyncImage(url: URL(string: resource.poster)!,
placeholder: { Text("Loading ...") },
image: {
Image(uiImage: $0).resizable()
})
.scaledToFit()
.frame(width: 156, height: 240)
VStack (alignment: .leading) {
Text(resource.cnName)
.font(.headline)
.fontWeight(.bold)
Text(resource.enName)
.font(.headline)
.fontWeight(.bold)
Text(resource.playStatus)
.padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0))
HStack {
colorScheme == .light ?
Image("tv1").resizable().frame(width: 18, height: 18):
Image("tv").resizable().frame(width: 18, height: 18)
Text(String(format: "S%d E%d", resource.season, resource.episode))
}.padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0))
Text(getTimeString(resource.updateTime)).padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0))
}.frame(maxHeight: 200, alignment: .topLeading)
.padding(EdgeInsets(top: 10, leading: 10, bottom: 0, trailing: 10))
}
Text(resource.content)
.frame(maxWidth: 300)
if (resourceDetail.resourceDetail != nil && resourceDetail.resourceDetail.resourceSeasonList.count > 0) {
List(resourceDetail.resourceDetail.resourceSeasonList) { resourceDetail in
VStack {
Text("第" + String(resourceDetail.season) + "季")
EpisodeView(episodeList: resourceDetail.episodeList)
}
}
} // 2
}.onAppear {
resourceDetail.getResourceDetail(resource.resourceId) // 3
}.navigationBarTitle("详情", displayMode: .inline) // 4
}
}
-
这里需要给resourceDetail 初始化一个空的默认值,否则在ResourceListView页面初始化ResourceDetailView时需要初始化这个变量,很麻烦。
-
因为前面初始化了一个空的resourceDetail,所以在使用的时候需要先判断一下是否为空,否则会崩溃
-
在onAppear的时候调用网络请求,获取资源的下载链接。
-
设置navigationBarTitle的内容和展示模式
-
List嵌套的时候,如果内部层级的List不设置高度,是无法显示的。
看一下真机效果:
