当前位置: 首页 > news >正文

SwiftUI 实战:构建一个复杂的图书首页长页面

一、引言

在日常开发中,我们经常会遇到需要构建多个内容模块纵向排列的首页,尤其是图书类、视频类、资讯类 App,这种页面结构尤为常见。相比于简洁的登录页或设置页,这类长页面涉及滚动嵌套、模块拆分、布局一致性、性能优化等多个方面,对开发者提出了更高的要求。

本篇文章将以 iPhone 自带的「图书」App 为参考,带你一步步用 SwiftUI 实现一个结构清晰、组件化良好、交互自然的首页布局

我们的页面结构包括:

  • 顶部导航栏 + 右上角的「我的」按钮
  • 「继续阅读」模块(横向滑动)
  • 「之前读过」模块(网格或列表)
  • 「阅读目标」模块(包含进度统计)

虽然功能不复杂,但它很好地代表了一个典型的长页面结构。通过这个实战例子,我们将一起探讨 SwiftUI 在应对复杂页面时的布局技巧、组件划分策略以及实际编码方式。

二、页面整体结构:PHHomeView

在图书类 App 中,首页往往需要承载多个信息区域,并支持垂直滚动浏览。在 SwiftUI 中,这种长页面非常适合用 ScrollView 加上 VStack 的组合来搭建结构化内容。

我们将内容稍微进行一下简化,只保留 “继续阅读” 和 “之前读过” 还有 “阅读目标”。

下面是我们首页的主视图 PHHomeView 的代码结构节选:

struct PHHomeView: View {@ObservedObject var presenter = PHHomePresenter.sharedvar body: some View {ScrollView {VStack(alignment: .leading, spacing: 20) {Spacer(minLength: 20)// 继续阅读区域if let readingEpisode = presenter.readingEpisode {PHHomeContinueReadView(episode: readingEpisode)}// 之前读过区域if let recentlyViewedEpisodes = presenter.recentlyViewedEpisodes,!recentlyViewedEpisodes.isEmpty {PHHomeHistoryView(recentlyViewedEpisodes: recentlyViewedEpisodes)}// 阅读目标区域PHHomeGoalView().padding(.top, 24)Spacer(minLength: 40)}}.background(Color.gray.opacity(0.2)).toolbar {ToolbarItem(placement: .topBarTrailing) {Button {// 点击“我的”} label: {Image(systemName: "person.circle").foregroundColor(.black)}}}.navigationTitle("首页").onAppear {presenter.fetchReadingEpisode()presenter.fetchRecentlyViewedEpisodes()}}
}

📌 核心结构说明:

  1. 整体使用 ScrollView + VStack 实现长页面滚动:所有区域按顺序依次排列,形成一个流畅的滚动体验。
  2. 数据驱动视图内容是否显示:「继续阅读」与「之前读过」模块都基于 presenter 的数据条件渲染。
  3. 模块隔离良好,每个区域都是独立 View:比如 PHHomeContinueReadView、PHHomeHistoryView、PHHomeGoalView,便于复用与维护。
  4. 导航栏按钮与标题通过 .toolbar 和 .navigationTitle 设置:“我的”按钮使用系统图标,放在右上角。
  5. 页面数据在 .onAppear 时加载:保证首页每次展示时都能拿到最新内容。

三、继续阅读区域:PHHomeContinueReadView

这个模块是一个非常典型的 SwiftUI 区块式布局,由一个标题文本和一个自定义卡片组件组成。整体采用 VStack实现垂直排列,间距、边距控制都比较标准,适合初中级页面构建参考。

📄 代码如下:

struct PHHomeContinueReadView: View {var episode: PHEpisodeEntityvar body: some View {VStack(alignment: .leading, spacing: 10) {Text("继续阅读").font(.title2).fontWeight(.semibold).padding(.horizontal)PHHistoryCardView(episode: episode).padding(.horizontal, 24)}}
}

四、之前读过区域:PHHomeHistoryView

「之前读过」模块的结构与「继续阅读」类似,也是由标题加上一个滑动区域组成,区别在于这里使用了 ScrollView(.horizontal) 搭配 HStack,用于展示多个横向排列的卡片组件。

📄 对应代码如下:

struct PHHomeHistoryView: View {var recentlyViewedEpisodes: [PHEpisodeEntity]var body: some View {VStack(alignment: .leading, spacing: 10) {Text("之前读过").font(.title2).fontWeight(.semibold).padding(.horizontal)ScrollView(.horizontal, showsIndicators: false) {HStack(spacing: 12) {ForEach(recentlyViewedEpisodes, id: \.id) { episode inPHHistoryCardView(episode: episode)}}.padding(.horizontal, 24)}}}
}

🧱 布局解析:

