SwiftUI中TabView(PageTabViewStyle的用法及无限滚动组件infinity carousel)

news2024/12/24 20:32:03

上一篇文章主要介绍了TabView的基本用法以及一些外观样式的设置,本篇文章主要介绍一下PageTabViewStyle样式下的TabView,该样式下的TabView允许用户整页滑动界面,在UIKit中我们用UIScrollViewUICollectionView制作滚动组件,本文采用TabView制作一个无限滚动的组件。

PageTabViewStyle的使用

先看一下效果:
在这里插入图片描述
上面是一个简单的整页滚动视图,显示了一组图片,设置TabView的样式使用下面的修饰符:

.tabViewStyle(PageTabViewStyle())

或者

.tabViewStyle(.page)

设置成PageTabViewStyle样式的时候可以传入是否要显示指示图标(PageControl)的参数indexDisplayMode,组件默认是有底部白色的指示图标的,系统已经提供了,还是很方便的。

.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))

或者

.tabViewStyle(.page(indexDisplayMode: .always))

无限轮播组件

首先定义一个要展示的model类型:

struct Page: Identifiable {
  var id: UUID = UUID()
  var title: String
}

修改一下上面的代码,给TabView绑定一个值currentPage,代码如下:

struct PagedTabViewDemo: View {

  @State private var currentPage: UUID = UUID()
  @State private var pages: [Page] = []

  var body: some View {
    VStack {
      TabView(selection: $currentPage) {
        ForEach(pages) { page in
          Image(page.title)
            .resizable()
            .aspectRatio(contentMode: .fill)
            .tag(page.id)
        }
      }
      .tabViewStyle(.page(indexDisplayMode: .never))
      .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 3 / 5)
      Spacer()
    }
    .onAppear {
      for index in 0..<6 {
        let page = Page(title: "Image_\(index)")
        pages.append(page)
      }
    }
  }
}

代码中给每个Image设置了一个tagtag的值为Pageid,而绑定的currentPage的类型和Pageid为同类型,都为UUID

onAppear中,创建了要展示的数据,填充pages数组。

目前的运行效果如下:

在这里插入图片描述
本文中要实现无限滚动的主要思想是:在当前数组中插入数据,取第一个元素,修改id后,插入到数组末尾去,这样就形成了一个新数组。

基于新数组:
当向左滑动到最后一个图片后,再滑动的一瞬间切换到数组的第一个(下标0)元素去显示。
当向右滑动到第一个图片后,再滑动的一瞬间切换到数组的最后一个元素去显示。

下面在onAppear中插入数据:

  // 当前显示页id
  @State private var currentPage: UUID = UUID()
  // 原始数组
  @State private var pages: [Page] = []
  // 新数组
  @State private var fakedPages: [Page] = []
  .onAppear {
    // 避免冲入插入数据。
    guard fakedPages.isEmpty else { return }

    // 初始化原始数据
    for index in 0..<6 {
      let page = Page(title: "Image_\(index)")
      pages.append(page)
    }

    // 新数组加入原始数据
    fakedPages.append(contentsOf: pages)
    // 在原始数组中取出第一个元素
    if var firstPage = pages.first {
      // 将取出的第一个元素的id给currentPage。
      currentPage = firstPage.id
      // 修改id并插入到新数组的末尾。
      firstPage.id = UUID()
      fakedPages.append(firstPage)
    }
  }

有了新数据以及实现思想后,就是如何判断临界条件了,本文通过手动拖动的时候的当前页的偏移量来计算。

下面给View添加一个扩展方法来计算偏移量:

struct OffsetKey: PreferenceKey {
  static var defaultValue: CGRect = .zero

  static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
    value = nextValue()
  }
}

extension View {
   /// 当addObserver为true的时候计算偏移量,否则不计算。
  @ViewBuilder
  func offsetX(_ addObserver: Bool, completion: @escaping (CGRect) -> Void) -> some View {
    self
      .frame(maxWidth: .infinity)
      .overlay {
        if addObserver {
          GeometryReader {
            let rect = $0.frame(in: .global)
            Color.clear
              .preference(key: OffsetKey.self, value: rect)
              .onPreferenceChange(OffsetKey.self, perform: completion)
          }
        }
      }
  }
}

