SwiftUI基础篇List

List

概述

文章主要分享SwiftUI Modifier的学习过程,将使用案例的方式进行说明。内容浅显易懂,List部分没有展示部分调试结果展示,不过测试代码是齐全的。如果想要运行结果,可以移步Github下载code -> github案例链接

1、List详细解释

SwiftUI的List视图类似于UITableView,可以根据需求显示静态的列表视图。对比于UITableView使用也更加简单,不需要通过Storyboard或者在代码中register,也不需要告诉它有多少行,也不需要去手动排列和配置cell。

相反,SwiftUI的List是为可组合性而设计的--设计成能够从小的东西构建更大的东西。所以,SwiftUI没有一个大的视图控制器,而是通过构建小的视图进行堆叠。

就代码大小而言,如果不考虑其他因素,差异是惊人的--可以删除几乎所有想象中的UITableView的视图代码,但仍然能得到那种很好的UI和感觉。

Swift 复制代码
List {
    Text("Hello, World!")
}

调试结果

仅3行代码就完成了UITableView的工作。

2、创建static List

要创建项目的静态列表,首先要定义列表中每一行应该是什么样子。这是一个与其他View一样的View,计划在行中显示的数据提供任何参数。一旦有了row,就可以创建一个List视图,该视图可以根据需要创建任意多的行。

Swift 复制代码
struct Pizzeria: View {
    let name: String
    
    var body: some View {
        Text("Restaurant: \(name)")
    }
}

struct FFListStaticitems: View {
    var body: some View {
        //例如,定义一个Pizzeria视图,它将显示一个名称字符串,然后将其用作带有三个固定数据快的Lists的一行。
        List {
            Pizzeria(name: "Joe's Original")
            Pizzeria(name: "The Real Joe's Original")
            Pizzeria(name: "Original Joe's")
        }
    }
}

调试结果

3、创建Dynamic List

为了处理动态项,必须告诉SwiftUI它如何识别哪个item是哪个。这可以通过手动指定标识属性或使用可识别的协议(Identifiable)来完成。协议只有一个要求,即类型必须具有某种类型的id值,SwiftUI可以使用id来标记。

Swift 复制代码
struct Restaurant: Identifiable {
    let id = UUID()
    let name: String
}

struct RestaurantRow: View {
    let restaurant: Restaurant
    
    var body: some View {
        Text("Come and eat at \(restaurant.name)")
    }
}

struct FFListDynamicItems: View {
    let restaurants = [
        Restaurant(name: "Joe's Original"),
        Restaurant(name: "The Real Joe's Original"),
        Restaurant(name: "Original Joe's")
    ]
    
    var body: some View {
        List(restaurants) { restaurant in
            RestaurantRow(restaurant: restaurant)
        }
    }
    //上面大部分的操作都是准备数据,最后一步才是真正的操作,使用List(restaurants)从restaurants数组
    //创建一个列表,对数组中的每个item执行一次闭包,通过RestaurantRow(restaurant: restaurant)创建cell
}

4、删除List中的cell

SwiftUI提供了deleteDisabled()修饰符可以从列表中删除cell。如果只想滑动从数组中删除项,而不添加任何额外的逻辑,那么简单的方法非常有效。使用与列表的数据绑定,并传入editActions参数

4.1、基础侧滑删除

Swift 复制代码
@State private var users = ["Glenn", "Malcolm", "Nicola", "Terri"]

//可以立即滑动来删除行,并且用户数组将更新,如果想移动cell,将.delete 改成.all
List($users, id: \.self, editActions: .delete) { $user in
    Text(user)
}

4.2、deleteDisabled()条件删除

Swift 复制代码
@State private var users = ["Glenn", "Malcolm", "Nicola", "Terri"]

//删除也可以使用deleteDisabled()和任何条件。例如,用户可以随意的删除,但至少满足row > 1
List($users, id: \.self, editActions: .delete) { $user in
    Text(user)
        .deleteDisabled(users.count < 2)
}

4.3、onDelete删除

通常会想要调用Swift的remove(at Offsets:)方法从你的序列中删除行。因为SwiftUI正在监视你的状态,你所做的任何更改都会自动反应在你的UI中。例如,创建一个包含三个项目的ContentView结构体,然后附加一个onDelete(perform:)修饰符,从列表中删除row

Swift 复制代码
@State private var users1 = ["BBlv", "Taylor", "Paul"]

