簡體   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