I'm building my first app and it's a Soundcloud client. Right now, everytime a track is selected to play, I have to go on and do a NSURLSessionDataTask fetch to get the binary data, but this takes a long time. And when I skip, to the next track, I have to add the logic in to download the next song ahead of time when the current song is playing...though, this is still slow if the user skips quickly:/
The Soundcloud native app skips instantly. How do it work? I've tried using Soundcloud iOS SDK but it is now deprecated.
Here is my song fetch:
-(void)fetchTrack: (SCTrack*)selectedTrack completionHandler: (void(^)(NSData *trackData, NSString *error)) completionHandler; {
NSString* clientID = #"41a5278fd8c704c3eb5a4a0ca38f9036";
NSString* streamURL = selectedTrack.stream_url;
NSString* urlString = [NSString stringWithFormat:#"%#?client_id=%#", streamURL, clientID];
NSURL* url = [[NSURL alloc]initWithString:urlString];
NSLog(#"%#", urlString);
NSMutableURLRequest* request = [[NSMutableURLRequest alloc]initWithURL:url];
request.HTTPMethod = #"GET";
NSURLSessionDataTask* dataTask = [[self session] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse* callResponse = (NSHTTPURLResponse*)response;
if ([callResponse isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger responseCode = [callResponse statusCode];
if (responseCode >= 200 && responseCode <= 299) {
NSData* trackData = data;
NSLog(#"STREAM 200");
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
NSLog(#"%#", data);
completionHandler(trackData, nil);
}];
}else{
NSLog(#"%ld", (long)responseCode);
}
}
}];
[dataTask resume];
}
This is how I'm playing and attempting to fetch the next song while the current track is playing:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.selectedTrack = self.SCTrackList[indexPath.row];
self.selectedTrackRow = indexPath.row;
[[SoundCloudAPI sharedInstance]fetchTrack:self.selectedTrack completionHandler:^(NSData *trackData, NSString *error) {
self.player = [[AVAudioPlayer alloc]initWithData: trackData error:nil];
[self.player prepareToPlay];
[self.player play];;
}];
[self prepareForNextTrack:self.selectedTrack];
}
-(void)prepareForNextTrack: (SCTrack*)trackPlaying {
self.selectedTrackRow += 1;
self.selectedTrack = self.SCTrackList[self.selectedTrackRow];
[[SoundCloudAPI sharedInstance]fetchTrack:self.selectedTrack completionHandler:^(NSData *trackData, NSString *error) {
self.trackDataToPlay = trackData;
}];
}
- (IBAction)nextPressed:(id)sender {
self.player = [[AVAudioPlayer alloc]initWithData: self.trackDataToPlay error:nil];
[self.player prepareToPlay];
[self.player play];;
[self prepareForNextTrack:self.selectedTrack];
}
Also, I'm new, so I'm sure my code is pretty clunky and would appreciated if anyone can point out ways to improve.
Thanks for pointing in the right direction!
So it turns I made things wayyyy more complicated than had to be. All I had to do was the dataWithContentsOfURL method to go retrieve the song passing in the token.
NSData *data =[NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#?oauth_token=%#",self.selectedTrack.stream_url, self.token]]];
And of course, put the data in the AVAudioPlayer.
Basically, getting rid of my fetchTrack function above.
Related
I am new to Objective C and iOS development in general. I am trying to create an app that would make an http request and display the contents on a label.
When I started testing I noticed that the label was blank even though my logs showed that I had data back. Apparently this happens because the the response is not ready when the label text gets updated.
I put a loop on the top to fix this but I am almost sure there's got to be a better way to deal with this.
ViewController.m
- (IBAction)buttonSearch:(id)sender {
HttpRequest *http = [[HttpRequest alloc] init];
[http sendRequestFromURL: #"https://en.wiktionary.org/wiki/incredible"];
//I put this here to give some time for the url session to comeback.
int count;
while (http.responseText ==nil) {
self.outputLabel.text = [NSString stringWithFormat: #"Getting data %i ", count];
}
self.outputLabel.text = http.responseText;
}
HttpRequest.h
#import <Foundation/Foundation.h>
#interface HttpRequest : NSObject
#property (strong, nonatomic) NSString *responseText;
- (void) sendRequestFromURL: (NSString *) url;
- (NSString *) getElementBetweenText: (NSString *) start andText: (NSString *) end;
#end
HttpRequest.m
#implementation HttpRequest
- (void) sendRequestFromURL: (NSString *) url {
NSURL *myURL = [NSURL URLWithString: url];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL: myURL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest: request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
self.responseText = [[NSString alloc] initWithData: data
encoding: NSUTF8StringEncoding];
}];
[task resume];
}
Thanks a lot for the help :)
Update
After reading a lot for the very useful comments here I realized that I was missing the whole point. So technically the NSURLSessionDataTask will add task to a queue that will make the call asynchronously and then I have to provide that call with a block of code I want to execute when the thread generated by the task has been completed.
Duncan thanks a lot for the response and the comments in the code. That helped me a lot to understand.
So I rewrote my procedures using the information provided. Note that they are a little verbose but, I wanted it like that understand the whole concept for now. (I am declaring a code block rather than nesting them)
HttpRequest.m
- (void) sendRequestFromURL: (NSString *) url
completion:(void (^)(NSString *, NSError *))completionBlock {
NSURL *myURL = [NSURL URLWithString: url];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL: myURL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest: request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//Create a block to handle the background thread in the dispatch method.
void (^runAfterCompletion)(void) = ^void (void) {
if (error) {
completionBlock (nil, error);
} else {
NSString *dataText = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
completionBlock(dataText, error);
}
};
//Dispatch the queue
dispatch_async(dispatch_get_main_queue(), runAfterCompletion);
}];
[task resume];
}
ViewController.m
- (IBAction)buttonSearch:(id)sender {
NSString *const myURL = #"https://en.wiktionary.org/wiki/incredible";
HttpRequest *http = [[HttpRequest alloc] init];
[http sendRequestFromURL: myURL
completion: ^(NSString *str, NSError *error) {
if (error) {
self.outputText.text = [error localizedDescription];
} else {
self.outputText.text = str;
}
}];
}
Please feel free to comment on my new code. Style, incorrect usage, incorrect flow; feedback is very important in this stage of learning so I can become a better developer :)
Again thanks a lot for the replies.
You know what, use AFNetworking to save your life.
Or just modify your HttpRequest's sendRequestFromURL:
- (void)sendRequestFromURL:(NSString *)url completion:(void(^)(NSString *str, NSError *error))completionBlock {
NSURL *myURL = [NSURL URLWithString: url];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL: myURL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest: request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
completionBlock(nil, error);
} else {
completionBlock([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], error);
}
});
}];
[task resume];
}
and invoke like this
[http sendRequestFromURL:#"https://en.wiktionary.org/wiki/incredible" completion:^(NSString *str, NSError *error) {
if (!error) {
self.outputLabel.text = str;
}
}];
Rewrite your sendRequestFromURL function to take a completion block:
- (void) sendRequestFromURL: (NSString *) url
completion: (void (^)(void)) completion
{
NSURL *myURL = [NSURL URLWithString: url];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL: myURL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest: request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
self.responseText = [[NSString alloc] initWithData: data
encoding: NSUTF8StringEncoding];
if (completion != nil)
{
//The data task's completion block runs on a background thread
//by default, so invoke the completion handler on the main thread
//for safety
dispatch_async(dispatch_get_main_queue(), completion);
}
}];
[task resume];
}
Then, when you call sendRequestFromURL, pass in the code you want to run when the request is ready as the completion block:
[self.sendRequestFromURL: #"http://www.someURL.com&blahblahblah",
completion: ^
{
//The code that you want to run when the data task is complete, using
//self.responseText
}];
//Do NOT expect the result to be ready here. It won't be.
The code above uses a completion block with no parameters because your code saved the response text to an instance variable. It would be more typical to pass the response data and the NSError as parameters to the completion block. See #Yahoho's answer for a version of sendRequestFromURL that takes a completion block with a result string and an NSError parameter).
(Note: I wrote the code above in the SO post editor. It probably has a few syntax errors, but it's intended as a guide, not code you can copy/paste into place. Objective-C block syntax is kinda nasty and I usually get it wrong the first time at least half the time.)
If you want easy way then Don't make separate class for call webservice. Just make meethod in viewController.m instead. I mean write sendRequestFromURL in your viewController.m and update your label's text in completion handler something like,
- (void) sendRequestFromURL: (NSString *) url {
NSURL *myURL = [NSURL URLWithString: url];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL: myURL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest: request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
self.responseText = [[NSString alloc] initWithData: data
encoding: NSUTF8StringEncoding];
self.outputLabel.text = self.responseText;
})
}];
[task resume];
}
I am creating an app that uses Google Place. Before I used to use Yahoo's api and had to use a url that was responsible for local search that was provided by yahoo. The url was following:
http://local.yahooapis.com/LocalSearchService/V3/localSearch?appid=SF0DVEvV34G4GnXEDU4SXniaDebJ_UvC1G1IuikVz3vpOJrBpyD.VqCJCVJHMh99He3iFz1Rzoqxb0b7Z.0-
Now since yahoo's api is discontinued I have decided to switch over to Google Place. But I cannot find an Url to use. I just dowlod the framework and use the api key. Where can I find such url for Google Place.
Register for the Google Places API by following the linke provided below:
https://code.google.com/apis/console
Refer Code Link for places Auto Search
https://github.com/AdamBCo/ABCGooglePlacesAutocomplete
NSString *const apiKey = #"*****23xAHRvnOf2BVG8o";
NSString * searchWord = #"search some place "
NSString *urlString = [NSString stringWithFormat:#"https://maps.googleapis.com/maps/api/place/autocomplete/json?input=%#&types=establishment|geocode&radius=500&language=en&key=%#",searchWord,apiKey];
pragma mark - Network Methods
-(void)retrieveGooglePlaceInformation:(NSString *)searchWord withCompletion:(void (^)(BOOL isSuccess, NSError *error))completion {
if (!searchWord) {
return;
}
searchWord = searchWord.lowercaseString;
self.searchResults = [NSMutableArray array];
if ([self.searchResultsCache objectForKey:searchWord]) {
NSArray * pastResults = [self.searchResultsCache objectForKey:searchWord];
self.searchResults = [NSMutableArray arrayWithArray:pastResults];
completion(YES, nil);
} else {
NSString *urlString = [NSString stringWithFormat:#"https://maps.googleapis.com/maps/api/place/autocomplete/json?input=%#&types=establishment|geocode&radius=500&language=en&key=%#",searchWord,apiKey];
NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *jSONresult = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if (error || [jSONresult[#"status"] isEqualToString:#"NOT_FOUND"] || [jSONresult[#"status"] isEqualToString:#"REQUEST_DENIED"]){
if (!error){
NSDictionary *userInfo = #{#"error":jSONresult[#"status"]};
NSError *newError = [NSError errorWithDomain:#"API Error" code:666 userInfo:userInfo];
completion(NO, newError);
return;
}
completion(NO, error);
return;
} else {
NSArray *results = [jSONresult valueForKey:#"predictions"];
for (NSDictionary *jsonDictionary in results) {
}
//[self.searchResultsCache setObject:self.searchResults forKey:searchWord];
completion(YES, nil);
}
}];
[task resume];
}
}
In Below code run so i get a response from url but when i try to get encodedPoints it give me a null value. also i update RegexKitLite but prob. not solve. Any suggestion are welcome Thank you advance.
NSString* saddr = [NSString stringWithFormat:#"%f,%f", f.latitude, f.longitude];
NSString* daddr = [NSString stringWithFormat:#"%f,%f", t.latitude, t.longitude];
NSString* apiUrlStr = [NSString stringWithFormat:#"http://maps.googleapis.com/maps/api/directions/json?origin=%#&destination=%#&sensor=false", saddr, daddr];
// http://maps.googleapis.com/maps/api/directions/json?origin=41.029598,28.972985&destination=41.033586,28.984546&sensor=false%EF%BB%BF%EF%BB%BF
NSURL *apiUrl = [NSURL URLWithString:[apiUrlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSLog(#"api url: %#", apiUrl);
NSString *apiResponse = [NSString stringWithContentsOfURL:apiUrl encoding:nil error:nil];
NSString* encodedPoints = [apiResponse stringByMatching:#"points:\\\"([^\\\"]*)\\\"" capture:1L];
NSLog(#"encodedPoints: %#", encodedPoints);
if (encodedPoints) {
return [self decodePolyLine:[encodedPoints mutableCopy]];
}
else {
return NO;
}
I think its not a good way to do API request synchronously, especially when user' phone has poor internet connection, it will slow down the responsiveness of your application. So you should do an asynchronous API request with NSURLSession.
Also, the Directions API might return more than one routes for your request. So its better to use a NSArray to store your polyline points.
Sample code:
- (void)getPolyline {
NSURL *url = [[NSURL alloc] initWithString:#"https://maps.googleapis.com/maps/api/directions/json?origin=Chicago,IL&destination=Los+Angeles,CA&key=YOUR_API_KEY"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL: url];
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithRequest:request completionHandler:
^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (!error) {
NSError *jsonError;
NSDictionary *dict = (NSDictionary*)[NSJSONSerialization JSONObjectWithData:data options:nil error:&jsonError];
if (!jsonError) {
NSArray *routesArray = (NSArray*)dict[#"routes"];
NSMutableArray *points = [NSMutableArray array];
for (NSDictionary *route in routesArray) {
NSDictionary *overviewPolyline = route[#"overview_polyline"];
[points addObject:overviewPolyline[#"points"]];
}
NSLog(#"%#", points);
}
} else {
//print error message
NSLog(#"%#", [error localizedDescription]);
}
}] resume];
}
I have an app that queries iTunes. When the data returns I want to segue from the search screen to a tableview that displays the songs.
My problem: I'm getting the data back but my segue is happening before the data is back so the numberOfRowsInSection count is still 0 and my app just stops. I don't understand why the conditional doesn't execute before my performselector method.
Here's my code:
- (IBAction)searchForTrack:(id)sender {
NSString *trackName = [[NSString alloc]init];
tracks = [[NSMutableArray alloc]init];
trackName = _searchText.text;
NSURLSession *session = [NSURLSession sharedSession];
NSString *appURL = [NSString stringWithFormat:#"https://itunes.apple.com/search?term=%#",trackName];
//NSLog(appURL);
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:appURL] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSMutableDictionary *jsonDict= [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
//NSLog(#"%#", jsonDict);
if (jsonDict)
{
for (id trackDict in [jsonDict objectForKey:#"results"]){
Track *track = [[Track alloc] initWithJSONDictionary:trackDict];
NSString *trackName = [trackDict objectForKey:#"trackName"];
NSString *artistName = [trackDict objectForKey:#"artistName"];
NSString *trackPrice = [trackDict objectForKey:#"trackPrice"];
NSString *releaseDate = [trackDict objectForKey:#"releaseDate"];
NSString *primaryGenreName = [trackDict objectForKey:#"primaryGenreName"];
[tracks addObject:track];
}
}
else
{
NSLog(#"No Darn Tracks Were Found or Something else is screwed up");
}
}];
[dataTask resume];
[self performSelector:#selector(switchToTableview)];
}
You don't show the full code, but the
}];
give's the clue that the conditional code is being executed in a block.
Your code is not executing in a one-line-at-a-time manner due to the block which is probably the cause of the problem, but you need to post all the code to confirm and to suggest the best solution (which will be remove the block, or add a completion part to the block).
I'm trying to page through a user's twitter friends using cursors. Since you get 20 at a time along with a next cursor, I thought perhaps recursion was the best way to handle this. However, I believe because I'm using completion handler blocks, it isn't working correctly. I keep getting just two pages of friends (40), and it returns.
- (void)fetchTwitterFriendsForCrush:(Crush*)crush
fromCursor:(NSString*)cursor
usingManagedObjectContext:(NSManagedObjectContext*)moc
withSender:(id) sender
usingCompletionHandler:(void(^)())completionHandler
{
// twitter returns "0" when there are no more pages to receive
if (([cursor isEqualToString:#"0"]) || (cursor == nil)) {
completionHandler();
return;
}
NSString *urlString =
[NSString stringWithFormat:#"https://api.twitter.com/1.1/friends/list.json?cursor%#skip_status=1", cursor];
NSURL *requestURL = [NSURL URLWithString:urlString];
SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodGET
URL:requestURL
parameters:nil];
request.account = self.twitterAccount;
[request performRequestWithHandler:
^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if (error) {
NSLog(#"Error getting twitter friends = %#", error);
}
if (responseData) {
NSError *jsonError;
NSString *nextCursor = nil;
NSMutableArray *friendsArray = [NSMutableArray arrayWithCapacity:100];
NSDictionary *friendsDictionary =
[NSJSONSerialization JSONObjectWithData:responseData
options:0
error:&jsonError];
if ([friendsDictionary valueForKey:#"next_cursor_str"]) {
nextCursor = [friendsDictionary valueForKey:#"next_cursor_str"];
}
if ([friendsDictionary valueForKey:#"users"]) {
[friendsArray addObjectsFromArray:[friendsDictionary valueForKey:#"users"]];
}
for (NSDictionary *singleFriend in friendsArray) {
NSString *twitterID = [singleFriend valueForKey:#"id_str"];
NSString *name = [singleFriend valueForKey:#"name"];
NSString *screenName = [singleFriend valueForKey:#"screen_name"];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^(void) {
// update model
TwitterFriend *newTwitterFriend =
[TwitterFriend twitterFriendWithTwitterID:twitterID
forCrush:crush
usingManagedObjectContext:moc];
newTwitterFriend.name = name;
newTwitterFriend.screenName = screenName;
});
}
[self fetchTwitterFriendsForCrush:crush
fromCursor:nextCursor
usingManagedObjectContext:moc
withSender:self
usingCompletionHandler:nil];
}
}];
}
And the method that calls it:
[self.twitterNetwork fetchTwitterFriendsForCrush:self.crush fromCursor:#"-1" usingManagedObjectContext:self.managedObjectContext withSender:self usingCompletionHandler:^{
//
[self reloadData];
}];
UPDATE: It appears that I'm receiving the same next_cursor data on every request. Has anyone experienced this? Or do you see anything in this code that would cause that?
I found that a more complex way is better. You may use https://api.twitter.com/1.1/friends/ids.json? to get your friends ids list. Then using 1.1/users/lookup.json you may get the full info for users. I wrote a small helper to drill down user friends with SLRequest (#iOS6) https://github.com/ArchieGoodwin/NWTwitterHelper