Today, we are writing applications. And each application is connected to the Internet. Let’s just talk about those applications which are connected to the Internet. Often, when users perform some operation in the application, a server response is needed, so while this response is being performed, the user is staring at the spinner.
There’s no problem with that if you really need to have a server response, and you need to have 100% correct data only. But the thing is that there are a lot of times when we can easily avoid showing the spinner to the user, and trick the user, showing him what he wants to see rather than showing spinners during every user interaction, even if those spinners are cute.
Types of user interaction
Let’s look at user interactions from the perspective of user expectations. When doing some operations in the application, the user can’t be sure about the result — before a publication is opened, it’s hard to predict its contents. On the other hand, some operations are highly predictable for the user:
liking a favorite post
writing a comment
following/unfollowing another user
Often all these operations require an internet connection in order to be executed, and in badly designed applications, we often see a spinner after each operation. In very badly designed applications, we see a spinner which locks the whole application screen (and which we don’t have the ability to cancel).
Idea
Somehow we need to convince the user that these operations are performing immediately. And here we’ll explain how we do that in our applications.
This approach doesn’t require big architecture solutions from the start and can be implemented at any moment of the application development cycle, without any harm to the original. Still, there are few things which are not required, but highly recommended. These things aren’t new and you can find a lot of information about how to use them correctly and why you need to have them in your application.
Requirements
View Layer without logic
Taken from MVVM pattern. The main idea is that the View Layer doesn’t know anything about your business logic. All that this View Layer should care about are simple, plain objects, which have no business logic in them, or at least the View Layer shouldn’t know anything about it:
// Something that can be drawn@protocol DrawablePerson@property(nonatomic, copy) NSString * name;@property(nonatomic, copy) NSString * followingStatus;@end@interface Person : NSObject <DrawablePerson>@property(nonatomic, copy) NSString * name;@property(nonatomic, copy) NSString * followingStatus;@end@interface PersonView : UIView// Even if person have some businnes logic in it,// View layer shouldn't care about it// It only should care about how to draw it correctly@property(nonatomic, strong) id<DrawablePerson> person@end
view rawexample.m hosted with ❤ by GitHub
Updateable View Layer
The View layer should reflect any data changes that are performed in the data it renders. Let’s say a person’s name was changed. Your view should behave correctly, and be able to update itself when this happens. In our examples UIViewController will be the object which will respond to the changes in the model and will tell View to update itself with new data.
Request Manager
This is the object that is responsible for network operations in your application. You should have one (I hope you do). Also, it’s even better if your ViewController speaks with the Model layer, and the Model layer itself speaks with Request Manager. Again, this is not required, but this is how it works in our applications.
@interface PersonModel : NSObject// Current person@property(nonatomic, readonly) Person *person;// Creates model for working with specified person- (id)initWithPerson:(Person *)person;// Follows current person// Calls specified block.// Result block can be called more than once- (void)follow:(void (^)(Person *person, NSError *error))resultBlock;// Unnfollows current person// Calls specified block.// Result block can be called more than once- (void)unfollow:(void (^)(Person *person, NSError *error))resultBlock;@end
view rawPersonModel.m hosted with ❤ by GitHub
Application Structure
Here’s how(probably) your application works now. This is waaay too schematic, but you can update it with your own Structural Units.
So here’s what happens:
Model gets the data from internet
ViewController listens to the data from Model and updates View
User Performs some Action on the View
Spinner starts
View Controller tells Model to handle action
Model goes to the internet(to the server, for example)
Model gets response from the server
ViewController’s callback got called
ViewController updates view
Spinner stops
Let’s add one small thing
In order to trick the user we’ll need to have just one simple thing — the Optimistic Model.
Optimistic Model
Do not let your user see spinners — Optimistic ModelThe idea behind that is pretty straightforward. When the user changes something, the optimistic model gets the original object, tries to predict the potential result, then calls ViewController’s callback with this updated object, and then performs the actual operation in the background like the default Model. Once the real result is returned from the server, the optimistic model calls callback with the real object, or in case of an error, the Model performs callback with the original object and then calls error callback.
If we highlight possible flows, they can look like this:
Do not let your user see spinners — Optimistic Model FlowsOptimistic Model Implementation
Now, let’s look how the Optimistic Model implementation can look like.
@implementation PersonModel- (void)follow:(void (^)(Person *person, NSError *error))resultBlock { __weak __typeof(self) weakSelf = self; // Saving original for later usage Person *original = self.person; // Create fake result Person *fake = [self.person copy]; fake.followingStatus = @"Following"; // Updating current object self.person = fake; resultBlock(fake, nil); // Calling request manager [self.requestManager followPerson:original result:^(Person *updatedPerson, NSError *error) { if (error) { // rollback weakSelf.person = original; resultBlock(original, error); } else { // Updating to the new value weakSelf.person = updatedPerson; resultBlock(updatedPerson, nil); } }];}@end
view rawOptimisticModel.m hosted with ❤ by GitHub
With this simple update we were able to perform network operations “immediately” (at least from the user perspective).
Let’s Summarize
What do we have as the result?
A happy user who doesn’t need to wait for each operation to finish:)
A happy user, who thinks that your application is blazingly fast(even if your server is really slow).
Notes
In the example, we’re copying the object. This is done to prevent some unexpected changes, which can occur, for example, when there are other references on this object in the application.
In the example, in server response, we have a fully updated object. In the real world application, this rarely true. Anyway, even if server responds with partial updates, this approach can work correctly.
The Optimistic Model can be simply used for independent properties or groups of properties of object.
Depending on your needs, you probably would need to have Local Storage, which will store data that has not been sent to the server yet.
In complex case, when user tries to perform different operations quickly on the same object, you will probably need to use Temporary/Shadow Storage, which will decide what to show. An example of the implementation of this object will be covered in next post.
Comentarios