*** 由于未捕获的异常 'NSInternalInconsistencyException' 而终止应用程序,原因:'Fatal: supplied identifiers are not unique.' ***

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Fatal: supplied identifiers are not unique.' ***

大家好 ‍♂️我正在使用 UITableViewDiffableDataSource 将以下 JSON 解析为 UITableView 以获得漂亮的搜索动画。

JSON : https://www.pathofexile.com/api/trade/data/items

这是回购协议:https://github.com/laurentdelorme/PathOfData

从 JSON,我能够加载模型中映射的所有 13 个不同类别 file.I 然后能够将这些类别中的数据推送到另一个 tableView(一个使用 UITableViewDiffableDataSource) 并很好地显示所有内容。

但是,当我尝试将其内容推送到 DetailViewController 时,有一个类别会导致我的应用程序崩溃,即初始 ViewController 上的 "Maps" 类别。

这是我的模型:

struct ItemCategories: Codable {
    var result: [ItemCategory]
}

struct ItemCategory: Codable {
    var label: String
    var entries: [Item]
}

struct Item: Codable, Hashable {
    var name: String?
    var type: String?
    var text: String?
}

这是我的 ViewController:

import UIKit

class ViewController: UITableViewController {

    let urlString = "https://www.pathofexile.com/api/trade/data/items"
    var categories = [ItemCategory]()


    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Path of Data"
        navigationController?.navigationBar.prefersLargeTitles = true
        parseJSON()

        for family: String in UIFont.familyNames
        {
            print(family)
            for names: String in UIFont.fontNames(forFamilyName: family)
            {
                print("== \(names)")
            }
        }
    }

    func parseJSON() {
        guard let url = URL(string: urlString) else { return }
        guard let data = try? Data(contentsOf: url) else { return }

        let decoder = JSONDecoder()

        guard let jsonItemCategories = try? decoder.decode(ItemCategories.self, from: data) else { return }

        categories = jsonItemCategories.result
        tableView.reloadData()
    }


    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return categories.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        var categoryName = categories[indexPath.row].label
        if categoryName == "" { categoryName = "Unknown" }
        cell.textLabel?.text = categoryName

        let font = UIFont(name: "Fontin-SmallCaps", size: 30)
        cell.textLabel?.font = font
        cell.textLabel?.textColor = .systemOrange

        let numberOfItemsInCategory = String(categories[indexPath.row].entries.count)
        cell.detailTextLabel?.text = numberOfItemsInCategory + " items"
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

        if let vc = storyboard?.instantiateViewController(identifier: "Detail") as? DetailViewController {

            let listLabel: String? = categories[indexPath.row].label
            vc.title = listLabel

            let itemList = categories[indexPath.row].entries
            vc.items = itemList

            print(itemList)

            vc.category = categories[indexPath.row].label

            navigationController?.pushViewController(vc, animated: true)
        }
    }
}

这是详细信息ViewController:

import UIKit
import SafariServices

class DetailViewController: UITableViewController {

    enum Section {
        case main
    }

    var category: String!
    var items: [Item] = []
    var transformedItems: [Item] = []
    var filteredItems: [Item] = []

    var isSearching: Bool = false

    var dataSource: UITableViewDiffableDataSource<Section,Item>!

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationController?.navigationBar.prefersLargeTitles = true
        navigationController?.navigationBar.tintColor = .systemOrange
        replacenNilNameFor(items: items)
        configureDataSource()
        updateData(on: items)
        congifureSearchController()
    }

    func replacenNilNameFor(items: [Item]) {
        for item in items {
            if item.name == nil {
                guard item.type != nil else { return }
                let newItem = Item(name: item.type, type: nil, text: nil)
                transformedItems.append(newItem)
            } else {
                transformedItems.append(item)
            }
        }
        self.items = transformedItems
    }

    func configureDataSource() {
        dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: self.tableView, cellProvider: { tableView, indexPath, item -> UITableViewCell? in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Detail", for: indexPath)
            cell.textLabel?.text = item.name
            cell.detailTextLabel?.text = item.type

            let font = UIFont(name: "Fontin-SmallCaps", size: 25)
            cell.textLabel?.font = font
            cell.textLabel?.textColor = self.setLabelColor(for: self.category)

            return cell
        })
    }

    func setLabelColor(for category: String) -> UIColor {
        switch category {
        case "Prophecies":
            return UIColor(red: 0.6471, green: 0.1569, blue: 0.7569, alpha: 1.0)
        default:
            return UIColor(red: 0.6392, green: 0.549, blue: 0.4275, alpha: 1.0)
        }
    }

    func updateData(on items: [Item]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: true)
    }

    func congifureSearchController() {
        let searchController = UISearchController()
        searchController.searchResultsUpdater = self
        searchController.searchBar.placeholder = "Search for an item"
        searchController.searchBar.delegate = self
        navigationItem.searchController = searchController
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let endpoint = "https://pathofexile.gamepedia.com/"

        let activeArray = isSearching ? filteredItems : items
        let item = activeArray[indexPath.row]

        let url = URL(string: endpoint + formatNameFor(item: item))
        let sf = SFSafariViewController(url: url!)
        present(sf, animated: true)
    }

    func formatNameFor(item: Item) -> String {
        let name = item.name!
        let firstChange = name.replacingOccurrences(of: " ", with: "_")
        let secondChange = firstChange.replacingOccurrences(of: "'", with: "%27")
        return secondChange
    }
}




extension DetailViewController: UISearchResultsUpdating, UISearchBarDelegate {

