【SwiftUI模块】0005、SwiftUI-粘性动画指示器引导页

SwiftUI小功能模块系列
0001、SwiftUI自定义Tabbar动画效果
0002、SwiftUI自定义3D动画导航抽屉效果
0003、SwiftUI搭建瀑布流-交错网格-效果
0004、SwiftUI-<探探App>喜欢手势卡片
0005、SwiftUI-粘性动画指示器引导页

技术:SwiftUI3.0、动画指示器、粘性动画指示器、引导页
运行环境:
SwiftUI3.0 + Xcode13.4.1 + MacOS12.5 + iPhone Simulator iPhone 13 Pro Max

概述

使用SwiftUI做一个粘性动画指示器引导页 的案例

详细

一、运行效果

请添加图片描述

二、项目结构图

在这里插入图片描述

三、程序实现 - 过程

思路:
1.创建主页 Home
2.搭建主页进行偏移的逻辑处理 OffsetTabView
3.添加主页介绍信息的模型Intro
4.处理滚动的时候 通过 主页进行绑定OffsetPageTabView的偏移量offset进行监听 是否要改变当前页面

1.创建一个项目命名为 AnimatedIndicator

在这里插入图片描述
在这里插入图片描述

1.1.引入资源文件和颜色

颜色
Blue #5EC7F9
Pink #EB627A
Yellow #F7CE46
引导页介绍图片3张

在这里插入图片描述

2. 创建一个虚拟文件New Group 命名为 View

在这里插入图片描述
在这里插入图片描述

2. 创建一个文件New File 选择SwiftUI View类型 命名为Home

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2. 创建一个文件New File 选择SwiftUI View类型 命名为OffsetTabView

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

3. 创建一个虚拟文件New Group 命名为 Model

在这里插入图片描述
在这里插入图片描述

3. 创建一个文件New File 选择SwiftUI View类型 命名为Intro 、并且删除预览视图、改造成模型 继承Identifiable

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Code

ContentView - 主窗口

主要是展示主窗口Home
并且获取屏幕总高度 - 传递给 Home页面

import SwiftUI

