New to iOS... I have a Rails/Devise application that is set up with token_authenticatable. I have a tokens controller that returns a token when a valid email and password is posted:
URL: http://localhost:3000/tokens.json
Body: email=example#example.com&password=foobar
Response:
{
"token": "xyzxyztoken"
}
Once created this token grants access to other sections of the site and this works in a test client (RESTClient). I have been stuck for a while connecting to it using RESTKit in iOS.
I create my RKObjectManager in AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RKURL *baseURL = [RKURL URLWithBaseURLString:#"http://localhost:3000"];
self.objectManager = [RKObjectManager objectManagerWithBaseURL:baseURL];
self.objectManager.client.baseURL = baseURL;
return YES;
}
I have a view controller and when you tap a button it calls this:
-(IBAction)btnLoginRegisterTapped:(UIButton*)sender
{
// get the object manager
RKObjectManager *objectManager = [RKObjectManager sharedManager];
objectManager.serializationMIMEType = RKMIMETypeJSON;
// set mapping
RKObjectMapping *nameMapping = [RKObjectMapping mappingForClass:[Token class]];
[nameMapping mapKeyPathsToAttributes:#"token", #"token", nil];
[objectManager.mappingProvider setMapping:nameMapping forKeyPath:#""];
// create url
NSDictionary *queryParams;
queryParams = [NSDictionary dictionaryWithObjectsAndKeys:#"example#example.com", #"email", #"foobar", #"password", nil];
RKURL *URL = [RKURL URLWithBaseURL:[objectManager baseURL] resourcePath:#"/tokens.json" queryParameters:queryParams];
[objectManager loadObjectsAtResourcePath:[NSString stringWithFormat:#"%#?%#", [URL resourcePath], [URL query]] delegate:self];
}
This is my error:
2013-01-10 16:32:55.554 MyApp[69314:14003] response code: 404
2013-01-10 16:32:55.555 MyApp[69314:14003] W restkit.network:RKObjectLoader.m:300 Unable to find parser for MIME Type 'text/html'
2013-01-10 16:32:55.555 MyApp[69314:14003] W restkit.network:RKObjectLoader.m:329 Encountered unexpected response with status code: 404 (MIME Type: text/html -> URL: http://localhost:3000/tokens.json?password=foobar&email=example%40example.com -- http://localhost:3000 -- http://localhost:3000)
2013-01-10 16:32:55.556 MyApp[69314:14003] Error: The operation couldn’t be completed. (org.restkit.RestKit.ErrorDomain error 4.)
This may be a simple question but I am lost. I think I just am not understanding POSTing from iOS in general. One thing to possibly note is that viewing /tokens.json in a browser returns a Routing Error because I do not actually have a view for that:
No route matches [GET] "/tokens.json"
Anyway the point of all this is for a user to log in and get a token stored and then use it to access other data from the rails app.
Please let me know if more clarification is needed.
In your rails app, open config/routes.rb
add
resources :tokens
Related
Um beginner with RestKit, first example for me was on foursquare API's and I've used RestKit with Blocks not delegates.
I want to retrive the name's of venues,this is the JSON response
and this is my code:
// App Delegate.m
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURLString:#"https://api.Foursquare.com/v2"];
RKManagedObjectStore *objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:#"Venue.sqlite"];
objectManager.objectStore = objectStore;
objectManager.serializationMIMEType = RKMIMETypeJSON;
RKManagedObjectMapping *venueMapping = [RKManagedObjectMapping mappingForClass:[Venue class] inManagedObjectStore:objectStore];
[venueMapping mapKeyPath:#"id" toAttribute:#"id"];
[venueMapping mapKeyPath:#"name" toAttribute:#"name"];
venueMapping.primaryKeyAttribute = #"id";
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venue"];
then in myViewController.m
-(void)loadVenues{
// When caling loadObjectsAtResourcePath method it specify RKObjectLoader which is the actual request.
// within these block you can take more options to controll the request.
[[RKObjectManager sharedManager]loadObjectsAtResourcePath:#"/venues/40a55d80f964a52020f31ee3?oauth_token=FNQPN5P5EKLJ5IQ44TMWO00I3W033M0Y1TKINW2OTF2VIOTP&v=20130512" usingBlock:^(RKObjectLoader* loader)
{
loader.objectMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[Venue class]];
loader.onDidLoadObject = ^(NSArray *objects)
{
NSLog(#"onDidLoadObject Blocks");
self.data = objects;
[self.tableView reloadData];
};
}
];
}
and the app is entering the block of onDidLoadObject but every time the array is empty !!
even when I test the link on browser it comes with data.
When I debug the loader.URL it always come with these
https://api.Foursquare.com/v2/venues/40a55d80f964a52020f31ee3?v=20130512&oauth_token=FNQPN5P5EKLJ5IQ44TMWO00I3W033M0Y1TKINW2OTF2VIOTP -- https://api.Foursquare.com/v2 -- https://api.Foursquare.com/v2
I don't know why load.URL is wrong ?!
I think um calling the 4square API's with the wrong way, anyone can help ? :)
-Put Your mapping as a class method to be accessed from all application classes, and done only once.
-change the Attribute "id" as it is reserved in Objective-C.
-add this to the block
[loader setObjectMapping:[RestKitMapping yourMapping]];
-then add this with your mapping code
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venue"];
-And use Delegates instead of blocks
I've two concerns regarding the above code:
1- Apparently the above JSON response, lists just one Venue .. So KeyPath should be "response.venue" not "response.venues"
2- Where's the mapping for ID? .. which is the primary key that RestKit uses to insert into DB? You need to set the primary key mapping.
I am a beginner to RESTKIT and have only just recently tested it out on foursquare public api from ray's tutorial http://www.raywenderlich.com/13097/intro-to-restkit-tutorial.
Although i get the gist of it, there are some part which i do not understand, and would like pointer for it, so that i can consume my own web service.
- (void)viewDidLoad
{
[super viewDidLoad];
RKURL *baseURL = [RKURL URLWithBaseURLString:#"https://api.Foursquare.com/v2"];
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:baseURL];
objectManager.client.baseURL = baseURL;
RKObjectMapping *venueMapping = [RKObjectMapping mappingForClass:[Venue class]];
[venueMapping mapKeyPathsToAttributes:#"name", #"name", nil];
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venues"];
[self sendRequest];
}
how do I change
[venueMapping mapKeyPathsToAttributes:#"name", #"name", nil];
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venues"];
to accommodate my own webMethod? (my webMethod is shown below)
Currently, I upload my file to IIS for testing purpose, and am using the IP for web service. (I am constantly changing work area, so I designate it as myIPAddress for easier communication)
-- My service code (changed EDIT:now return JSON)
[WebMethod]
[ScriptMethod( ResponseFormat = ResponseFormat.Json)]
public void testTextJSON()
{
string text = "Testing for Json!";
List<string> arrayList = new List<string>();
arrayList.Add(text);
JavaScriptSerializer js = new JavaScriptSerializer();
string name = js.Serialize(arrayList);
Context.Response.Write(name);
}
return - ["Testing for Json!"]
EDIT- what I changed currently for viewDidLoad and sendRequest to test for my own service
- (void)viewDidLoad
{
[super viewDidLoad];
RKURL *baseURL = [RKURL URLWithBaseURLString:#"http://192.168.1.12"];
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:baseURL];
objectManager.client.baseURL = baseURL;
RKObjectMapping *venueMapping = [RKObjectMapping mappingForClass:[venue class]];
[venueMapping mapKeyPathsToAttributes:#"name", #"name", nil];
[self sendRequest];
}
and
- (void)sendRequest
{
RKObjectManager *objectManager = [RKObjectManager sharedManager];
RKURL *URL = [RKURL URLWithBaseURL:[objectManager baseURL] resourcePath:#"/webService/webService1.asmx/testTextJSON"];
objectManager.acceptMIMEType = RKMIMETypeJSON;
objectManager.serializationMIMEType = RKMIMETypeJSON;
[[RKParserRegistry sharedRegistry] setParserClass:[RKJSONParserJSONKit class] forMIMEType:#"text/plain"];
[objectManager loadObjectsAtResourcePath:[NSString stringWithFormat:#"%#", [URL resourcePath]] delegate:self];
}
EDIT n+1 - here are some of my error message, maybe someone can tell me what went wrong?
2012-11-25 06:49:20.925 fourSquareAPI[352:12e03] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0x8354430> valueForUndefinedKey:]: this class is not key value coding-compliant for the key venues.'
If I remove [objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"venues"];
I would get
2012-11-25 06:52:47.495 fourSquareAPI[368:11603] response code: 200
2012-11-25 06:52:47.499 fourSquareAPI[368:12e03] W restkit.object_mapping:RKObjectMapper.m:87 Adding mapping error: Could not find an object mapping for keyPath: ''
2012-11-25 06:52:47.499 fourSquareAPI[368:12e03] E restkit.network:RKObjectLoader.m:231 Encountered errors during mapping: Could not find an object mapping for keyPath: ''
2012-11-25 06:52:47.502 fourSquareAPI[368:11603] Error: Could not find an object mapping for keyPath: ''
Can someone please teach me what to do?? Any help would be greatly appreciated, I would really like to learn how to use RESTKIT to consume a webservice.
The key path response.venues is a path to the attribute with respect to the server response that is in JSON or XML format. In this case, the server returns "response" which has the key "venues". This is most likely a list of venues that you would apply the mapping to. You have to adapt that based on your server response.
If you want to handle a specific value before the mapping, use this function:
- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout __autoreleasing id *)mappableData; {
if([*mappableData valueForKey:#"result"] != nil){
[*mappableData removeObjectForKey:#"result"]; //This key won't be mapped now
}
}
It's the first time I use restkit and I am interested in the automatic mapping (and maybe later also in the integration with CoreData).
Currently I managed to perform a GET request and map the response to a simple object with the following code:
RKObjectMapping *myMapping = [[self class] objectMappingForClass:[MyClass class]];
[myMapping mapKeyPath:#"Name" toAttribute:#"name"];
[myMapping mapKeyPath:#"Value" toAttribute:#"value"];
....
+ (RKObjectMapping*)objectMappingForClass: (Class)class
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:class];
mapping.rootKeyPath = #"Data";
mapping.performKeyValueValidation = NO;
// mapping.ignoreUnknownKeyPaths = YES;
mapping.setNilForMissingRelationships = YES;
mapping.setDefaultValueForMissingAttributes = YES;
return mapping;
}
I retrieve the data from the server in this way:
RKObjectMapping *defaultPropertiesMapping = [self.objectManager.mappingProvider objectMappingForClass:[MyClass class]];
RKObjectLoader *request = [self.objectManager loaderWithResourcePath:GetDefaultPropertiesURL];
request.objectMapping = defaultPropertiesMapping;
RKResponse *response = [request sendSynchronously];
The problem is that for the response the server does not use the HTTP status, but an attribute in the JSON payload (the payload is something like:
{ "Status" : "OK", "Data" : { //real payload } }
How can I check that Status attribute??
If you set the object loader delegate to the class you are sending the request from, you can implement the following method:
- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout __autoreleasing id *)mappableData; {
if([*mappableData valueForKey:#"Status"] isEqualToString:#"OK"){
// Do something
}
}
You can then check the value of the status and handle the response accordingly. This method is called before the object is mapped to Core Data. Also, make sure your class adopts the RKObjectLoaderDelegate protocol or this won't work.
If you ever want to remove data from the the response, use the following method:
[*mappableData removeObjectForKey:#"Data"];
Any keys removed from *mappableData won't make it to the object mapper.
I'm just starting to use Restkit and am trying to send a regular POST request "serverUrl/account/authenticate.xml?OPTIONS" to get a response as XML. This is the code I use to call the URL:
-(void) function{
// OBJECT MANAGER
RKObjectManager* manager = [RKObjectManager objectManagerWithBaseURL:serverUrl];
[manager.router routeClass:[CBUser class] toResourcePath:#"/account/authenticate" forMethod:RKRequestMethodPOST];
// OBJECT MAPPING
RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[CBUser class]];
mapping = [manager.mappingProvider objectMappingForKeyPath:#"user"];
[mapping mapKeyPath:#"id" toAttribute:#"userId"];
[manager loadObjectsAtResourcePath:str objectMapping:mapping delegate:self];
}
// DELEGATE
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects {
CBUser* user = [objects objectAtIndex:0];
NSLog(#"Loaded Contact ID #%# -> Firstname: %#, Lastname: %#", user.userId, user.firstname, user.lastname);
}
- (void)objectLoader:(RKObjectLoader*)objectLoader didFailWithError:(NSError*)error {
NSLog(#"Encountered an error: %#", error);
}
Looking at the server log it seems it receives a GET request instead of a POST even though I'm using the "RKRequestMethodPOST" option.
Here is my error form my logs:
Started GET "/account/authenticate.xml?commit=Login&authenticity_token=iPhone&user%5Bpassword=XXXXX&user%5Bemail=XXXXXXX%5D&user%5Bdevice_token=XXXXXXX" for 192.168.106.30 at 2012-01-25 19:20:03 -0800
AbstractController::ActionNotFound (The action 'show' could not be found for AccountController):
Rendered /Users/Guillaume/.rvm/gems/ruby-1.9.2-p290/gems/actionpack-3.0.9/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb within rescues/layout (1.2ms)
What am I missing?
Hopefully this code snippet of my app will help. You setup your mappings, say in your delegate. Note the "forMethod"
RKObjectRouter *router;
RKObjectMapping* createAccountSerializationMapping = [RKObjectMapping mappingForClass:[Cr eateAccount class]];
[createAccountSerializationMapping mapAttributes:#"email", #"pwd", #"uname", nil];
[[RKObjectManager sharedManager].mappingProvider setSerializationMapping:createAccountSerializationMapping forClass:[CreateAccount class]];
router = [RKObjectRouter new] ;
[router routeClass:[CreateAccount class] toResourcePath:#"/registration/rest/users/create_account" forMethod:RKRequestMethodPOST];
[RKObjectManager sharedManager].router = router;
and then later, when you want to post an object
[[RKObjectManager sharedManager] postObject:user delegate:self];
Since I couldn't figure out how to set up two different POST resource paths for the same class , I tried manually creating the RKObjectLoader request but it seems to keep sending a GET request instead of a POST even though I've set the method to POST. Here is my code
User *user = [[User alloc] init];
user.uname = uname;
user.pwd = pwd;
RKObjectManager *svc = [RKObjectManager sharedManager];
RKObjectMapping* mapping = [svc.mappingProvider objectMappingForClass:[User class]];
// what I was using before I needed two post resource paths//[svc postObject:user mapResponseWith:mapping delegate:self];
RKObjectLoader *loader = [svc loadObjectsAtResourcePath:authResourcePath objectMapping:mapping delegate:self];
[loader setMethod:RKRequestMethodPOST];
loader.userData = [NSNumber numberWithInt:RequestLogin];
loader.params = [NSDictionary dictionaryWithObjectsAndKeys:
uname, #"uname",
pwd, #"pwd",
nil];
[loader setSourceObject:user];
[loader send];
[user release];
In cases where you have more than one path to POST or PUT to, the easiest thing to do is use the block form of the postObject: invocation and specify the destination resourcePath yourself:
[[RKObjectManager sharedManager] postObject:foo delegate:bar block:^(RKObjectLoader *loader) {
loader.resourcePath = #"/my/destinationPath";
}];
We may introduce a named route concept at some point that would let you disambiguate the routes using names, but for now its purely based on the HTTP verb.
Note that you do NOT and cannot register the secondary path on the router -- you are sidestepping it completely for the secondary path.
In order to complete Blake Watters answer if the different route need different objectMapping you will need to do:
[[RKObjectManager sharedManager] postObject:query delegate:saveJobQueryHandler block:^(RKObjectLoader* loader) {
loader.objectMapping = NEW_MAPPING;
loader.resourcePath = #"/other/url";
loader.targetObject = nil; // Important
}];
For more information about loader.targetObject = nil; read sendObject:delegate:block: