In my VC I have a UITableView. Each cell has a UITableView as one of its contents. Timer is set updating each cell every 10secs. Events are handled which also reloads the respective cell.
Method that timer calls :-
-(void) updateVisitotsLists {
NSLog(#"UPDATING VISITORS LIST ***************************");
// Call API's to get lists
[api getVisitorsList];
// Init Arrays
browsingList = [MainAppDataObject sharedAppDataObject].visitors_browsingList;
secondList = [MainAppDataObject sharedAppDataObject].visitors_secondList;
thirdList = [MainAppDataObject sharedAppDataObject].visitors_thirdList;
fourthList = [MainAppDataObject sharedAppDataObject].visitors_fourthList;
// AS these are no more useful, so make it nil & save memory
[MainAppDataObject sharedAppDataObject].visitors_browsingList = nil;
[MainAppDataObject sharedAppDataObject].visitors_secondList = nil;
[MainAppDataObject sharedAppDataObject].visitors_thirdList = nil;
[MainAppDataObject sharedAppDataObject].visitors_fourthList = nil;
// Reload all lists with latest data
[self reloadBrowsingRow];
[self reloadSecondRow];
[self reloadThirdRow];
[self reloadFourthRow];
}
Event Handler Method :-
-(void) handleNewVisitor : (NSNotification *) notification {
// UPDATE VISITOR'S LIST
Visitor *v = [notification object];
#try {
if (v != nil) {
// Add V to browsing list
[browsingList addObject:v];
// Reload browsing list
[self reloadBrowsingRow];
}
}#catch (NSException *e) {
NSLog(#"EXCEP - %#", e);
}
v = nil;
return;
}
Reloading Method -
-(void)reloadBrowsingRow {
// Browsing
VisitorsListsCell *bcell = (VisitorsListsCell*)[self.visitorlistsTv cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]];
[bcell.listsTableView reloadData];
[bcell updateButtonText];
[bcell setNeedsDisplay];
bcell = nil;
return;
}
The Problem :-
When updateVisitotsLists is called thru timer, the updated contents are not reflected on cell.
When event handler method calls the same [self reloadBrowsingRow]; method, the contents of the cell are updated and reflected.
Due to this despite cells contents are updated but are not reflected until the state of cell is changed - expanded or collapsed.
I tried removing timer and cell updates properly on event caught, but when timer was on and event was caught, method is called but contents are not reflected on the screen.
I feel both methods may be calling reload method at same time, hence this must be happening or what ? How can this be handled making sure that the contents of cells are updated in any respect ? Any help is highly appreciated. Thanks.
Use [tableview reloadData]; on that method.
Because Reload on tableView will reload all data, so it will be more helpful.
Related
In my one of UITableView have more then 10 rows. I want to scroll till last row while UITestCase running.
I have written below code to scroll till last row.
-(void)scrollToElement:(XCUIElement *)element application:(XCUIApplication *)app{
while ([self visible:element withApplication:app]) {
XCUIElement *searchResultTableView = app.tables[#"searchResultView"];
XCUICoordinate *startCoord = [searchResultTableView coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)];
XCUICoordinate *endCoord = [startCoord coordinateWithOffset:CGVectorMake(0.0, -262)];
[startCoord pressForDuration:0.01 thenDragToCoordinate:endCoord];
}
}
-(BOOL)visible:(XCUIElement *)element withApplication:(XCUIApplication *)app{
if (element.exists && !CGRectIsEmpty(element.frame) && element.isHittable) {
return CGRectContainsRect([app.windows elementBoundByIndex:0].frame, element.frame);
} else {
return FALSE;
}
}
An i have called above method in my one of UITestCase method by below code
XCUIElement *searchResultTableView = app.tables[#"searchResultView"];
[self waitForElementToAppear:searchResultTableView withTimeout:30];
XCUIElement *table = [app.tables elementBoundByIndex:0];
XCUIElement *lastCell = [table.cells elementBoundByIndex:table.cells.count - 1];
[self scrollToElement:lastCell application:app];
By this code i can scroll to last row but after reaching last row, it continue doing scroll means can't stop scrolling.
Please help me to scroll to only last row and then it should stop to scroll so that i can perform next action event.
I have refer StackOverFlow answer but none of them meet my requirement.
Thanks in advance.
I faced similar issue in one of my project.
In that I wanted to test "Load More" feature by TestKit framework.
Here is some workaround to achieve the same scenario.
//app : is your current instance of appliaction
//listTable : is a Table which you've found via accessibility identifier
//loadMoreTest : is a parameter to determine whether code should perform test for loadmore feature or not
- (void)testScrollableTableForApplication:(XCUIApplication *)app
forTable:(XCUIElement *)listTable
withLoadMoreTest:(BOOL)loadMoreTest {
[listTable accessibilityScroll:UIAccessibilityScrollDirectionUp];
[listTable swipeUp];
if (loadMoreTest) {
__block BOOL isLoadMoreCalled;
__block XCUIElement *lastCell;
__block __weak void (^load_more)();
void (^loadMoreCall)();
load_more = loadMoreCall = ^() {
XCUIElementQuery *tablesQuery = app.tables;
XCUIElementQuery *cellQuery = [tablesQuery.cells containingType:XCUIElementTypeCell identifier:#"LoadMoreCell"];
lastCell = cellQuery.element;
if ([lastCell elementIsWithinWindowForApplication:app]) {
[self waitForElementToAppear:lastCell withTimeout:2];
[lastCell tap];
isLoadMoreCalled = true;
[self wait:2];
}
[listTable swipeUp];
if (!isLoadMoreCalled) {
load_more();
}
};
loadMoreCall();
}
}
- (void)waitForElementToAppear:(XCUIElement *)element withTimeout:(NSTimeInterval)timeout
{
NSUInteger line = __LINE__;
NSString *file = [NSString stringWithUTF8String:__FILE__];
NSPredicate *existsPredicate = [NSPredicate predicateWithFormat:#"exists == 1"];
[self expectationForPredicate:existsPredicate evaluatedWithObject:element handler:nil];
[self waitForExpectationsWithTimeout:timeout handler:^(NSError * _Nullable error) {
if (error != nil) {
NSString *message = [NSString stringWithFormat:#"Failed to find %# after %f seconds",element,timeout];
[self recordFailureWithDescription:message inFile:file atLine:line expected:YES];
}
}];
}
create one category for XCUIElement
XCUIElement+Helper.m and import it into your respective Test class.
#import <XCTest/XCTest.h>
#interface XCUIElement (Helper)
/// Check whether current XCUIElement is within current window or not
- (BOOL)elementIsWithinWindowForApplication:(XCUIApplication *)app ;
#end
#implementation XCUIElement (Helper)
/// Check whether current XCUIElement is within current window or not
/*
#description: we need to check particular element's frame and window's frame is intersecting or not, to get perfectly outcome whether element is currently visible on screen or not, because if element has not yet appeared on screen then also the flag frame, exists and hittable can become true
*/
- (BOOL)elementIsWithinWindowForApplication:(XCUIApplication *)app {
if (self.exists && !CGRectIsEmpty(self.frame) && self.hittable)
return CGRectContainsRect(app.windows.allElementsBoundByIndex[0].frame, self.frame);
else
return false;
}
#end
To get the "Load More" cell, i've given the
cell.accessibilityIdentifier = #"LoadMoreCell";
Rest of the code is, recursive function in testScrollableTableForApplication to make Tableview scroll to reach to bottom so i can have the access of load more cell(in your case last cell). Then i am performing the action Tap to fetch new records from server. Then again i am scrolling the Table to verify if the new records has been fetched from server or not.
Tip : you can replace recursive function with do while or while loop to achieve the same.
Hope this helps!
Happy Coding!!
There is a problem with reusing cells in UITableView:
lets say I have a http request in a cell which may take about 20 seconds to execute. Its completion block effects the cell appearance. User might want to scroll the tableview at this time (while request is executing). After the request has been executed, completion block will effect not a proper cell, because the cells are reused while tableview is scrolling.
Also if you just make a check in the completion block that the cell is the proper one (for example by tagging each cell and checking if it is the same) you will not see a change in the cell appearance when you scroll back to the proper cell.
Do you have any idea of an elegant solution for the problem?
Here is a simplified example of my code which has this problem. It is an IBAction method for a button in a cell which may change the cell by method setInviteStatus
- (void)setInviteStatus:(INVITE_STATUS)status withOldTag:(NSInteger)oldTag {//old tag is to change only if cell is related to the invitation, because tableView reuses cells
if (self.tag != oldTag)
return;
switch (status) {
case INVITE_STATUS_ACCEPTED:
[self.btnInvite setTitle:#"Accepted" forState:UIControlStateNormal];
break;
case INVITE_STATUS_INVITE:
[self.btnInvite setUserInteractionEnabled:YES];
[self.btnInvite setTitle:#"Invite" forState:UIControlStateNormal];
break;
case INVITE_STATUS_REJECTED:
[self.btnInvite setTitle:#"Rejected" forState:UIControlStateNormal];
break;
default:
break;
}
}
- (IBAction)invitePressed:(UIButton *)sender {
NSInteger tag = self.tag; //to save it, it might change due to cell reusing
[[JLAccountManager sharedManager] inviteUser:tag withCompletion:^(NSDictionary *response, NSError *error) {
if ([response[ResponseAnswer] isEqual: #YES]) {
[self setInviteStatus:INVITE_STATUS_ACCEPTED withOldTag:tag];
} else {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setInviteStatus:INVITE_STATUS_INVITE withOldTag:tag];
});
[self setInviteStatus:INVITE_STATUS_REJECTED withOldTag:tag];
}
}];
}
I am using EAWiFiUnconfiguredAccessoryBrowser to detect EAWiFiUnconfiguredAccessory. The code to start the accessory search it's the following:
- (void)viewDidLoad {
[super viewDidLoad];
if (_accessories == nil) {
_accessories = [[NSMutableArray alloc] init];
}
if (_browser == nil) {
_browser = [[EAWiFiUnconfiguredAccessoryBrowser alloc] initWithDelegate:self queue:nil];
_browser.delegate = self;
}
}
Unfortunately it does find accessories only the first time the View loads. If I go back to the previous view and then reload the view it does not find them.
I tried:
recreating the browser accessory and restarting the search (does not work)
stopping the search and restarting it (does not work)
This is the latest code I got (refer to this together with the code above):
- (void) viewWillAppear:(BOOL)animated{
NSLog(#"view will appear");
if (_accessories != nil) {
[_accessories removeAllObjects];
}
[self.tableView reloadData];
[self initializeBrowswerAndStartSearch];
}
- (void) initializeBrowswerAndStartSearch{
if (_browser != nil) {
[_browser stopSearchingForUnconfiguredAccessories];
}
[_browser startSearchingForUnconfiguredAccessoriesMatchingPredicate:nil];
}
- (void) viewWillDisappear:(BOOL)animated{
[_browser stopSearchingForUnconfiguredAccessories];
}
It seems that the accessory list information is cached somewhere within the APP. If I restart the APP it will find them so I guess there is something that I am missing.
Any help?
so i have the same problem..you should use the unconfiguredAccessories array. Also, try keeping the instance of the browser alive. If you discover the device once, and you re-instantiate the browser, you wont find it again
EAWiFiUnconfiguredAccessoryBrowser has issues,and doesn't provide reliable result in certain use cases. i think you should try this
- (void) viewWillAppear:(BOOL)animated{
NSLog(#"view will appear");
if (_accessories != nil) {
[_accessories removeAllObjects];
}
[self.tableView reloadData];
[self initializeBrowswerAndStartSearch];
}
below method makes browser object nil and reinitialises it, in this case browser object will always return you updated(i.e, proper) values . it worked perfectly for me.
-(void) initializeBrowswerAndStartSearch
{
// Make EAWiFiUnconfiguredAccessoryBrowser object nil and reinitiate ,start searching again
_browser = nil;
_browser = [[EAWiFiUnconfiguredAccessoryBrowser alloc] initWithDelegate:self queue:nil];
[_browser startSearchingForUnconfiguredAccessoriesMatchingPredicate:nil];
}
anytime you feel EAWiFiUnconfiguredAccessoryBrowser isn't providing proper result , try this.
I also have this issue. So I build a singleton called WAC service, then you can keep this singleton alive during the app life cycle. Anywhere you want to load the unconfigured accissories. Just load it from [_browser unconfiguredAccessories].
I am using setNeedsDisplay on my GUI, but there update is sometimes not done. I am using UIPageControllView, each page has UIScrollView with UIView inside.
I have the following pipeline:
1) application comes from background - called applicationWillEnterForeground
2) start data download from server
2.1) after data download is finished, trigger selector
3) use dispatch_async with dispatch_get_main_queue() to fill labels, images etc. with new data
3.1) call setNeedsDisplay on view (also tried on scroll view and page controller)
Problem is, that step 3.1 is called, but changes apper only from time to time. If I swap pages, the refresh is done and I can see new data (so download works correctly). But without manual page turn, there is no update.
Any help ?
Edit: code from step 3 and 3.1 (removed _needRefresh variables pointed in comments)
-(void)FillData {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *stateID = [DataManager ConvertStateToStringFromID:_activeCity.actual_weather.state];
if ([_activeCity.actual_weather.is_night boolValue] == YES)
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#_noc", [_bgs objectForKey:stateID]]];
if (_isNight == NO)
{
_bgTransparencyInited = NO;
}
_isNight = YES;
}
else
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#", [_bgs objectForKey:stateID]]];
if (_isNight == YES)
{
_bgTransparencyInited = NO;
}
_isNight = NO;
}
[self.contentBgImage setNeedsDisplay]; //refresh background image
[self CreateBackgroundTransparency]; //create transparent background if colors changed - only from time to time
self.contentView.parentController = self;
[self.contentView FillData]; //Fill UIView with data - set labels texts to new ones
//_needRefresh is set to YES after application comes from background
[self.contentView setNeedsDisplay]; //This do nothing ?
[_grad display]; //refresh gradient
});
}
And here is selector called after data download (in MainViewController)
-(void)FinishDownload:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[_activeViewController FillData]; //call method shown before
//try call some more refresh - also useless
[self.pageControl setNeedsDisplay];
//[self reloadInputViews];
[self.view setNeedsDisplay];
});
}
In AppDelegate I have this for application comes from background:
-(void)applicationWillEnterForeground:(UIApplication *)application
{
MainViewController *main = (MainViewController *)[(SWRevealViewController *)self.window.rootViewController frontViewController];
[main UpdateData];
}
In MainViewController
-(void)UpdateData
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(FinishForecastDownload:) name:#"FinishDownload" object:nil]; //create selector
[[DataManager SharedManager] DownloadForecastDataWithAfterSelector:#"FinishDownload"]; //trigger download
}
try this:
[self.view performSelectorOnMainThread:#selector(setNeedsLayout) withObject:nil waitUntilDone:NO];
or check this link:
http://blackpixel.com/blog/2013/11/performselectoronmainthread-vs-dispatch-async.html
setNeedsDisplay triggers drawRect: and is used to "redraw the pixels" of the view , not to configure the view or its subviews.
You could override drawRect: and modify your labels, etc. there but that's not what it is made for and neither setNeedsLayout/layoutSubviews is.
You should create your own updateUI method where you use your fresh data to update the UI and not rely on specialized system calls meant for redrawing pixels (setNeedsDisplay) or adjusting subviews' frames (drawRect:).
You should set all your label.text's, imageView.image's, etc in the updateUI method. Also it is a good idea to try to only set those values through this method and not directly from any method.
None of proposed solutions worked. So at the end, I have simply remove currently showed screen from UIPageControllView and add this screen again. Something like changing the page there and back again programatically.
Its a bit slower, but works fine.
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