NSClipView bounds - Problems to find out the scroll position - cocoa

I try to find out, when the user scrolls to the end of a NSTableView. So of course, the tableview is embedded as usual in a NSClipView an this is embedded in a NSScrollView. There are no more changes except the fact of adding insets to the ClipView (Top: 20, Left/Right: 20, Bottom: 0).
To get notified about the scrolling, I connected an outlet of the NSClipView.
So I use this code:
self.clipView.postsBoundsChangedNotifications = true
NotificationCenter.default.addObserver(self,
selector: #selector(scrollViewDidScroll(notification:)),
name: NSView.boundsDidChangeNotification,
object: self.clipView)
...
#objc func scrollViewDidScroll(notification: Notification) {
print("\(self.clipView.contentInsets.bottom) - \(self.clipView.contentInsets.top)")
print(self.clipView.bounds)
}
If I'm at the top of the ScrollView, the print-results look like this:
0.0 - 20.0
(-20.0, -20.0, 600.0, 556.0)
// x y width height
This seems ok for me. But if I'm at the bottom, it looks like this:
0.0 - 20.0
(-20.0, 595.0, 600.0, 556.0)
In my understanding, the y-value should be 536, not 595. Where is this difference coming from?

I found the solution by my one by observing the different values especially after resizing the window. So:
clipView.documentVisibleRect is that, what you really can see. Of course I cannot use the height value of this part. The origin value tells me about the position in the ScrollView.
So necessary is the height of the total content. Therefore I have to use clipView.documentRect.
Now it's very easy: If the visibleRects y-position + the height of the visible rect is the same as the height of the total clipview, we are at the end. This code works:
#objc func scrollViewDidScroll(notification: Notification) {
let scrollY = self.clipView.documentVisibleRect.origin.y
let visibleHeight = self.clipView.documentVisibleRect.size.height
let totalHeight = self.clipView.documentRect.size.height
if scrollY + visibleHeight >= totalHeight {
print("end")
}
}

Related

Exclusion rects not shifting text properly with NSTextView/UITextView

I have an NSTextView with some exclusion rects so I can shift the text vertically without having to resort to adding newlines to the string. However, I've noticed that the exclusion rect is rather limited or perhaps even buggy because I cannot shift the text vertically more than ~45% of the textview's height. I have found a workaround where I increase the textview's height by 2x but this feels gross and I'd rather do it "properly"
In the image above, I have three text views (from left to right)
Programmatically without encapsulating it inside an NSScrollView, 2x height
Programmatically with NSScrollView encapsulation. 2x height
Using interface builder, regular height.
The exclusion rect is drawn with a CAShapeLayer and you can see that "hello world new world" isn't properly positioned outside of the exclusion rect.
I tried all three examples to make sure I wasn't missing something with regards to IB default settings or the dynamics of the text view when encapsulated in an NSScrollView (Im new to AppKit and TextKit), however all 3 examples exhibit the same bug.
Code to update the text exclusion rect
(Each time the slider moves on the bottom right, it will update their text rect)
label.stringValue = "excl: \(sender.integerValue): height: \(view.frame.height)"
print(sender.integerValue)
let exclHeight: CGFloat = CGFloat(slider.integerValue)
[aTextView, bTextView, cTextView]
.compactMap { $0 }
.forEach {
let rect = CGRect(
x: 5,
y: 5,
width: aTextView.frame.width - 10,
height: exclHeight)
$0.wantsLayer = true
$0.textContainer?.exclusionPaths.removeAll()
$0.textContainer?.exclusionPaths.append(.init(rect: rect))
$0.layer?.sublayers?.forEach {
$0.removeFromSuperlayer()
}
let layer = CAShapeLayer()
layer.frame = rect
layer.backgroundColor = NSColor.blue.withAlphaComponent(0.2).cgColor
layer.borderWidth = 1
layer.borderColor = NSColor.white.cgColor
$0.layer?.addSublayer(layer)
}
}
The problem seems to be that the exclusionPath is before the first line.
Just playing around with the parameters, a two line sample text with the rect y positioned after the first line works without any problems.
So it looks like the issue is calculating the container height when it starts with a exclusionPaths in -[NSLayoutManager usedRectForTextContainer:]
#interface LOLayoutManager : NSLayoutManager
#end
#implementation LOLayoutManager
- (NSRect)usedRectForTextContainer:(NSTextContainer *)container
{
NSRect rect = [super usedRectForTextContainer:container];
NSRect newRect = NSMakeRect(0, 0, NSMaxX(rect), NSMaxY(rect));
return newRect;
}
#end
Instead of returning y position from the exclusionPaths and the line fragments height, this returns a big rect starting at 0, 0. This should work as long as the NSTextView only contains one text container.

