Apologies for the noob question - coming from an iOS background I'm struggling a little with OSX.
The good news - I have an NSScrollView with a large NSView as it's documentView. I have been adjusting the bounds of the contentView to effectively zoom in on the documentView - and all works well with respect to anything I do in drawRect (of the documentView)
The not so good news - I have now added another NSView as a child of the large documentView and expected it to simply zoom just like it would in iOS land - but it doesn't. If anyone can help fill in the rather large gap in my understanding of all this, I'd be extremely grateful
Thanks.
[UPDATE] Fixed it myself - 'problem' was that autolayout (layout constraints) were enabled. Once I disabled them and set the autosizing appropriately then everything was ok. I guess I should learn about layout constraints...
I know this is very old but I just implemented mouse scroll zooming using the following after spending days trying to figure it out using various solutions posted by others, all of which had fundamental issues. As background I and using a CALayers in a NSView subclass with a large PDF building layout in the background and 100+ draggable CALayer objects overplayed on top of that.
The zooming is instant and smooth and everything scales perfectly with no pixellation that I was expecting from something called 'magnification'. I wasted many days on this.
override func scrollWheel(with event: NSEvent) {
guard event.modifierFlags.contains(.option) else {
super.scrollWheel(with: event)
return
}
let dy = event.deltaY
if dy != 0.0 {
let magnification = self.scrollView.magnification + dy/30
let point = self.scrollView.contentView.convert(event.locationInWindow, from: nil)
self.scrollView.setMagnification(magnification, centeredAt: point)
}
}
LOL, I had exactly the same problem. I lost like two days messing around with autolayout. After I read your update I went in and just added another NSBox to the view and it gets drawn correctly and zooms as well.
Though, does it work for NSImageViews as subviews as well?
Related
The problem to be solved here is how to zoom in a UIScrollView while staying centered. If you don't take some sort of precautions, the default is that as we zoom out, the zoomed view slides up to the top left corner of the scroll view, like this:
So how to prevent this, and keep the zoomed view in the center as we zoom? As you probably know, there are traditional ways of handling this by messing with the scroll view's layout, as described by Josh and Eliza in the brilliant classic WWDC video 104 from 2010. This can be done by using a delegate or by subclassing UIScrollView, and gives the desired result:
Now comes WWDC 2017 video 201 (https://developer.apple.com/videos/play/wwdc2017/201/?time=1496), and there's Eliza making a claim that the new (iOS 11) contentLayoutGuide solves the problem of zooming while staying centered in a new way: she says to center the content view at the center of the content layout guide.
But she doesn't demonstrate. And when I try it for myself, I find it isn't solving the problem. I'm zooming in just fine, but when zooming out, so that the zoom scale is smaller than 1, the content view moves up to the top left, just as it always has.
Has anyone figured out what this claim in the video actually means? How does iOS 11 make it easier to zoom centered than in the past?
EDIT I actually received a sample project from Apple in response to my bug report, which they claimed illustrated how to solve this, and it didn't! So I conclude that even Apple doesn't know what they're talking about here.
The view goes to the top left because the contentSize of the scroll view is not defined. When using the new Auto Layout guides in iOS 11, it's still necessary to define the contentSize.
Add the following constraints:
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
scrollView.contentLayoutGuide.heightAnchor.constraint(equalTo: scrollView.frameLayoutGuide.heightAnchor)
This worked for me, when I had a contentView with a fixed width/height and the following additional constraints:
// give the centerView explicit height and width constraints
centerView.widthAnchor.constraint(equalToConstant: 500),
centerView.heightAnchor.constraint(equalToConstant: 500),
// pin the center of the centerView to the center of the scrollView's contentLayoutGuide
centerView.centerXAnchor.constraint(equalTo: scrollView.contentLayoutGuide.centerXAnchor),
centerView.centerYAnchor.constraint(equalTo: scrollView.contentLayoutGuide.centerYAnchor)
This is the solution you are / everybody is looking for. In my case I want to center a view inside a table view scroll view. So if the table view scrolls the custom view will always be in the center of the scroll view content.
// create a view
let v:UIView = UIView(frame:CGRect.zero) // use zero if using constraints
ibTableView.addSubview(v)
ibTableView.bringSubview(toFront:v)
v.translatesAutoresizingMaskIntoConstraints = no
v.backgroundColor = .yellow
v.widthAnchor.constraint(equalToConstant:100).isActive = yes
v.heightAnchor.constraint(equalToConstant:100).isActive = yes
// set scrollview guides
ibTableView.contentLayoutGuide.widthAnchor.constraint(equalTo:ibTableView.frameLayoutGuide.widthAnchor).isActive = yes
ibTableView.contentLayoutGuide.heightAnchor.constraint(equalTo:ibTableView.frameLayoutGuide.heightAnchor).isActive = yes
// anchor view
v.centerXAnchor.constraint(equalTo:ibTableView.contentLayoutGuide.centerXAnchor).isActive = yes
v.centerYAnchor.constraint(equalTo:ibTableView.contentLayoutGuide.centerYAnchor).isActive = yes
I am drawing graphs on NSView using standard Cocoa drawing APIs. See image below. There are multiple graphs on the NSView which is in a scrollView. Each graph has around 1440 data points and scrolling performance struggles a bit because of redrawing that is being done.
Is there any way to ensure the graphics are only drawn once such that the image can be scrolled up and down smoothly ?
This same view is used to generate a PDF output file.
Given the drawing does not actually need to change unless the view is resized, and this does not happen, is there any way to prevent the view from redrawing itself during scrolling. Hopefully there is a simple switch to ensure the view draw itself once and keeps that in memory!?
The basic code is in the NSView subclass draw() function.
override func draw(_ dirtyRect: NSRect) {
drawAxis()
// Only draw the graph axis during live resize
if self.inLiveResize {
return
}
plot1()
plot2()
plot3()
...
}
func plot1(){
for value in plot1Data {
path = NSBezierPath()
if isFirst {
path?.move(to: value)
} else {
path?.line(to: value)
}
}
}
Apple has pretty comprehensive advice in their View Programming Guide: Optimizing View Drawing article.
It doesn't appear that you're taking even the minimal steps to avoid drawing what you don't have to. For example, what code you've shown doesn't check the dirtyRect to see if a given plot falls entirely outside of it and therefore doesn't need to be drawn.
As described in that article, though, you can often do even better using the getRectsBeingDrawn(_:count:) and/or needsToDraw(_:) methods.
The scroll view can, under some circumstances, save what's already been drawn so your view doesn't need to redraw it. See the release notes for Responsive Scrolling. One requirement of this, though, is that your view needs to be opaque. It needs to override isOpaque to return true. It's not enough to just claim to be opaque, though. Your view actually has to be opaque by drawing the entirety of the dirty rect on every call to draw(). You can fill the dirty rect with a background color before doing other drawing to satisfy this requirement.
Be sure the clip view's copiesOnScroll property is set to true, too. This can be done in IB (although it's presented as an attribute of the scroll view, there) or in code. It should be true by default.
Note that the overdraw that's part of Responsive Scrolling will happen incrementally during idle time. That will involve repeated calls to your view's draw() method. If you haven't optimized that to only draw the things that intersect with the dirty rect(s), then those calls are going to be slow/expensive. So, be sure to do that optimization.
I have setup a collection view with 10 subviews in a cell.
The subviews are
-imageview with label on it
-text view
-imageview
-uilabel
-imageview
-uilabel
Initially the collection view have 15 cells displayed at the time on an iPad. No when I scroll the scroll pauses when it is time to replace the cells at the bottom or top(reusing cells). I removed the shadow but still the same issue.
So the problem happens when the old cell is reused causing a lag.
Btw, no images loaded via the network.
I had the answer to this long time ago but for the benefit of others and who may be in the same issue.
Apart from removing shadows, you also need to remove "clear color" backgrounds. Any additional drawing that will require additional processing should be removed or replaced with an alternative. Any heavy lifting, text formatting, date formatting should be done before even showing the collection or table views. Make sure you cell only does the presenting and not processing. If you can't avoid it do the processing at another thread.
To measure the rate of the scroll you will need to use the instruments > graphics > core animation tool to measure the frame rate.
Try it and you will notice a difference.
EDIT: No need to experiment with autoresizing masks, just read this short article about UICollectionView performance boost http://noxytrux.github.io/blog/2014/09/25/ios8-weirdness-part3-laggy-uicollectionview/
It is probably an autolayout overhead. Consider trying autoresizing masks instead.
You can just commit everything and make an experiment:
turn off autolayout on your cell xib file
run an app to test performance (don't worry about messed up layout)
setup autoresizing masks (and do layout in code if needed) instead of autolayout if the effect is noticeable
I fixed my UICollectionView performance problems this way. It helps most when you have a lot of visible cells at a time.
Also, if you have image views, please see this answer Setting image property of UIImageView causes major lag
Don't forget about Instruments: run Time Profiler and see what eats your main thread time most.
I guess the issue can be also because of ImageView.
Using Kingfisher 5 for prefetching, loading, showing and caching images will solve the issue.
let url = URL(fileURLWithPath: path)
let provider = LocalFileImageDataProvider(fileURL: url)
imageView.kf.setImage(with: provider)
https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#image-from-local-file
fileprivate func downloadForPerformance()->void{
for i in urlToFetchImageArray {
var image: UIImage = dowloadImageFunc(link: i.urlString() );
imageArray.append( image );
}
}
fileprivate func dowloadImageFunc(link: String)->UIImage {
let url = URL(string: link)
let data = try? Data(contentsOf: url!)
return UIImage(data: data!) ?? UIImage(named: "default.png")!;
}
cell?.mainImage.image = self.imageArrayPerformance[indexPath.row];
Usually the problem is slow download as each cell is dequeue.
Call downloadForPerformance func before the view has appeared.
This will fill an array called imageArray as a global variable
Then use that array in cellForItem func
basically have an array of already downloaded images you will need, make this a [UIImage] array, then use theImageArray[indexPath.row]
As discussed elsewhere, NSTabView does not have a setBackgroundColor method and subclassing NSTabView and using an drawRect to control it does no longer work - as it does not paint the top 10%, the bit just below the segmented control button.
Now I am a bit surprised by the amounts of work arounds I had to do solving this; see
code: https://github.com/dirkx/CustomizableTabView/blob/master/CustomizableTabView/CustomizableTabView.m
and am wondering if i went down the wrong path. And how to do this better & simpler:
The NSSegmentStyleTexturedSquare seems to yield me a semi-transparent segmented Control. Which means I need to do extra work to hide any bezel lines (line 240, 253).
is there a better way to do this ? I.e. negate its transparency ?
or is there a way I can use the actual/original segmented choise button ?
I find that the colours I need - like the [NSColor windowBackgroundColour] are not set to anything useful (i.e. that one is transparent) -- so right now I hardcode them (lines 87, 94).
Is there a better way to do this ?
I find I need a boatload of fluffy methods to keep things in sync ( line 128, 134, etc).
can this be avoided ?
I find that mimicking the cleverness on rescaling means I need to keep a constant eye on the segemented Control box and remove/resize it. And even then - it is not quite as good as the original
is there a better way to do this than line 157 -- i.e. hear about resizing ? Rather than do it all the time ?
The segementControl fades dark when focus is removed from the window - unlike the real McCoy.
can that easily be prevented ? is there a cheap way to track this ?
Or is this the wrong approach - and should I focus on just a transparent hole here - and let the NSTabViewItem draw a background ? But in any case - then I still have the issue with the Segemented COntrol box - or is there than a way to make that be the default again.
when trying this - I get stuck on the top 20-30 pixels being drawn in the 'real' windows background colour - which is 'transparent' - and hence the colour will not run all the way to the top or behind the segment bar and up to the bezel - but instead stop some 8 pixels below the bottom of the segment controls.
Feedback appreciated - as this feels so far off/suboptimal for such a simple things --
Thanks a lot. Brownie points for hacking/forking the github code :) :) :) As a line of running code says more than a thousand words.
Dw.
PSMTabBarControl is probably the best workaround for you. I have created several custom tab views, but cocoa does not play well with this control. PSMTabBarControl has been updated to support Xcode 4. https://github.com/ciaran/psmtabbarcontrol
Have you tried setting the background color of its underlying CALayer? (Make it a layer-backed view, if it isn't already, by setting wantsLayer = YES.)
If your situation can tolerate some fragility, a very simple and quick approach is to subclass NSTabView and manually adjust the frame of the item subviews. This gives each item a seamless yellow background:
- (void)drawRect:(NSRect)dirtyRect {
static const NSRect offsetRect = (NSRect) { -2, -16, 4, 18 };
NSRect rect = self.contentRect;
rect.origin.x += offsetRect.origin.x;
rect.origin.y += offsetRect.origin.y;
rect.size.width += offsetRect.size.width;
rect.size.height += offsetRect.size.height;
[[NSColor yellowColor] set];
NSRectFill(rect);
[super drawRect:dirtyRect];
}
A future change in the metrics of NSTabView would obviously be a problem so proceed at your own risk!
I needed to display some Cocoa widgets on top of an NSOpenGLView in an existing app. I followed the example in Apple's LayerBackedOpenGLView example code. The NSOpenGLView is given a backing layer using:
[glView setWantsLayer:YES]
Then the Cocoa NSView with the widgets is added as a subview of the glView. This is basically working and is twice ad fast as my previous approach where I added the NSView containing the widgets to a child window of the window containing the glView (this was the other solution I found on the web).
There were two problems.
The first is that some textures that I use with blending were no longer getting the blend right. After searching around a bit it looked like I might need to clear the alpha channel of the OpenGLView. This bit of code that I call after drawing a frame seems to have fixed this problem:
Code:
glColorMask(FALSE, FALSE, FALSE, TRUE); //This ensures that only alpha will be effected
glClearColor(0, 0, 0, 1); //alphaValue - Value to which you need to clear
glClear(GL_COLOR_BUFFER_BIT);
glColorMask(TRUE, TRUE, TRUE, TRUE); //Put color mask back to what it was.
Can someone explain why this is needed when using the CALayer, but not without?
The second problem I don't have a solution for. It seems that when I pan to the part of the scene where problem is #1 was observed, the frame rate drops from something like 110 FPS down to 10 FPS. Again, this only started happening after I added the backing layer. This doesn't always happen. Sometimes the FPS stays high when panning over this part of the scene but that is rare. I assume it must have something with how the textures here are blended, but I have no idea what.
Any thoughts?
I did figure out a workaround to the slowdown. The OpenGL view has a HUD (heads up display) view that goes on top of it. I had installed another NSView as a subview if it. Both the HUD and the subview have lots of alpha manipulation and for some reason that tickled a real slowdown in compositing the layers. I could easily instal this subview as a subview of the OpenGL view and when I did this everything sped up again. So although I don't fully understand the slowdown, I do have a good work around for it.