现在给要显示的每个Image添加上这个偏移量:

TabView(selection: $currentPage) {
  ForEach(fakedPages) { page in
    Image(page.title)
      .resizable()
      .frame(width: size.width, height: size.height)
      .aspectRatio(contentMode: .fill)
      .tag(page.id)
      // 只有当前显示的图片计算偏移量,因此传入currentPage == page.id。闭包中返回偏移数据rect。
      .offsetX(currentPage == page.id) { rect in
      	// 得到图片的偏移量
        let minX = rect.minX
        
      }
  }
}

下面就是计算两个临界值的判断了,因为需要知道整个组件的宽度,所以TabView外包裹一层GeometryReader,并给每个图片设置frame,具体代码及分析见下面代码:

GeometryReader { geometry in
  let size = geometry.size

  TabView(selection: $currentPage) {
    ForEach(fakedPages) { page in
      Image(page.title)
        .resizable()
        .frame(width: size.width, height: size.height)
        .aspectRatio(contentMode: .fill)
        .tag(page.id)
        // 只有当前显示的图片计算偏移量,因此传入currentPage == page.id。闭包中返回偏移数据rect。
        .offsetX(currentPage == page.id) { rect in
          // 得到图片的偏移量,向左移动,得到的为负值,向右移动,得到的值为正值。
          let minX = rect.minX
          print("---> minX: \(minX)")

          // 计算TabView的偏移量,越向左移动偏移量越大。只有当在第一页的时候,向右滑动的瞬间,该值为正数,其他情况均为负数。
          let pageOffset = minX - (size.width * CGFloat(fakeIndex(page)))
          print("---> pageOffset: \(pageOffset)")
          
          // TabView的偏移量除以TabView的宽度,得到一个基于TabView的宽度的偏移倍数。
          let pageProgess = pageOffset / size.width
          print("---> pageProgess: \(pageProgess)")
          
          // 当在第一页向右滑动的时候,满足该条件,切换到最后一页。
          if -pageProgess < 0.0 {
            if fakedPages.indices.contains(fakedPages.count - 1) {
              currentPage = fakedPages[fakedPages.count - 1].id
            }
          }
          
          // 当在最后一页向左滑动的时候,满足该条件,切换到第一页。
          if -pageProgess > CGFloat(fakedPages.count - 1) {
            if fakedPages.indices.contains(0) {
              currentPage = fakedPages[0].id
            }
          }
        }
    }
  }
  .tabViewStyle(.page(indexDisplayMode: .never))
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 3 / 5)

上面代码中有一个计算在新数组中index的方法:

// 当前页在新数组中的index。
func fakeIndex(_ page: Page) -> Int {
  return fakedPages.firstIndex(where: { $0.id == page.id}) ?? 0
}

效果如下:

在这里插入图片描述
上面的代码已经完成了无限滚动功能,因为我们加了数据,所以不能用系统提供的指示图标(PageControl),下面自定义一个PageControl

struct PageControl: UIViewRepresentable {

  var totalPages: Int
  var currentPage: Int

  func makeUIView(context: Context) -> UIPageControl {
    let control = UIPageControl()
    control.numberOfPages = totalPages
    control.currentPage = currentPage
    control.backgroundStyle = .minimal
    return control
  }

  func updateUIView(_ uiView: UIPageControl, context: Context) {
    uiView.numberOfPages = totalPages
    uiView.currentPage = currentPage
  }
}

UIPageControl封装一下用在SwiftUI中,这样很多功能以及样式就不需要我们自己去实现了。现在将封装好的PageControl添加到组件中。

TabView(selection: $currentPage) { ... }
.tabViewStyle(.page(indexDisplayMode: .never))
.overlay(alignment: .bottom) {
  PageControl(totalPages: pages.count, currentPage: originalIndex(currentPage))
   .offset(y: -15)
}

上面代码中有个计算当前页currentPage在原始数组中的index的方法:

// 当前pageId在原始数组中的index。
func originalIndex(_ pageId: UUID) -> Int {
  return pages.firstIndex(where: { $0.id == pageId}) ?? 0
}

