![](/img/trans.png)
[英]How to create an alphabetical section index for a List in SwiftUI?
[英]SwiftUI List with Section Index on right hand side?
是否有可能在右侧有一个带有索引的列表,如下面 SwiftUI 中的示例?
我一直在寻找相同问题的解决方案,但目前我们可能拥有的唯一选择是使用UITableView
作为视图。
import SwiftUI
import UIKit
struct TableView: UIViewRepresentable {
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
final class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellId = "cellIdentifier"
let cell = tableView.dequeueReusableCell(withIdentifier: cellId) ?? UITableViewCell(style: .default, reuseIdentifier: cellId)
cell.textLabel?.text = "\(indexPath)"
return cell
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
["a", "b"]
}
}
我在 SwiftUI 中做了这个
//
// Contacts.swift
// TestCalendar
//
// Created by Christopher Riner on 9/11/20.
//
import SwiftUI
struct Contact: Identifiable, Comparable {
static func < (lhs: Contact, rhs: Contact) -> Bool {
return (lhs.lastName, lhs.firstName) < (rhs.lastName, rhs.firstName)
}
var id = UUID()
let firstName: String
let lastName: String
}
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
struct Contacts: View {
@State private var searchText = ""
var contacts = [Contact]()
var body: some View {
VStack {
ScrollViewReader { scrollProxy in
ZStack {
List {
SearchBar(searchText: $searchText)
.padding(EdgeInsets(top: 0, leading: -20, bottom: 0, trailing: -20))
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter).id(letter)) {
ForEach(contacts.filter({ (contact) -> Bool in
contact.lastName.prefix(1) == letter
})) { contact in
HStack {
Image(systemName: "person.circle.fill").font(.largeTitle).padding(.trailing, 5)
Text(contact.firstName)
Text(contact.lastName)
}
}
}
}
}
.navigationTitle("Contacts")
.listStyle(PlainListStyle())
.resignKeyboardOnDragGesture()
VStack {
ForEach(alphabet, id: \.self) { letter in
HStack {
Spacer()
Button(action: {
print("letter = \(letter)")
//need to figure out if there is a name in this section before I allow scrollto or it will crash
if contacts.first(where: { $0.lastName.prefix(1) == letter }) != nil {
withAnimation {
scrollProxy.scrollTo(letter)
}
}
}, label: {
Text(letter)
.font(.system(size: 12))
.padding(.trailing, 7)
})
}
}
}
}
}
}
}
init() {
contacts.append(Contact(firstName: "Chris", lastName: "Ryan"))
contacts.append(Contact(firstName: "Allyson", lastName: "Ryan"))
contacts.append(Contact(firstName: "Jonathan", lastName: "Ryan"))
contacts.append(Contact(firstName: "Brendan", lastName: "Ryaan"))
contacts.append(Contact(firstName: "Jaxon", lastName: "Riner"))
contacts.append(Contact(firstName: "Leif", lastName: "Adams"))
contacts.append(Contact(firstName: "Frank", lastName: "Conors"))
contacts.append(Contact(firstName: "Allyssa", lastName: "Bishop"))
contacts.append(Contact(firstName: "Justin", lastName: "Bishop"))
contacts.append(Contact(firstName: "Johnny", lastName: "Appleseed"))
contacts.append(Contact(firstName: "George", lastName: "Washingotn"))
contacts.append(Contact(firstName: "Abraham", lastName: "Lincoln"))
contacts.append(Contact(firstName: "Steve", lastName: "Jobs"))
contacts.append(Contact(firstName: "Steve", lastName: "Woz"))
contacts.append(Contact(firstName: "Bill", lastName: "Gates"))
contacts.append(Contact(firstName: "Donald", lastName: "Trump"))
contacts.append(Contact(firstName: "Darth", lastName: "Vader"))
contacts.append(Contact(firstName: "Clark", lastName: "Kent"))
contacts.append(Contact(firstName: "Bruce", lastName: "Wayne"))
contacts.append(Contact(firstName: "John", lastName: "Doe"))
contacts.append(Contact(firstName: "Jane", lastName: "Doe"))
contacts.sort()
}
}
struct Contacts_Previews: PreviewProvider {
static var previews: some View {
Contacts()
}
}
看看Federico Zanetello 的这个教程,它是一个 100% SwiftUI 解决方案。
完整代码(作者:Federico Zanetello):
let database: [String: [String]] = [
"iPhone": [
"iPhone", "iPhone 3G", "iPhone 3GS", "iPhone 4", "iPhone 4S", "iPhone 5", "iPhone 5C", "iPhone 5S", "iPhone 6", "iPhone 6 Plus", "iPhone 6S", "iPhone 6S Plus", "iPhone SE", "iPhone 7", "iPhone 7 Plus", "iPhone 8", "iPhone 8 Plus", "iPhone X", "iPhone Xs", "iPhone Xs Max", "iPhone Xʀ", "iPhone 11", "iPhone 11 Pro", "iPhone 11 Pro Max", "iPhone SE 2"
],
"iPad": [
"iPad", "iPad 2", "iPad 3", "iPad 4", "iPad 5", "iPad 6", "iPad 7", "iPad Air", "iPad Air 2", "iPad Air 3", "iPad Mini", "iPad Mini 2", "iPad Mini 3", "iPad Mini 4", "iPad Mini 5", "iPad Pro 9.7-inch", "iPad Pro 10.5-inch", "iPad Pro 11-inch", "iPad Pro 11-inch 2", "iPad Pro 12.9-inch", "iPad Pro 12.9-inch 2", "iPad Pro 12.9-inch 3", "iPad Pro 12.9-inch 4"
],
"iPod": [
"iPod Touch", "iPod Touch 2", "iPod Touch 3", "iPod Touch 4", "iPod Touch 5", "iPod Touch 6"
],
"Apple TV": [
"Apple TV 2", "Apple TV 3", "Apple TV 4", "Apple TV 4K"
],
"Apple Watch": [
"Apple Watch", "Apple Watch Series 1", "Apple Watch Series 2", "Apple Watch Series 3", "Apple Watch Series 4", "Apple Watch Series 5"
],
"HomePod": [
"HomePod"
]
]
struct HeaderView: View {
let title: String
var body: some View {
Text(title)
.font(.title)
.fontWeight(.bold)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct RowView: View {
let text: String
var body: some View {
Text(text)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct ContentView: View {
let devices: [String: [String]] = database
var body: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
devicesList
}
}
.overlay(sectionIndexTitles(proxy: proxy))
}
.navigationBarTitle("Apple Devices")
}
var devicesList: some View {
ForEach(devices.sorted(by: { (lhs, rhs) -> Bool in
lhs.key < rhs.key
}), id: \.key) { categoryName, devicesArray in
Section(
header: HeaderView(title: categoryName)
) {
ForEach(devicesArray, id: \.self) { name in
RowView(text: name)
}
}
}
}
func sectionIndexTitles(proxy: ScrollViewProxy) -> some View {
SectionIndexTitles(proxy: proxy, titles: devices.keys.sorted())
.frame(maxWidth: .infinity, alignment: .trailing)
.padding()
}
}
struct SectionIndexTitles: View {
let proxy: ScrollViewProxy
let titles: [String]
@GestureState private var dragLocation: CGPoint = .zero
var body: some View {
VStack {
ForEach(titles, id: \.self) { title in
SectionIndexTitle(image: sfSymbol(for: title))
.background(dragObserver(title: title))
}
}
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .global)
.updating($dragLocation) { value, state, _ in
state = value.location
}
)
}
func dragObserver(title: String) -> some View {
GeometryReader { geometry in
dragObserver(geometry: geometry, title: title)
}
}
func dragObserver(geometry: GeometryProxy, title: String) -> some View {
if geometry.frame(in: .global).contains(dragLocation) {
DispatchQueue.main.async {
proxy.scrollTo(title, anchor: .center)
}
}
return Rectangle().fill(Color.clear)
}
func sfSymbol(for deviceCategory: String) -> Image {
let systemName: String
switch deviceCategory {
case "iPhone": systemName = "iphone"
case "iPad": systemName = "ipad"
case "iPod": systemName = "ipod"
case "Apple TV": systemName = "appletv"
case "Apple Watch": systemName = "applewatch"
case "HomePod": systemName = "homepod"
default: systemName = "xmark"
}
return Image(systemName: systemName)
}
}
struct SectionIndexTitle: View {
let image: Image
var body: some View {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.foregroundColor(Color.gray.opacity(0.1))
.frame(width: 40, height: 40)
.overlay(
image
.foregroundColor(.blue)
)
}
}
请参阅 DirectX 在此页面上提供的解决方案,并考虑给予支持。 这是正确的答案。
我采用了他的 View 并创建了一个 ViewModifier,您可以将其与包含带有部分的 SwiftUI 列表(表格视图)的任何视图一起使用。
只需确保提供与要添加索引的视图中的标题相对应的 header(部分)标题列表。 单击字母滚动到列表的该部分。 请注意,我只提供了在调用视图修饰符时可以实际滚动到的索引。
像任何视图修饰符一样使用:
SimpleDemoView().modifier(VerticalIndex(indexableList: contacts))
这是修改器的代码:
struct VerticalIndex: ViewModifier {
let indexableList: [String]
func body(content: Content) -> some View {
var body: some View {
ScrollViewReader { scrollProxy in
ZStack {
content
VStack {
ForEach(indexableList, id: \.self) { letter in
HStack {
Spacer()
Button(action: {
withAnimation {
scrollProxy.scrollTo(letter)
}
}, label: {
Text(letter)
.font(.system(size: 12))
.padding(.trailing, 7)
})
}
}
}
}
}
}
return body
}
}
使用 DirectX 提供的示例如下所示:
为了完整起见,这里是重现显示的代码:
struct SimpleDemo_Previews: PreviewProvider {
static var previews: some View {
SimpleDemoView().modifier(VerticalIndex(indexableList: contacts))
}
}
struct SimpleDemoView: View {
var body: some View {
List {
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter).id(letter)) {
ForEach(contacts.filter({ (contact) -> Bool in
contact.lastName.prefix(1) == letter
})) { contact in
HStack {
Image(systemName: "person.circle.fill").font(.largeTitle).padding(.trailing, 5)
Text(contact.firstName)
Text(contact.lastName)
}
}
}
}
}
.navigationTitle("Contacts")
.listStyle(PlainListStyle())
}
}
这是用于提供演示的示例数据(根据 DirectX 的解决方案修改):
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"] //swiftlint:disable comma
let contacts: [Contact] = {
var contacts = [Contact]()
contacts.append(Contact(firstName: "Chris", lastName: "Ryan"))
contacts.append(Contact(firstName: "Allyson", lastName: "Ryan"))
contacts.append(Contact(firstName: "Jonathan", lastName: "Ryan"))
contacts.append(Contact(firstName: "Brendan", lastName: "Ryaan"))
contacts.append(Contact(firstName: "Jaxon", lastName: "Riner"))
contacts.append(Contact(firstName: "Leif", lastName: "Adams"))
contacts.append(Contact(firstName: "Frank", lastName: "Conors"))
contacts.append(Contact(firstName: "Allyssa", lastName: "Bishop"))
contacts.append(Contact(firstName: "Justin", lastName: "Bishop"))
contacts.append(Contact(firstName: "Johnny", lastName: "Appleseed"))
contacts.append(Contact(firstName: "George", lastName: "Washingotn"))
contacts.append(Contact(firstName: "Abraham", lastName: "Lincoln"))
contacts.append(Contact(firstName: "Steve", lastName: "Jobs"))
contacts.append(Contact(firstName: "Steve", lastName: "Woz"))
contacts.append(Contact(firstName: "Bill", lastName: "Gates"))
contacts.append(Contact(firstName: "Donald", lastName: "Trump"))
contacts.append(Contact(firstName: "Darth", lastName: "Vader"))
contacts.append(Contact(firstName: "Clark", lastName: "Kent"))
contacts.append(Contact(firstName: "Bruce", lastName: "Wayne"))
contacts.append(Contact(firstName: "John", lastName: "Doe"))
contacts.append(Contact(firstName: "Jane", lastName: "Doe"))
return contacts.sorted()
}()
let indexes = Array(Set(contacts.compactMap({ String($0.lastName.prefix(1)) }))).sorted()
我对@Mozahler 和@DirectX 的代码进行了一些更改,改进了结果。
我不希望主列表包含没有内容的标题,因此在实现中 List { 下的行变为:
ForEach(indexes, id: \.self) { letter in
而不是
ForEach(alphabet, id: \.self) { letter in
为索引列设置一个背景和统一的宽度,可以将它从任何背景中分离出来并统一结果:
Text(letter).frame(width: 16).foregroundColor(Constants.color.textColor).background(Color.secondary.opacity(0.5)).font(Constants.font.customFootnoteFont).padding(.trailing, 7)
如果你需要一个符合UITableViewDataSource, UITableViewDelegate
协议的 class,那么:
import SwiftUI
struct SelectRegionView: View {
var body: some View {
TableWithIndexView(sectionItems: [["Alex", "Anna"], ["John"]], sectionTitles: ["A", "J"])
}
}
#if DEBUG
struct SelectRegionView_Previews: PreviewProvider {
static var previews: some View {
SelectRegionView()
}
}
#endif
struct TableWithIndexView<T: CustomStringConvertible>: UIViewRepresentable {
/// the items to show
public var sectionItems = [[T]]()
/// the section titles
public var sectionTitles = [String]()
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView(frame: .zero, style: .plain)
let coordinator = context.coordinator
coordinator.sectionTitles = sectionTitles
coordinator.sectionItemCounts = sectionItems.map({$0.count})
// Create cell for given `indexPath`
coordinator.createCell = { tableView, indexPath -> UITableViewCell in
let cellId = "cellIdentifier"
let cell = tableView.dequeueReusableCell(withIdentifier: cellId) ?? UITableViewCell(style: .default, reuseIdentifier: cellId)
cell.textLabel?.text = "\(sectionItems[indexPath.section][indexPath.row])"
return cell
}
tableView.delegate = coordinator
tableView.dataSource = coordinator
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
final class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
/// the items to show
fileprivate var createCell: ((UITableView, IndexPath)->(UITableViewCell))?
fileprivate var sectionTitles = [String]()
fileprivate var sectionItemCounts = [Int]()
/// Section titles
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sectionTitles[section]
}
/// Number of sections
func numberOfSections(in tableView: UITableView) -> Int {
return sectionTitles.count
}
/// Number of rows in a section
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
sectionItemCounts[section]
}
/// Cell for indexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellId = "cellIdentifier"
return createCell?(tableView, indexPath) ?? UITableViewCell(style: .default, reuseIdentifier: cellId)
}
/// Section index title
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
/// Get first letters
return sectionTitles.map({ String($0.first!).lowercased() })
}
}
}
我喜欢这个答案: https://stackoverflow.com/a/63996814/1695772 ,所以如果你赞成这个,也给他/她一个赞。 ;)
import SwiftUI
struct AlphabetSidebarView: View {
var listView: AnyView
var lookup: (String) -> (any Hashable)?
let alphabet: [String] = {
(65...90).map { String(UnicodeScalar($0)!) }
}()
var body: some View {
ScrollViewReader { scrollProxy in
ZStack {
listView
HStack(alignment: .center) {
Spacer()
VStack(alignment: .center) {
ForEach(alphabet, id: \.self) { letter in
Button(action: {
if let found = lookup(letter) {
withAnimation {
scrollProxy.scrollTo(found)
}
}
}, label: {
Text(letter)
.font(.caption)
.padding(.trailing, 4)
})
}
}
}
}
}
}
}
像这样使用它:
AlphabetSidebarView(listView: AnyView(contactsListView)) { letter in
contacts.first { $0.name.prefix(1) == letter }
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.