I have a time that repeats itself every .1 seconds (I do this because I need to constantly look for a change on the website's text I am pulling from). My problem is that I need to be able to tell when the text changes from one word to another. So when the user open the app and let's say the text is displaying one song title but then it changes to a different one, Where it changes, I need to be able to detect it and preform an action.
Also it has to be when the app first loads it will not preform the action but when the text changes it will display it(It has to be this because I am pulling song titles). So I need to be able to change the duration of each song, but if a song is playing and they open the app in the middle of it, the duration will be mixed up. so I have two labels, one that shows text and another that shows the time. When the app first loads up, I want it to display nothing until the text in the song title changes, then pulls the duration from a if statement (below). It might be a little confusing but ask any question you need and I will try to explain.
Here is my code for pulling from the website and anything else that might help:
- (void)viewDidLoad {
[super viewDidLoad];
timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(recentTracksText) userInfo:nil repeats:YES];
}
-(void)recentTracksText {
textForBlog = [webViewForRecents stringByEvaluatingJavaScriptFromString:#"document.getElementById('current_song').textContent;"];
self.strippedTextForBlog = [self stringByStrippingHTMLFromString:textForBlog];
continuousLabel.text = self.strippedTextForBlog;
}
if ([continuousLabel.text isEqual: #"Lady Gaga - Gypsy"]) {
imageView1.image = [UIImage imageNamed:#"Lady Gaga - Gypsy.jpg"];
imageView.hidden = YES;
[self.view insertSubview:toolbar belowSubview:imageView];
[toolbar insertSubview:darkView belowSubview:imageView];
[self.view insertSubview:backgroundImage belowSubview:toolbar];
if([darkView.subviews containsObject:continuousLabel]) {
} else{
dispatch_queue_t queue = dispatch_get_global_queue(0,0);
dispatch_async(queue, ^{
[darkView addSubview:Label];
[Label setText:#"1:30"];
[darkView addSubview:continuousLabel];
});
}
}
UPDATE
-(void)recentTracksText {
textForBlog = [webViewForRecents stringByEvaluatingJavaScriptFromString:#"document.getElementById('current_song').textContent;"];
self.strippedTextForBlog = [self stringByStrippingHTMLFromString:textForBlog];
if (![self.strippedTextForBlog isEqualToString:continuousLabel.text])// this does not find when the text changes {
[self.pLabel setHidden:NO];
[self.pLabel setProgress:1
timing:TPPropertyAnimationTimingEaseOut
duration:150.0
delay:0.0]; //This does not go smoothy.
// this does not
}
continuousLabel.text = self.strippedTextForBlog;
}
Use Key-value observing if you want to observe the property directly
[continuousLabel addObserver:self
forKeyPath:#"text"
options:0
context:NULL];
Make sure you implement the observing method
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
// Insert your code here to deal with the change.
}
and a little bit of ARC management to taste:
- (void)dealloc {
NSLog(#"dealloc %#", [self class]);
if (continuousLabel) {
[continuousLabel removeObserver:self forKeyPath:#"text"];
}
}
Well, you already have the NSTimer that fires each 0.1 seconds, so you just have to check if the text wasn't changed.
-(void)recentTracksText {
textForBlog = [webViewForRecents stringByEvaluatingJavaScriptFromString:#"document.getElementById('current_song').textContent;"];
self.strippedTextForBlog = [self stringByStrippingHTMLFromString:textForBlog];
if (![self.self.strippedTextForBlog isEqualToString:continuousLabel.text]) {
//The text isn't the same. Do something.
}
continuousLabel.text = self.strippedTextForBlog;
}
Related
I have a custom container navigation. I have a diary view controller and a diary detail view controller. When user clicks on a picture in diary, it goes to diary detail using cycleFromViewController:toViewController method as described in the Apple docs about container view controllers.
When the detail view loads, I want the container view controller to remove one of it's subviews and add another one. I use KVC to accomplish this. It's my first time using KVC. addObserver method is in viewWillAppear in diary detail vc.
Problem: diary detail VC is loaded, observeValueForKeypath is called once the first time, twice the second time, and so on. Additionally, in observeValueForKeypath, I add a subview - UIButton - and when it is clicked, cycleFromViewController:toViewController is called again and the previous subview is added back. It works on the first go around, but on subsequent ones, the original subview is not added back, the UIButton just sticks around.
Diary Detail.m
-(void)viewWillAppear:(BOOL)animated{
[self addObserver:self.parentViewController forKeyPath:#"didLoadNumber" options:0 context:nil];
[self setValue:[NSNumber numberWithInt:0] forKey:#"didLoadNumber"];}
Main Container VC (observer/parent VC)
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(#"KVO called");
[self.usernameLabel removeFromSuperview];
self.backButton = [[UIButton alloc]initWithFrame:CGRectMake(12, 28, 28, 28)];
self.backButton.backgroundColor = [UIColor blackColor];
[self.view addSubview:self.backButton];
[self.backButton addTarget:self action:#selector(removeButtonAndAddLogo)
forControlEvents:UIControlEventTouchUpInside];
}
-(void)removeButtonAndAddLogo{
NSLog(#"got to remove button");
[self.backButton removeFromSuperview];
self.usernameLabel = [[UILabel alloc]initWithFrame:CGRectMake(12, 28, 28, 28)];
self.usernameLabel.text = #"username";
self.usernameLabel.textColor = [UIColor blackColor];
[self.view addSubview:self.usernameLabel];
[self cycleFromViewController:self.diaryViewController.diaryDetailVC toViewController:self.diaryViewController];
}
For this you need to remove it in viewWillDisappear method. If you will come at this view controller more then one time , it'll register this notification again and again and whenever you call this notification it'll call multiple time(number of time's you have register it).
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self addObserver:self.parentViewController forKeyPath:#"didLoadNumber" options:0 context:nil];
[self removeObserver:self.parentViewController forKeyPath:#"didLoadNumber"];
}
i hope it will help you.
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'm developing a radio streaming app with XCode 4.5 for iOS 6 and mainly using storyboards.
I successfully made it to be able to play in background. So wherever I go from my app, whether to other tabs or even after clicking the Home button on the simulator,it keeps playing.
I'm using Matt Gallagher's audio streamer, which I include in my app delegate .m file below
#pragma mark - audio streaming
- (void)playAudio:(indoRadio *)np withButton:(NSString *)currentButton
{
if ([currentButton isEqual:#"playbutton.png"])
{
[self.nowplayingVC.downloadSourceField resignFirstResponder];
[self createStreamer:np.URL];
[self setButtonImageNamed:#"loadingbutton.png"];
[audioStreamer start];
}
else
{
[audioStreamer stop];
}
}
- (void)createStreamer:(NSString *)url
{
if (audioStreamer)
{
return;
}
[self destroyStreamer];
NSString *escapedValue = (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(nil,(CFStringRef)url,NULL,NULL,kCFStringEncodingUTF8);
NSURL *streamurl = [NSURL URLWithString:escapedValue];
audioStreamer = [[AudioStreamer alloc] initWithURL:streamurl];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(playbackStateChanged:)
name:ASStatusChangedNotification
object:audioStreamer];
}
- (void)playbackStateChanged:(NSNotification *)aNotification
{
NSLog(#"playback state changed? %d",[audioStreamer isWaiting]);
if ([audioStreamer isWaiting])
{
[self setButtonImageNamed:#"loadingbutton.png"];
}
else if ([audioStreamer isPlaying])
{
[self setButtonImageNamed:#"stopbutton.png"];
}
else if ([audioStreamer isIdle])
{
[self destroyStreamer];
[self setButtonImageNamed:#"playbutton.png"];
}
}
- (void)destroyStreamer
{
if (audioStreamer)
{
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:ASStatusChangedNotification
object:audioStreamer];
[audioStreamer stop];
audioStreamer = nil;
}
}
- (void)setButtonImageNamed:(NSString *)imageName
{
if (!imageName)
{
imageName = #"playButton";
}
self.nowplayingVC.currentImageName = imageName;
UIImage *image = [UIImage imageNamed:imageName];
[self.nowplayingVC.playBtn.layer removeAllAnimations];
[self.nowplayingVC.playBtn setImage:image forState:0];
if ([imageName isEqual:#"loadingbutton.png"])
{
[self.nowplayingVC spinButton];
}
}
The problem I got is that when I click the play button, it starts the audio streamers but doesn't change into either loading button or stop button. This makes me unable to stop the audio since the button image is not changed. And since I put an NSLog inside the playbackStateChanged: method,I know that the playback state did change. It seems like the setButtonImagedNamed: method is not firing. Any thoughts?
Please have look of my answer:Disabled buttons not changing image in
For a button there is normal, selected, highlighted and disabled state.
So in your case you have to handle the normal and selected state.
In the xib, set image to a button for normal and selected state.
Now in the function of playAudio instead of checking the image name check the button state as below:
- (void)playAudio:(indoRadio *)np withButton:(NSString *)currentButton
{
if (playButton.selected)
{
//Stop the streaming
//set selection NO
[playButton setSelected:NO];
}
else
{
//Start the streaming
//set selection YES
[playButton setSelected:YES];
}
}
playButton is the IBOutlet of play button.
As you have set the images in xib, so the image automatically get changed as per the state of the image
In case you have added the playBtn programmatically, check if the button is declared as weak or its getting released somewhere. If it is weak, make it strong.
In case the button is in the nib file, then the problem is in the IBOutlet. Redo the connection properly.
It turned out that the delegate didn't know which nowplayingVC I was calling. #sarp-kaya's question about Calling a UIViewController method from app delegate has inspired me.
I actually have put this code in my view controller viewDidLoad:
myAppDelegate* appDelegate = [UIApplication sharedApplication].delegate;
but I forgot to add this line:
appDelegate.nowplayingVC = self;
So, all I need to do is adding that one line and it works now. okay, so silly of me for not noticing such a basic thing. But thanks for your helps :D
I'm new to programming for the iPhone and i'm having a problem with this function
-(IBAction)changeX {
[self timere:field1];
[self timere:field2];
}
this is a button to move an uitextfield object across the screen. The problem is i want to run this method on the first field , complete it then go on to the second. The timere function uses NSTimer to continously move the object until it reaches a certain point at which it terminates. I have two other functions shown below. The actual program im making is much longer but the objective is the same and that code is too long. The problem is running the first function then the second. Thank you for the help.
-(void)timere:(UITextField*)f1 {
UITextField*fielder1=f1;
NSInvocation*timerInvocation = [NSInvocation invocationWithMethodSignature:
[self methodSignatureForSelector:#selector(moveP:)]];
[timerInvocation setSelector:#selector(moveP:)];
[timerInvocation setTarget:self];
[timerInvocation setArgument:&fielder1 atIndex:2];
timer1 = [NSTimer scheduledTimerWithTimeInterval:0.03 invocation:timerInvocation repeats:YES];
}
-(void) moveP:(UITextField*)f1 {
UITextField*fielder1=f1;
fielder1.center = CGPointMake(fielder1.center.x+4, fielder1.center.y);
if (fielder1.center.x>237) {
[timer1 invalidate];
}
}
The standardized method of animating UIView objects is by using methods such as the following.
+ animateWithDuration:delay:options:animations:completion:
The syntax for these methods might be a bit daunting if you are unfamiliar with Objective-C Blocks. Here's a usage example which moves the first field, then the second field afterwards.
[UIView animateWithDuration:0.3 animations:^{
[field1 setCenter:CGPointMake(FINAL_CENTER_X1, FINAL_CENTER_Y1)];
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.3 animations:^{
[field2 setCenter:CGPointMake(FINAL_CENTER_X2, FINAL_CENTER_Y2)];
}];
}];
EDIT: For a general fix on your specific problem, I would make the following modifications:
-(IBAction)changeX {
[self timere:field1];
}
-(void) moveP:(UITextField*)f1 {
UITextField*fielder1=f1;
fielder1.center = CGPointMake(fielder1.center.x+4, fielder1.center.y);
if (fielder1.center.x>237) {
[timer1 invalidate];
// I'm really not sure about the scope of field1 and field2, but
// you can figure out the encapsulation issues
if (f1 == field1) {
[self timere:field2];
}
}
}
A more generic and indeed low-level solution would be to have a state variable. Perhaps you would have some sort of NSArray *fields of UITextField * and int state = 0. Where I added the conditional in moveP above, you would state++ and call [self timere:[fields objectAtIndex:state]] if state < [fields count]. You timer invalidation code is correct, anyway.
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