Crash - UITableViewCell subclass KVO'ing UITableView.panGestureRecogonizer.state - ios

Situation:
I've subclassed UITableViewCell because I need to add custom action buttons on either side of the UITableViewCell. There are certain situations where I need to set the UITableView back to normal (hide the custom action buttons). e.g. When the user scrolls upwards in the UITableView. To do this I am adding my custom UITableViewCell as an observer of the containing UITableView's UIPangestureRecognizer's state.
Problem:
When popping the UIViewController that contains the UITableView and custom UITableViewCells I receive the following error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x7b21b920 of
class UIScrollViewPanGestureRecognizer was deallocated while key value
observers were still registered with it. Current observation info:
( Context: 0xb83618, Property: 0x7b3e13b0>
Context: 0xb83618,
Property: 0x7b3e13b0>
Context: 0xb83618, Property: 0x7b3e13b0> Context: 0xb83618, Property: 0x7b3e13b0> )'
Which is obviously saying that the UIPanGestureRecognizer is being deallocated before the custom UITableViewCell's are.
Question:
Where should I remove the custom UITableViewCell as an observer of the UITableView's UIPanGestureRecognizer so I don't encounter this exception?
Code: (I hope this isn't too much code to comb through. I apologize if it is.)
CustomUITableViewCell.m
#pragma mark - Setter Methods
- (void)setContainingTableView:(UITableView *)containingTableView
{
if (self.isObservingContainingTableViewPanGestureRecognizer)
{
self.observingContainingTableViewPanGestureRecognizer = NO;
[_containingTableView.panGestureRecognizer removeObserver:self forKeyPath:kUITableViewPanGestureRecognizerStateKeyPath];
}
_containingTableView = containingTableView;
if (containingTableView)
{
self.observingContainingTableViewPanGestureRecognizer = YES;
[containingTableView.panGestureRecognizer addObserver:self forKeyPath:kUITableViewPanGestureRecognizerStateKeyPath options:0 context:UITableViewPanGestureRecogonizerContext];
}
}
#pragma mark -
#pragma mark - Overrides
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
self.containingTableView = nil;
UIView * view = self.superview;
while (view)
{
if ([view isKindOfClass:[UITableView class]])
{
self.containingTableView = (UITableView *)view;
break;
}
view = view.superview;
}
}
- (void)dealloc
{
self.containingTableView = nil;
}
#pragma mark -
#pragma mark - Key Value Observing
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == UITableViewPanGestureRecogonizerContext)
{
if ([keyPath isEqual:kUITableViewPanGestureRecognizerStateKeyPath])
{
UIPanGestureRecognizer * panGestureRecognizer = (UIPanGestureRecognizer *)object;
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan)
{
CGPoint velocity = [panGestureRecognizer velocityInView:self.contentCellView];
if (fabs(velocity.y) >= fabs(velocity.x))
{
[self.scrollView setContentOffset:CGPointZero animated:YES];
}
}
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark -
As always, any help is greatly appreciated! Also, if anyone needs any more information please let me know.
EDIT:
Oddly enough, the custom UITableViewCell's dealloc method is called and the custom UITableViewCell is removed as an observer before I the exception is thrown.

It turns out that I needed to keep a reference to the UITableView's UIPanGestureRecognizer. More than likely I'll end up subclassing UITableView to eliminate some of the complications.

UITableView is subclass of UIScrollView. If you only want to detect when user is scrolling it you can use scrollview delegate method:
(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;

I think you should pass weak reference of table view to cell (as delegate) in cellForRowAtIndexPath rather than looking for the tableview in didMoveToSuperview. Overall, I don't think it's a good idea to have table view's gesture observer in cell. However if you really want it, make sure you register/unregister properly.
Also make sure isObservingContainingTableViewPanGestureRecognizer flag's initial value is right when the cell gets reused.

Related

Listen to UIPickerView subview dragging and decelerating start/end

I'm trying to extend the current Picker component for ios in react native:
https://github.com/facebook/react-native/blob/master/React/Views/RCTPicker.m
https://github.com/facebook/react-native/blob/master/React/Views/RCTPickerManager.m
I would like to add the possibility to listen for startDragging and endDragging, startDecelerating and endDecelerating events.
I've found there that we can actually know if the user is currently dragging the Picker by checking its subviews dragging and decelerating flags. That's good but an event/delegate like pattern would suit the React native bridging model better.
Could you please suggest me how to listen to the Picker subviews dragging and decelerating values changes?
--- EDIT ---
Here is what I've tried so far:
I edited the Picker Manager file and added this:
// ...
#implementation RCTPickerManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
//return [RCTPicker new];
RCTPicker* picker = [RCTPicker new];
[self setDelegateForScrollViews:picker];
return picker;
}
// ...
RCT_EXPORT_VIEW_PROPERTY(onScrollChange, RCTBubblingEventBlock)
// ...
-(void)setDelegateForScrollViews:(UIView*)view
{
if([view isKindOfClass:[UIScrollView class]]){
UIScrollView* scroll_view = (UIScrollView*) view;
RCTLogInfo(#"DEBUG setting a delegate on SV");
scroll_view.delegate = self;
}
else {
for(UIView *sub_view in [view subviews]){
[self setDelegateForScrollViews:sub_view];
}
}
}
// UIScrollViewDelegate methods
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
RCTLogInfo(#"DEBUG scrollViewWillBeginDragging");
((RCTPicker*)self.view).onScrollChange(#{ #"state": [NSNumber numberWithBool:YES] });
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate {
RCTLogInfo(#"DEBUG scrollViewDidEndDragging");
((RCTPicker*)self.view).onScrollChange(#{ #"state": [NSNumber numberWithBool:NO] });
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
RCTLogInfo(#"DEBUG scrollViewDidEndDecelerating");
// TODO
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
RCTLogInfo(#"DEBUG scrollViewDidEndScrollingAnimation");
}
#end
But the UIScrollView Delegate methods are never raised when I scroll the picker :(...
Anyone any idea?
Try subclassing the picker view and implementing the scrollview delegate methods. Don't forget to call super. I don't see why you're wanting to do this though. These functions are hidden by design. What are you trying to achieve by this? Even if you have a legit reason, you should probably reconsider what you are trying to do. If you really need to know when a user is scrolling to select something, use a UITableView and you can get its scrollview delegate events.

How to make floating view?

What I mean by floating view is custom view that is a subview (or appears to be a subview) of scrollview which scrolls along until it anchors on certain point. Similar behavior would be UITableView's section header. Attached image below
My content view (the view underneath the floating view) is not in tableview layout. Meaning if I use tableview only for the floating view, I have to put my content view inside 1 giant cell or break it to several cells with different layouts. The content view will have a lot of dynamic elements which is why I don't want to put it inside UITableViewCell unless I have to. Can I make floating view programmatically / using autolayout on scrollview?
Using the tableview section header is probably the best solution, you can always easily customise the number of cells or cells themselves to achieve a particular layout.
However if you definitely don’t want to deal with a tableview, this component seems really cool, it's actually is meant to be added to a tableview, but I tested it with the twitter example and you can actually add it to a scrollview, so you don’t need a table view and it will work, give props the guy who made it. GSKStretchyHeaderView
Hope this helps, comment if you have any questions, good luck.
Use KVO to update the floating view's frame.
Here is the sample code written in Objective-C:
// ScrollView.m
// ScrollView is a subclass of UIScrollView
#interface ScrollView ()
#property (nonatomic, strong) UIView *floatingView;
#property (nonatomic) CGRect originalBorderFrame;
#property (nonatomic) CGFloat anchorHeight;
#end
#implementation ScrollView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.floatingView = [UIView new];
self.floatingView.backgroundColor = [UIColor colorWithRed:0.8211 green:0.5 blue:0.5 alpha:1.0];
self.floatingView.frame = CGRectMake(0, 150, frame.size.width, 20);
self.originalBorderFrame = self.floatingView.frame;
[self addSubview:self.floatingView];
self.anchorHeight = 44;
[self addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
- (void)dealloc {
[self removeObserver:self forKeyPath:#"contentOffset"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:#"contentOffset"]) {
if (self.contentOffset.y > self.originalBorderFrame.origin.y-self.anchorHeight) {
self.floatingView.frame = CGRectOffset(self.originalBorderFrame, 0, self.contentOffset.y - (self.originalBorderFrame.origin.y-self.anchorHeight));
}
}
}
#end
Here is the capture:

IOS, UIView, Detect Hidden State Change in Subview

Is there anyway to detect a hidden state change (or other change) in a sub view in a UIView (not UIViewController). Would like to detect this async somehow.
There are reasons for my madness.
You can use KVO (key value observing) to detect a change to the value of the property hidden.
Add your observer (self in this example) in the following way:
UIView* viewToObserve = [self getViewToObserve]; // implement getViewToObserve
[viewToObserve addObserver:self forKeyPath:#"hidden" options:0 context:NULL];
Now add the following method to your observer class:
- (void) observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
UIView* viewToObserve = [self getViewToObserve];
if (object == viewToObserve)
{
if ([keyPath isEqualToString:#"hidden"])
{
// react to state change
}
}
}
The observer method will be invoked whenever the hiddenproperty changes its value. If I am not mistaken, the method will be invoked synchronously in the context of the thread that makes the change to the property. If you need asynchronous notification you can add that yourself, for instance by using one of the NSObject methods performSelector:withObject:afterDelay: or performSelector:onThread:withObject:waitUntilDone:.
BTW: You don't need the checks in the observer method, obviously, if you only observe a single object and/or property. I left the checks in for illustration purposes. I also recommend reading Apple's documentation on KVO and KVC (key value coding) to understand what's going on here.
The runtime happily continues notifying your observer even if the observer is deallocated - resulting in an application crash! So don't forget to remove the observer before it is de-allocated, at the latest this should happen in the observer's dealloc:
- (void) dealloc
{
UIView* viewToObserve = [self getViewToObserve];
[viewToObserve removeObserver:self forKeyPath:#"hidden"];
[super dealloc];
}
You can override the property in the UIView subclass and do anything in didSet
class MyView: UIView {
override var isHidden: Bool {
didSet {
//do something
}
}
}

UITableView cell just disappeared callback?

I have a UITableView with heavy images content. So the scrolling is not fluid anymore.
I want to add a timer to load the images, while you scroll I create the timer for each row. If the cell quits the view, I cancel the timer. If not I fade in the images.
My question is : is there a callback for a cell going out of view ? I'm reading the doc, but I'm not sure there is anything for my needs.
Thanks for the help !
EDIT: The code I'm using (this is the three20 library, I'm using a custom TTTableItemCell. The "_tabBar1.tabItems = item.photos" is the line hoging resources. On the first load it's okay because the photos are being loaded asynchronously from the server, but when I scroll back or reload the view, they are all loaded synchronously, and the scrolling isn't smooth anymore, especially on an iPhone 3G. :
- (void)setObject:(id)object {
if (_item != object) {
[super setObject:object];
Mission* item = object;
self.textLabel.text = item.name;
_tabBar1.tabItems = nil;
timerFeats = [NSTimer scheduledTimerWithTimeInterval:(0.5f) target:self selector:#selector(updateFeats) userInfo:nil repeats: NO];
//_tabBar1.tabItems = item.photos;
}
}
-(void)updateFeats {
DLog(#"timer ended");
Mission* item = self.object;
self._tabBar1.tabItems = item.photos;
}
If you're using iOS 6 and up, simply override this method:
- tableView:didEndDisplayingCell:forRowAtIndexPath:
It'll be called when the cell has already gone out of the view, and you'll get it and its indexPath.
Alright, I found a way.
There is actually a callback to know what cell is about to get out of view. :
- (void)willMoveToSuperview:(UIView *)newSuperview;
So my code is :
- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
if(!newSuperview) {
DLog(#"timer invalidated");
if ([timerFeats isValid]) {
[timerFeats invalidate];
}
}
}
If there is no newSuperview the cell is going out of the view and so I verify first that my timer hasn't been invalidated yet, and then I cancel it.
I suggest to use a KVO approach:
On your awakeFromNib method (or whatever method you use to instantiate the cell) add the following:
- (void)awakeFromNib {
[self addObserver:self forKeyPath:#"hidden" options:NSKeyValueObservingOptionNew context:nil];
...
}
Be sure to implement the delegate method for the observer as follow:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if([keyPath isEqualToString:#"hidden"]) {
NSLog(#"cell is hidden");
}
}
When a UITableView is initially shown it calls this method once for each row
- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
after this the method is called when ever a new row is required which is often the result of scrolling to reveal a new row and pushing an old view off the other end.
So this is an ideal hook to find out which rows are visible. To check which cells are visible you can call
- (NSArray *)indexPathsForVisibleRows
Because the tableview is the only thing holding a reference to your cells before they are recycled or freshly creaetd you can not get a handle on those timers. What I suggest is creating an NSMutableDictionary ivar and when you create your cells add the timer to the NSMutableDictionary
[timersForIndexs setObject:yourTimer forKey:indexPath];
Now when you recieve - (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath you need to do something like
NSMutableDictionary *tmpDictionary = [timersForIndexs copy];
[tmpDictionary removeObjectsForKeys:[self.tableView indexPathsForVisibleRows]];
NSArray *timers = [tmpDictionary allKeys];
[timers makeObjectsPerformSelector:#selector(invalidate)];
I'm not in front of xcode so this is dry coded so please let me know if you have any problems

Objective-C, how to Generally resignFirstResponder?

(my boss says) that I have to implement a "Done" button on a navBar so that the various items in the view (that contain an edit box) will dismiss their keyboard (if they were in focus).
It seems that I must iterate through all items and then call resignFirstResponder on each on the off-chance that one of them is in focus? This seems a bit messy (and hard to maintain if e.g. someone else adds more items in future) - is there a better way to do it?
I have found it!
Thanks to this
I discovered that all I need do is this:-
-(void) done {
[[self.tableView superview] endEditing:YES];
}
// also [self.view endEditing:YES]; works fine
[remark]
Also I learn how to do the equivalent of an "eventFilter" to stop UITableViewController from swallowing background touch events by intercepting them before they get there - from the same, wonderful post on that thread - see "DismissableUITableView".
[end of remark]
You don't have to iterate through the controls since only one can be first responder at the moment.
This will reset the responder to the Window itself:
[[self window] makeFirstResponder:nil]
One solution is to use a currentTextField Object,
In .h file have an instance variable as
UITextField *currentTextField;
Now in .m file.
Note : Dont forget to set the delegates of all the textField to this class
- (void)textViewDidBeginEditing:(UITextView *)textView
{
currentTextField = textField;
}
- (void)textViewDidEndEditing:(UITextView *)textView
{
currentTextField = nil;
}
Now in your button action method
-(IBAction)buttonTap
{
if([currentTextField isFirstResponder])
[currentTextField resignFirstResponder];
}
This avoids iterating through all the text field.
I think best way to handle it by searching all subviews of main view with recursive function, check example below
- (BOOL)findAndResignFirstResponder {
if (self.isFirstResponder) {
[self resignFirstResponder];
return YES;
}
for (UIView *subView in self.subviews) {
if ([subView findAndResignFirstResponder]) {
return YES;
}
}
return NO;
}
and also you can put this method to your utility class and can use from tap gesture. All you have to do is simply adding to gesture to view.
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(hideEverything)];
[self.tableView addGestureRecognizer:gestureRecognizer];
and than you can call hideEverything method;
- (void) hideKeyboard {
[self.view findAndResignFirstResponder];
...
...
}

Resources