//对于更复杂的删除方法,将onDelete(perform:)修饰符附加到列表中的ForEach,并让它发生在删除操作时调用这个方法,这个处理需要有一个特定的函数来接受多个索引来删除
func delete(at offsets: IndexSet) {
    users1.remove(atOffsets: offsets)
}

List {
    ForEach(users1, id: \.self) { user in
        Text(user)
    }
    .onDelete(perform: { indexSet in
        delete(at: indexSet)
    })
    .navigationTitle("Users")
}

onDelete()作为ForEach的修饰符,不能直接用于List,因为此修饰符是在DynamicViewContent中定义的

5、移动List中的cell

SwiftUI的moveDisabled()修饰符可以移动行,类似于deleteDisabled()修饰符

Swift 复制代码
struct FFListMoveRows: View {
    @State private var users = ["Glenn", "Malcolm", "Nicola", "Terri"]
    @State private var usres1 = ["BBlv", "Taylor", "Paul"]
    var body: some View {
        
        List($users, id: \.self, editActions: .move) { $user in
            Text(user)
        }
        
        //通过moveDisable()修饰符和条件来移动rows
        List($users, id: \.self, editActions: .move) { $user in
            Text(user)
                .moveDisabled(user == "Glenn")
        }
        
        NavigationStack {
            List {
                ForEach(users, id: \.self) { user in
                    Text(user)
                }
                .onMove(perform: { indices, newOffset in
                    move(from: indices, to: newOffset)
                })
            }
            .toolbar {
                EditButton()
            }
        }
    }
    //对于更复杂的move方法,将onMove(preform:)修饰符附加到list中
    func move(from source: IndexSet, to destination: Int) {
        users.move(fromOffsets: source, toOffset: destination)
    }
}

6、List添加Section

创建一个cell备用

Swift 复制代码
struct TaskRow: View {
    var body: some View {
        Text("Task data goes here")
    }
}

6.1、创建附加header的section

Swift 复制代码
List {
    Section {
        TaskRow()
        TaskRow()
        TaskRow()
    } header: {
        Text("Important tasks")
    }
    
    Section {
        TaskRow()
        TaskRow()
        TaskRow()
    } header: {
        Text("Others tasks")
    }
}

6.2、创建附加footer的section

Swift 复制代码
Section {
    Text("Row 1")
    Text("Row 2")
    Text("Row 3")
} footer: {
    Text("End")
}

6.3、headerProminence()修改section的header

默认情况下,section header匹配默认的iOS样式,可以通过headerProminence()修饰符更改

Swift 复制代码
Section {
    TaskRow()
} header: {
    Text("Header")
}
.headerProminence(.increased)

调试结果

7、设置cell的背景颜色

SwiftUI有一个专门的修饰符来设置cell的背景视图,是listRowBackground()。它接受任何类型的视图--包括颜色、图像和形状。

Swift 复制代码
struct SwiftListListRowBackground: View {
    var body: some View {
        List {
            ForEach(0..<10) {
                Text("Row \($0)")
            }
            .listRowBackground(Color.green)
        }
    }
}

调试结果

8、通过listStyle创建List

SwiftUI的列表视图有一个listStyle()修饰符来控制list的样式:

  • grouped
  • insetGrouped
Swift 复制代码
struct ExampleRow: View {
    var body: some View {
        Text("Example Row")
    }
}

struct FFListInsetGroup: View {
    var body: some View {
        //.grouped
        List {
            Section {
                ExampleRow()
                ExampleRow()
                ExampleRow()
            } header: {
                Text("Examples")
            }
        }
        .listStyle(.grouped)
        
        //insetGrouped
        List(0..<100) { i in
            Text("Row \(i)")
        }
        .listStyle(.insetGrouped)
    }
}

9、创建可折叠的List

SwiftUI的List视图有一个增强初始化器,可以创建带有子节点的扩展--以可以点击的箭头的方式呈现,当点击时,箭头转动,显示子节点。要使用这种形式的List,需要有精确的数据,数据模型应该拥有相同类型的可选子节点,这样可以创建树。通常情况下,可能会从JSON或类似的地方加载这类数据。

Swift 复制代码
struct Bookmark: Identifiable {
    let id = UUID()
    let name: String
    let icon: String
    let items: [Bookmark]?
    
    static let apple = Bookmark(name: "Apple", icon: "1.circle", items: nil)
    static let bbc = Bookmark(name: "BBC", icon: "square.and.pencil", items: nil)
    static let swift = Bookmark(name: "Swift", icon: "bolt.fill", items: nil)
    static let twitter = Bookmark(name: "Twitter", icon: "mic", items: nil)
    
