I have a tableview and collectionview in view controller. The collection view gets populated based on selections made in the table view. I want the collectionview to be updated without animation. The collection view gets updated when a selection is made on the table. I'm also using a custom collectionview layout (UICollectionViewLayout). Here's the code that updates the collection view:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
guard var data = measurementData?[indexPath.item] else { return }
if data.name != unitData.selectedMeasurement {
unitData.selectedMeasurement = data.name
unitConversionTable?.collectionViewLayout.invalidateLayout()
//self.unitConversionTable?.reloadData()
UIView.performWithoutAnimation({ self.unitConversionTable?.reloadSections(NSIndexSet(index: 0)) })
}
}
The problem is that the application crashes if the entire collection view does not fit on the screen; unless I use reloadData() (which comes with the unwanted animations). Here's the actual message.
2016-04-13 22:42:23.367 aio Calculator[6288:273528] * Assertion
failure in -[UICollectionViewData
layoutAttributesForItemAtIndexPath:],
/BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3512.60.7/UICollectionViewData.m:666
2016-04-13 22:42:23.978 xxx [6288:273528] *Terminating app due to
uncaught exception 'NSInternalInconsistencyException', reason: 'no
UICollectionViewLayoutAttributes instance for
-layoutAttributesForItemAtIndexPath: {length = 2, path = 0 - 10}'
This particular case has 12 cells in the CV. But only 10 are displayed based on the result of layoutAttributesForElementsInRect (because only 10 fit).
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
cache.forEach({attributes in
if CGRectIntersectsRect(attributes.frame, rect) {
layoutAttributes.append(attributes)
}
})
print("layoutAttributesForElementsInRect \(rect) has \(layoutAttributes.count) attributes")
return layoutAttributes }
My print statements show that there are 12 items.
prepareLayout NumOfitems:12
But only 10 attributes.
layoutAttributesForElementsInRect (0.0, 0.0, 414.0, 736.0) has 10
attributes
So, everything appears to be correct; at least as far as I understand how it's suppose to work.
Why is the app crashing?
What am I missing?
Thanks!
==================
Update:
OK. I think I know what's going on. I re-read all the documentation. The definition for layoutAttributesForElementsInRect(rect: CGRect) clearly states:
Returns the layout attributes for all of the cells and views in the specified rectangle.
But then I started looking at the a sample projects and found
Collection View Transition which clearly returns all the cached attributes instead of just the ones that intersect with the given rect. I changed layoutAttributesForElementsInRect to return all cached attributes and now everything works fine.
Related
I stumbled upon a special problem in my current project.
We have UICollectionView using a custom layout called SquareMosaicLayout.
Within this collection view the first cell it presenting a UITextView.
This text view again shall show an html text created with:
NSMutableAttributedString(fromHTMLString: htmlString, textColor: textColor, font: font)
Now when this string is assigned to the text view the collection view somehow stops working which means it does not ask the datasource for new cells when scrolling through the collection view.
This results in the collection view showing blank space.
Unfortunately I did not have time to isolate the problem in terms of if the custom layout breaks anything but it definitely has to do with assigning the string. If we don't do that the collection view works as expected.
We still did not found the reason for the behaviour but a first workaround.
It is possible to set the NSMutableAttributedString asynchronously.
extension UITextView {
[ ... ]
DispatchQueue.main.async {
let optionalAttributedText = NSMutableAttributedString(fromHTMLString: htmlString, textColor: self.textColor, font: self.font)
guard let attributedText = optionalAttributedText else { return }
self.attributedText = attributedText
[ ... ]
In that case the UICollectionView behaves normal showing all the cells.
I have created a Touch Bar with many colourful buttons.
The user will often change the color using the buttons.
When a button is pressed some graphical elements of the current window change accordingly with the color of the button.
I would like that sliding the finger among the buttons the elements will change their color, without having to wait for a "touch up inside" event.
Is there a quick way to do that?
EDIT: I think that a way to do that would be use a button of type momentaryPushIn or to create a class subclassing a button and handing something like a "touch inside" event.
I would prefer the first method but I am finding a very poor documentation.
Since you approved Scrubber solution here it is:
Firstly you need to screate instance of NSScrubber and insert it into your touch bar. Make sure that selection mode will be fixed.
yourScrubber.mode = .fixed
Then for purposes of highlighting(just to see outline be eyes, without this outline showed everything will work correctly) set Selection Overlay Style to .outlineOverlay.
youtScrubber.mode = .selectionBackgroundStyle = .outlineOverlay
Next set delegates for data source and for selection events.
yourScrubber.delegate = self
yourScrubber.dataSource = self
Then return amount of items in scrubber
public func numberOfItems(for scrubber: NSScrubber) -> Int {
return 8
}
Now you need to insert items(NSScrubberItemView). Which can be also filled with NSScrubberImageItemView or NSScrubberTextItemView or even your custom NSView.
There are many ways how to fill content of scrubber, for easy case where you will provide images it will look like this:
public func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
let itemView = scrubber.makeItem(withIdentifier: "colorIdentifier", owner: nil) as! NSScrubberImageItemView
itemView.image = yourImage
return itemView
}
Now you should be able to see something like this: (I used random images for my scrubber)
Also you should be able to visible(if you enabled outline) select items:
Ok and now the most important part, how to get the effect you desire: (use of delegate protocol method)
public func scrubber(_ scrubber: NSScrubber, didHighlightItemAt highlightedIndex: Int) {
print("highlight = \(highlightedIndex)")
//your code here
}
Which results to something like this: (notice output window)
You can also add spacing between cells be defining cell width:
func scrubber(_ scrubber: NSScrubber, layout: NSScrubberFlowLayout, sizeForItemAt itemIndex: Int) -> NSSize {
return NSSize(width: yourWidth, height: 30)
}
Also maybe you will find use for:
optional public func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int)
I'm using the today widget for iOS8 to display information relevant to the current day. The problem is I don't want to display the widget/section at all if there are no relevant messages to show.
I know it must be possible as the BA app does it (it only shows the widget when there is a flight, the rest of the time its not visible at all). I just cant figure out a way to achieve this behaviour.
Does anyone know how this can be done?
I found the way to do this is using the NCWidgetController. This allows you to easily specify when the today widget should be displayed based on whatever criteria you see fit.
Simply add the following into your viewDidLoad method (or anywhere that will be called when the widget reloads) in the today widget view controller and it will work:
BOOL hasDataToDisplay = NO;
NCWidgetController *widgetController = [NCWidgetController widgetController];
[widgetController setHasContent:hasDataToDisplay forWidgetWithBundleIdentifier:#"com.my-company.my-app.my-widget"];
Apple Docs: https://developer.apple.com/library/ios/documentation/NotificationCenter/Reference/NCWidgetController_Class/index.html#//apple_ref/occ/cl/NCWidgetController
WARNING: The NCWidgetController cannot be reset from the widget itself once you have set there is no content to display. In other words once you set it to NO then there is no going back unless you trigger it from the parent/containing app.
In the widget's ViewController's viewDidLoad method add the following:
BOOL DisplayWidget = NO;
[[NCWidgetController widgetController] setHasContent:DisplayWidget
forWidgetWithBundleIdentifier:#"<widget's bunder identifier>"];
This will disable the widget from showing.
To enable it again, you must do that from the containing app using the same line passing YES to setHasContent parameter. Make sure to add the necessary imports to the containing app in the ViewController which will re-enable the widget:
#import <NotificationCenter/NotificationCenter.h>
#interface ViewController () <NCWidgetProviding> {...}
[Check out page 41 of the documentations for widgets
https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensibilityPG.pdf ]
The approach which I used, though not perfect and has a small remnant in Notification Center, but worked for me:
In viewDidLoad() set preferred content size height to 1:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view from its nib.
preferredContentSize = CGSizeMake(0, 1)
view.setNeedsLayout()
}
then when widget updates, gets real height and set it:
var data: NSData?
func updateData() {
// fetch data
if let data = data {
let viewHeight: CGFloat
// UI preperation and initialize viewHeight var
preferredContentSize = CGSizeMake(0, viewHeight);
} else {
preferredContentSize = CGSizeMake(0, 1);
}
}
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)) {
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResult.Failed
// If there's no update required, use NCUpdateResult.NoData
// If there's an update, use NCUpdateResult.NewData
updateData()
completionHandler(data != nil ? NCUpdateResult.NewData : NCUpdateResult.NoData)
}
It is better use
+ (instancetype)widgetController
then call
- (void)setHasContent:(BOOL)flag forWidgetWithBundleIdentifier:(NSString *)bundleID
I've got implemented Drag and Drop from Finder to my app to NSTableView, and I created link to document, etc.
But, I want to make delete operation by drag item from NSTableView and drop this row onto Trash icon. How can I do this correctly? How enable drop onto trash?
(It's been a long time since I've done this, and I'm doing it from memory and a glance at the docs. If this doesn't work, let me know and I'll double check w/ code.)
In draggingSession:sourceOperationMaskForDraggingContext: you should include NSDragOperationDelete as one of the legal operations. You will then receive NSDragOperationDelete back in your draggingSession:endedAtPoint:operation: to indicate that the item was dropped on the trash.
Use dropSessionDidEnd delegate method. there you can get the location of dropping point and there's no need for another UICollectionView/UITableView :
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession) {
guard let item = session.items.first?.localObject as? YourObject else {
return
}
let dropLocation = session.location(in: self.view)
let itemDropedInTrash = TrashImageView.frame.contains(dropLocation)
if itemDropedInTrash {
//here update your datasource and reload your collectioView/tableView
//deleteItemFromDataSource(item: item)
//collectionView.reloadData()
}
}
We have a parent Split view (NSSplitView), and two subviews, Content and SideBar (the sidebar is on the right).
What would be the optimal Cocoa-friendly way to toggle the SideBar view?
I would really love it, if the suggested solution includes animation
I really don't need any suggestions related to external plugins, etc (e.g. BWToolkit)
HINT : I've been trying to do that, but still I had issues hiding the divider of the NSSplitView as well. How could I do it, while hiding it at the same time?
Here's a pretty decent tutorial that shows how to do this: Unraveling the Mysteries of NSSplitView.
Hiding the divider is done in NSSplitView's delegate method splitView:shouldHideDividerAtIndex:.
You will have to animate the frame size change yourself if you don't like the way NSSplitView does it.
Easiest way to do it is as follows - and it's animated: [SWIFT 5]
splitViewItems[1].animator().isCollapsed = true // Show side pane
splitViewItems[1].animator().isCollapsed = false // hide side pane
I wrote a Swift version of the content in the link from #Nathan's answer that works for me. In the context of my example splitView is set elsewhere, probably as an instance property on an encompassing class:
func toggleSidebar () {
if splitView.isSubviewCollapsed(splitView.subviews[1] as NSView) {
openSidebar()
} else {
closeSidebar()
}
}
func closeSidebar () {
let mainView = splitView.subviews[0] as NSView
let sidepanel = splitView.subviews[1] as NSView
sidepanel.hidden = true
let viewFrame = splitView.frame
mainView.frame.size = NSMakeSize(viewFrame.size.width, viewFrame.size.height)
splitView.display()
}
func openSidebar () {
let sidepanel = splitView.subviews[1] as NSView
sidepanel.hidden = false
let viewFrame = splitView.frame
sidepanel.frame.size = NSMakeSize(viewFrame.size.width, 200)
splitView.display()
}
These functions will probably methods in a class, they are for me. If your splitView can be nil you obviously have to check for that. This also assumes you have two subviews and the one at index 1, here as sidePanel is the one you want to collapse.
In Xcode 9.0 with Storyboards open Application Scene select View->Menu->Show sidebar. CTRL-click Show Sidebar, in sent actions delete the provided one, click on x. From the circle CTRL drag to First Responder in application scene and select toggleSideBar to connect to. Open storyboard and select the first split view item and in attributes inspector change behaviour from default to sidebar. Run and try with view menu item show/hide. All done in interface builder no code. toggleSideBar handles the first split view item. https://github.com/Dis3buted/SplitViewController
I got some artifacts with the code above, likely because it was out of context. I am sure it works where it was meant to. Anyway, here is a very streamlined implementation:
// this is the declaration of a left vertical subview of
// 'splitViewController', which is the name of the split view's outlet
var leftView: NSView {
return self.splitViewController.subviews[0] as NSView
}
// here is the action of a button that toggles the left vertical subview
// the left subview is always restored to 100 pixels here
#IBAction func someButton(sender: AnyObject) {
if splitViewController.isSubviewCollapsed(leftView) {
splitViewController.setPosition(100, ofDividerAtIndex: 0)
leftView.hidden = false
} else {
splitViewController.setPosition(0, ofDividerAtIndex: 0)
leftView.hidden = true
}
}
To see a good example using animations, control-click to download this file.
If your NSSplitView control is part of a NSSplitViewController object, then you can simply use this:
splitViewController.toggleSidebar(nil)