    func updateSearchResults(for searchController: UISearchController) {
        guard let filter = searchController.searchBar.text, !filter.isEmpty else { return }
        isSearching = true
        filteredItems = items.filter { ([=14=].name?.lowercased().contains(filter.lowercased()))! }
        updateData(on: filteredItems)
    }

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        isSearching = false
        updateData(on: items)
    }
}

这是我尝试访问 "Maps" 类别时收到的错误消息:

2020-02-28 14:40:20.470098+0100 PathOfData[2789:224548] *** Assertion failure in -[_UIDiffableDataSourceUpdate initWithIdentifiers:sectionIdentifiers:action:desinationIdentifier:relativePosition:destinationIsSection:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3901.4.2/_UIDiffableDataSource.m:1417
2020-02-28 14:40:20.474313+0100 PathOfData[2789:224548] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Fatal: supplied identifiers are not unique.'
*** First throw call stack:
(
    0   CoreFoundation                      0x00000001069f327e __exceptionPreprocess + 350
    1   libobjc.A.dylib                     0x0000000105077b20 objc_exception_throw + 48
    2   CoreFoundation                      0x00000001069f2ff8 +[NSException raise:format:arguments:] + 88
    3   Foundation                          0x0000000104a9fb51 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
    4   UIKitCore                           0x0000000119c4dcdf -[_UIDiffableDataSourceUpdate initWithIdentifiers:sectionIdentifiers:action:desinationIdentifier:relativePosition:destinationIsSection:] + 725
    5   UIKitCore                           0x0000000119c4e04e -[_UIDiffableDataSourceUpdate initWithItemIdentifiers:appendingToDestinationSectionIdentifier:] + 90
    6   UIKitCore                           0x0000000119c43408 -[__UIDiffableDataSource appendItemsWithIdentifiers:intoSectionWithIdentifier:] + 165
    7   libswiftUIKit.dylib                 0x0000000105e9f061 $s5UIKit28NSDiffableDataSourceSnapshotV11appendItems_9toSectionySayq_G_xSgtF + 241
    8   PathOfData                          0x0000000104723b41 $s10PathOfData20DetailViewControllerC06updateC02onySayAA4ItemVG_tF + 369
    9   PathOfData                          0x000000010472231f $s10PathOfData20DetailViewControllerC11viewDidLoadyyF + 767
    10  PathOfData                          0x00000001047223db $s10PathOfData20DetailViewControllerC11viewDidLoadyyFTo + 43
    11  UIKitCore                           0x0000000119e22f01 -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 83
    12  UIKitCore                           0x0000000119e27e5a -[UIViewController loadViewIfRequired] + 1084
    13  UIKitCore                           0x0000000119e28277 -[UIViewController view] + 27
    14  UIKitCore                           0x0000000119d773dd -[UINavigationController _startCustomTransition:] + 1039
    15  UIKitCore                           0x0000000119d8d30c -[UINavigationController _startDeferredTransitionIfNeeded:] + 698
    16  UIKitCore                           0x0000000119d8e721 -[UINavigationController __viewWillLayoutSubviews] + 150
    17  UIKitCore                           0x0000000119d6f553 -[UILayoutContainerView layoutSubviews] + 217
    18  UIKitCore                           0x000000011a98c4bd -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 2478
    19  QuartzCore                          0x000000010bbe7db1 -[CALayer layoutSublayers] + 255
    20  QuartzCore                          0x000000010bbedfa3 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 517
    21  QuartzCore                          0x000000010bbf98da _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 80
    22  QuartzCore                          0x000000010bb40848 _ZN2CA7Context18commit_transactionEPNS_11TransactionEd + 324
    23  QuartzCore                          0x000000010bb75b51 _ZN2CA11Transaction6commitEv + 643
    24  UIKitCore                           0x000000011a4d03f4 _afterCACommitHandler + 160
    25  CoreFoundation                      0x0000000106955867 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
    26  CoreFoundation                      0x00000001069502fe __CFRunLoopDoObservers + 430
    27  CoreFoundation                      0x000000010695097a __CFRunLoopRun + 1514
    28  CoreFoundation                      0x0000000106950066 CFRunLoopRunSpecific + 438
    29  GraphicsServices                    0x0000000109100bb0 GSEventRunModal + 65
    30  UIKitCore                           0x000000011a4a6d4d UIApplicationMain + 1621
    31  PathOfData                          0x000000010471fe6b main + 75
    32  libdyld.dylib                       0x00000001078c5c25 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)

我不知道这里发生了什么,所以如果有人有想法,那就太棒了

非常感谢!

您的故事板中 UIViewController 的标识符值重复。 Search for text Detail 在你的故事板中为不同的 UIViewControllers 设置不同的标识符。

例如Detail1Detail2

错误显然与 UIDiffableDataSource 有关。

可区分数据源需要项目标识符的唯一哈希值。显然有两个item相同name, type and text.

为了确保散列值是唯一的,添加一个 uuid 属性 并且只使用这个 属性 作为散列值(实现协议方法)。要正确解码 Item,您必须指定 CodingKeys 以防止 uuid 属性 被解码。

struct Item: Codable {
    let uuid = UUID()

    private enum CodingKeys : String, CodingKey { case name, type, text }

    var name: String?
    var type: String?
    var text: String?
}

extension Item : Hashable {
    static func ==(lhs: Item, rhs: Item) -> Bool {
        return lhs.uuid == rhs.uuid
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(uuid)
    }
}

在 iOS 13+ 中,您可以采用 Identifiable 来摆脱 Hashable 扩展

struct Item: Codable, Identifiable {
    let id = UUID()

    private enum CodingKeys : String, CodingKey { case name, type, text }

    var name: String?
    var type: String?
    var text: String?
}

并且强烈建议您不要使用 Data(contentsOf: 同步加载数据,不要那样做。使用异步 URLSession