I do heavily use auto layout in my new project, but I've got one issue related to NSWindow during resizing ...
NSWindow is borderless window,
during initial setup, frame of this window is set based on status item position and initial content view size (intrinsicContentSize of contentView),
vertical anchor attribute is set to NSLayoutAttributeTop,
horizontal anchor attribute is set to NSLayoutAttributeCenterX
... so far, so good. NSWindow is placed correctly, size is correct and everything looks good.
Whenever contentView is resized automatically because of auto layout, etc. final window position is correct, size is correct, ..., so again, so far so good.
What's the problem? When animation is in progress (window is vertically resizing), top of my window is jumping +- 1 pixel down/up/down/up/down/up/down/up/... until animation is finished. It looks pretty ugly ...
It behaves like this pseudo code ...
NSRect frameRect = window.frame;
while ( frameRect.size.height != desiredHeight ) {
frame.origin.y -= 1; // Move window down by 1px
[self setFrame:frame display:YES animate:YES];
frame.size.height += 1; // Increase window height
[self setFrame:frame display:YES animated:YES];
}
... it looks like auto layout changes origin of window and then auto layout realizes that the height should be changed as well, ...
Anyone did see this behavior?
Mea culpa, how can I missed it, it's because I do use NSLayoutConstraint for height of one of my views and I'm animating it via animator and it produces non integer values - so the height sometimes does contain real numbers and this is the cause for jumping top of NSWindow. Problem solved.
Related
I'm trying to implement a custom slider in Cocoa with 5 values. See my demo project, which can be downloaded here: http://s000.tinyupload.com/index.php?file_id=07311576247413689572.
I've subclassed the NSSliderCell and implemented methods like drawKnob:(NSRect)knobRect and drawBarInside:(NSRect)cellFrame flipped:(BOOL)flipped etc.
I'm facing some issues:
I'm not able to position the knob correctly regarding to the background image. I know that I'm able to change the knob's frame, and I've tried doing some calculation to position the knob correctly, but I'm not able to make it work for my custom slider. Could someone please help me with this?
The height of my custom slider background is 41px. In the drawBarInside:(NSRect)cellFrame flipped:(BOOL)flipped I change the height of the frame to 41px as well, but the entire background is not visible. Why?
I've noticed that the included images (the background and knob) are flipped vertically. Why? Note that the border top is darker in the background compared to the bottom, but this is reversed when I draw the background.
I found a mistake in your calculation of the x position of the knob rectangle: You used the height of the image where you should have used the width.
The cell drawing is being clipped to the frame of the control. Maybe you could expand the control frame when your cell awakes.
You need to use the NSImage method drawInRect:fromRect:operation:fraction:respectFlipped:hints:, and pass YES for the respectFlipped: parameter. Apple's controls generally do use flipped coordinates.
Added: Expanding the frame in awakeFromNib doesn't seem to work, the frame gets set back. Here's something that does work. Instead of overriding drawBarInside:flipped:, add this override:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSRect controlFrame = [controlView frame];
float bgHeight = self.backgroundImage.size.height;
if (controlFrame.size.height < bgHeight)
{
controlFrame.size.height = bgHeight;
[controlView setFrame: controlFrame];
}
[self.backgroundImage
drawInRect: [controlView bounds]
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0
respectFlipped: YES
hints: NULL];
[self drawKnob];
}
I’m letting the user show/hide the statusBar at will, and I want all the views to slide down/up with it. I assumed setting the autoresizing mask would take care of this. I’ve added the navigation controller programmatically, so I did this:
[self.view setAutoresizingMask:UIViewAutoresizingFlexibleHeight];
[self.navigationController.view setAutoresizingMask:UIViewAutoresizingFlexibleHeight];
[self.navigationController.navigationBar setAutoresizingMask:UIViewAutoresizingFlexibleTopMargin];
This has no effect.
I printed the frame rects of self.view and self.navigationController.view before and after hiding the statusBar, and they remain exactly the same height.
Since autoresizesSubviews defaults to YES, I doubt that is the problem. I must not be setting the autoresizing mask correctly. Does anybody know what I'm doing wrong?
The answer seems to be that the container view's autoresizing mask is simply not coordinated with the status bar. There’s no choice but to write code adjusting the layout.
Since Apple didn’t provide automatic coordination between the statusBar and other elements, you’d think they’d let us do it ourselves. But, no, we are not permitted to set the statusBar frame directly. The only way to animate statusBar repositioning is via UIStatusBarAnimationSlide, which uses its own timeline and will never match other animations.
However, we can control the timing of a statusBar fade and slide the container view along with it. The subviews' autoresize masks will do the rest. This actually looks pretty good, and the coding, although complicated by some weird framing behavior, is not too painful:
UIApplication *appShared = [UIApplication sharedApplication];
CGFloat fStatusBarHeight = appShared.statusBarFrame.size.height; // best to get this once and store it as a property
BOOL bHideStatusBarNow = !appShared.statusBarHidden;
CGFloat fStatusBarHeight = appDelegate.fStatusBarHeight;
if (bHideStatusBarNow)
fStatusBarHeight *= -1;
CGRect rectView = self.view.frame; // must work with frame; changing the bounds won't have any effect
// Determine the container view's new frame.
// (The subviews will autoresize.)
switch (self.interfaceOrientation) {
case UIInterfaceOrientationPortrait:
case UIInterfaceOrientationPortraitUpsideDown:
rectView.origin.y += fStatusBarHeight;
rectView.size.height -= fStatusBarHeight;
break;
case UIInterfaceOrientationLandscapeLeft:
case UIInterfaceOrientationLandscapeRight:
// In landscape, the view's frame will sometimes complement the orientation and sometimes not.
/*
Specifically, if view is loaded in landscape, its frame will reflect that; otherwise, the frame will always be in portrait orientation.
This is an issue only when the navBar is present. Regular view controllers can count on the frame staying in portrait orientation no matter what.
But regular view controllers have another oddity: In the upside-down orientations, you should adjust the height only; the origin takes care of itself.
*/
if (rectView.size.width < rectView.size.height) {
rectView.origin.x += fStatusBarHeight;
rectView.size.width -= fStatusBarHeight;
}
else {
rectView.origin.y += fStatusBarHeight;
rectView.size.height -= fStatusBarHeight;
}
break;
default:
break;
}
// The navBar must also be explicitly moved.
CGRect rectNavBar = [self.navigationController.navigationBar frame];
rectNavBar.origin = CGPointMake(0.0f, rectNavBar.origin.y + fStatusBarHeight);
// Perform the animated toggling and reframing.
[UIView animateWithDuration:kAnimDurationToggleStatusBar animations:^{
[appShared setStatusBarHidden:bHideStatusBarNow]; // you can add withAnimation:UIStatusBarAnimationSlide here and it will work, but the timing won't match
[self.navigationController.navigationBar setFrame:rectNavBar];
[self.view setFrame:rectView];
}];
There’s no need to do anything to the toolbar, which remains glued to the bottom of the screen -- as long as you have not set the window frame to mainScreen.bounds.
One snag is how to get the statusBar height when you want to re-display it, since statusBarFrame returns a 0-area rect if it is currently hidden. It turns out that doing a preliminary show/hide, without animation, just to get the rect, works fine. There’s no visible flash.
Also, if you are using a xib/nib, be sure that its view's statusBar is set to None.
Maybe some day Apple will enhance the statusBar layout behavior. Then all this code, for every view controller, will have to be redone...
I have a UIScrollView whose content size is 1200x480. I have some image views on it, whose width adds up to 600. When scrolling towards the right, I simply increase the content size and set the offset so as to make everything smooth (I then want to add other images, but that's not important right now). So basically, the images currently in the viewport remain somewhere on the left, eventually to be removed from the superview.
Now, the problem that I have happens when scrolling towards the left. What I do is I move the images to the end of the content size (so add 600 to each image view's origin.x), and then set the content offset accordingly. It works when the finger is on the screen and the user drags (scrollView.isTracking = YES). When the user scrolls towards the left and lets go (scrollView.isTracking = NO), the image views end up moving too fast towards the right and disappear almost instantly. Does anyone know how I could have the images move nicely and not disappear even when the user's not manually dragging the view and has already let go?
Here's my code for dragging horizontally:
-(void) scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint offset = self.scrollView.contentOffset;
CGSize size = self.scrollView.contentSize;
CGPoint newXY = CGPointMake(size.width-600, size.height-480);
// this bit here allows scrolling towards the right
if (offset.x > size.width - 320) {
[self.scrollView setContentSize:CGSizeMake(size.width+320, size.height)];
[self.scrollView setContentOffset: offset];
}
// and this is where my problem is:
if (offset.x < 0) {
for (UIImageView *imageView in self.scrollView.subviews) {
CGRect frame = imageView.frame;
[imageView setFrame:CGRectMake
(frame.origin.x+newXY.x, frame.origin.y, 200, frame.size.height)];
}
[self.scrollView setContentOffset:CGPointMake(newXY.x+offset.x, offset.y)];
}
}
EDIT: This is now working - I had a look at StreetScroller and it's all good now.
However, I now want to zoom in on the scrollview, but viewForZoomingInScrollView is never called. Is it not possible to zoom in on a scrollview with a large content size?
There are some approaches floating around here. Just use the site search …
If you want an more "official" example created by Apple take a look at the StreetScroller Demo. For some more information about this example take a look at last years WWDC session no. 104 Advanced Scroll View Techniques.
There is also an UIScrollView subclass on Github called BAGPagingScrollView, which is paging & infinite, but it has a few bugs you have to fix on your own, because it's not under active development (especially the goToPage: method leads to problems).
I have a UIScrollView which is scrollable both vertically and horizontally. This view is filled with lots of buttons, each of them with its own width (but all with the same height).
When one of these buttons gets tapped, a slider-like interface is brought to life. If this interface goes over the selected button, the whole scroll view must be scrolled so that the button becomes visible once again.
My app behaves as expected when the Y coordinate of the scroll view's content offset is set to a limit (this limit can be 0 or the view's height). But if the content offset is located in an intermediate vertical position, the scrolling just doesn't seem to happen.
At first, I tried the following approach:
CGPoint newOffset = CGPointMake(self.scrollView.contentOffset.x + horizontalVar,
self.scrollView.contentOffset.y);
[self.scrollView setContentOffset: newOffset animated: YES];
Which didn't work, as I mentioned.
Then, I tried to manually animate the view, using its property setter:
[UIView animateWithDuration: 0.3 animations: ^{
CGPoint newOffset = CGPointMake(self.scrollView.contentOffset.x + horizontalVar, self.scrollView.contentOffset.y);
self.scrollView.contentOffset = newOffset;
}];
That approach produced the following result: if the scroll view is in an intermediate vertical position when one of its buttons gets tapped, the content offset update causes a visual change, but the view almost immediately returns to its original state.
I have no other clues on the subject. Could you please help me?
Maybe it's because you didn't set the ContentSize of your scrollView.
I have two instances of NSScrollView both presenting a view on the same content. The second scroll view however has a scaled down version of the document view presented in the first scroll view. Both width and height can be individually scaled and the original width - height constraints can be lost, but this is of no importance.
I have the synchronised scrolling working, even taking into account that the second scroll view needs to align its scrolling behaviour based on the scaling. There's one little snag I've been pulling my hairs out over:
As both views happily scroll along the smaller view needs to slowly catch up with the larger view, so that they both "arrive" at the end of their document at the same time. Right now this is not happening and the result is that the smaller view is at "end-of-document" before the larger view.
The code for synchronised scrolling is based on the example found in Apple's documentation titled "Synchronizing Scroll Views". I have adapted the synchronizedViewContentBoundsDidChange: to the following code:
- (void) synchronizedViewContentBoundsDidChange: (NSNotification *) notification {
// get the changed content view from the notification
NSClipView *changedContentView = [notification object];
// get the origin of the NSClipView of the scroll view that
// we're watching
NSPoint changedBoundsOrigin = [changedContentView documentVisibleRect].origin;;
// get our current origin
NSPoint curOffset = [[self contentView] bounds].origin;
NSPoint newOffset = curOffset;
// scrolling is synchronized in the horizontal plane
// so only modify the x component of the offset
// "scale" variable will correct for difference in size between views
NSSize ownSize = [[self documentView] frame].size;
NSSize otherSize = [[[self synchronizedScrollView] documentView] frame].size;
float scale = otherSize.width / ownSize.width;
newOffset.x = floor(changedBoundsOrigin.x / scale);
// if our synced position is different from our current
// position, reposition our content view
if (!NSEqualPoints(curOffset, changedBoundsOrigin)) {
// note that a scroll view watching this one will
// get notified here
[[self contentView] scrollToPoint:newOffset];
// we have to tell the NSScrollView to update its
// scrollers
[self reflectScrolledClipView:[self contentView]];
}
}
How would I need to change that code so that the required effect (both scroll bars arriving at an end of document) is achieved?
EDIT: Some clarification as it was confusing when I read it back myself: The smaller view needs to slow down when scrolling the first view reaches the end. This would probably mean re-evaluating that scaling factor... but how?
EDIT 2: I changed the method based on Alex's suggestion:
NSScroller *myScroll = [self horizontalScroller];
NSScroller *otherScroll = [[self synchronizedScrollView] horizontalScroller];
//[otherScroll setFloatValue: [myScroll floatValue]];
NSLog(#"My scroller value: %f", [myScroll floatValue]);
NSLog(#"Other scroller value: %f", [otherScroll floatValue]);
// Get the changed content view from the notification.
NSClipView *changedContentView = [notification object];
// Get the origin of the NSClipView of the scroll view that we're watching.
NSPoint changedBoundsOrigin = [changedContentView documentVisibleRect].origin;;
// Get our current origin.
NSPoint curOffset = [[self contentView] bounds].origin;
NSPoint newOffset = curOffset;
// Scrolling is synchronized in the horizontal plane so only modify the x component of the offset.
NSSize ownSize = [[self documentView] frame].size;
newOffset.x = floor(ownSize.width * [otherScroll floatValue]);
// If our synced position is different from our current position, reposition our content view.
if (!NSEqualPoints(curOffset, changedBoundsOrigin)) {
// Note that a scroll view watching this one will get notified here.
[[self contentView] scrollToPoint: newOffset];
// We have to tell the NSScrollView to update its scrollers.
[self reflectScrolledClipView:[self contentView]];
}
Using this method the smaller view is "overtaken" by the larger view when both scrollers reach a value of 0.7, which is not good. The larger view then scrolls past its end of document.
I think you might be approaching this in the wrong way. I think you should be getting a percentage of how far down each scroll be is scrolled in relation to itself and apply that to the other view. One example of how this could be done is this way using NSScroller's -floatValue:
NSScroller *myScroll = [self verticalScroller];
NSScroller *otherScroll = [otherScrollView verticalScroller];
[myScroll setFloatValue:otherScroll.floatValue];
I finally figured it out. The answer from Alex was a good hint but not the full solution as just setting the float value of a scroller doesn't do anything. That value needs translation to specific coordinates to which the scroll view needs to scroll its contents.
However, due to differences in size of the scrolled document view, you cannot just simply use this value, as the scaled down view will be overtaken by the "normal" view at some point. This will cause the normal view to scroll past its end of document.
The second part of the solution was to make the normal sized view wait with scrolling until the scaled down view has scrolled its own width.
The code:
// Scrolling is synchronized in the horizontal plane so only modify the x component of the offset.
NSSize ownSize = [[self documentView] frame].size;
newOffset.x = MAX(floor(ownSize.width * [otherScroll floatValue] - [self frame].size.width),0);
The waiting is achieved by subtracting the width of the scroll view from the width times the value of the scroller. When the scaled down version is still traversing its first scroll view width of pixels, this calculation will result in a negative offset. Using MAX will prevent strange effects and the original view will quietly wait until the value turns positive and then start its own scrolling. This solution also works when the user resizes the app window.