autohide only horizontal scroller in NSScrollView - objective-c

I have an NSTableView, created from IB, that I want to only autohide the horizontal scroller on. The main reason I want to do this is because it seems the NSTableView corverView only get's displayed if there is a vertical scroller.
I can't find any method to do this with the base class. So I tried subclassing NSScrollView and observing the hidden key on the horizontal scroller (code below). This works; however, the view tries to reset the current visible options every time the user resizes the window. This makes my implementation somewhat expensive; and it seems inelegant. Any better ideas about how to do this?
Thanks in advance!
Current implementation:
#interface PVScrollView : NSScrollView {
BOOL autohidesHorizontalScroller;
}
#property(assign) BOOL autohidesHorizontalScroller;
- (void) viewResized:(NSNotification*)notification;
#end
#implementation PVScrollView
#synthesize autohidesHorizontalScroller;
- (void) setAutohidesHorizontalScroller:(BOOL)val
{
autohidesHorizontalScroller = val;
[self setAutohidesScrollers:NO];
[[self horizontalScroller] addObserver:self
forKeyPath:#"hidden"
options:0
context:nil];
}
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (!([self documentVisibleRect].size.width < [[self documentView] frame].size.width) )
{
// remove observer
[[self horizontalScroller] removeObserver:self
forKeyPath:#"hidden"];
[[self horizontalScroller] setHidden:YES];
//[[self horizontalScroller] setNeedsDisplay:YES];
// add it back
[[self horizontalScroller] addObserver:self
forKeyPath:#"hidden"
options:0
context:nil];
}
}
#end

Give this a shot in your NSScrollView subclass:
- (void)setFrameSize:(NSSize)newSize;
{
NSSize minFrameSize = [NSScrollView frameSizeForContentSize:[self contentSize] hasHorizontalScroller:NO hasVerticalScroller:YES borderType:[self borderType]];
BOOL wantScroller = minFrameSize.width > newSize.width;
[self setHasHorizontalScroller:wantScroller];
[super setFrameSize: newSize];
}
You'll need to check "Show Vertical Scroller" and uncheck "Automatically Hide Scrollers" for it to work; I didn't bother making it robust to changes in IB. Also, you'll need to do the same thing when the window is first displayed (in the NSScrollView constructor).
I compared CPU usage with and without this change; it seems to vary at most 1% (19%→20%) in my test application.

Related

notification for a view x position

