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

上一篇文章主要介绍了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
  }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

家政项目day2 需求分析(模拟入职后熟悉业务流程)

目录 1 项目主体介绍1.1 项目背景1.2 运营模式1.3 项目业务流程 2 运营端需求2.1 服务类型管理2.2 服务项目&#xff08;服务&#xff09;管理2.3 区域管理2.4 区域服务管理2.5 相关数据库表的管理2.6 设计工程结构2.7 测试接口&#xff08;接口断点查看业务代码&#xff09; 1…

Java实现链表

链表 前言一、链表的概念及结构二、链表的分类三、链表的实现无头单向非循环链表实现无头双向链表实现具体代码 四、链表习题五、顺序表和链表的区别 前言 推荐一个网站给想要了解或者学习人工智能知识的读者&#xff0c;这个网站里内容讲解通俗易懂且风趣幽默&#xff0c;对我…

Autodesk Flame 2025 for Mac:视觉特效制作的终极利器

在数字时代&#xff0c;视觉特效已经成为电影、电视制作中不可或缺的一部分。Autodesk Flame 2025 for Mac&#xff0c;这款专为视觉特效师打造的终极工具&#xff0c;将为您的创作提供无尽的可能。 Autodesk Flame 2025 for Mac拥有强大的三维合成环境&#xff0c;能够支持您…

05.配置tomcat管理功能

认证失败&#xff0c;需要配置tomcat-users.xml文件 配置用户信息 [rootweb01 /application/tomcat/conf\]# tail tomcat-users.xml <role rolename"admin-gui"/> <role rolename"host-gui"/><role rolename"mana…

数学建模--LaTeX的基本使用

目录 1.回顾 2.设置这个页眉和页脚 3.对于字体的相关设置 4.对于这个分级标题的设置 5.列表的使用 6.插入图片 1.回顾 &#xff08;1&#xff09;昨天我们了解到了这个latex的使用基本常识&#xff0c;以及这个宏包的概念&#xff0c;区域的划分&#xff0c;不同的代码代…

PCL 法向量加权的RANSAC拟合分割平面

目录 一、算法原理1、原理概述2、主要函数二、代码实现三、结果展示四、相关链接本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、算法原理 1、原理概述

树与二叉树的概念介绍

一.树的概念及结构&#xff1a; 1.树的概念&#xff1a; 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的 有…

IDEA 上方添加左右箭头按钮

IDEA 版本&#xff1a;2021.3.3 按钮&#xff1a; 左箭头&#xff08;Back&#xff09;&#xff08;快捷键&#xff1a;Ctrl Alt 左箭头&#xff09; 右箭头&#xff08;Forward&#xff09;&#xff08;快捷键&#xff1a;Ctrl Alt 右箭头&#xff09; 日常写代码中经常…

Java与GO语言对比分析

你是不是总听到go与java种种对比&#xff0c;其中在高并发的服务器端应用场景会有人推荐你使用go而不是 java。 那我们就从两者运行原理和基本并发设计来对比分析&#xff0c;看看到底怎么回事。 运行原理对比 java java 中 jdk 已经帮我们屏蔽操作系统区别。 只要我们下载并…

开源金融AI代理平台FinRobot;支持多翻译引擎和模式的高效浏览器翻译开源插件;使用自然语言控制生成视频的通用世界模型

✨ 1: finrobot FinRobot 是一个基于大语言模型的开源金融AI代理平台&#xff0c;适用于多种金融应用。 FinRobot是一个综合性的AI代理平台&#xff0c;超越了原有的FinGPT&#xff0c;旨在满足金融行业的多元化需求。它集成了各种AI技术&#xff0c;不仅仅局限于语言模型&am…

APM2.8飞控

ArduPilotMega 主控可应用于 固定翼、直升机、多旋翼、地面车辆 APM2.8飞控供电有两种 1.电流计供电&#xff0c; 2.带BEC&#xff08;稳压功能&#xff09;的电调供电 ArduPilotMega 内部的硬件结构图&#xff1a; 调试时&#xff0c;不要使用向导&#xff0c;由于向导功能不…

【JUC编程】-多线程和CompletableFuture的使用

多线程编程 文章目录 多线程编程[toc]引言创建多线程的方式继承Thread类实现Runnable接口实现Callable接口Callable和Runnable的区别 Lambda表达式 线程的实现原理Future&FutureTask具体使用submit方法Future到FutureTask类Future注意事项局限性 CompletionService引言使用…

【蓝桥杯——物联网设计与开发】拓展模块2 - 电位器模块

一、电位器模块 &#xff08;1&#xff09;资源介绍 &#x1f505;原理图 蓝桥杯物联网竞赛实训平台提供了一个拓展接口 CN2&#xff0c;所有拓展模块均可直接安装在 Lora 终端上使用&#xff1b; 图1 拓展接口 电位器模块电路原理图如下所示&#xff1a; 图2 …

通用代码生成器应用场景三,遗留项目反向工程

通用代码生成器应用场景三&#xff0c;遗留项目反向工程 如果您有一个遗留项目&#xff0c;要重新开发&#xff0c;或者源代码遗失&#xff0c;或者需要重新开发&#xff0c;但是希望复用原来的数据&#xff0c;并加快开发。 如果您的项目是通用代码生成器生成的&#xff0c;…

无线蓝牙耳机品牌推荐:倍思M2s Pro,让旅途更添乐趣

随着端午节的临近,许多人开始规划起出游计划。出游除了要做好行程安排,还需准备一些实用的物品来提升旅途的舒适度。特别是在高铁等长途旅行中,一款优质的降噪蓝牙耳机无疑是消磨时光、享受音乐的绝佳选择。那么,在众多的无线蓝牙耳机品牌中,有哪些值得推荐的呢?今天,我们就来…

IT廉连看——UniApp——事件绑定

IT廉连看——UniApp——事件绑定 这是我们上节课最终的样式&#xff1b; 一、现在我有这样一个需求&#xff0c;当我点击“生在国旗下&#xff0c;长在春风里”它的颜色由红色变为蓝色&#xff0c;该怎么操作&#xff1f; 这时候我们需要一个事件的绑定&#xff0c;绑定一个单…

指纹识别经典图书、开源算法库、开源数据库

目录 1. 指纹识别书籍 1.1《精通Visual C指纹模式识别系统算法及实现》 1.2《Handbook of Fingerprint Recognition》 2. 指纹识别开源算法库 2.1 Hands on Fingerprint Recognition with OpenCV and Python 2.2 NIST Biometric Image Software (NBIS) 3. 指纹识别开源数…

react-native 默认停用 flipper 通知

react-native 0.74 默认停用 flipper &#xff0c;但仍然可以手动安装 flipper 官方声明文档 英语好的可以直接阅读。 integration with React Native will no longer be enabled 原因 增加编译时间有时候会有连接问题升级会导致不能使用 之后调试推荐 我们建议团队使用 A…

谷粒商城实战(029 业务-订单支付模块-支付宝支付2)

Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第305p-第p310的内容 代码编写 前端代码 这里使用的是jsp 在这里引用之前配置的各种支付信息 在AlipayConfig.java里 这里是调用阿里巴巴写…

单片机的内存映射和重映射

内存映射 在单片机内&#xff0c;不管是RAM还是ROM还是寄存器&#xff0c;他们都是真实存在的物理存储器&#xff0c;为了方便操作&#xff0c;单片机会给每一个存储单元分配地址&#xff0c;这就叫做内存映射。 单片机的内存映射是指将外部设备或外部存储器映射到单片…