Custom Xib With Defined Frame Constraints Is Changing At Runtime

I've spent several hours googling and in the debugger and I cannot figure out why my parent view in this xib is changing at runtime.
I have created a simple Xib:
In the container I set the width and height constraints (I tried setting constraints in the top parent but I can't seem to be able to):
At runtime I load the Xib programatically and I add it to a view. However after I add it to the view and set the position, the frame of the parent is smaller and the position is wrong.
Here I am explicitly set the x to 16, and the y to 400. When I look at it in the inspector debug tool however, I get different results than I want because the parent frame has changed and the Container position is wrong as a result. I turned the inspector to the side so you can see the parent (in blue) and the child container (in white) and how the parent is smaller than the child:
The details for the parent (the root xib view 'Item Detail Size Widget') are as follows. Notice the height is now 32 instead of 76:
The details for the top level child (Container) are as follows:
So the constraints I set for the container are being honored but the parent is resizing (I assumed since I couldn't set constraints it would use the frame I set).
I tried turning off and on translatesAutoResizingMaskIntoConstraints and a few other things but I can't seem to get the parent to be the exact same size as the Container.
Do you have any suggestions as to why the root Item Detail Size Widget will not match the size of the Container and is changing at run time?
Update
For reference here is the code where I add the widget:
let sizer: ItemDetailSizeWidget = .fromNib()
sizer.x = 16
sizer.y = 400
contentView.addSubview(sizer)
I have UIView extensions that set x and y as follows:
var x: CGFloat {
set { self.frame = CGRect(x: newValue,
y: self.y,
width: self.width,
height: self.height)
}
get { return self.frame.origin.x }
}
var y: CGFloat {
set { self.frame = CGRect(x: self.x,
y: newValue,
width: self.width,
height: self.height)
}
get { return self.frame.origin.y }
}
And here is the fromNib extension
class func fromNib<T: UIView>() -> T {
return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
}
Xcode always uses the autoresizing mask for a top-level view in a xib. You can see that your first screen shot: it has the autoresizing control shown.
Your top-level view's autoresizingMask is set to flexible width and height.
You have not set any width or height constraints between your top-level view and the “Container” subview.
You also have this code:
contentView.addSubview(sizer)
I suspect (since you mention contentView) that you're adding sizer to the view hierarchy of a table view cell or a collection view cell. When the cell is first created, it might not yet be at its final size. The collection view (or table view) might resize the cell after you return it from collectionView:cellForRowAtIndexPath:.
Since your sizer view has translatesAutoresizingMaskIntoConstraints set to true when it's loaded from the nib, and since its autoresizingMask is [.flexibleWidth, .flexibleHeight], this means that it will grow or shrink when its superview grows or shrinks.
To fix, try these steps:
Change the autoresizing mask to this:
Constrain the width of “Item Size Detail Widget” to equal the width of “Container”, and constrain the heights to equal also:

Issue working with UITableView prototype cell in StoryBoard when using Content Offset in viewDidLoad()

I have a UIViewController that contains a UITableView and a UISearchBar. The two controls are at the same level in the view hierarchy.
The Search Bar covers the first cell of the Table View, so in viewDidLoad() I adjust the Content Inset of the Table View by Y = 64 (20 for the status bar + 44 for the Search Bar).
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset = UIEdgeInsets(top: 64, left: 0, bottom: 0, right: 0)
}
Everything works fine at runtime.
The problem is when I try to work with the prototype cell at design time it is covered by the Search Bar as shown below:
What is the proper approach here?
You can do this entirely in the storyboard.
Remove the constraint Table View.top = top
Add a new constraint that connects the top of the table view to the bottom of the search bar
You'll likely need to adjust the origin and height of the tableview, since its new origin.y will be the search bar's origin.y + size.height
And yes - hard coding magic numbers that correspond to things in your storyboard is a bad idea.

CGWindowListCreateImage yields blurred cgImage when zoomed

I'm developing a magnifying glass like application for mac. My goal is to be able to pinpoint individual pixels when zoomed in. I'm using this code in mouseMoved(with event: NSEvent):
let captureSize = self.frame.size.width / 9 //9 is the scale factor
let screenFrame = (NSScreen.main()?.frame)!
let x = floor(point.x) - floor(captureSize / 2)
let y = screenFrame.size.height - floor(point.y) - floor(captureSize / 2)
let windowID = CGWindowID(self.windowNumber)
cgImageExample = CGWindowListCreateImage(CGRect(x: x, y: y, width: captureSize,
height: captureSize), CGWindowListOption.optionOnScreenBelowWindow, windowID,
CGWindowImageOption.bestResolution)
The creation of the cgImage takes place in the CGWindowListCreateImage method. When I later draw this in an NSView, the result looks like this:
It looks blurred / like some anti-aliasing was applied during the creation of the cgImage. My goal is to get a razor sharp representation of each pixel. Can anyone point me in the right direction?
Ok, I figured it out. It was a matter of setting the interpolation quality to none on the drawing context:
context.interpolationQuality = .none
Result:
On request some more code:
//get the context
guard let context = NSGraphicsContext.current()?.cgContext else { return }
//get the CGImage
let image: CGImage = //pass the result from CGWindowListCreateImage call
//draw
context.draw(image, in: (CGRect of choice))

Can NSCollectionView autoresize the width of its subviews to display one column

I have an NSCollectionView that contains a collection of CustomViews. Initially it tiled the subviews into columns and rows like a grid. I then set the Columns property in IB to 1, so now it just displays them one after another in rows. However, even though my CustomView is 400px wide, it's set to autoresize, the NSCollectionView is 400px wide, and it's set to 1 column, the subviews are drawn about 80px wide.
I know I can get around this by calling:
CGFloat width = collectionView.bounds.size.width;
NSSize size = NSMakeSize(width, 85);
[collectionView setMinItemSize:size];
[collectionView setMaxItemSize:size];
But putting this code in the awakeFromNib method of my WindowController only sets the correct width when the program launches. When I resize the window (and the NSCollectionView autoresizes as I've specified), the CustomViews stay at their initially set width.
I'm happy to take care of resizing the subviews myself if need be, but I'm quite new to Cocoa and can't seem to find any articles explaining how to do such a thing. Can someone point me in the right direction?
Anthony
The true answer is to set the maxItemSize to 0,0(NSZeroSize). Otherwise, it is computed.
[self.collectionView setMaxItemSize:NSZeroSize];
This can be set in awakeFromNib.
I couldn't get this to work with a default layout - but it is fairly easy to implement a custom layout:
/// Simple single column layout, assuming only one section
class SingleColumnLayout: NSCollectionViewLayout {
/// Height of each view in the collection
var height:CGFloat = 100
/// Padding is wrapped round each item, with double an extra bottom padding above the top item, and an extra top padding beneath the bottom
var padding = EdgeInsets.init(top: 5, left: 10, bottom: 5, right: 10)
var itemCount:Int {
guard let collectionView = collectionView else {
return 0
}
return collectionView.numberOfItems(inSection:0)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool {
return true
}
override open func layoutAttributesForItem(at indexPath: IndexPath) -> NSCollectionViewLayoutAttributes? {
let attributes = NSCollectionViewLayoutAttributes(forItemWith: indexPath)
guard let collectionView = collectionView else {
return attributes
}
let bounds = collectionView.bounds
let itemHeightWithPadding = height + padding.top + padding.bottom
let row = indexPath.item
attributes.frame = NSRect(x: padding.left, y: itemHeightWithPadding * CGFloat(row) + padding.top + padding.bottom , width: bounds.width - padding.left - padding.right , height: height)
attributes.zIndex = row
return attributes
}
//If you have lots of items, then you should probably do a 'proper' implementation here
override open func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
var attributes = [NSCollectionViewLayoutAttributes]()
if (itemCount>0){
for index in 0...(itemCount-1) {
if let attribute = layoutAttributesForItem(at: NSIndexPath(forItem: index, inSection: 0) as IndexPath) {
attributes.append(attribute)
}
}
}
return attributes
}
override open var collectionViewContentSize: NSSize {
guard let collectionView = collectionView else {
return NSSize.zero
}
let itemHeightWithPadding = height + padding.top + padding.bottom
return NSSize.init(width: collectionView.bounds.width, height: CGFloat(itemCount) * itemHeightWithPadding + padding.top + padding.bottom )
}
}
then all you need is
var layout=SingleColumnLayout()
collectionView.collectionViewLayout = layout
I know this is a very late response but I got the same problem and hope my solution will help somebody. Solution is to access bounds of enclosing scroll view not of collection view itself. So to solve it you need to replace first line with:
CGFloat width = collectionView.enclosingScrollView.bounds.size.width;
another late one - I just switched to using an NSTableView and providing an NSView by the delegate method.
Autoresizing comes for free, one column is easy, and it renders massively faster.
Lets say you want your CollectionViewItem with a size of 200x180px, then you should set:
[myCollectionView setMinItemSize:NSMakeSize(200, 180)];
[myCollectionView setMaxItemSize:NSMakeSize(280, 250)];
Your Max-Size should be big enough to look good and give enough space for stretching to fit the collectionView-Width.
If you have a fixed number of columns, you can probably use (0,0), but if you want a dynamic number of rows and columns like I wanted.. you should set a fixed min-size and a bigger max.size.
While you might get a collection view to behave as you want, I think you have a design problem
You should use a NSTableView and set columns to 1 and their sizing to anything but "None". NSTableView is intended for tabular data, plus it can recycle cells which gives a great performance boost for large amount of items. NSCollectionView is more like a linear layout which arranges items in a grid, with vertical scrolling. It is useful when the column number can change dynamically to allow more items to be shown on screen, usually depending on device orientation and screen size.
I tried all of solutions proposed here and none of them helped. If you use flow layout (it's used by default) you can extend it with the following code and delegate's sizeForItem method will be called on each change
class MyCollectionViewFlowLayout: NSCollectionViewFlowLayout {
override func shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool {
return true
}
override func invalidationContext(forBoundsChange newBounds: NSRect) -> NSCollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! NSCollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = true
return context
}
}
Hope it helps someone as it took me couple of evenings to find solution
Matt Gallagher's wonderful blog Cocoa with Love is about to address this. This week, he shared the bindings details of a one-column view like the one in question:
http://cocoawithlove.com/2010/03/designing-view-with-bindings.html
Next entry, he promises to share the rest of the implementation, which should give you what you're looking for.
(I should note that he is subclassing NSView directly and re-implementing many of NSCollectionView's features. This seems to be common though; not many people are fond of subclassing NSCollectionView.)
Edit: Seems he broke his promise... we never did receive that post. See tofutim's answer below instead.

Resources