I'm trying to implement UIRefreshControl in a UIViewController. I can't use UITableViewController because the tableView is just one segment of my viewController.
In most cases this workaround works like charm. But sometimes (random occurrence) the app crashes with EXC_BAD_ACCESS code=1
- (void)viewDidLoad {
[super viewDidLoad];
UIRefreshControl * refCon = [[UIRefreshControl alloc] init];
[refCon addTarget:self selector:#selector(refresh:) forControlEvent:UIControlEventValueChanged];
[tableView addSubView:refCon];
}
- (void)refresh:(UIRefreshControl *)sender {
[NSThred detachNewThreadSelector:#selector(doRefresh:) toTarget:self withObject:sender];
}
- (void)doRefresh:(UIRefreshControl *)sender {
[self checkUpdate];
[self loadData];
[sender endRefreshing];
}
You're invoking -endRefreshing on a background thread. Don't do that.
And also, adding a UIRefreshControl directly as a subview of UITableView is not guaranteed to work. You should be using a UITableViewController.
The first you put a tag at UIRefreshControl
UIRefreshControl * refCon = [[UIRefreshControl alloc] init];
[refCon addTarget:self selector:#selector(refresh:) forControlEvent:UIControlEventValueChanged];
refCon.tag = 101 //for example
[tableView addSubView:refCon];
When the tableView finish of reload then you put:
UIRefreshControl *getRefreshControl = (UIRefreshControl*)[self.tablaPildoras viewWithTag:101];
[getRefreshControl endRefreshing];
Related
I've added a 'pull down to refresh' function to my TableView inside a ViewController. The data refresh works perfectly, but I'm wondering how I can add the classic activity indicator into the top when the refresh is executed?
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
[_refreshControl addTarget:self action:#selector(refreshData) forControlEvents:UIControlEventValueChanged];
//[self.mytable addSubview:refreshControl];
UITableViewController *tableViewController = [[UITableViewController alloc] init];
tableViewController.tableView = self.tableView;
tableViewController.refreshControl = _refreshControl;
}
-(void)refreshData
{
UITableViewController *tableViewController = [[UITableViewController alloc] init];
tableViewController.tableView = self.tableView;
[self.tableView reloadData];
[tableViewController.refreshControl endRefreshing];
}
It looks like the issue is that both viewDidLoad and refreshData are creating new tableViewControllers but they should be updating your existing one.
If your class in ViewController.m is a subclass of UITableViewController you can update your code to use the current view controller.
- (void)viewDidLoad {
[super viewDidLoad];
[_refreshControl addTarget:self action:#selector(refreshData) forControlEvents:UIControlEventValueChanged];
self.refreshControl = _refreshControl;
}
-(void)refreshData
{
[self.tableView reloadData];
[self.refreshControl endRefreshing];
}
Another issue is that refreshData immediately calls endRefreshing so the refresh Controller wont have any time to be visible before it ends. Normally you'd have an asynchronous task running, like a network request, then call endRefreshing once that completes. To fake it you can delay calling endRefreshing with the following.
-(void)refreshData
{
// Wait 1 sec before ending refreshing for testing
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.refreshControl endRefreshing];
});
}
This is what ended up working!
ViewController.h
#property (nonatomic, strong) UIRefreshControl *refreshControl;
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.refreshControl = [[UIRefreshControl alloc] init];
self.refreshControl.attributedTitle = [[NSAttributedString alloc]initWithString:#"Pull To Refresh"];
[self.refreshControl addTarget:self action:#selector(refreshData) forControlEvents:UIControlEventValueChanged];
[self.tableView addSubview:self.refreshControl];
}
-(void)refreshData
{
// Wait 1 sec before ending refreshing for testing
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.refreshControl endRefreshing];
});
}
I'm creating UICollectionView like below.
What I want to do about the UICollectionView is,
Each UICollectionViewCell has UIButton ([cell.contentView addSubview:button];)
UIButton need to handle UIControlEventTouchDown and UIControlEventTouchUpInside
Begin interactiveMovement by Long tap of UICollectionView Cell
Don't want to begin interactiveMovement by Long tap of UIButton. UIButton just handle UIControlEvent assigned at Step 2.
Want to handle (didSelectItemAtIndexPath:) when the area of UICollectionViewCell other than UIButton is tapped.
My problem is that handleLongPress for UICollectionView responds and start interactiveMovement even when long tapping UIButton .
I want to start interactiveMovement only when the area of UICollectionView other than UIButton is tapped.
When UIButton is long tapped, I just want to handle UIConrolEvent assigned to UIButton and don't want UICollectionView to respond.
I'd appreciate if you would have some solution.
My code is like below, thank you.
MyCollectionView : UICollectionView
- (id)init
{
self = [super initWithFrame:CGRectZero collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
if (self) {
self.delegate = self;
self.dataSource = self;
--- snip ---
[self addLongPressGesture];
--- snip ---
}
return self;
}
- (void)addLongPressGesture
{
UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(handleLongPress:)];
[self addGestureRecognizer:gesture];
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender
{
CGPoint point = [sender locationInView:sender.view];
NSIndexPath *indexPath = [self indexPathForItemAtPoint:point];
switch (sender.state) {
case UIGestureRecognizerStateBegan:
[self beginInteractiveMovementForItemAtIndexPath:indexPath];
break;
case UIGestureRecognizerStateChanged:
[self updateInteractiveMovementTargetPosition:point];
break;
case UIGestureRecognizerStateEnded:
[self endInteractiveMovement];
break;
default:
[self cancelInteractiveMovement];
break;
}
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
// something to do
}
MyUICollectionViewCell : UICollectionViewCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self initButton];
}
return self;
}
- (void)initButton
{
_button = [[UIButton alloc] init];
[_button addTarget:self action:#selector(touchUpButton:) forControlEvents:UIControlEventTouchUpInside];
[_button addTarget:self action:#selector(touchDownButton:) forControlEvents:UIControlEventTouchDown];
--- snip ---
[self addSubview:_button];
}
- (void)touchUpButton:(UIButton *)button
{
// something to do
}
- (void)touchDownButton:(UIButton *)button
{
// something to do
}
Add a transparent UIView below UIButton and add long press to it not to the whole collection view as UIButton is considered a sub-Control of the cell
OR
see the long press point x,y if it's intersects with button's frame return from the method
I've resolved my problem. It my not be a good solution but it's working as I expect.
I've added ButtonBaseView(:UIView) under UIButton and add LongPressGesture on the ButtonBaseView.
The LongPressGesture do nothing as it's action. The role is to block LongPressGesture not to pass the event to UICollectionView.
At the same time, set cancelsTouchesInView=NO to handle TouchUpInside or TouchDragExit
My code is like below,
- (void)addDummyLongTapGesture
{
UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:nil];
gesture.cancelsTouchesInView = NO;
[buttonBaseView addGestureRecognizer:gesture];
}
In my main view, I want to create a tableView with some data.
For this, when you select a row in the tableView from the second view, it should be creating the tableView in the main.
OpenTableView.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
ViewController *vc = [[ViewController alloc] init];
[vc makeView];
}
ViewController.m
- (void)makeView
{
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, 700, 704) style:UITableViewStylePlain];
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.backgroundColor = [UIColor blackColor];
[self.view addSubview:self.tableView];
[self.tableView reloadData];
[self viewDidLoad];
}
Why is this not working?
If you need more details or pictures, just ask.
Thanks in advance to all of you.
You need to move makeView method into your viewDidLoad method. This will let the life cycle process of the application take over initializing your view frame and when it is ready you can set up your views.
OpenTableView.m
ViewController *vc = [[ViewController alloc] init];
Within your new view controller in the viewDidLoad method is when you should programmatically setup any views and add them to the main view.
ViewController.m
- (void) viewDidLoad(...){
[super viewDidLoad:...];
[self makeView];
}
You must present view controller
makeView is necessary to call the method after loading View
I'm using the following code to create a UIRefreshControl:
- (void) viewDidLoad
{
[super viewDidLoad];
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(doLoad) forControlEvents:UIControlEventValueChanged];
self.refreshControl = refreshControl;
}
- (void) doLoad
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// Instead of sleeping, I do a webrequest here.
[NSThread sleepForTimeInterval: 5];
dispatch_async(dispatch_get_main_queue(), ^{
[tableView reloadData];
[self.refreshControl endRefreshing];
});
});
}
It works great. If I navigate to my view, drag the table, the code runs and the data displays.
However, what I would like to do is have the view in the 'loading' state as soon as it appears (that way the user knows something is going on). I have tried adding the following:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.refreshControl beginRefreshing];
}
But it does not seem to work. When I navigate to the view, it looks like a regular view (refresh control is not visible), plus when I try to pull the refresh control, it never finished loading.
Obviously I'm going about this the wrong way. Any suggestions on how I should handle this?
Try this:
- (void) viewWillAppear: (BOOL) animated
{
[super viewWillAppear: animated];
self.tableView.contentOffset = CGPointMake(0, -self.refreshControl.frame.size.height);
[self.refreshControl beginRefreshing];
// kick off your async refresh!
[self doLoad];
}
Remember to call endRefreshing at some point!
EDIT to add full working sample:
This sample view controller, built and run in iOS6.1 as the root viewcontroller starts with the UIRefreshControl already visible and animating when the app launches.
TSTableViewController.h
#interface TSTableViewController : UITableViewController
#end
TSTableViewController.m
#import "TSTableViewController.h"
#implementation TSTableViewController
{
NSMutableArray* _dataItems;
}
- (void) viewDidLoad
{
[super viewDidLoad];
self.refreshControl = [UIRefreshControl new];
[self.refreshControl addTarget: self
action: #selector( onRefresh: )
forControlEvents: UIControlEventValueChanged];
}
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear: animated];
self.tableView.contentOffset = CGPointMake(0, -self.refreshControl.frame.size.height);
[self.refreshControl beginRefreshing];
[self onRefresh: nil];
}
- (void) onRefresh: (id) sender
{
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
_dataItems = [NSMutableArray new];
for ( int i = 0 ; i < arc4random() % 100 ; i++ )
{
CFUUIDRef uuid = CFUUIDCreate( NULL );
[_dataItems addObject: CFBridgingRelease(CFUUIDCreateString( NULL, uuid)) ];
CFRelease( uuid );
}
[self.refreshControl endRefreshing];
[self.tableView reloadData];
});
}
#pragma mark - Table view data source
- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView
{
return 1;
}
- (NSInteger) tableView:(UITableView *) tableView numberOfRowsInSection: (NSInteger) section
{
return _dataItems.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
reuseIdentifier: nil];
cell.textLabel.text = [_dataItems objectAtIndex: indexPath.row];
return cell;
}
#end
Manually modifying the contentOffset is insecure and wrong and can lead to unexpected behavior in some cases. This solution works without touching the contentOffset at all:
func showRefreshControl(show: Bool) {
if show {
refreshControl?.beginRefreshing()
tableView.scrollRectToVisible(CGRectMake(0, 0, 1, 1), animated: true)
} else {
refreshControl?.endRefreshing()
}
}
Another option is fire a UIControlEventValueChanged in your viewDidAppear: to trigger an initial refresh.
- (void) viewDidAppear: (BOOL) animated
{
[super viewDidAppear: animated];
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(doLoad) forControlEvents:UIControlEventValueChanged];
self.refreshControl = refreshControl;
[self.refreshControl beginRefreshing];
}
You never set self.refreshControl
Now, I can change UITableView Background color like
-(void)viewDidLoad {
[super viewDidLoad];
[self.tableView setBackgroundView:nil];
[self.tableView setBackgroundView:[[[UIView alloc] init] autorelease]];
[self.tableView setBackgroundColor:[UIColor colorWithPatternImage:[UIImage imageNamed:#"tblbg.png"]]];
}
But I need to add codes in all of the files. I don't want to create UITableView sub class.
Is it possible to make category for UITableView to change background color ?
Seems like an easy enough thing to try out?
UITableView+CustomBackgroundColor.h:
#interface UITableView (CustomBackgroundColor)
#end
UITableView+CustomBackgroundColor.m:
#implementation UITableView (CustomBackgroundColor)
- (void) viewDidLoad {
[super viewDidLoad];
[self.tableView setBackgroundView:nil];
[self.tableView setBackgroundView:[[[UIView alloc] init] autorelease]];
[self.tableView setBackgroundColor:[UIColor colorWithPatternImage:[UIImage imageNamed:#"tblbg.png"]]];
}
#end