UITableViewDiffableDataSource Invalid sections - uikit

I try to use UITableViewDiffableDataSource
class SecondViewController: UIViewController {
enum Section {
case main
}
struct Model: Hashable {
let title: String
}
var tableView: UITableView!
var dataSource: UITableViewDiffableDataSource<Section, Model>!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView(frame: view.bounds)
view.addSubview(tableView)
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
view.layoutIfNeeded()
dataSource = UITableViewDiffableDataSource<Section, Model>(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = item.title
return cell
})
var snapshot = NSDiffableDataSourceSnapshot<Section, Model>()
snapshot.appendSections([.main])
snapshot.appendItems([Model(title: "1")], toSection: .main)
dataSource.apply(snapshot)
}
}
if I use view.layoutIfNeeded() before create dataSource, it will crash:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (1) must be equal to the number of sections contained in the table view before the update (1), plus or minus the number of sections inserted or deleted (1 inserted, 0 deleted).'

I encountered the same problem, when populating a UITableView with results from a PassthroughSubject, as soon as the UIViewController was displayed.
Animating updates was very important to me, so my workaround was to first "prime" the DiffableDataSource by creating a snapshot immediately after initialisation, appending the sections I wanted, appending the items with an empty array, and applying the snapshot with animatingDifferences: false.
Afterwards I was able to apply animated snapshots with data with no issue.

I ran into the same problem. In general, on iOS 14, causing a layout before the table view's data source is set can cause crashes.
You can fix these crashes by setting the data source as soon as the table view has been created. If you want to animate your initial snapshot, then first set an empty snapshot followed by a populated one with animatingDifferences: true.

Same problem, I found that even without animations, some users in production will still crash。
When I setupUI in viewDidLoad, DispatchQueue.main.async help me fix this problem
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
var snapshot = NSDiffableDataSourceSnapshot<Section, Row>()
snapshot.appendSections([...])
snapshot.appendItems([...], toSection: .main)
dataSource.apply(snapshot, animatingDifferences: true, completion: nil)
}
More specifically, just setting animatingDifferences to false will still crash. This crash only happened on iOS 15.0.0 and iOS15.0.1

Related

UITextView in collectionview cell on mac spikes cpu