    static let example1 = Bookmark(name: "Favorites", icon: "star", items: [Bookmark.swift, Bookmark.apple, Bookmark.bbc, Bookmark.swift, Bookmark.twitter])
    static let example2 = Bookmark(name: "Recent", icon: "timer", items: [Bookmark.apple, Bookmark.bbc, Bookmark.twitter])
    static let example3 = Bookmark(name: "Recommended", icon: "hand.thumbsup", items: [Bookmark.apple, Bookmark.swift, Bookmark.bbc,  Bookmark.twitter])
}

struct FFListExpanding: View {
    let items: [Bookmark] = [.example1, .example2, .example3]
    var body: some View {
        List(items, children: \.items) { row in
            HStack {
                Image(systemName: row.icon)
                Text(row.name)
            }
        }
        
    }
}

调试结果

10、滚动到list中的特定cell

让SwiftUI的List移动来显示特定的行。应该把它嵌入到ScrollViewReader中。这在其代理上提供了一个scrollTo()方法,该方法可以移动到列表中的任意行,只需要提供id和锚点。

10.1、scrollTo

Swift 复制代码
ScrollViewReader(content: { proxy in
    VStack {
        Button("Jump to #50") {
            proxy.scrollTo(50)
        }
        
        List(0..<100, id: \.self) { i in
            Text("Example \(i)")
                .id(i)
        }
    }
})

10.2、anchor

将锚点标记为.top

Swift 复制代码
ScrollViewReader(content: { proxy in
    VStack {
        Button("Jump to #50") {
            proxy.scrollTo(50, anchor: .top)
        }
        
        List(0..<100, id: \.self) { i in
            Text("Example \(i)")
                .id(i)
        }
    }
})

10.3、withAnimation

将锚点标记为.top,以动画效果过渡

Swift 复制代码
ScrollViewReader(content: { proxy in
    VStack {
        Button("Jump to #50") {
            withAnimation {
                proxy.scrollTo(50, anchor: .top)
            }
        }
        
        List(0..<100, id: \.self) { i in
            Text("Example \(i)")
                .id(i)
        }
    }
})

11、List支持多选

SwiftUI的list支持多选,但仅限于编辑模式之下。为了支持多选,首先添加一个与列表中使用的类型相同的可选属性集合。例如,如果一个整数list,那么将有一个可选的Int。使用他的选择参数将它传递给list,确保list处于编辑模式。

Swift 复制代码
struct FFListSelectionRow: View {
    @State private var selection = Set<String>()
    let names = ["BBLv", "Cyril", "Lana", "Mallory", "Sterling"]
    
    var body: some View {
        NavigationStack {
            List(names, id: \.self, selection: $selection) { name in
                Text(name)
            }
            .navigationTitle("List Selection")
            .toolbar {
                EditButton()
            }
        }
    }
}

12、List的分隔符的隐藏和颜色

SwiftUI提供了两个修饰符来控制其列表中行分隔符的外观:

  • listRowSeparator()用于控制行分隔符是否可见,
  • listRowSeparatorTint()用于控制分隔符的颜色
Swift 复制代码
struct FFListSeparator: View {
    var body: some View {
        //隐藏行分隔符
        List {
            ForEach(1..<100) { index in
                Text("Row \(index)")
                    .listRowSeparator(.hidden)
            }
        }
        //设置分隔符颜色
        List {
            ForEach(1..<100) { index in
                Text("Row \(index)")
                    .listRowSeparatorTint(.red)
            }
        }
    }
}

13、List的下拉刷新

SwiftU的refreshable()修饰符可以附加到list上,当发生拖拽且有一段距离时触发。只要的你的代码完成运行,iOS会自动显示一个菊花。

Swift 复制代码
struct NewsItem: Decodable, Identifiable {
    let id: Int
    let title: String
    let strap: String
}

struct FFListPullRefresh: View {
    @State private var news = [
        NewsItem(id: 0, title: "Want the latest news?", strap: "Pull to refresh!")
    ]
    
    var body: some View {
        NavigationStack {
            List(news) { item in
                VStack(alignment: .leading) {
                    Text(item.title)
                        .font(.headline)
                    Text(item.strap)
                        .foregroundStyle(.secondary)
                }
            }
            .refreshable {
                do {
                    let url = URL(string: "https://www.hackingwithswift.com/samples/news-1.json")!
                    let (data, _) = try await URLSession.shared.data(from: url)
                    news = try JSONDecoder().decode([NewsItem].self, from: data)
                } catch {
                    news = []
                }
            }
        }
    }
}