struct ContentView: View {
    
    
    var body: some View {
    
    
//        Home().preferredColorScheme(.dark)
        
        // Getting screenSzie Globally....
        GeometryReader{
    
    proxy in
            let size = proxy.size
            Home(screentSize:size).preferredColorScheme(.dark)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    
    
    static var previews: some View {
    
    
        ContentView()
    }
}

Home - 主页

思路

  1. 顶部模块 - 吃豆人图像
  2. 中间部分核心模块 - 滚动页面 - UI创建包含图片、两个文本
  3. 底部模块 - 指示器 和 下一页的按钮 指示器使用Capsule创建
  4. 创建介绍页面数据、和滚动核心UIOffsetPageTabView
//
//  Home.swift
//  AnimatedIndicator (iOS)
//
//  Created by 李宇鸿 on 2022/8/16.
//

import SwiftUI

struct Home: View {
    
    
    
    var screentSize : CGSize
    @State var offset : CGFloat = 0
    
    var body: some View {
    
    
    
        VStack{
    
    
            
            Button  {
    
    
                
            } label: {
    
    
                Image("pacman")
                    .resizable()
                    .renderingMode(.template)
                    .foregroundColor(.white)
                    .frame(width: 30, height: 30)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
            
            OffsetPageTabView(offset: $offset) {
    
    
        
                HStack(spacing:0){
    
    
                    ForEach(intros){
    
     intro in
                        VStack{
    
    
                            Image(intro.image)
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                    .frame(height: screentSize.height/3)
                            
                            VStack(alignment: .leading, spacing: 22) {
    
    
                                Text(intro.title)
                                    .font(.largeTitle.bold())
                                
                                Text(intro.description)
                                    .fontWeight(.semibold)
                                    .foregroundColor(.secondary)

                            }
                            .foregroundStyle(.white)
                            .padding(.top,50)
                            .frame(maxWidth:.infinity,alignment:.leading)
                        }
                        .padding()
                        // 设置最大宽度
                        .frame(width: screentSize.width)
                    }
                }
                
            }
            
            // 动画指标……
            HStack(alignment:.bottom){
    
    
                
                // 指示器
                HStack(spacing:12){
    
    
                    ForEach(intros.indices,id:\.self){
    
    index in
                        Capsule()
                            .fill(.white)
                        // 仅为当前索引增加宽度…
                        // 当前指示器宽度为20 其他为7
                            .frame(width:getIndex() == index ? 20 :  7, height: 7)
                          
                    }
                }
                
                .overlay(
                    Capsule()
                        .fill(.white)
                        .frame(width:20, height: 7)
                        .offset(x:getIndicatorOffset())
                    ,alignment: .leading
                )
                
                .offset(x:10,y:-15)
                
                Spacer()
                Button  {
    
    
                    // 更新偏移量
                    let index = min(getIndex() + 1, intros.count - 1)
                    offset = CGFloat(index) * screentSize.width
                    
                } label: {
    
    
                    Image(systemName: "chevron.right")
                        .font(.title2.bold())
                        .foregroundColor(.white)
                        .padding(20)
                        .background(
                            
                            intros[getIndex()].color,
                            in: Circle()
                        )
                }

            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
        // 测试滚动监听
//        .onChange(of: offset) { _ in
//            print(offset)
//        }
        
        //   当索引改变时动画…
        .animation(.easeOut,value: getIndex())

    }
    
    
    // 偏移量为指标……
    func getIndicatorOffset()->CGFloat{
    
    
        let progress = offset / screentSize.width
        // 12 =空格
        // 7 =圆的大小。
        let maxWidth : CGFloat = 12 + 7
        return progress * maxWidth
        
        
    }
    
    // 据offset扩展索引…
    func getIndex()-> Int{
    
    
        let progress = round(offset / screentSize.width)
        
        // 安全操作
        let index = min(Int(progress),intros.count - 1)
        return index
//        return Int(progress)
    }
    
}

struct Home_Previews: PreviewProvider {
    
    
    static var previews: some View {
    
    
        ContentView()
    }
}

OffsetPageTabView.swift -偏移Tab视图逻辑处理

主要是做 滚动页面的逻辑处理
思路

  1. 基于ScrollView进行处理
  2. 提供初始化构造器 - 方便上层通过偏移量进行 调用初始化 init(offset: Binding<CGFloat> , @ViewBuilder content: @escaping()->Content)
  3. 监听ScrollView滚动的代理、更新当前的偏移量 class Coordinator
  4. 提供滚动的代理 滚动到下一个页面 进行是否更新当前的偏移量updateUIView

import SwiftUI

// 自定义视图泰式将返回填充控件的偏移量…
struct OffsetPageTabView<Content: View>: UIViewRepresentable  {
    
    
    
    var content: Content
    @Binding var offset : CGFloat
    func makeCoordinator() -> Coordinator {
    
    
        return OffsetPageTabView.Coordinator(parent: self)
    }
    
    
    init(offset: Binding<CGFloat>  , @ViewBuilder content: @escaping()->Content){
    
    
        self.content = content()
        self._offset = offset
    }
    

    func makeUIView(context: Context) -> UIScrollView {
    
    
        let scrollview = UIScrollView()
        
        // 提取SwiftUI View并嵌入到UIKit ScrollView…
        let hostview = UIHostingController(rootView: content)
        hostview.view.translatesAutoresizingMaskIntoConstraints = false
        
        let constraints = [
            hostview.view.topAnchor.constraint(equalTo: scrollview.topAnchor),
            hostview.view.leadingAnchor.constraint(equalTo: scrollview.leadingAnchor),
            hostview.view.trailingAnchor.constraint(equalTo: scrollview.trailingAnchor),
            hostview.view.bottomAnchor.constraint(equalTo: scrollview.bottomAnchor),
            
            //如果你使用的是垂直填充…
            //然后不要声明高度限制…
            hostview.view.heightAnchor.constraint(equalTo: scrollview.heightAnchor)

        ]
        
        scrollview.addSubview(hostview.view)
        scrollview.addConstraints(constraints)
        
        // 启用分页
        scrollview.isPagingEnabled = true
        scrollview.showsVerticalScrollIndicator = false
        scrollview.showsHorizontalScrollIndicator = false
        
        
        // 设置代理
        scrollview.delegate = context.coordinator
        return scrollview
        
    }
    func updateUIView(_ uiView: UIScrollView, context: Context) {
    
    
        //只有当offset被手动更改时才需要更新…
        //检查当前和滚动视图的偏移量…
        let currentOffset = uiView.contentOffset.x
        if currentOffset != offset {
    
    
            print("updating");
            uiView.setContentOffset(CGPoint(x: offset, y: 0),animated:true)
        }
   
    }
    
    // 页面抵消……
    class Coordinator : NSObject,UIScrollViewDelegate {
    
    
        var parent : OffsetPageTabView
        init(parent: OffsetPageTabView){
    
    
            self.parent = parent
        }
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
    
    
            let offset = scrollView.contentOffset.x
            parent.offset = offset
        }
    }
    
    
}

struct OffsetPageTabView_Previews: PreviewProvider {
    
    
    static var previews: some View {
    
    
        ContentView()
    }
}

Intro - 模型

介绍模型

import SwiftUI

// 介绍模型和样本介绍的…
struct Intro: Identifiable{
    
    
    var id = UUID().uuidString
    var image: String
    var title: String
    var description: String
    var color: Color
}

var intros : [Intro] = [

    Intro(image: "food2", title: "Choose your favourite menu", description: "But they are not the inconvenience that our pleasure.",color: Color("Blue")),
    Intro(image: "food1", title: "Find the best price", description: "There is no provision to smooth the consequences are.",color: Color("Yellow")),
    Intro(image: "food3", title: "Your food is ready to be delivered", description: "ter than the pain of the soul to the task.",color: Color("Pink")),

]




demo源码

如需看源码,请点击下载!

猜你喜欢

转载自blog.csdn.net/qq_42816425/article/details/126367573