I have a simple UIKit application that has a UITextView in a UICollectionViewCell. The app is designed for iOS/iPadOS and works just fine on those platforms. However, when run on Mac (Designed for iPad) as soon as I start scrolling the collectionview, the cpu usage spikes to ~85% and stays there indefinitely. The only way to lower the cpu is to click outside of the application window, but once it comes to the foreground again, the cpu usage jumps right back up. I've tried running on Mac in Catalyst mode too, but the same problem occurs with slightly less cpu usage (~45%).
Additionally the debugger constantly spits out [API] cannot add handler to 3 from 3 - dropping while scrolling.
Does anyone have an explanation or solutions for this?
I’m using Xcode Version 14.1 (14B47b) on macOS Ventura 13.0 (22A380).
class ViewController: UIViewController {
var dataSource: UICollectionViewDiffableDataSource<Section, String>! = nil
var collectionView: UICollectionView! = nil
var items = Array(0...100).map{"Item \($0)"}
enum Section: String {
case main
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "List"
configureCollectionView()
configureDataSource()
applyInitialSnapshot()
}
private func createLayout() -> UICollectionViewLayout {
return UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item])
return NSCollectionLayoutSection(group: group)
}
}
private func configureCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground
view.addSubview(collectionView)
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<TestCell, String> { (cell, indexPath, item) in
cell.configure(title: item, row: indexPath.item)
}
dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: collectionView) {
(collectionView, indexPath, identifier) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
}
}
private func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: false)
}
}
class TestCell: UICollectionViewCell {
private let annotationsTextView = UITextView()
override init(frame: CGRect) {
super.init(frame: frame)
addViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(title: String, row: Int) {
annotationsTextView.attributedText = .init(string: "Row: \(row) Item: \(title)", attributes: [.font: UIFont.preferredFont(forTextStyle: .title1)])
}
private func addViews() {
annotationsTextView.isScrollEnabled = false
annotationsTextView.isEditable = false
annotationsTextView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(annotationsTextView)
NSLayoutConstraint.activate([
annotationsTextView.topAnchor.constraint(equalTo: contentView.topAnchor),
annotationsTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
annotationsTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
annotationsTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
}
}
I am still on 13.0.1 and had the same problem (tons of [API] cannot add handler to 3 from 3 - dropping). I narrowed it down to a call to UITableView.scrollToNearestSelectedRow with animated = true.
Setting animated to false stops the logging. I guess I’ll have to check out 13.1
I'm experiencing this exact behavior in Ventura whenever there is a UITextView anywhere in the view hierarchy with either isSelectable or isEditable set to true, but only while the UITextView is NOT first responder. This occurs whether the UITextView is visible or hidden.
The excess CPU usage can be prevented by any of the following:
Remove the UITextView from the view hierarchy.
Set isSelectable AND isEditable to false.
Make the UITextView first responder.
I'm still investigating and will update here if I find more. We should all probably report the issue to Apple as I imagine they will need to fix this at the system level.
2022-12-01: Update
This issue appears to have been resolved as of macOS 13.1 beta 4 (22C5059b). Hallelujah!

NSTextFields using bindings not updating after initialization

I am importing data from JSON to be used as the models for some items in a CollectionView, and it seems that they are being initialized, and with the correct number of elements. But for some reason the representedObject (aliased as morpheme below) is returning nil initially. Hence the placeholder if nil values being the ones showing up.
If you click the items, I have it set up to show up in the log the name of the item clicked, and it works fine, and doesn't return the debugging defaults. So I'm guessing there is a concurrency issue going on.
For more details, this are items being manually prototyped because XCode 7 still hasn't fixed the segue bug with collection item prototypes.
Here is a screenshot I hopefully managed to get it all the important info in:
Here is the cell's controller/delagating class code in detail:
///Acts as view controller for the items of the morpheme collection
public class MorphemeCell: NSCollectionViewItem, NSTextViewDelegate{
var backgroundColor = NSColor.clearColor()
var morphemeLabel: String{
get{
return morpheme?.morphemeDisplayName ?? "Morpheme"
}
}
var allomorphsLabel: String{
get{
return (morpheme?.allomorphsAsString ?? "Allomorphs")
}
}
///The morpheme data contained in the cell
public var morpheme : Morpheme?{
get{
return representedObject as? Morpheme
}
set{
representedObject = newValue
}
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
}
///Detects clicks on each item
public override func mouseUp(theEvent: NSEvent) {
Swift.print("Clicked on " + morphemeLabel)
backgroundColor = NSColor.blueColor()
}
}
Not sure if this is needed, but just in case here is the main window's ViewController doing some setup/loading functionality.
The loading code itself:
///Loads morpheme data into memory and then into the collection view
func loadMorphemeData(){
//Open morphemes.json and begin parsing
let morphemeDataPath = "morphemes"
if let file = NSBundle.mainBundle().URLForResource(morphemeDataPath, withExtension: "json")
{
let data = NSData(contentsOfURL: file)
let json = JSON(data:data!)
//Create Morpheme objects passing in JSON elements
for morphemeElement in json{
let toAdd = Morpheme(JSONElement: morphemeElement)
fullMorphemesList.append(toAdd)
}
///TODO Use full range or filters in final product
let morphemesToLoad = fullMorphemesList[0...100]
collectionView.content.appendContentsOf(Array(morphemesToLoad) as [AnyObject])
}
else
{
print("Resource Failure")
}
So, recap: It seems that I either need to delay the collectionView's setup, or find out how to update the Labels once the data is in.
Thanks very much for any help! I'm very new to the Cocoa framework so it's been a doozy.
Take different route: bind your label to self.morpheme.morphemeDisplayName. Then set "Null Placeholder" text to be "Morpheme" (in that right panel see the list of text edits below binding settings). Finally make property morphemeDisplayName dynamic:
dynamic var morphemeDisplayName: String?
Obviously, you dont need morphemeLabel property inside cell anymore.
morpheme property of cell must be dynamic as well, or if there is setter-based property, you can call:
set {
willChangeValueForKey("morpheme")
<whatever variable> = newValue
didChangeValueForKey("morpheme")
}
Edit by original poster:
Also, in order to avoid binding synchrony issues, it turns out using viewWillAppear() instead of viewDidLoad() was causing issues with data loading and "freezing the labels".

NSSplitViewController Child View Not Added

I've created a simple NSViewController and want to add a split view with just one child view. The split view should be controlled by a NSSplitViewController, because I'd like to use the NSSplitItem's facilities for collapsing/expanding split items. After adding a child view controller, the split item is created, but no child view is added to the view tree.
override func viewDidLoad() {
super.viewDidLoad()
let splitViewController = NSSplitViewController()
view.addSubview(splitViewController.splitView)
let myController = MyController(nibName: "MyController", bundle: nil)
splitViewController.addChildViewController(myController)
printTree(view)
}
func printTree(view: AnyObject, _ n: Int = 1) {
if let view = view as? NSView {
NSLog("\(n): \(view)")
for child in view.subviews {
printTree(child, n + 1)
}
}
}
Output:
1: <NSView: 0x618000120140>
2: <NSSplitView: 0x6180001205a0>
Why does the split view have no child view?
To compare, here's the version without split view:
override func viewDidLoad() {
super.viewDidLoad()
let myController = MyController(nibName: "MyController", bundle: nil)
view.addSubview(myController.view)
printTree(view)
}
Output:
1: <NSView: 0x6100001203c0>
2: <NSView: 0x6000001208c0> <-- here's my child view
3: <NSButton: 0x600000140580>
And adding the child view directly as a subview to the split view doesn't work either:
A SplitView managed by a SplitViewController cannot have its subviews modified
So, my question is, why is the child view not added to the view tree inside the split view?
"You're doing it wrong"
You're using base class methods when NSSplitViewController has a very particular API.
See: https://developer.apple.com/library/prerelease/mac/samplecode/Exhibition/Listings/Exhibition_GalleryWindowController_swift.html for an example.
You want the addSplitViewItem: method.
I figured it out. My mistake was that I added the splitView instead of the view:
// this won't work:
self.view.addSubview(splitViewController.splitView)
// this will work:
self.view.addSubview(splitViewController.view)
BTW: using splitViewController.addChildViewController(myController) as I did before is just a shorter way of saying the following:
let item = NSSplitViewItem(viewController: myController)
splitViewController.addSplitViewItem(item)
which didn't work for me because of my mistake described above.

Parse SDK 1.7.1 not working in Xcode 6.3

My code worked fine in Xcode 6.2. After the update to Xcode 6.3 I had some Nullabilty Errors.
I could solve these errors after I downloaded the Parse SDK 1.7.1. So I deleted the old Parse framework files in my project and pasted the new ones into it. Additional I convert my code to the latest swift syntax "Edit/Convert/latest swift syntax". Now I haven't problems with Nullabilty Errors but several others.
In my project I have a simple Tableviewcontroller with the following code:
import UIKit
class HaendlerTableViewController: PFQueryTableViewController {
// Initialise the PFQueryTable tableview
override init!(style: UITableViewStyle, className: String!) { //1. Falialbe initialize init/style:className:)' cannot override a non-failable initializer
super.init(style: style, className: className)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Configure the PFQueryTableView
self.parseClassName = "Haendler"
self.textKey = "name"
self.pullToRefreshEnabled = true
self.paginationEnabled = false
}
// Define the query that will provide the data for the table view
override func queryForTable() -> PFQuery! { //2. Ovverriding method with selector queryForTable has incompatitble typ () -> PFQuery
var query = PFQuery(className: "Haendler")
query.orderByAscending("name")
return query
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject) -> PFTableViewCell { //3. Ovverriding method with selector 'tableView:cellForRowAtindexPath:object:' has incompatible type '(UITableView, NSIndexPath, PFObject) -> PFTableViewCell
var cell = tableView.dequeueReusableCellWithIdentifier("HaendlerCell") as! HaendlerCell!
if cell == nil {
cell = HaendlerCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
}
// Extract values from the PFObject to display in the table cell
cell.haendlerName.text = object["name"] as! String!
var thumbnail = object["logo"] as! PFFile
var initialThumbnail = UIImage(named: "haendler")
cell.haendlerBild.image = initialThumbnail
cell.haendlerBild.file = thumbnail
cell.haendlerBild.loadInBackground()
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var detailScene = segue.destinationViewController as! HaendlerDetailViewController
// Pass the selected object to the destination view controller.
if let indexPath = self.tableView.indexPathForSelectedRow() {
let row = Int(indexPath.row)
detailScene.currentObject = objects[row] as? PFObject //4. Could not find an overload for 'subscript' that accepts the supplied agruments
}
}
}
I wrote the errors in a comment on the right side of the code and
below.
Falialbe initialize init/style:className:)' cannot override a non-failable initializer
Ovverriding method with selector queryForTable has incompatitble typ () -> PFQuery
Ovverriding method with selector 'tableView:cellForRowAtindexPath:object:' has incompatible type '(UITableView, NSIndexPath, PFObject) -> PFTableViewCell
Could not find an overload for 'subscript' that accepts the supplied agruments
I have the same errors when I make a new Swift project from the Parse Quickstart and add one Tableviewcontroller. In my old project was an objective-C bridging header which one I deleted because I had the oppurtunity to add the Parse SDK 1.7.1 directly in my Swift project.
Now I need help because I don't see what I have to change..
PS: Sorry for the mix of German and English code I'll adjust it once the project is running again
I had the same issue as I just updated Xcode to 6.3 about 20 minutes ago.
For your 2nd error, remove the '!' after 'PFQuery'. So it should now look like..
override func queryForTable() -> PFQuery {
This solved my problem in regards to that specific error.
I never used an init method as you did in your first error, but try removing it and see what you get. My PFQueryTableViewController works fine without it.
Had the same issues.
To solve the first initialise issue remove the '!' after 'override init'. Should look like this:
// Initialise the PFQueryTable tableview
override init(style: UITableViewStyle, className: String!) { //1. Falialbe initialize init/style:className:)' cannot override a non-failable initializer
super.init(style: style, className: className)
}
Do the same for the 2nd error after 'PFQuery'
override func queryForTable() -> PFQuery {
Hope its helpful. Since the latest update unwrapping elements usually needs to be revised for possible errors.

Xcode Swift: How to stop variables from changing back after each viewDidLoad, how to save and update data from different ViewControllers?

I have some vars in my Main VC and when user clicks a button in another VC the prepareForSegue passes along a new value to the Main VC and updates a label.
But when the user clicks again it's back to initial value, so it doesn't increment since the value is set back in the viewDidLoad?
MainVC:
var statsHealth:Int = 0
override func viewDidLoad() {
super.viewDidLoad()
healthLabel.text = String("Health: \(statsHealth)/10")
}
Another VC:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "startSegue") {
let startVC = segue.destinationViewController as ViewController
startVC.statsHealth += 1
}
It's displayed as 0, then 1 but then 0 again and then 1 instead of 2,3,4 etc.
Any ideas?
BR
Nils
Perhaps not the most 'Swift' way to do it, but certainly works well....
Create a file called Variables.swift which will hold all your 'universal' variables (if these are going to be on every page, I see no reason this isn't the 'best' way to do it - certainly it is the most simple to understand!)
in Variables.swift, hold all your universal variables
struct Variables {
static var statsHealth = 0
.....
}
Then, in each other page, access them at any time
healthLabel.text = String("Health: \(Variables.statsHealth)/10")
or set them
Variables.statsHealth += 1
So based on your description, I assume the view controller structure is like this:
AnotherVC -> MainVC
MainVC is presented on top of AnotherVC. When you go back to AnotherVC, did you dismiss MainVC completely? If so, then every time you go from AnotherVC to MainVC, it initiate a new ViewController, and the variables you saved before doesn't exist anymore.
If you want to keep this structure and change variables in MainVC, keep a reference of mainVC in AnotherVC. Then instead of connecting in storyboard, you may want to present it programmatically.
class AnotherVC {
var mainVC: MainVC?
func presentMainVC() {
var targetVC = UIViewController()
if self.mainVC != nil {
targetVC = self.mainVC
} else {
let storyboard = UIStoryboard(name: "Your-storyboard-name", bundle: nil)
targetVC: MainVC = storyboard.instantiateViewControllerWithIdentifier("The-main-VC-identifier") as MainVC
self.mainVC = targetVC
}
//you can change your variable here
mainVC.statsHealth += 1
self.presentViewController(self.mainVC, animated: true, completion: nil)
}
If you mainVC is on top of AnotherVC in any case, you can just revert the reference direction.

Resources