  • 最外层依然是 VStack,结构一致,方便模块化管理;
  • 标题使用 .title2 + semibold 样式,与上一模块保持统一;
  • ScrollView(.horizontal) 实现横向滑动区域,并关闭默认滚动指示器;
  • 内容区域使用 HStack 横向排列所有卡片,每张卡片由 PHHistoryCardView 渲染;
  • 外部 .padding(.horizontal, 24) 保证整体边距一致,与上一模块的卡片对齐。

五、复用组件:PHHistoryCardView 卡片结构解析

在前面两个模块「继续阅读」和「之前读过」中,我们都使用了同一个组件 PHHistoryCardView 来展示单个剧集的信息。这种组件复用的方式可以大大提升开发效率,并保持 UI 风格一致。

下面是 PHHistoryCardView 的完整实现:

struct PHHistoryCardView: View {var episode: PHEpisodeEntityvar body: some View {HStack(spacing: 12) {// 封面图if let packageId = episode.packageId,let fileName = episode.episodeCoverFileName {Image(uiImage: PHFileManger.loadImage(packageId: packageId, imageName: fileName) ?? UIImage()).resizable().aspectRatio(599.0 / 337.0, contentMode: .fill).frame(width: 100, height: 56).clipped().cornerRadius(6)}// 剧集信息文本VStack(alignment: .leading, spacing: 4) {Text(episode.drama?.title ?? "未知剧名").font(.subheadline).foregroundColor(.white).lineLimit(1)Text(episode.title ?? "第 X 集").font(.caption).foregroundColor(.white.opacity(0.8)).lineLimit(1)Text("共 \(episode.cardList?.count ?? 0) 张卡片").font(.caption2).foregroundColor(.white.opacity(0.6))}Spacer()}.padding(.horizontal, 12).frame(height: 80).background(randomColor).cornerRadius(8)}var randomColor: Color {let colors: [Color] = [.red, .green, .blue]return colors.randomElement()!.opacity(0.3)}
}

🧱 布局结构说明:

  • 整体为 HStack:左侧封面图 + 中间文字信息 + 右侧空白占位 Spacer();
  • 封面图尺寸为 100 × 56,保持 16:9 比例,使用 .resizable() 和 .aspectRatio() 控制缩放行为;
  • 文字信息区使用 VStack 垂直排列三行文本,每行文字样式略有区分,强调主次;
  • 背景颜色为红/绿/蓝三选一,带透明度 0.3,当前主要用于调试或展示状态区别,后续可替换为统一主题色;
  • 外部加圆角 + 水平内边距,让卡片在页面中更加紧凑且不贴边。

这个组件设计追求简单、紧凑、信息清晰,适合在横向滑动视图中频繁出现。通过 PHHistoryCardView(episode:)这样的结构,我们可以在多个模块中共享样式逻辑,只需要传入数据即可灵活复用。

六、阅读目标区域:PHHomeGoalView

首页的尾部我们加入了一个带有进度环和统计信息的视图组件 —— PHHomeGoalView。这个模块相较前两个更为视觉化,包含一个弧形进度圈、文字信息和按钮,布局略复杂一些,但仍然可以通过 SwiftUI 的组合方式轻松实现。

📄 对应代码如下:

struct PHHomeGoalView: View {var body: some View {VStack(alignment: .center, spacing: 10) {Text("阅读目标").font(.title2).fontWeight(.semibold).padding(.horizontal)Text("坚持每天阅读,积累知识和灵感").font(.subheadline).foregroundColor(.gray).padding(.horizontal).padding(.bottom, 10)ZStack {let circleSize: CGFloat = 200// 背景圆Circle().stroke(Color.gray.opacity(0.2), lineWidth: 10).frame(width: circleSize, height: circleSize)// 弧形进度(目前写死 50%)Path { path inlet center = CGPoint(x: circleSize / 2, y: circleSize / 2)let radius = circleSize / 2let startAngle = Angle(degrees: 90)let endAngle = Angle(degrees: 90 + 360 * 0.5)path.addArc(center: center,radius: radius,startAngle: startAngle,endAngle: endAngle,clockwise: false)}.stroke(Color.blue, lineWidth: 10).frame(width: circleSize, height: circleSize)// 中间文字VStack {Text("今日阅读").font(.headline).foregroundColor(.blue)Text("0:00").font(.largeTitle).fontWeight(.bold).foregroundColor(.blue)Text("(阅读目标10分钟)").font(.subheadline).foregroundColor(.gray)}}.padding(.bottom, 20)// 开始阅读按钮Button(action: {print("开始阅读")}) {Text("开始阅读").font(.headline).padding().frame(maxWidth: .infinity).background(Color.blue).foregroundColor(.white).cornerRadius(8)}}.padding().background(Color.white)}
}

🧱 布局结构说明:

