What I want to achieve
I have a custom UITableViewCell featuring a UITextField for in-place-editing. A cell's field should be enabled while the UITableView is in edit mode, but NOT while the cell's delete confirmation is showing.
How I'm achieving it
I've subclassed UITableViewCell and overridden its willTransitionToState method thusly:
- (void)willTransitionToState:(UITableViewCellStateMask)state {
[super willTransitionToState:state];
self.nameField.enabled = !(state & UITableViewCellStateShowingDeleteConfirmationMask)
&& (state & UITableViewCellStateEditingMask);
}
The problem
I'm 90% of the way there.
'willTransitionToState' is called after the user presses the cell's '-' button. The delete confirmation is shown and my text field is disabled as desired. But what if the user decides not to delete the cell and hides the delete confirmation by swiping right? In this case, 'willTransitionToState' is not called.
As a result, my text field is stuck in its disabled state even though it should be enabled when the delete confirmation is hidden. You would think that given the fact that a 'UITableViewCellStateShowingDeleteConfirmationMask' flag exists, that 'willTransitionToState' would be called symmetrically, but this doesn't seem to be the case.
UPDATE
It seems that the 'showingDeleteConfirmation' property of UITableViewCell always gives the correct result. So theoretically I could iterate through each cell calling 'showingDeleteConfirmation' and enable or disable each text field accordingly. This is inefficient and kludgy. I'm considering filing a bug report on 'willTransitionToState', but I need more data points. Has anybody else encountered this problem?
4/29/2014
Apple confirms this is a bug. As of today, the bug report is still OPEN.
Declare a context for KVO:
static int KVOContext;
Get the scrollView in the cell with a custom getter:
- (UIScrollView *)scrollViewToObserve
{
UIView *view = self.subviews[0];
return (view != nil && [view isKindOfClass:[UIScrollView class]]) ? (UIScrollView *)view : nil;
}
Add observer to scrollView's contentOffset:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self.scrollViewToObserve addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:&KVOContext];
}
return self;
}
Remove observer in dealloc:
- (void)dealloc
{
[self.scrollViewToObserve removeObserver:self forKeyPath:#"contentOffset" context:&KVOContext];
}
Use KVO to observe changes to contentOffset, but use showingDeleteConfirmation for the enabled state:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == &KVOContext) {
if ([keyPath isEqualToString:#"contentOffset"]) {
self.textField.enabled = !self.showingDeleteConfirmation;
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Warning: View hierarchy can change in future updates
Related
I am trying to implement a KVO example for clipsToBounds property of all subviews in my UIView. I do not quite understand how to change the value in observeValueForKeyPath method. I am using this code:
-(void)ViewDidLoad{
[self.navigationController.view addObserver:self forKeyPath:#"clipsToBounds" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(#"Triggered...")
}
It is triggered when ever i change the property clipToBounds of a subview that exists in the UIView i have. I need to change the value back to false for every trigger that happens. What should i write inside the observeValueForKeyPath to change the clipsToBounds property? Any help appreciated.
of course adding the Observer must be done before it works.
Guessing your typo in "ViewDidLoad" would just never be called because it should be "viewDidLoad".
Apart from that your KVO pattern could look like..
static void *kvoHelperClipsToBounds = &kvoHelperClipsToBounds;
-(void)viewDidLoad {
[self.navigationController.view addObserver:self forKeyPath:#"clipsToBounds" options:NSKeyValueObservingOptionNew context:&kvoHelperClipsToBounds];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == kvoHelperClipsToBounds) {
NSLog(#"context compared successful...");
//be careful what you cast to.. i dont check isKindOf here.
UINavigationBar* navbar = (UINavigationBar*)object;
if (navbar.subviews.count > 1) {
__kindof UIView *sub = navbar.subviews[1];
if (sub.clipsToBounds) {
dispatch_async(dispatch_get_main_queue(),^{
sub.clipsToBounds = NO;
[self.navigationItem.titleView layoutIfNeeded];
});
}
}
}
// or compare against the keyPath
else if ([keyPath isEqualToString:#"clipsToBounds"]) {
NSLog(#"classic Key compare Triggered...");
}
else
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
[super observeValueForKeyPath...] passes not recognized keyPath to super to let super's class KVO work, otherwise those would be ignored if super's implementation would rely on observing them. Which should also explain how you could observe all subviews if needed. But think about that there could be potentially hundrets of subviews triggering observeValueForKeyPath if a subclass of UIView would implement it and all subviews (or the ones you like) would be inherited also from this special subclass.
When you change clipsToBounds inside the KVO where you observe it, you possibly invoke a loop, specially when you watch both - old and new values. you would change the property, the property triggers kvo, kvo changes the property, the property triggers kvo and on and on.
set [self.navigationController.view setClipsToBounds:YES] to change the property. But if done inside KVO it will trigger KVO again as explained.
Usually you would set clipsToBounds in -initWithFrame: or in -initWithCoder: or via Interface Builder and maybe just observe if it gets changed to adapt some other code.
Sidenote: the context just needs to be unique to distinguish it from other KVO.. it could also be reference to a real objects pointer.
Don't forget added Observers must be removed before deallocation.
I'm building a UILabel library that is mostly intended to be used programmatically but I want it to work with Interface Builder as well. When the user calls setText on my UILabel subclass, I watch it there and update the text as per the label's use, but it doesn't appear to be called with IB.
Should I just be watching awakeFromNib?
Question should read: Using KVO in UIView subclass
If so, -initWithCoder: is the preferred location while -awakeFromNib still works as mentioned by #KhanhNguyen.
The problem becomes How do I find my UILabel? and here is a possible answer using -viewWithTag:
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if(self = [super initWithCoder:aDecoder]) {
[self registerKVO:[self viewWithTag:1]
forKeyPath:NSStringFromSelector(#selector(text))];
}
return self;
}
Answering the general UIViewController question
Note - (instancetype)initWithCoder:(NSCoder *)aDecoder is not the correct place to install the KVO in a UIViewController since the view hasn't loaded and neither has the label.
Register KVO using a typical Referencing Outlet
- (void)viewDidLoad {
[super viewDidLoad];
[self registerKVO:self.label
forKeyPath:NSStringFromSelector(#selector(text))];
}
Regardless of your approach, responding to KVO looks like this:
#pragma mark - NSObject(NSKeyValueObserving)
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqualToString:NSStringFromSelector(#selector(text))]) {
NSLog(#"%#",change[NSKeyValueChangeNewKey]);
}
}
...and setup + teardown like that:
- (void)registerKVO:(NSObject *)object forKeyPath:(NSString *)keypath {
[object addObserver:self
forKeyPath:keypath
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)dealloc {
[self removeObserver:self forKeyPath:NSStringFromSelector(#selector(text))];
#if !__has_feature(objc_arc)
[super dealloc];
#endif
}
At the begining I have to mention that I don't want to subclassing UIButton due to any other reasons.
Let's assume we have UIButton category which, along with some formatting methods, is also observing on self state changes (enabled, highlighted, selected) to invoke fancy button formatting based on CALayer.
I've managed to set observer for that but while UIButton is deallocating I should also remove it as a observer of itself.
Found already possible solutions:
1) SFObservers - crafted global automatic remover of all in-app NSNotificationCenter and KVO observers: http://www.merowing.info/2012/03/automatic-removal-of-nsnotificationcenter-or-kvo-observers/
2) Associate another dummy object to UIButton to obtain my own dealloc mentioned here: Hooking end of ARC dealloc
and its ready-to-use solution here: https://github.com/krzysztofzablocki/NSObject-SFExecuteOnDealloc
But...
Question: Isn't there any simpler solution for that, like for example observing self dealloc?
Maybe, I don't need to remove KVO of self (deallocated object won't send any changes to itself)?
My current code:
#pragma mark - State changes KVO
// Define context
static void * atwContext = &atwContext;
// Method to register for state changes notifications
- (void)atwStartObservingStateChanges {
[self addObserver:self forKeyPath:#"enabled" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:atwContext];
}
// Method to receive all registered state changes notifications
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == atwContext) {
if ([#"enabled" isEqualToString:keyPath]) {
NSNumber *newState = [change objectForKey:NSKeyValueChangeNewKey];
NSNumber *oldState = [change objectForKey:NSKeyValueChangeOldKey];
if (oldState && [newState isEqualToNumber:oldState]) {
NSLog(#"Enabled state has not changed");
} else {
NSLog(#"Enabled state has changed to %d", [object isEnabled]);
}
}
}
}
Okay I tested the following with the tabbed application template on Xcode 4.5/iOS 6.
Created a tabbed application.
Created a UIButton subclass called SampleButton and implemented
the following mothods:
- (void)touchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
}
- (void) touchesMoved:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
}
Added this SampleButton to the first tab.
Added breakpoints to all the touch methods.
Run on device.
Tested that all the touch methods are firing as expected.
Now touch the SampleButton and then also press the second tab.
RESULT: View switches to second tab but touchesCancelled and/or touchesEnded are never called in SampleButton. Shouldn't one or the other of those fire if the view changes while I'm touching that button? This is proving to be a huge issue because, in my app I'm playing a sound while that button is down and it never stops playing if the user switches tabs while pressing it. Seems like this used to work fine in iOS3 and iOS4.
It appears that when a view is removed from its window, it dissociates itself from any touches that were associated with it. So when the touch finally ends, the system doesn't send touchesEnded:… or touchesCancelled:… to the view.
Workaround by disabling tab switching
If you want to just disable tab switching while the button is pressed, you can do that by giving the tab bar controller a delegate and having the delegate return NO from tabBarController:shouldSelectViewController:. For example, in the your test app, you can have FirstViewController make itself the tab bar controller's delegate:
- (void)viewWillAppear:(BOOL)animated {
self.tabBarController.delegate = self;
}
And the view controller can allow the tab bar controller to select a tab only when the button is not pressed (highlighted):
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
return !_button.highlighted;
}
Workaround by detecting button highlight resetting to NO
When the button is removed from its window, it resets its highlighted property to NO. So one generic way to work around this problem is by using key-value observing (KVO) to monitor the button's state (instead of relying on the button to send you actions). Set yourself up as an observer of the button's highlighted property like this:
static int kObserveButtonHighlightContext;
- (void)viewDidLoad {
[super viewDidLoad];
[_button addObserver:self forKeyPath:#"highlighted"
options:NSKeyValueObservingOptionOld
context:&kObserveButtonHighlightContext];
}
- (void)dealloc {
[_button removeObserver:self forKeyPath:#"highlighted"
context:&kObserveButtonHighlightContext];
}
I discovered in testing that the button sends an extra KVO notification when it's removed from the window, before it resets its highlighted property back to NO. So when handling the KVO notification, check that the value has actually changed:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == &kObserveButtonHighlightContext) {
if ([change[NSKeyValueChangeOldKey] boolValue] != _button.highlighted) {
[self updatePlaybackForButtonState];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Finally, start or stop playback according to the highlighted property of the button:
- (void)updatePlaybackForButtonState {
if (_button.highlighted) {
NSLog(#"start playback");
} else {
NSLog(#"end playback");
}
}
Subclassing the button seems like the hard way to do this. Just tell the audio player to stop playing in the viewDidDisappear:animated: method, as R.A. suggested.
I worked around this by setting targets for the actions instead of using the touch methods:
[self addTarget:self action:#selector(handleKeyPressed) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:#selector(handleKeyReleased) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:#selector(handleKeyReleased) forControlEvents:UIControlEventTouchCancel];
These seems to get fired correctly even when the view swaps out.
I am working on integrating an ad provider into my app currently. I wish to place a fading message in front of the ad when the ad displays but have been completely unsuccessful.
I made a function which adds a subview to my current view and tries to bring it to the front using
[self.view bringSubviewToFront:mySubview]
The function fires on notification that the ad loaded (the notification is from the adprovider's SDK). However, my subview does not end up in front of the ad. I imagine this is made purposely difficult by the ad provider, but I wish to do it regardless. I am currently discussing with the provider whether or not this can be allowed. But for the time being, I just want to see if it's even possible.
Is there anyway I can force a subview of mine to be the top-most view such that it will not be obstructed by anything?
try this:
self.view.layer.zPosition = 1;
What if the ad provider's view is not added to self.view but to something like [UIApplication sharedApplication].keyWindow?
Try something like:
[[UIApplication sharedApplication].keyWindow addSubview:yourSubview]
or
[[UIApplication sharedApplication].keyWindow bringSubviewToFront:yourSubview]
Swift 2 version of Jere's answer:
UIApplication.sharedApplication().keyWindow!.bringSubviewToFront(YourViewHere)
Swift 3:
UIApplication.shared.keyWindow!.bringSubview(toFront: YourViewHere)
Swift 4:
UIApplication.shared.keyWindow!.bringSubviewToFront(YourViewHere)
Hope it saves someone 10 seconds! ;)
I had a need for this once. I created a custom UIView class - AlwaysOnTopView.
#interface AlwaysOnTopView : UIView
#end
#implementation AlwaysOnTopView
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == self.superview && [keyPath isEqual:#"subviews.#count"]) {
[self.superview bringSubviewToFront:self];
}
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
- (void)willMoveToSuperview:(UIView *)newSuperview {
if (self.superview) {
[self.superview removeObserver:self forKeyPath:#"subviews.#count"];
}
[super willMoveToSuperview:newSuperview];
}
- (void)didMoveToSuperview {
[super didMoveToSuperview];
if (self.superview) {
[self.superview addObserver:self forKeyPath:#"subviews.#count" options:0 context:nil];
}
}
#end
Have your view extend this class. Of course this only ensures a subview is above all of its sibling views.
As far as i experienced zposition is a best way.
self.view.layer.zPosition = 1;
Let me make a conclusion. In Swift 5
You can choose to addSubview to keyWindow, if you add the view in the last.
Otherwise, you can bringSubViewToFront.
let view = UIView()
UIApplication.shared.keyWindow?.addSubview(view)
UIApplication.shared.keyWindow?.bringSubviewToFront(view)
You can also set the zPosition. But the drawback is that you can not change the gesture responding order.
view.layer.zPosition = 1
In c#, View.BringSubviewToFront(childView);
YourView.Layer.ZPosition = 1;
both should work.
In Swift 4.2
UIApplication.shared.keyWindow!.bringSubviewToFront(yourView)
Source: https://developer.apple.com/documentation/uikit/uiview/1622541-bringsubviewtofront#declarations