A simple basic hierarchy for iOS development with Objective-C. It is based on MVCS and KVO.
The following image is the all elements of this hierarchy.
There are some aims for this hierarchy:
- Divide the codes to different layers to make sure one class will be not too large;
- Decouple the View, ViewController, Store layers, and you can reuse or replace one of the layers which is relied on the same shared pipeline;
- Each layer just does their own job;
We will describe it in detail.
In Minya, MVCS means Model-View-ViewController-Store. You must know the MVC, so it is very simple to understand MVCS. We just put the business logic code to the Store layer. However, our aims are not only the lighter view controller.
In Minya, each layer has the special job to do, and all the jobs are related to the data:
- Model Layer: it is just a data structure most of the time; sometimes it do some light data operation, such as formatting a date string;
- View Layer: show the data to the users and receive the users' input data;
- View Controller Layer: only pass data between different view controllers;
- Store Layer: it is the business logic processing layer; its duty is to request data from the server or local storage, and process the data from the views;
Note: The whole store layer in the previous image contain three elements: Store, Service, Storage.
Obviously, it is simple, and there is nothing special.
Some differences are the organization between layers. Our aims are to decouple the View, ViewController and Store. You can see the MIViewController, which is the base view controller class for all the view controllers:
@interface MIViewController : UIViewController
@property (nonatomic, strong, readonly, nonnull) id<MIStore> store; //!< Store for the business logic
@property (nonatomic, strong, readonly, nonnull) UIView *containerView; //!< The container view of the view hierarchy
@end
View controller knows nothing except a container view and a store protocol object. We organize the View, ViewController and Store with a Scene:
@interface MIScene : NSObject
@property (nonatomic, copy, nonnull) NSString *viewName; //!< view name
@property (nonatomic, copy, nonnull) NSString *controllerName; //!< controller name
@property (nonatomic, copy, nonnull) NSString *storeName; //!< store name
@end
You can create a scene with any of the three elements which share the same pipeline, which we will talk later. And then you can create a view controller with a scene object like this:
MIScene *scene = [MIScene sceneWithView:@"SearchView" controller:@"SearchViewController" store:@"SearchStore"];
UIViewController *viewController = [[MIMediator sharedMediator] viewControllerWithScene:scene context:nil];
UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:viewController];
Of course, there are some questions: how do they communicate with each other? And which they are relied on? We will talk this later.
Let's come back to Store
. Store's job is to process business logic. A store has three data source: Server
, Local Storage
and User inputs
. We just talk about the Server
and Local Storage
.
We declare two protocols: MIService
and MIStorage
, which declare some methods that classes conforming to them should implement. Take the MIService
for example, if you have a RESTful API to request a list data from the server, you can define a subclass of MIService
class which has conformed to MIService
protocol and implement the -requestWithParameters:success:fail:
method. In the subclass, you can create request object and start a request, and handle the response, then pass the data to the store object by the two blocks as you need. And a store just know about the protocol object, and not know the concrete service class. We create a service instance with the service class's name, just like the following code:
@interface PhotoDetailStore ()
@property (nonatomic, strong) id<MIService> searchDetailService; // Service for get photo's detail
@property (nonatomic, strong) id<MIService> getPhotoContextService; // Service for get photo's context
@end
#pragma mark - PhotoDetailStore implementation
@implementation PhotoDetailStore
// ...
- (void)fetchData {
@weakify(self)
[self.searchDetailService requestWithParameters:@{@"photo_id": self.photoID ?: @""} success:^(id _Nullable data) {
@strongify(self)
self.detailPipeline.photo = data;
self.detailPipeline.flagRequestFinished = YES;
} fail:^(id _Nullable data, NSError * _Nullable error) {
// You can do something if the data request fail.
}];
// ...
}
// ...
- (id<MIService>)searchDetailService {
if (!_searchDetailService) {
_searchDetailService = [MIService serviceWithName:@"SearchPhotoDetailService"];
}
return _searchDetailService;
}
- (id<MIService>)getPhotoContextService {
if (!_getPhotoContextService) {
_getPhotoContextService = [MIService serviceWithName:@"GetPhotoContextService"];
}
return _getPhotoContextService;
}
@end
Storage
is the same.
Store Layer has three element, and each element has it own job:
- Store: process the business logic, fetch data from service or storage object; it does not need to know the details about the data, such as RESTful API, the implementation of network layer, the storage type of the local data. It just cares about the data.
- Service: request data from server and do some job to process the response data as you need. May be in some day later, you want to change you network layer implementation, then you just need change the code here.
- Storage: operate the local storage data, such as read, write, remove data. If you want to change your way to storage data, such as from sqlite to realm, just do the job here.
OK, we describe the jobs of all the layer elements. We still don't know how the View, ViewController and Store communicate with each other. Let's talk about it.
Yes, data. In Minya, data is all, data is everything. View
, ViewController
and Store
are relied on data. View
shows data from server and capture data from the users; ViewController
passes data to the other ViewController; Store
processes data from the server or user inputs; Service
request data from server; Storage
read data from local storage or write data to it.
Of course, data has to have a container. You may say store it in the store object. Yes, but not all. We define a pipeline object to contain the data that want to pass through the View
, ViewController
and Store
. We can say that pipeline object is a contain for data and a data pipeline for View
, ViewController
and Store
. And in fact, View
, ViewController
and Store
are relied on Pipeline
. In the previous section, we say View, ViewController and Store don't know each other, buy they know the same thing: a shared pipeline object. For example, in the demo code, we have a PhotoList
scene. The scene's elements(PhotoListView
, PhotoListViewController
, PhotoListStore
) share the same PhotoListPipeline
object.
The store object will create a pipeline object, because in Minya it is the data distribution center. When we create a scene, we will pass the pipeline object from the store object to the view controller object and view object. In the MIViewController
, we have do the job:
@implementation MIViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Build the view hierarchy
self.containerView = [[self.viewClass alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.containerView];
// Set up pipeline
[self setupPipeline:self.store.pipeline];
[self.containerView setupPipeline:self.store.pipeline];
// Add observers of the pipeline data.
[self addObservers];
}
// ...
@end
Then the three elements have shared the pipeline object. Now you can get the data you need from the pipeline at any layer.
We suggest that you can put five type data in the pipeline:
Normal data
: these data are from the server and then showed in the view.Flag data
: now the KVO is not good for collection data, so we can set a flag for these data which from server. The view layer can observe the flag property to know the collection data are updated.Status data
: sometimes, the view want to show different subviews in different status. For example, if the user does not sign in, we show asign in
button in a view, and if the user has signed in, we show some other information in the same view. We can define some status data for these situations.Input data
: when the users have some inputs, such as tapping a button, you should do some business logic works. The store layer want to know that, so you can define a input property in the pipeline, and the store layer can observe this property, and when the property changed you can do what you want to do.Context data
: context data are used for data transmission between view controller. In Minya MVCS, dictionary is used for data transmission. The view controller layer does not store any data but the pipeline does, and the view controller do the transmission job, so it can fetch data from pipeline.
Well, we have had five type data. But there still have a question: When a user tap a button, how does the store know that it want to do something? Our answer is KVO, just like described in the data types.
We describe three scenarios and the data process flow:
-
User taps a button to fetch data from the server. User taps the button, we change a
inputTapButon
property of the pipeline in the button action. The store which observesinputTapButon
property will prepare the parameter that RESTful API needed, then call the service's request method. The service start a request... -
List data are back from the server, and the view should update the list view. Store changes the
flagForRequestFinished
property of the pipeline, and the view which observes this property will receive KVO message, then call the tableview'sreloadData
method. The view fetch the list data from the pipeline's property. -
User selects a cell in the table view and then push a detail view controller. User tap the cell, and app call the
-tableView:didSelectRowAtIndexPath:
method. In this method, we change theinputSelectedIndex
property of the pipeline. The view controller which observesinputSelectedIndex
will fetch the context from the pipeline, create a scene, and then push the next view controller.
You see, it is not hard. You just want to know who will change the pipeline's property, and who cares about the property.
Well, you must know that apple's KVO API is hard to use. However, if you have used the Reactive Cocoa, you may find its KVO API is more friendly. We extract Reactive Cocoa's KVO API code, and do some changes for Minya. You can see it in the source code.
I think you are very familiar with View tree hierarchy. A parent view have some subviews. In Minya, if the business logic is complicated, you may build three type tree hierarchies:
View Tree
: separate you view code in some subviews; so you want to define some subclass ofUIView
;Pipeline Tree
: in our opinion, every custom subclass ofUIView
should have a pipeline object to provide data for it. So, corresponding to theView Tree
, you may want to build up a Pipeline Tree for theView Tree
. Of course, there are some situations that Minya may be not satisfied, we will talk later.Store Tree
: If the business logic in a store is too complicated, you can divide different business logics to different store objects. TheStore Tree
does not need to be corresponded to theView Tree
andPipeline Tree
. For example, one sub store can have two pipelines, you just make sure that the two pipelines will be attached to thePipeline Tree
correctly.
You can see the InterestingnessStore
class in the demo code.
One coin has two side. The Minya also has some problems:
- Class explosion: One scene has 4 classes and 8 files at least. And if the business if very complicated, you may be want to create a lot of classes for a single scene. This has some consequences, such as influencing the launch time.
- Pipeline class may contain a lot of properties if you don't build up the
Pipeline Tree
correctly. - If you are not familiar with KVO, you may be confused, especially when you debug the code. In this situation, you just know who changes the data and who cares the changed data.
- The three elements of a scene are decoupled, however they are relied on the pipeline object. When you want to reuse or change one element object, you must make sure the pipeline is correct.
- It is not friendly to the cells of table view and collection view.
- It is not friendly to rename a View/ViewController/Store/Service/Storage, because we create the instance of these classes with the string of the Class.
So, if you want to use Minya, you should know these problems. And there will be more problems in practice.
Minya is just a hierarchy and a thought. There are still a lot of things should be done. In our project, we have done some optimization to meet the actual needs. So we did not set it for the CocoaPods. If you want to use it, you can import the source code in the MVCS directory.
Another thing is that we use the Flickr API for the url request, if you want to run the demo, you should change the Flickr API key and shared secret of your own in the AppDelegate.m
.
It is a boring work to create 4 classes and 8 files for a single scene, so I write a python script minya.py
to help you to create these files. Of course, you must do something, such as changing the project_name, author_name, company_name in the script file. Run the following command in the directory of the file:
python minya.py SCENE_NAME
Then it will create all the 8 files for you.
And some Xcode 8 extensions may be in plan.
All source code is licensed under the MIT License.