效果如下:

在这里插入图片描述
整体看起来效果还不错,到此为止,无限滚动的组件就完成了,整个代码会附在文章末尾。

写在最后

本文主要介绍了TabViewPageTabViewStyle样式,一个可以按页滚动的组件,使用起来还是挺简单便捷的。文章内页提供了一个基于TabView实现的无限滚动的一个组件,当然还有用其他基础组件实现的这个功能,这里就不过多说明了。

最后,希望能够帮助到有需要的朋友,如果您觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

完整Demo代码:

//
//  PagedTabViewDemo.swift
//  SwiftUILearning
//
//  Created by GuoYongming on 5/26/24.
//

import SwiftUI

struct Page: Identifiable {
  var id: UUID = UUID()
  var title: String
}

struct PagedTabViewDemo: View {

  // 当前显示页id
  @State private var currentPage: UUID = UUID()
  // 原始数组
  @State private var pages: [Page] = []
  // 新数组
  @State private var fakedPages: [Page] = []

  var body: some View {
    VStack {
    GeometryReader { geometry in
      let size = geometry.size
      TabView(selection: $currentPage) {
        ForEach(fakedPages) { page in
          Image(page.title)
            .resizable()
            .frame(width: size.width, height: size.height)
            .aspectRatio(contentMode: .fill)
            .tag(page.id)
            // 只有当前显示的图片计算偏移量,因此传入currentPage == page.id。闭包中返回偏移数据rect。
            .offsetX(currentPage == page.id) { rect in
              // 得到图片的偏移量,向左移动,得到的为负值,向右移动,得到的值为正值。
              let minX = rect.minX
              print("---> minX: \(minX)")

              // 计算TabView的偏移量,越向左移动偏移量越大。只有当在第一页的时候,向右滑动的瞬间,该值为正数,其他情况均为负数。
              let pageOffset = minX - (size.width * CGFloat(fakeIndex(page)))
              print("---> pageOffset: \(pageOffset)")

              // TabView的偏移量除以TabView的宽度,得到一个基于TabView的宽度的偏移倍数。
              let pageProgess = pageOffset / size.width
              print("---> pageProgess: \(pageProgess)")

              // 当在第一页向右滑动的时候,满足该条件,切换到最后一页。
              if -pageProgess < 0.0 {
                if fakedPages.indices.contains(fakedPages.count - 1) {
                  currentPage = fakedPages[fakedPages.count - 1].id
                }
              }

              // 当在最后一页向左滑动的时候,满足该条件,切换到第一页。
              if -pageProgess > CGFloat(fakedPages.count - 1) {
                if fakedPages.indices.contains(0) {
                  currentPage = fakedPages[0].id
                }
              }
            }
        }
      }
      .tabViewStyle(.page(indexDisplayMode: .never))
      .overlay(alignment: .bottom) {
        PageControl(totalPages: pages.count, currentPage: originalIndex(currentPage))
          .offset(y: -15)
      }
    }
    .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 3 / 5)
      Spacer()
    }
    .onAppear {
      // 避免冲入插入数据。
      guard fakedPages.isEmpty else { return }

      // 初始化原始数据
      for index in 0..<5 {
        let page = Page(title: "Image_\(index)")
        pages.append(page)
      }

      // 新数组加入原始数据
      fakedPages.append(contentsOf: pages)
      // 在原始数组中取出第一个元素
      if var firstPage = pages.first {
        // 将取出的第一个元素的id给currentPage。
        currentPage = firstPage.id
        // 修改id并插入到新数组的末尾。
        firstPage.id = UUID()
        fakedPages.append(firstPage)
      }
    }
  }

  func fakeIndex(_ page: Page) -> Int {
    return fakedPages.firstIndex(where: { $0.id == page.id}) ?? 0
  }

  func originalIndex(_ pageId: UUID) -> Int {
    return pages.firstIndex(where: { $0.id == pageId}) ?? 0
  }
}

#Preview {
  PagedTabViewDemo()
}

struct OffsetKey: PreferenceKey {
  static var defaultValue: CGRect = .zero

  static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
    value = nextValue()
  }
}