调试结果

由于加载的速度有一点快,中间菊花没截到。

14、Cell添加自定义侧滑Button

SwiftUI的swipeActions()修饰符可以添加一个或多个滑动button到cell,可选的控制它们属于哪一边,以及它们是否应该被触发使用一个完整的滑动。

14.1、swipeActions

button使用了tint进行了作色,如果未使用,默认是灰色的

Swift 复制代码
Text("Pepperoni pizza")
    .swipeActions {
        Button("Order") {
            print("Awesome!")
        }
        .tint(.green)
    }
Text("pepperoni with pineapple")
    .swipeActions {
        Button("Burn") {
            print("Right on")
        }
        .tint(.red)
    }

14.2、allowsFullSwipe

默认情况下,如果滑动足够远,将自动触发第一个滑动动作。如果想要禁用,在创建滑动时将allowsFullSwipe设置为false。

Swift 复制代码
let friends = ["Antoine", "Bas", "Curt", "Dave", "Erica"]

ForEach(friends, id: \.self) { friend in
    Text(friend)
        .swipeActions(allowsFullSwipe:false) {
            Button {
                print("Muting conversation")
            } label: {
                Label("Mute", systemImage: "bell.slash.fill")
            }
            .tint(.indigo)
            
            Button(role: .destructive) {
                print("Deleting conversation")
            } label: {
                Label("Delete", systemImage: "trash.fill")
            }

        }
}

对于真正删除性质的按钮,应该通过role设定,避免通过tint指定红色

14.3、swipeActions(edge: .leading)

如果想要在cell的两端都添加不同的滑动操作,只需要了用两次swipeActions在边缘

Swift 复制代码
@State private var total = 0

Section {
    ForEach(1..<100) { i in
        Text("\(i)")
            .swipeActions(edge: .leading) {
                Button {
                    total += i
                } label: {
                    Label("Add \(i)", systemImage: "plus.circle")
                }
                .tint(.indigo)
            }
            .swipeActions(edge: .trailing) {
                Button {
                    total -= i
                } label: {
                    Label("Subtract \(i)", systemImage: "minus,circle")
                }
            }
    }
} header: {
    Text("Total: \(total)")
}
.headerProminence(.increased)

调试结果

15、List的Binding

SwiftUI可以直接从绑定中创建List或ForEach,然后提供内容闭包,显示的数据中的每个元素都提供单独的绑定

Swift 复制代码
struct UserA: Identifiable {
    let id = UUID()
    let name: String
    var isContacted = false
}

struct FFListForEach: View {
    @State private var users = [
        UserA(name: "Taylor"),
        UserA(name: "Justin"),
        UserA(name: "Adele")
    ]
    var body: some View {
        List($users) { $user in
            HStack {
                Text(user.name)
                Spacer()
                Toggle("User has been contacted", isOn: $user.isContacted)
                    .labelsHidden()
            }
        }
    }
}

以这种方式使用绑定是修改列表的最有效方式,因为他不会在只有一个项更改时导致整个视图重新加载

调试结果

相关推荐
小溪彼岸2 天前
【iOS小组件】小组件尺寸及类型适配
swiftui·swift
文件夹__iOS7 天前
[SwiftUI 开发] @dynamicCallable 与 callAsFunction:将类型实例作为函数调用
ios·swiftui·swift
小溪彼岸7 天前
【iOS小组件】iOS17与低版本兼容适配
swiftui·swift
Mamong8 天前
SwiftUI疑难杂症(1):sheet content多次执行
ios·swiftui·swift
AUV110711 天前
Mac剪贴板历史全记录!
macos·swiftui·mac·效率工具·实用工具·剪贴板·clipboard
AUV110712 天前
Mac 上哪个剪切板增强工具比较好用? 好用剪切板工具推荐
macos·swiftui·mac·剪贴板·clipboard·剪贴板增强·app 推荐
多彩电脑14 天前
SwiftUI里的ForEach使用的注意事项
macos·ios·swiftui·swift
Swift社区15 天前
Apple 新品发布会亮点有哪些 | Swift 周报 issue 61
ios·swiftui·swift
humiaor17 天前
Xcode报错:No exact matches in reference to static method ‘buildExpression‘
swiftui·xcode
humiaor25 天前
Xcode报错:Return from initializer without initializing all stored properties
swiftui·binding