[英]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.