extension View {
  /// 当addObserver为true的时候计算偏移量,否则不计算。
  @ViewBuilder
  func offsetX(_ addObserver: Bool, completion: @escaping (CGRect) -> Void) -> some View {
    self
      .frame(maxWidth: .infinity)
      .overlay {
        if addObserver {
          GeometryReader {
            let rect = $0.frame(in: .global)
            Color.clear
              .preference(key: OffsetKey.self, value: rect)
              .onPreferenceChange(OffsetKey.self, perform: completion)
          }
        }
      }
  }
}

struct PageControl: UIViewRepresentable {

  var totalPages: Int
  var currentPage: Int

  func makeUIView(context: Context) -> UIPageControl {
    let control = UIPageControl()
    control.numberOfPages = totalPages
    control.currentPage = currentPage
    control.backgroundStyle = .minimal
    return control
  }

  func updateUIView(_ uiView: UIPageControl, context: Context) {
    uiView.numberOfPages = totalPages
    uiView.currentPage = currentPage
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1704889.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++进阶 | [4] map and set

摘要&#xff1a;set&#xff0c;multiset&#xff0c;map&#xff0c;multimap 前言 1. 容器 序列式容器&#xff1a;只存储数据&#xff0c;数据之间无关联关系。例如&#xff0c;vector、list、deque、……关联式容器&#xff1a;不仅存储数据&#xff0c;且数据之间有关联…

Chrome谷歌浏览器如何打开不安全页面的禁止权限?

目录 一、背景二、如何打开不安全页面被禁止的权限&#xff1f;2.1 第一步&#xff0c;添加信任站点2.2 第二步&#xff0c;打开不安全页面的权限2.3 结果展示 一、背景 在开发过程中&#xff0c;由于测试环境没有配置 HTTPS 请求&#xff0c;所以谷歌浏览器的地址栏会有这样一…

《Python侦探手册:用正则表达式破译文本密码》

在这个信息爆炸的时代&#xff0c;每个人都需要一本侦探手册。阿佑今天将带你深入Python的正则表达式世界&#xff0c;教你如何像侦探一样&#xff0c;用代码破解文本中的每一个谜题。从基础的字符匹配到复杂的数据清洗&#xff0c;每一个技巧都足以让你在文本处理的领域中成为…

代码随想录——最大二叉树(Leetcode654)

题目链接 递归 二叉树 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode rig…

Django Celery技术详解

文章目录 简介安装和配置创建并调度任务启动Celery Worker在视图中调用异步任务拓展功能 简介 Django Celery 是一个为Django应用程序提供异步任务处理能力的强大工具。它通过与消息代理&#xff08;如RabbitMQ、Redis&#xff09;集成&#xff0c;可以轻松地处理需要长时间运…

[C#]winform部署官方yolov10目标检测的onnx模型

【框架地址】 https://github.com/THU-MIG/yolov10 【算法介绍】 今天为大家介绍的是 YOLOv10&#xff0c;这是由清华大学研究团队最新提出的&#xff0c;同样遵循 YOLO 系列设计原则&#xff0c;致力于打造实时端到端的高性能目标检测器。 方法 创新 双标签分配策略 众所…

shell脚本-函数

一、函数 1.函数的定义和格式 函数定义&#xff1a;封装的可重复利用的具有特定功能的代码 先定义函数&#xff0c;再调用函数&#xff0c;注意顺序 函数类似于命令的别名&#xff0c;别名一些简单的小命令 函数是某一个脚本的别名&#xff0c;有些脚本会重复使用 函数格…

【链表】Leetcode 92. 反转链表 II【中等】

反转链表 II 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], left 2, right 4 输出&#x…

【一刷《剑指Offer》】面试题 24:二叉搜索树的后序遍历系列

力扣对应题目链接&#xff1a;LCR 152. 验证二叉搜索树的后序遍历序列 - 力扣&#xff08;LeetCode&#xff09; 牛客对应题目链接&#xff1a;二叉搜索树的后序遍历序列_牛客题霸_牛客网 (nowcoder.com) 核心考点 &#xff1a; BST 特征的理解。 一、《剑指Offer》对应内容 二…

NASA数据集——严格校准的臭氧(O3)、甲醛(HCHO)、二氧化碳(CO2)和甲烷(CH4)混合比,以及包括三维风在内的气象数据

Alpha Jet Atmopsheric eXperiment Meteorological Measurement System (MMS) Data 阿尔法喷气式大气实验气象测量系统&#xff08;MMS&#xff09;数据 简介 Alpha Jet Atmospheric eXperiment (AJAX) 是美国国家航空航天局艾姆斯研究中心与 H211, L.L.C. 公司的合作项目&a…

LAMP网络服务架构

目录 LAMP 网站服务架构 LAMP的组成部分 LAMP的构建顺序 安装论坛 0.电脑已编译安装Apache&#xff0c;MySQL&#xff0c;PHP 1.创建数据库&#xff0c;并进行授权 2.上传论坛压缩包到 /opt ,并解压 3.上传站点更新包 4.更改论坛目录的属主 5.浏览器访问验证 LAMP 网…

2024年03月 Python(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python等级考试(1~6级)全部真题・点这里 一、单选题(共25题,共50分) 第1题 运行如下代码,若输入整数3,则最终输出的结果为?( ) def f(x):if x==1:s=1else:s

各种情况下的线缆大小选择

开口线鼻子和导线对应大小 开口铜鼻子对应线径大小 变压器容量对应高压侧电流大小 开关电流线缆功率对照表 家庭/工业最常用电线铜线电流承载功率 电工常用名词对应符号 导线面积承载的安全载流量及允许负荷对照表 漏电保护器选择参考表 电动机功率换算电流 电机功…

应用程序中的会话管理和Cookie安全指南

应用程序中的会话管理和Cookie安全指南 在现代应用程序中&#xff0c;会话管理和Cookie安全是确保用户信息和数据安全的重要组成部分。本文将详细介绍会话管理的最佳实践以及如何通过安全的Cookie设置来保护会话ID的交换。 单点登录&#xff08;SSO&#xff09;及会话管理机制…

其二:使用递归法实现二分搜索

开篇 本文主要是利用递归法来实现一个简单的二分搜索程序。题目来源是《编程珠玑》第4章课后习题3。 问题概要 编写并验证一个递归的二分搜索程序, 并返回t在数组x[0…n-1]中第一次出现的位置。 思路分析 本题的思路与第一版相似&#xff0c;不过不同的是&#xff0c;为确保返回…

Android 通过布局生成图片

通过布局生成图片 首先效果图 在竖屏的情况下通过&#xff0c;一般情况下&#xff0c;只要布局在页面上可见&#xff0c;并显示全&#xff0c;通过布局生成图片&#xff0c;都可以&#xff0c;但是横屏就不行了&#xff0c;会出现图片显示不完全的情况。 val bitmap Bitmap.c…

KingbaseES数据库物理备份还原sys_rman

数据库版本&#xff1a;KingbaseES V008R006C008B0014 简介 sys_rman 是 KingbaseES 数据库中重要的物理备份还原工具&#xff0c;支持不同类型的全量备份、差异备份、增量备份&#xff0c;保证数据库在遇到故障时及时使用 sys_rman 来恢复到数据库先前状态。 文章目录如下 1.…

Laravel和ThinkPHP框架比较

一、开发体验与易用性比较 1. 代码可读性&#xff1a; - Laravel以其优雅的语法和良好的代码结构著称&#xff0c;使得代码更加易读易懂。 - 相比之下&#xff0c;ThinkPHP的代码可读性较为一般&#xff0c;在一些复杂业务场景下&#xff0c;可能会稍显混乱。 让您能够一站式…

【leetcode 141】环形链表——快慢指针(龟兔赛跑)

给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;…

浙江大学数据结构MOOC-课后习题-第六讲-图2 Saving James Bond - Easy Version

题目汇总 浙江大学数据结构MOOC-课后习题-拼题A-代码分享-2024 题目描述 测试点 思路分享 ①解题思路概览 我的想法是&#xff0c;先建立一个图&#xff0c;然后再利用DFS或者BFS来遍历判断当前顶点能否跳到岸上去 ②怎么建图&#xff1f; 首先要考虑采用什么数据结构来存储图…