右手边有章节索引的SwiftUI列表?

7cwmlq89  于 2023-01-16  发布在  Swift
关注(0)|答案(7)|浏览(77)

有没有可能在右手边有一个索引的列表,就像下面SwiftUI中的例子一样?

cngwdvgl

cngwdvgl1#

我在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()
    }
}
y3bcpkx1

y3bcpkx12#

看看这个tutorial by Federico Zanetello,它是一个100%的SwiftUI解决方案。

最终结果:

完整代码(作者:费德里科·萨内泰罗):

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)
      )
  }
}
roqulrg3

roqulrg33#

我一直在寻找同一问题的解决方案,但目前我们唯一的选择是使用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"]
        }
    }

vulvrdjw

vulvrdjw4#

请参阅DirectX在本页提供的解决方案,并考虑投赞成票。这是正确答案。
我采用了他的视图并创建了一个ViewModifier,您可以将其用于任何包含SwiftUI List with Sections(表视图)的视图。
只需确保提供一个标题(节)列表,该列表与要添加索引的视图中的标题相对应。单击字母滚动到列表的该节。注意,我只提供了在调用视图修改器时实际上可以滚动到的索引。
像任何视图修改器一样使用:

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()
ebdffaop

ebdffaop5#

我对@Mozahler和@DirectX的代码做了一些修改,以优化结果。
1.我不希望主列表包含没有内容的头,所以在实现中List {下面的行变为:

ForEach(indexes, id: \.self) { letter in

而不是

ForEach(alphabet, id: \.self) { letter in

1.为索引列设置背景和统一宽度可使其与任何背景相分离并统一结果:

Text(letter)
    .frame(width: 16)
    .foregroundColor(Constants.color.textColor)
    .background(Color.secondary.opacity(0.5))
    .font(Constants.font.customFootnoteFont)
    .padding(.trailing, 7)
niknxzdl

niknxzdl6#

我喜欢这个答案:所以如果你赞成这个,也给予他/她一个赞成票。)

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, anchor: .top)
                  }
                }
              }, label: {
                Text(letter)
                  .foregroundColor(.label)
                  .minimumScaleFactor(0.5)
                  .font(.subheadline)
                  .padding(.trailing, 4)
              })
            }
          }
        }
      }
    }
  }
}

像这样使用它:

AlphabetSidebarView(listView: AnyView(contactsListView)) { letter in
  contacts.first { $0.name.prefix(1) == letter }
}
8ljdwjyq

8ljdwjyq7#

如果需要符合UITableViewDataSource, UITableViewDelegate协议的类,则:

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() })
        }
    }
}

相关问题