in a slide menu I'm developing for my project i would like to add a black view over the content view when it's slide out. To do this i need to create a method that check continuously the view x-position and darken or brighten up the black layer. The position of this view is the same as the content view.
I thought i can use a NSNotificationCenter like this:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(incomingNotification:) name:#"notification" object: darkViewController.view.frame.origin.x]];
and a method:
- (void) incomingNotification:(NSNotification *)notification{
// the dark layer alpha will be 0 at origin=0 and 0.8 at bounds.size.width
float alphaToUse = (darkViewController.view.frame.origin.x / self.view.bounds.size.width) * 0.8;
[darkViewController.view setAlpha:alphaToUse];
}
The problem is that i must use an object as parameter.
I'm new to notifications so i'm asking: is it wrong to use them for this kind of things?
Is it better to solve this in another way?
EDIT:
Following Denis advice i'm now trying to use the key-value-observe solution.
My app is structured like this:
MenuViewController-->ContainerViewController-->DarkViewController
In MenuViewController.m :
#interface MenuViewController ()
#property (strong,nonatomic) ContainerViewController *containerViewController;
#property (strong,nonatomic) DarkViewController *darkViewController;
#end
#implementation MenuViewController
#synthesize containerViewController,darkViewController;
# pragma mark - Views
- (void)viewDidLoad{
[super viewDidLoad];
containerViewController = [[ContainerViewController alloc]init];
[self addChildViewController:containerViewController];
[self.view addSubview:containerViewController.view];
[containerViewController didMoveToParentViewController:self];
darkViewController = [[DarkViewController alloc]init];
[containerViewController addChildViewController:darkViewController];
[containerViewController.view addSubview:darkViewController.view];
[darkViewController didMoveToParentViewController:containerViewController];
[UIView animateWithDuration:slideDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
[darkViewController.view setAlpha:0.7];
containerViewController.view.frame = CGRectMake(self.view.frame.size.width - slideWidth, 0, self.view.frame.size.width, self.view.frame.size.height);
}
completion:^(BOOL finished) {
if (finished) {
}
}];
[darkViewController addObserver:self forKeyPath:#"darkViewController.view.frame.origin.x" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change: (NSDictionary *)change context:(void *)context
{
NSLog(#"x is changed");
}
When i run this i get this exception:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<DarkViewController 0x10962d280> addObserver:<MenuViewController 0x10922c890> forKeyPath:#"darkViewController.view.frame.origin.x" options:1 context:0x0] was sent to an object that is not KVC-compliant for the "darkViewController" property.'
Ok, it seems that i found a solution following this example Notificationsin IOS
I just added this in the viewDidLoad of my ContainerViewController
[self addObserver:self forKeyPath:#"view.frame" options:0 context:nil];
and implemented the observer method with a for cycle to find my DarkViewController view
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
for (UIViewController * vc in self.childViewControllers) {
if ([vc isKindOfClass:[DarkViewController class]]) {
float alphaToUse = (self.view.frame.origin.x / self.view.bounds.size.width) * 0.8;
[vc.view setAlpha:alphaToUse];
}
}
}
Now i just have to understand where to put the removeObserver method, since my ContainerViewController will be always loaded...
There is another machanism in iOS for such kind of things called Key value coding and Key value observing.
From Notification Center documentation:
As you design your application, do not simply assume that you should send a notification to communicate with interested parties. You should also consider alternatives such as key-value observing, key-value binding, and delegation.
Key-value binding and key-value observing were introduced in OS X version 10.3 and provide a way of loosely coupling data. With key-value observing, you can request to be notified when the properties of another object change. Unlike regular notifications, there is no performance penalty for unobserved changes. There is also no need for the observed object to post a notification because the key-value observing system can do it for you automatically, although you can still choose do it manually.
So if you'll have another notification observers while making slide menu animation it may reduce its handling performance.
And the best solution would be to invoke incomingNotification: method inside the animation block (the method where animation performs).
Apple docs again:
Though key-value coding is efficient, it adds a level of indirection that is slightly slower than direct method invocations. You should use key-value coding only when you can benefit from the flexibility that it provides.
ANSWERING EDITED QUESTION:
This answer describes exactly what you're trying to do. When add the observer on some object's property object's name shouldn't be included in the property key path. So in you case adding an observer looks like this:
[darkViewController addObserver:self forKeyPath:#"view.frame" options:NSKeyValueObservingOptionNew context:nil];
When trying to observe some object property don't forget to ensure the object's class is KVC compliant for that property!
And also don't forget to remove the observers after job is done.

UIKeyboard avoidance and Auto Layout

Given the focus on Auto Layout in iOS 6, and the recommendation by Apple engineers (see WWDC 2012 videos) that we no longer manipulate a views' frame directly, how would one go about avoiding the keyboard using only Auto Layout and NSLayoutConstraint?
Update
This looks like a reasonable solution: An example of keyboard sensitive layout (GitHub source) but one potential issue I see is what happens when a user rotates the device and the keyboard is already on screen?
That blog post is great, but I'd like to suggest some improvements to it. First, you can register to observe frame changes, so you don't need to register to observe both show and hide notifications. Second, you should convert the CGRects for the keyboard from screen to view coordinates. Last, you can copy the exact animation curve used by iOS for the keyboard itself, so the keyboard and the tracking views move in synchrony.
Putting it all together, you get the following:
#interface MyViewController ()
// This IBOutlet holds a reference to the bottom vertical spacer
// constraint that positions the "tracking view",i.e., the view that
// we want to track the vertical motion of the keyboard
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomVerticalSpacerConstraint;
#end
#implementation MyViewController
-(void)viewDidLoad
{
[super viewDidLoad];
// register for notifications about the keyboard changing frame
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
object:self.view.window];
}
-(void)keyboardWillChangeFrame:(NSNotification*)notification
{
NSDictionary * userInfo = notification.userInfo;
UIViewAnimationCurve animationCurve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue];
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
// convert the keyboard's CGRect from screen coords to view coords
CGRect kbEndFrame = [self.view convertRect:[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]
fromView:self.view.window];
CGRect kbBeginFrame = [self.view convertRect:[[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]
fromView:self.view.window];
CGFloat deltaKeyBoardOrigin = kbEndFrame.origin.y - kbBeginFrame.origin.y;
// update the constant factor of the constraint governing your tracking view
self.bottomVerticalSpacerConstraint.constant -= deltaKeyBoardOrigin;
// tell the constraint solver it needs to re-solve other constraints.
[self.view setNeedsUpdateConstraints];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:duration];
[UIView setAnimationCurve:animationCurve];
[UIView setAnimationBeginsFromCurrentState:YES];
// within this animation block, force the layout engine to apply
// the new layout changes immediately, so that we
// animate to that new layout. We need to use old-style
// UIView animations to pass the curve type.
[self.view layoutIfNeeded];
[UIView commitAnimations];
}
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillChangeFrameNotification
object:nil];
}
#end
This will work, as long as you don't change orientation while the keyboard is up.
It was an answer on How to mimic Keyboard animation on iOS 7 to add "Done" button to numeric keyboard? showed how to mimic the keyboard animation curve correctly.
One last thing to beware of with respect to all these notification-based solutions: they can produce unexpected effects if some other screen in your app also uses the keyboard, because your view controller will still receive the notifications as long as it has not been deallocated, even if it's views are unloaded. One remedy for this is to put a conditional in the notification handler to ensure it only operates when the view controller is on screen.
Using the KeyboardLayoutConstraint in the Spring framework is the simplest solution I've found so far.
My idea is to create a UIView, let's call it keyboard view, and place it to your view controller's view. Then observe keyboard frame change notifications UIKeyboardDidChangeFrameNotification and match the frame of the keyboard to the keyboard view (I recommend to animate the change). Observing this notification handles the rotation you mentioned and also moving keyboard on iPad.
Then simply create your constraints relative to this keyboard view. Don't forget to add the constraint to their common superview.
To get the keyboard frame correctly translated and rotated to your view coordinates check out the docs for UIKeyboardFrameEndUserInfoKey.
I created a view like this that would watch the keyboard and change its own constraints when the keyboard comes on/off the screen.
#interface YMKeyboardLayoutHelperView ()
#property (nonatomic) CGFloat desiredHeight;
#property (nonatomic) CGFloat duration;
#end
#implementation YMKeyboardLayoutHelperView
- (id)init
{
self = [super init];
if (self) {
self.translatesAutoresizingMaskIntoConstraints = NO;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillShow:) name:#"UIKeyboardWillShowNotification" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillHide:) name:#"UIKeyboardWillHideNotification" object:nil];
}
return self;
}
- (void)keyboardWillShow:(NSNotification *)notification
{
// Save the height of keyboard and animation duration
NSDictionary *userInfo = [notification userInfo];
CGRect keyboardRect = [userInfo[#"UIKeyboardBoundsUserInfoKey"] CGRectValue];
self.desiredHeight = CGRectGetHeight(keyboardRect);
self.duration = [userInfo[#"UIKeyboardAnimationDurationUserInfoKey"] floatValue];
[self setNeedsUpdateConstraints];
}
- (void)keyboardWillHide:(NSNotification *)notification
{
// Reset the desired height (keep the duration)
self.desiredHeight = 0.0f;
[self setNeedsUpdateConstraints];
}
- (void)updateConstraints
{
[super updateConstraints];
// Remove old constraints
if ([self.constraints count]) {
[self removeConstraints:self.constraints];
}
// Add new constraint with desired height
NSString *constraintFormat = [NSString stringWithFormat:#"V:[self(%f)]", self.desiredHeight];
[self addVisualConstraints:constraintFormat views:#{#"self": self}];
// Animate transition
[UIView animateWithDuration:self.duration animations:^{
[self.superview layoutIfNeeded];
}];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#end
Ive written a library that will do it all for you (supports Auto Layout and Springs & Struts)
IHKeyboardAvoiding https://github.com/IdleHandsApps/IHKeyboardAvoiding
Just call [IHKeyboardAvoiding setAvoidingView:self.myView];
For auto layout with keyboard case, I use static table view. This keeps your codes much simpler and not need to keep track of keyboard height. One thing I learned about table view is to keep each table row as narrow as possible. If you put too many UIs vertically in one row, you may get keyboard overlap.

KVO not working for class property

I'm trying to understand key value observation in iOS but I think I'm not doing something correctly.
As an idea, I tried to add an observer to a view controller's property (a view connected with an IBOutlet). This view (tableIndicator) is animated so I wanted to see if I can get the observer to react when the view's frame changes.
So I did the following, inside the view controller's viewDidLoad:
[tableInidicator addObserver:self forKeyPath:#"frame" options:0 context:nil];
tableIndicator is my view/class property, I'm adding the view controller (self) as the observer, 0 for the default options and frame as the key value being observed.
Then, I'm waiting to see if this function is triggered as the frame changes:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(#"value changed");
}
...but nothing happens.
I'm not necessarily looking for fix to this code since it serves no purpose other than for me to understand it and I would be really grateful if someone could point out to me what I'm doing wrong. Some good examples/tutorials would be awesome too.
The ones I found ( http://iphonedevelopment.blogspot.ro/2009/02/kvo-and-iphone-sdk.html / http://nachbaur.com/blog/back-to-basics-using-kvo ) did not cover such cases. They were only observers applied to a class to watch for one of its properties, not for the property of a (custom)object inside a class, something that I think would be more useful for me.
Thank you in advance
[edit]For those who will miss my comment on the accepted answer:
Changing a view's center will apparently not trigger an observer for the frame property. You have to change the frame itself.
There is something which is not entirely clear about your code. Is tableIndicator a custom class derived from UIView? The method observeValueForKeyPath should be defined inside that class, and it would be then called. But I am not sure this is the best approach.
In general, a sounder approach is to define your controller (not your view) as an observer. In this case you do:
[self addObserver:self forKeyPath:#"view.frame" options:0 context:nil];
from inside the controller at some point; observeValueForKeyPath would also be defined as a method in the controller.
Check that your IBOutlet is connected correctly, probably the tableIndicator ivar points to a nil.
Consider this simple code below, it works. It just creates a window, add a red square on it, then register using KVO the object to be notified for frame change. Finally it instantiates a button: each time you tap on it the frame is reduced by size, and the notification is triggered correctly (you will see the message in the debug console).
So you must check your code.
#import "AppDelegate.h"
#implementation AppDelegate
#synthesize window = _window;
#synthesize v = _v;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
self.v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
_v.backgroundColor=[UIColor redColor];
[self.window addSubview:_v];
UIButton *b = [UIButton buttonWithType:UIButtonTypeRoundedRect];
b.frame=CGRectMake(0, 300, 40, 10);
[b setTitle:#"A" forState:UIControlStateNormal];
[b addTarget:self action:#selector(changeFrame) forControlEvents:UIControlEventTouchUpInside];
[_window addSubview:b];
[_v addObserver:self forKeyPath:#"frame" options:0 context:NULL];
return YES;
}
-(void)changeFrame {
CGRect _f = self.v.frame;
_f = CGRectInset(_f, 20, 20);
_v.frame=_f;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(#"Observing...");
}

Different transparencies in Cocoa?

I have overloaded NSWindow and have created a custom window of my own (Borderless and transparency of 0.3 alphaValue). I am going to be drawing images in this window. Is there any way I can get the images that will be drawn in the window opaque? I want the window to remain transparent but want the images to be opaque. How would I do this?
Mac OS X Snow Leopard
Xcode 3.2.6
#ughoavgfhw is on the right track, but it's actually much easier. You just need to set opaque to NO and set backgroundColor to semi-transparent.
#implementation MYWindow
- (void)setup
{
[self setStyleMask:NSBorderlessWindowMask];
[self setOpaque:NO];
[self setBackgroundColor:[NSColor colorWithCalibratedWhite:1.0 alpha:0.3]];
}
// We override init and awakeFromNib so this works with or without a nib file
- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
{
self = [super initWithContentRect:contentRect styleMask:aStyle backing:bufferingType defer:flag];
if (self)
{
[self setup];
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self setup];
}
#end
There's a potential problem with this approach.
Requesting "[self setStyleMask:NSBorderlessWindowMask];" (with any of the possible StyleMask values) will cause the loss of keystroke events to that window on subsequent presentations of the window as a sheet. I reported a bug to Apple today on this point.
Leave the alphaValue property to 1, and set the opaque property to NO. Then, replace the default contentView with one which fills itself with a color whose alpha component is 0.3 in its drawRect: method. When you change the alphaValue property, it changes how everything drawn in the window is displayed. When you make it non-opaque, it simply doesn't draw a black background beneath the content view, so if the content view is transparent, the window will be too, but anything drawn on top of that will not be affected.
Here's an example which uses a white background with a 0.3 alpha component. Note that I overrode the setContentView: method. This is so that I can copy any views from the passed view into the transparent content view, and is especially necessary if you load the window from a nib, since the nib loading will change the content view when it is loaded. (You could change the content view's class in IB instead.)
#interface MyWindow_ContentView : NSView
#end
#implementation MyWindow
- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag {
if(self = [super initWithContentRect:contentRect styleMask:aStyle backing:bufferingType defer:flag]) {
[super setOpaque:NO];
[super setContentView:[[[MyWindow_ContentView alloc] init] autorelease]];
}
return self;
}
- (void)setOpaque:(BOOL)ignored {}
- (void)setContentView:(NSView *)newView {
NSArray *views = [[self.contentView subviews] copy];
[views makeObjectsPerformSelector:#selector(removeFromSuperview)];
views = [[newView subviews] copy];
[views makeObjectsPerformSelector:#selector(removeFromSuperview)];
for(NSView *view in views) [self.contentView addSubview:view];
[views release];
}
#end
#implementation MyWindow_ContentView
- (void)drawRect:(NSRect)rect {
[[NSColor colorWithCalibratedWhite:1 alpha:0.3] set];
NSRectFill(rect);
}
#end

How can I get notified when a UIView becomes visible?

Is there a way to get a notification, a callback or some other means to call a method whenever a UIView becomes visible for the user, i.e. when a UIScrollview is the superview of some UIViews, and the ViewController of such a UIView shall get notified when its view is now visible to the user?
I am aware of the possible, but not so elegant solution of checking to which position the ScrollView scrolled (via UIScrollViewDelegate-methods) and compute if either one of the subviews is visible...
But I'm looking for a more universal way of doing this.
I've managed to solve the problem this way:
First, add a category for UIView with the following method:
// retrieve an array containing all super views
-(NSArray *)getAllSuperviews
{
NSMutableArray *superviews = [[NSMutableArray alloc] init];
if(self.superview == nil) return nil;
[superviews addObject:self.superview];
[superviews addObjectsFromArray:[self.superview getAllSuperviews]];
return superviews;
}
Then, in your View, check if the window-property is set:
-(void)didMoveToWindow
{
if(self.window != nil)
[self observeSuperviewsOnOffsetChange];
else
[self removeAsSuperviewObserver];
}
If it is set, we'll observe the "contentOffset" of each superview on any change. If the window is nil, we'll stop observing. You can change the keyPath to any other property, maybe "frame" if there is no UIScrollView in your superviews:
-(void)observeSuperviewsOnOffsetChange
{
NSArray *superviews = [self getAllSuperviews];
for (UIView *superview in superviews)
{
if([superview respondsToSelector:#selector(contentOffset)])
[superview addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}
}
-(void)removeAsSuperviewObserver
{
NSArray *superviews = [self getAllSuperviews];
for (UIView *superview in superviews)
{
#try
{
[superview removeObserver:self forKeyPath:#"contentOffset"];
}
#catch(id exception) { }
}
}
Now implement the "observeValueForKeyPath"-method:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:#"contentOffset"])
{
[self checkIfFrameIsVisible];
}
}
Finally, check if the view's frame is visible inside the window's frame:
-(void)checkIfFrameIsVisible
{
CGRect myFrameToWindow = [self.window convertRect:self.frame fromView:self];
if(myFrameToWindow.size.width == 0 || myFrameToWindow.size.height == 0) return;
if(CGRectContainsRect(self.window.frame, myFrameToWindow))
{
// We are visible, do stuff now
}
}
If your view is exhibiting behavior, it should be within a view controller. On a view controller, the viewDidAppear method will be called each time the view appears.
- (void)viewDidAppear:(BOOL)animated
I don't think there's a universal way to do this for views. Sounds like you're stuck with scrollViewDidEndScrolling and other ScrollViewDelegate methods. But I'm not sure why you say it's elegant, they're quite straightforward.
view's layer property should tell us if that view is visible or not
[view.layer visibleRect];
but this isnt working for me.
So work around could be to use UiScrollView contentOffset property to calculate if particular view is visible or not.