Reputation: 83
Hello everyone 🙋♂️I'm parsing the following JSON
to a UITableView
using a UITableViewDiffableDataSource
for nice searching animations.
The JSON : https://www.pathofexile.com/api/trade/data/items
And here's the repo : https://github.com/laurentdelorme/PathOfData
From the JSON, I'm able to load all 13 different categories, as mapped in the Model file.I'm then able to push the data inside these categories to another tableView (the one using UITableViewDiffableDataSource
) and to display everything nicely.
However, there is ONE category that make my app crash when I try to push its content to the DetailViewController, which is the "Maps" category on the initial ViewController
.
Here's my model :
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?
}
Here's my 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)
}
}
}
Here's the DetailViewController :
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 { ($0.name?.lowercased().contains(filter.lowercased()))! }
updateData(on: filteredItems)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
isSearching = false
updateData(on: items)
}
}
And here's the error message I get when I try to accees the "Maps" category :
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)
I can't figure out what's going on here, so if someone has an idea, that would be awesome 😅
Thank you very much !
Upvotes: 3
Views: 12975
Reputation: 52612
If you have trouble with unexpected exceptions, you go into the debugger, set a “breakpoint on exceptions”, wait until it is hit, and then the debugger shows the call stack with all variables, giving you a chance to figure out what is wrong.
Upvotes: 0
Reputation: 402
@vadian's answer is not a great idea since this does not use Diffable Data source for it's benefits.
By adding UUID
to each model the Data Source will treat each snapshot as unique each reload, therefore reloading the entire collection view each time data is updated. Potentially animating awkwardly.
Much better off removing duplicates prior to adding to snapshot, you can use an extension such as:
extension Sequence where Element: Hashable {
func uniqued() -> [Element] {
var set = Set<Element>()
return filter { set.insert($0).inserted }
}
}
Upvotes: 0
Reputation: 285210
The error is clearly related to UIDiffableDataSource
.
The diffable datasource requires unique hash values of the item identifiers. Obviously there are two items with the same name
, type
and text
.
To ensure that the hash value is unique add an uuid
property and use only this property for the hash value (implement the protocol methods). To decode Item
properly you have to specify CodingKeys
to prevent the uuid
property from being decoded.
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)
}
}
In iOS 13+ you can adopt Identifiable
to get rid of the Hashable
extension
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?
}
And you are strongly discouraged from loading the data synchronously with Data(contentsOf:
, Don't do that. Use asynchronous URLSession
Upvotes: 15
Reputation: 9354
There is duplicate identifier values for UIViewController
in your storyboard.
Search for text Detail
in your storyboards and set different identifiers for different UIViewControllers.
For example Detail1
and Detail2
Upvotes: -4