SwiftUI使用SwiftUI开发一个APP

使用SwiftUI开发一个APP - 详情页

2021-08-27  本文已影响0人  LazyGunner

前面已经完成了从首页跳转到搜索页面,接下来要做的是进入详情页。既然我们要使用swiftUI,那么就不能不使用其预览的功能,所以我在写详情页的时候,首先先通过预览来完成页面的布局。

1. 详情页数据结构

首先我们要看一下详情页用到的数据结构

    @Environment(\.colorScheme) var colorScheme
    var resource: Resource // 1
    @StateObject var resourceDetail: ResourceDetailViewModel = ResourceDetailViewModel() // 2
  1. Resource模型是我们一开始就定义过的,且需要从ResourceListView跳转过来时带过来的。这里我们也可以看一下,如何在Navigation页面跳转的时候带参数过去。
NavigationLink(destination:ResourceDetailView(resource: resource))

其实,很简单,只要在NavigationLink中destination参数的对象初始化的时候,初始化该参数就可以了。

  1. 因为详情页中还有其他需要的数据,所以这里新定义一个数据模型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))
    }
}

通过预览数据,我们可以通过预览直接看到页面结构

image

这里会有一个图片预览的问题,因为图片是用的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
    }
}
  1. 这里需要给resourceDetail 初始化一个空的默认值,否则在ResourceListView页面初始化ResourceDetailView时需要初始化这个变量,很麻烦。

  2. 因为前面初始化了一个空的resourceDetail,所以在使用的时候需要先判断一下是否为空,否则会崩溃

  3. 在onAppear的时候调用网络请求,获取资源的下载链接。

  4. 设置navigationBarTitle的内容和展示模式

  5. List嵌套的时候,如果内部层级的List不设置高度,是无法显示的。

看一下真机效果:

image
上一篇下一篇

猜你喜欢

热点阅读