IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> SwiftUI的多列列表 -> 正文阅读

[移动开发]SwiftUI的多列列表

源代码:https://github.com/chr1s78/MultipleColumnsListSwiftUI/tree/main

尝试手动实现一个SwiftUI的多列列表,效果如下:

在列表的标题滚动到顶部时,显示对应的标题内容,并固定,实现此方法需要用到?preference 首选项功能。关于preference,可以参阅这篇文章?The magic of view preferences in SwiftUI

简单来说,preference允许我们将子视图的属性,传递给父视图。在这个例子中,我们将标题栏的位置和大小,传递给父视图,由父视图来判断当前需要显示哪个标题栏。

我们要做的具体内容为:

1、固定显示第一个标题栏

2、获得所有标题栏的尺寸和位置

3、在滚动时判断是否有标题栏滚动到了固定的第一个标题栏的位置

4、显示对应的标题栏内容

首先定义一个preferenceKey

struct ListRowPreferenceKey: PreferenceKey {
    typealias Value = [ListRowPreferenceData]

    static var defaultValue: [ListRowPreferenceData] = []
    
    static func reduce(value: inout [ListRowPreferenceData], nextValue: () -> [ListRowPreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}


struct ListRowPreferenceData: Equatable {
    let rect: CGRect
}

?ListRowPreferenceData中,存放需要记录的子视图的位置数据

我们将列表划分为两种视图组成,标题栏视图和列表内容视图

标题栏视图代码如下

// MARK: 列表标题行
struct MCListTitleRow: View {
    var title: String
    var body: some View {
        GeometryReader { geo in
      
                Text(title).font(.title).bold()
                    .frame(width: UIScreen.main.bounds.width, height: 60)
                    .background(Color.black)
                    .foregroundColor(.white)
                    .SetPreference(geo: geo, save: true)
    
        }
        .frame(width: UIScreen.main.bounds.width, height: 60)
    }
}
/// 获得子视图 列表标题栏的大小
/// 将perference定义为modifier模式
struct setPreference: ViewModifier {
    var geo: GeometryProxy
    var save: Bool
    func body(content: Content) -> some View {
        if save {
             content
                .preference(key: ListRowPreferenceKey.self,
                            value:
                                [ ListRowPreferenceData(rect: geo.frame(in: .named("VSTACK"))) ])
        } else {
             content
        }
    }
}

extension View {
    func SetPreference(geo: GeometryProxy, save: Bool) -> some View {
        self.modifier(setPreference(geo: geo, save: save))
    }
}

在标题栏视图中,使用GeometryReader,配合在主视图中定义的"VSTACK"区域,来确定标题栏的尺寸,并“记录”在ListRowPreferenceKey中。

列表内容视图

struct MultiColumnListData: Identifiable, Hashable, Codable {
    var id = UUID()
    var column: Int = 1
    var widths: [CGFloat] = []
    var rowData: [String] = []
}


// MARK: 列表内容行
struct MCListRow: View {
    
    var data: MultiColumnListData = MultiColumnListData()
    
    init(data: MultiColumnListData) {
        self.data = data
        self.data.column = self.data.rowData.count == 0 ? 1 : self.data.rowData.count

        if self.data.widths.count == 0 {
            let widthAverage = UIScreen.main.bounds.width / CGFloat(self.data.column)
            for _ in 0..<self.data.column {
                self.data.widths.append(widthAverage)
            }
        }
    }
    
    var body: some View {
        HStack(spacing: 0) {
            ForEach(0..<data.column) { i in
      
                    Text(data.rowData[i])
                        .frame(width: data.widths[i], height: 50)
                        .background(((i % 2) == 0) ? Color.white : Color.gray.opacity(0.3))
  
            }
        }
        .frame(height: 50)
    }
}

根据传入的rowData,来判断分为几列显示,默认按照屏幕宽度平均等分单元格的宽度

主视图关键代码如下:

            VStack {
                ...
                ScrollView {}
            }
            .coordinateSpace(name: "VSTACK")
            .onPreferenceChange(ListRowPreferenceKey.self) { preferences in
                
                // 获得第一行标题栏中心点的Y坐标值
                let fixedMidY = preferences[0].rect.midY

                let sequence = (fixedMidY - offset...fixedMidY + offset)
                
                for i in 1..<preferences.count {
         
                    // 判断其他标题栏是否穿过第一行标题栏的位置
                    if sequence.contains(preferences[i].rect.midY) {
                        // 更新第一行标题栏显示
                        if self.direction {
                            // 向上滚动情况
                            DispatchQueue.main.async {
                                self.indexHeader = i
                            }
                            
                        } else {
                            // 向下滚动情况
                            DispatchQueue.main.async {
                                self.indexHeader = i - 1 < 0 ? 0 : i - 1
                            }
                        }
                    }
                }
            }

在onPreferenceChange中,获取子视图我们设置到preference中的尺寸数据,并进行判断

完整代码如下:

MCListRow.swift

//
//  MCListRow.swift
//  MulitiColumnListTesting
//
//  Created by Chr1s on 2021/8/29.
//

import SwiftUI

struct ListRowPreferenceKey: PreferenceKey {
    typealias Value = [ListRowPreferenceData]

    static var defaultValue: [ListRowPreferenceData] = []
    
    static func reduce(value: inout [ListRowPreferenceData], nextValue: () -> [ListRowPreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}


struct ListRowPreferenceData: Equatable {
    let rect: CGRect
}

// MARK: 列表标题行
struct MCListTitleRow: View {
    var title: String
    var body: some View {
        GeometryReader { geo in
      
                Text(title).font(.title).bold()
                    .frame(width: UIScreen.main.bounds.width, height: 60)
                    .background(Color.black)
                    .foregroundColor(.white)
                    .SetPreference(geo: geo, save: true)
    
        }
        .frame(width: UIScreen.main.bounds.width, height: 60)
    }
}

// MARK: 列表内容行
struct MCListRow: View {
    
    var data: MultiColumnListData = MultiColumnListData()
    
    init(data: MultiColumnListData) {
        self.data = data
        self.data.column = self.data.rowData.count == 0 ? 1 : self.data.rowData.count

        if self.data.widths.count == 0 {
            let widthAverage = UIScreen.main.bounds.width / CGFloat(self.data.column)
            for _ in 0..<self.data.column {
                self.data.widths.append(widthAverage)
            }
        }
    }
    
    var body: some View {
        HStack(spacing: 0) {
            ForEach(0..<data.column) { i in
      
                    Text(data.rowData[i])
                        .frame(width: data.widths[i], height: 50)
                        .background(((i % 2) == 0) ? Color.white : Color.gray.opacity(0.3))
  
            }
        }
        .frame(height: 50)
    }
}

/// 获得子视图 列表标题栏的大小
/// 将perference定义为modifier模式
struct setPreference: ViewModifier {
    var geo: GeometryProxy
    var save: Bool
    func body(content: Content) -> some View {
        if save {
             content
                .preference(key: ListRowPreferenceKey.self,
                            value:
                                [ ListRowPreferenceData(rect: geo.frame(in: .named("VSTACK"))) ])
        } else {
             content
        }
    }
}

extension View {
    func SetPreference(geo: GeometryProxy, save: Bool) -> some View {
        self.modifier(setPreference(geo: geo, save: save))
    }
}

struct MCListRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            MCListRow(data: MultiColumnListData(rowData: ["1","Lex","24"]))
                .previewLayout(.sizeThatFits)
            MCListTitleRow(title: "前锋")
            .previewLayout(.sizeThatFits)
            .preferredColorScheme(.dark)
}
    }
}

MCListViewModel.swift

//
//  MultiColumnListViewModel.swift
//  MulitiColumnListTesting
//
//  Created by Chr1s on 2021/8/30.
//

import SwiftUI

struct MultiColumnListData: Identifiable, Hashable, Codable {
    var id = UUID()
    var column: Int = 1
    var widths: [CGFloat] = []
    var rowData: [String] = []
}

class MultiColumnListViewModel: ObservableObject {
    @Published var listData: [MultiColumnListData] = []
    @Published var titleData: [String] = ["前锋", "中场", "后卫"]
    @Published var listColumn: Int = 1
    