  • 外层使用 VStack 垂直排列标题、副标题、图形区域和按钮;
  • 视觉核心是 ZStack,用于绘制背景圆形 + 蓝色进度弧线 + 居中文字;
  • 弧线绘制使用 Path 手动构造半圆(起始角度 90° 向后延展 180°),当前进度写死为 50%,可后续接入动态进度;
  • 文字排布在弧形中央,大小层次清晰,强调阅读时长;
  • 最下方的“开始阅读”按钮占满整行,使用 .frame(maxWidth: .infinity) 与 .cornerRadius 实现标准按钮样式;
  • 整个组件嵌套在白色背景 + 内边距中,保持与其他模块视觉一致。

这个模块展示了 SwiftUI 中 Path 与 ZStack 的组合能力,也验证了即使不借助 Core Graphics 或第三方库,也能轻松绘制出可定制的图形视图。

七、结语

本文通过 SwiftUI 分模块构建了一个图书类 App 的首页页面,页面内容参考了 iPhone 自带的图书应用,包含以下几个区域:

  • 顶部导航栏(带“我的”按钮)
  • 继续阅读模块(单卡片展示)
  • 之前读过模块(横向滑动卡片列表)
  • 阅读目标模块(弧形进度 + 阅读时间)

整个首页采用了 ScrollView + VStack 作为主结构,配合多个独立组件完成页面构建。每个模块都遵循了清晰的层级布局与样式一致性,体现了 SwiftUI 在构建长页面时的简洁性与灵活性。

在这个过程中我们重点实践了:

  • 如何将多个逻辑区域封装为可维护的独立视图
  • 如何控制模块之间的间距与对齐
  • 如何使用 ZStack 和 Path 绘制简单图形组件

SwiftUI 的组件组合理念,使得即使面对复杂的长页面,也能通过良好的结构拆分和样式统一,实现一套清晰、高可维护的实现方式。

http://www.dtcms.com/a/294948.html

相关文章:

  • 本地部署 Stable Diffusion:零基础搭建 AI文生图模型
  • Linux中scp命令传输文件到服务器报错
  • 直播软件搭建与原生直播系统开发全解析
  • 【2025目标检测】最新论文
  • VulhubDVWA靶场环境搭建及使用
  • 【Mysql】 Mysql zip解压版 Win11 安装备忘
  • Neo4j 框架 初步简单使用(基础增删改查)
  • OMS监考系统V2版本无法启动问题解决办法
  • [每日随题15] 前缀和 - 拓扑排序 - 树状数组
  • 海信IP501H-IP502h_GK6323处理器-原机安卓9专用-TTL线刷烧录可救砖
  • 【Java学习|黑马笔记|Day21】IO流|缓冲流,转换流,序列化流,反序列化流,打印流,解压缩流,常用工具包相关用法及练习
  • C++面试7——继承与多态
  • Xorg占用显卡内存问题和编译opencv GPU版本
  • InnoDB的redo log和 undo log
  • 智能小e-集成配置
  • Nestjs框架: 基于Prisma的多租户功能集成和优化
  • 使用抓取 API 可靠高效地提取亚马逊 (Amazon)数据
  • CCD工业相机系统设计——基于FPGA设计
  • SQL执行顺序
  • LLM 隐藏层特征增强技术
  • 同步型降压转换器的“同步”是什么意思?
  • Vite 7.0 引入的几个重要新 API 详解
  • 三极管与场效应管的对比
  • Python脚本服务器迁移至K8S集群部署
  • k8s中的configmap存储
  • JavaWeb-Servlet
  • 内外网互传文件 安全、可控、便捷的跨网数据交换
  • 服务器版本信息泄露-iis返回包暴露服务器版本信息
  • Node.js 倒计时图片服务部署与 Nginx 反向代理实战总结
  • RCE随笔-奇技淫巧(2)