How to properly implement a Services class into my

2019-03-16 19:47发布

问题:

My current head scratcher: implementing a model class specifically for service calls to my rails application.

Here is the scenario:

  • I have a class named Service that is a subclass of NSObject.
  • The implementation file has a few methods defined… lets look at doSignUp.
  • I am using AFNetworking to communicate with the api.
  • From my SignUpViewController, I create an instance of my Service class and call doSignUp
  • The method works, as expected and I receive the proper response from the server.

Now Comes the part I don't fully understand:

  • AFNetworking utilizes blocks for its service calls.
  • Inside the success block I call a helper method called handleSignUp (also in Service class). This method essentially parses the JSON and I create a new User (NSObject subclass) out of it. The handSignUp method then returns the User object.

At this point I have a new User object and I need to send that object back to my SignUpViewController... How can I do that?

  • Should I try to add that object to the AppDelegate and access it from the SignUpViewController? This solution could work to access various global properties but when would the SignUpViewController know when to access it?
  • Should I try to add a reference to the SignUpViewController in the Service class? That seems counter productive... I might as well add the method doSignUp and handSignUp to the SignUpViewController. It seems to me like my Service class should not be aware of any other viewControllers.

See below for my code examples:

Service.h

//Service.h
#import <UIKit/UIKit.h>
#import "AFNetworking.h"
@interface Services : NSObject
- (void) doSignUp:(NSMutableDictionary*)params;
@end

Service.m

// Service.m
 #import "Services.h"
 #import "Config.h"
 @implementation Services

 - (void) doSignUp:(NSMutableDictionary*)params {
     NSURL *url = [NSURL URLWithString:@"http://MYURL.COM"];
     AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
     NSURLRequest *request = [httpClient requestWithMethod:@"POST" path:@"signup.json" parameters:params];
     AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
                                    success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
         [self handleSignUp:JSON];
     } failure:nil];
     [operation start];
 }

 - (User*) handleSignUp:(NSMutableArray*)json {
     User * user = nil;
     if ([[json valueForKey:@"success"] isEqualToString:@"true"]) {
           // create user here ...
     }
     return user;
 }

SignUpViewController.h

#import "Service.h"
 @interface SignUpViewController : UIViewController {
     Service * service;
 }

 @property (nonatomic, strong) Service * service;

SignUpViewController.m

#import "SignUpViewController.h"
 @interface SignUpViewController ()
 @end

 @implementation SignUpViewController

 @synthesize service = __service;
 - (IBAction) initSignUp:(id)sender {
      // create params...
      [self.service doSignUp:params];
 }

Again, all this code does what it is supposed to do... I just need to know how it should all communicate. How can I alert the SignUpViewController the handleSignUp has been called and a new User object is available?

Thanks for your time, Andre

回答1:

As far as i can see you are not realizing the asynchronous nature of this process: actually since network or IO operations take a long time to complete we tend to prefer asynchronous access, i mean: the caller (View Controller) calls a message on the web API through the service and then STOPS WAITING FOR A RESPONSE. Actually the network processing could be done on a different process or thread or processor's core.

Now then, how can the caller BE NOTIFIED by the service when the operation completes? For this to happen we must bind the two objects using Delegation: we create a protocol (it's just a declaration of messages that an object will be responding to) and we implement it in our View Controller, that way whoever will be using it will know for sure what messages are available.

Then we will declare a delegate of type id in our service that will be called once the operations will be completed.

It's almost like importing your ViewController.h in the service class and giving the service a pointer to the view controller, but done in the right way (without circular references and respecting SOLID principles)

Now some code:

//service.h
@protocol RemoteApiProtocol <NSObject>
@required
-(void) UserLoggingIn;
-(void) UserLoggedIn:(User*)user;
-(void) UserLoginError:(NSError *)error;
@end

The protocol works like a small interface, it's a contract between two classes/objects. Then in your service you can declare field & property for the protocol:

//Service.h
#import <UIKit/UIKit.h>
#import "AFNetworking.h"
@interface Services : NSObject{
      __weak id<RemoteApiProtocol> delegate;
}

@property (nonatomic, assign) id<RemoteApiProtocol> delegate;

- (void) doSignUp:(NSMutableDictionary*)params;

@end

At this point you implement the protocol in your view controller, integrating the contract you just build. This way the server will know that the delegate will always be able to respond to the protocol messages.

//SignUpViewController.h
#import "Service.h"
 @interface SignUpViewController : UIViewController <RemoteApiProtocol> {
     Service * service;
 }

 @property (nonatomic, strong) Service * service;

After implementing the protocol you can assign your view controller as delegate for the server, this way the server, after calling the apis will be able to call the delegate with the result of the call. For this to happen you implement the protocols messages that will be called by the service:

//SignUpViewController.m
#import "SignUpViewController.h"

 @implementation SignUpViewController

 @synthesize service = __service;
 - (IBAction) initSignUp:(id)sender {
      //create the service with params
      //...

      //assign self as the delegate
      self.service.delegate = self;
      [self.service doSignUp:params];
 }

#pragma mark - RemoteApiProtocol
-(void) UserLoggingIn
{
      //login started, 
      //you can show a spinner or animation here
}
-(void) UserLoggedIn:(User*)user
{
      //user logged in , do your stuff here
}
-(void) UserLoginError:(NSError *)error
{
      NSLog(@"error: %@",error);
}

@end

And finally you call the messages in your service, after calling the apis in the success and error block:

// Service.m
 #import "Services.h"
 #import "Config.h"
 @implementation Services

 - (void) doSignUp:(NSMutableDictionary*)params {
     NSURL *url = [NSURL URLWithString:@"http://MYURL.COM"];
     AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
     NSURLRequest *request = [httpClient requestWithMethod:@"POST" path:@"signup.json" parameters:params];
     AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
                                    success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
         [self handleSignUp:JSON];
     } failure:nil];


     //Signaling the start of the signup operation to the delegate
     if(self.delegate){
          [self.delegate UserLoggingIn];
     }

     [operation start];
 }

 - (User*) handleSignUp:(NSMutableArray*)json {
     User * user = nil;
     if ([[json valueForKey:@"success"] isEqualToString:@"true"]) {
           // create user here ...
           //call the delegate (view controller) passing the user
           if(self.delegate){
               [self.delegate UserLoggedIn:user];
           }
     }else{
           //error handling
           if(self.delegate){
               //NSError *error = ...
               //[self.delegate UserLoginError:error];
           }
     }


     //return user;
 }
@end


回答2:

You might want to look into Delegation and or a Data Source. Check my accepted answer in this post for an example, and reference to Apple Docs on the subject.

Objective C View to Controller Communication

Hope this helps.



回答3:

Add callback block parameters to the doSignUp (should probably be renamed to signUpWithParams:success:failure:), so that the controller can respond to the signup succeeding or failing, and having the context for either case available to present relevant information to the user.



回答4:

The delegation method would definitely work. You could basically add a delegate to the Service class, and set the delegate equal to the view controller before calling doSignup.

However, I would implement it using blocks. Add a parameter to the doSignUp method which is a completion block, which is declared in the SignUpViewController. For an example of how to pass blocks as parameters, just check out the source from AFNetworking.