    init() {

        listData.append(MultiColumnListData(rowData: ["7号", "格列兹曼", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["9号", "德佩", "24岁"]))
        listData.append(MultiColumnListData(rowData: ["11号", "登贝莱", "24岁"]))
        listData.append(MultiColumnListData(rowData: ["7号", "格列兹曼", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["9号", "德佩", "24岁"]))
        listData.append(MultiColumnListData(rowData: ["11号", "登贝莱", "24岁"]))
 
  
        listData.append(MultiColumnListData(rowData: ["5号", "布斯克茨", "33岁"]))
        listData.append(MultiColumnListData(rowData: ["21号", "德容", "22岁"]))
        listData.append(MultiColumnListData(rowData: ["20号", "比达尔", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["8号", "皮亚尼奇", "25岁"]))
        listData.append(MultiColumnListData(rowData: ["5号", "布斯克茨", "33岁"]))
        listData.append(MultiColumnListData(rowData: ["21号", "德容", "22岁"]))
        listData.append(MultiColumnListData(rowData: ["20号", "比达尔", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["8号", "皮亚尼奇", "25岁"]))
        
        listData.append(MultiColumnListData(rowData: ["3号", "皮克", "33岁"]))
        listData.append(MultiColumnListData(rowData: ["18号", "阿尔巴", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["24号", "埃里克加西", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["23号", "乌姆蒂蒂", "25岁"]))
        listData.append(MultiColumnListData(rowData: ["3号", "皮克", "33岁"]))
        listData.append(MultiColumnListData(rowData: ["18号", "阿尔巴", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["24号", "埃里克加西", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["23号", "乌姆蒂蒂", "25岁"]))
        listData.append(MultiColumnListData(rowData: ["24号", "埃里克加西", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["23号", "乌姆蒂蒂", "25岁"]))
        listData.append(MultiColumnListData(rowData: ["3号", "皮克", "33岁"]))
        listData.append(MultiColumnListData(rowData: ["18号", "阿尔巴", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["24号", "埃里克加西", "30岁"]))
        listData.append(MultiColumnListData(rowData: ["23号", "乌姆蒂蒂", "25岁"]))
        
    }
}

MCListView.swift

//
//  MultiColumnList.swift
//  MulitiColumnListTesting
//
//  Created by Chr1s on 2021/8/29.
//

import SwiftUI

struct MultiColumnList: View {
    
    @StateObject var vm = MultiColumnListViewModel()

    @State var indexHeader: Int = 0
    @State var direction: Bool = false

    let offset: CGFloat = 5.0
  
    var body: some View {
        NavigationView {
            VStack(spacing: 0.0) {
                /// 固定的第一行列表,显示第一个Title
                MCListTitleRow(title: vm.titleData[self.indexHeader])
                
                /// 滚动列表
                ScrollView(/*@START_MENU_TOKEN@*/.vertical/*@END_MENU_TOKEN@*/, showsIndicators: false) {
                    VStack(spacing: 0) {
                        ForEach(1..<7) { i in
                            MCListRow(data: vm.listData[i])
                            Divider()
                        }
                        MCListTitleRow(title: vm.titleData[1])
                        ForEach(7..<15) { i in
                            MCListRow(data: vm.listData[i])
                            Divider()
                        }
                        MCListTitleRow(title: vm.titleData[2])
                        ForEach(15..<vm.listData.count) { i in
                            MCListRow(data: vm.listData[i])
                            Divider()
                        }
                    }
                }
                .gesture(
                   DragGesture().onChanged { value in
                      if value.translation.height > 0 {
                        self.direction = false
                      } else {
                        self.direction = true
                      }
                   }
                )
            }
            .navigationTitle("多列列表")
            .coordinateSpace(name: "VSTACK")
            .onPreferenceChange(ListRowPreferenceKey.self) { preferences in
                
                // 获得第一行标题栏中心点的Y坐标值
                let fixedMidY = preferences[0].rect.midY

                let sequence = (fixedMidY - offset...fixedMidY + offset)
                
                for i in 1..<preferences.count {
         
                    // 判断其他标题栏是否穿过第一行标题栏的位置
                    if sequence.contains(preferences[i].rect.midY) {
                        // 更新第一行标题栏显示
                        if self.direction {
                            // 向上滚动情况
                            DispatchQueue.main.async {
                                self.indexHeader = i
                            }
                            
                        } else {
                            // 向下滚动情况
                            DispatchQueue.main.async {
                                self.indexHeader = i - 1 < 0 ? 0 : i - 1
                            }
                        }
                    }
                }
            }
        }
        .onAppear {
            self.indexHeader = 0
        }
    }
}

struct MultiColumnList_Previews: PreviewProvider {
    static var previews: some View {
        MultiColumnList()
    }
}

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-31 15:33:43  更:2021-08-31 15:35:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年5日历 -2025/5/3 23:13:56-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码