繁体   English   中英

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 的代码进行了一些更改,改进了结果。

  1. 我不希望主列表包含没有内容的标题,因此在实现中 List { 下的行变为:

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

    而不是

    ForEach(alphabet, id: \.self) { letter in
  2. 为索引列设置一个背景和统一的宽度,可以将它从任何背景中分离出来并统一结果:

     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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM