import SwiftUI

struct Post: Identifiable {
    
    var id = UUID().uuidString
    var postImage: String
}
import SwiftUI

struct SnapCarousel<Content: View, T: Identifiable>: View {

    var content: (T) -> Content
    var list: [T]
    
    // properties
    var spacing: CGFloat
    var trailingSpace: CGFloat
    @Binding var index: Int
    
    init(spacing: CGFloat = 15, trailingSpace: CGFloat = 100, index: Binding<Int>, items: [T], @ViewBuilder content: @escaping (T) -> Content) {
        
        self.list = items
        self.spacing = spacing
        self.trailingSpace = trailingSpace
        self._index = index
        self.content = content
    }
    
    @GestureState var offset: CGFloat = 0
    @State var currentIndex: Int = 0
    
    var body: some View {
        
        GeometryReader { proxy in
            
            let width = proxy.size.width - (trailingSpace - spacing)
            let adjustMentWidth = (trailingSpace / 2) - spacing
            
            HStack(spacing: spacing) {
                
                ForEach(list) { item in
                    content(item)
                        .frame(width: proxy.size.width - trailingSpace)
                }
            }
            .padding(.horizontal, spacing)
            .offset(x: (CGFloat(currentIndex) * -width) + (currentIndex != 0 ? adjustMentWidth : 0) + offset)
            .gesture(
            DragGesture()
                .updating($offset, body: { value, out, _ in
                    
                    out = value.translation.width
                })
                .onEnded({ value in
                    
                    let offsetX = value.translation.width
                    
                    let progress = -offsetX / width
                    
                    let roundIndex = progress.rounded()
                    
//                    currentIndex += Int(roundIndex)
                    currentIndex = max(min(currentIndex + Int(roundIndex), list.count - 1), 0)
                    
                    currentIndex = index
                })
                .onChanged({ value in
                    
                    let offsetX = value.translation.width
                    
                    let progress = -offsetX / width
                    
                    let roundIndex = progress.rounded()
                    
                    index = max(min(currentIndex + Int(roundIndex), list.count - 1), 0)
                
                })
            )
        }
        .animation(.easeInOut, value: offset == 0)
    }
}
  • SnapCarousel 是一个通用结构体,它接收两个类型参数:Content:表示视图的内容,必须符合 View 协议。T:表示数据模型,并且必须遵循 Identifiable 协议,这样SwiftUI 能够识别和管理数据集中的每个元素。

    • content: 一个闭包,用于生成视图内容。

    • list: 一个数组,包含要在轮播中显示的元素。

    • 使用 DragGesture 来捕捉用户的拖动。

    • 当手势更新时,通过 updating 方法来设置偏移量。

    • 在手势结束时计算出当前应该显示的索引,并更新 currentIndex 和绑定的 index

    • 在手势变化时,也会更新绑定的 index

import SwiftUI

struct Banner: View {
    @State var currentIndex: Int = 0
    @State var posts: [Post] = []
    @State var currentTab = "Slide Show"
    @Namespace var animation
    
    var body: some View {
        
        VStack(spacing: 15) {
            
            VStack(alignment: .leading, spacing: 12) {
                
                Button {
                    
                }label: {
                    
                    Label {
                        Text("Back")
                            .fontWeight(.semibold)
                    } icon: {
                        Image(systemName: "chevron.left")
                            .font(.title2.bold())
                    }
                    .foregroundColor(.primary)
                }
                
                Text("My Wishes")
                    .font(.title)
                    .fontWeight(.black)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
            
            // segment Control
            HStack(spacing: 0) {
                
                TabButton(title: "Slide Show", animation: animation, currentTab: $currentTab)
                TabButton(title: "List", animation: animation, currentTab: $currentTab)
            }
            .background(Color.black.opacity(0.04), in: RoundedRectangle(cornerRadius: 10))
            .padding(.horizontal)
            
            // Snap Carousel
            SnapCarousel(index: $currentIndex, items: posts) { post in
                
                GeometryReader { proxy in
                    let size = proxy.size
                    
                    Image(post.postImage)
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width: size.width)
                        .cornerRadius(12)
                }
            }
            .padding(.vertical, 40)
            
            // Indicator
            HStack(spacing: 10) {
                
                ForEach(posts.indices, id: \.self) { index in
                    
                    Circle()
                        .fill(Color.black.opacity(currentIndex == index ? 1 : 0.1))
                        .frame(width: 10, height: 10)
                        .scaleEffect(currentIndex == index ? 1.4 : 1)
                        .animation(.spring(), value: currentIndex == index)
                }
            }
            .padding(.bottom, 40)
        }
        .frame(maxHeight: .infinity, alignment: .top)
        .onAppear {
            for index in 1...4 {
                posts.append(Post(postImage: "banner\(index)"))
            }
        }
    }
}

struct Banner_Previews: PreviewProvider {
    static var previews: some View {
        Banner()
    }
}


// TabButton
struct TabButton: View {
    
    var title: String
    var animation: Namespace.ID
    @Binding var currentTab: String
    
    var body: some View {
        
        Button {
            
            withAnimation(.spring()) {
                
                currentTab = title
            }
        }label: {
            
            Text(title)
                .fontWeight(.bold)
                .foregroundColor(currentTab == title ? .white : .black)
                .frame(maxWidth: .infinity)
                .padding(.vertical, 8)
                .background(
                
                    ZStack {
                        
                        if currentTab == title {
                            RoundedRectangle(cornerRadius: 10)
                                .fill(.black)
                                .matchedGeometryEffect(id: "TAB", in: animation)
                        }
                    }
                )
        }
    }
}

更多推荐