In my iOS MVC framework, Building a control layer (Part 1), I introduced some methods of building a control layer, and this article continues to discuss some methods. MVC is criticized by many developers for the expansion of THE C layer for the following reasons:
- All of the view building and layout code is done in the controller. A lot of you don’t like the storyboards and XIBs that are provided by the system to build your view. Instead, you like to do your view interface layout in the form of code, which is usually concentrated in loadView or viewDidLoad or scattered around in lazy loading. The amount of code that builds and lays out a view through code can exceed 50% of your view controller’s total code.
- Requests to the server are usually wrapped in a very thin request layer, usually called APIService. This part of the code simply encapsulates the request for the URL of the server, and converts the message directly into the data model through some third-party frameworks that transfer the message to the data model, and then sends the message back to the controller or view in the form of asynchronous callback. The simple implementation of APIService increases the load on the controller, causing the controller to take on a lot of business logic besides building views and requesting network services.
- Failure to properly disintegrate and design functional interfaces with complex presentation logic results in all the code being done in one view controller, resulting in bloated controllers.
- The most commonly used UITableView in the application and the handling mechanism for data updates in the UITableViewCell cause the delegate methods to be very complicated to implement exceptions, especially when the updating of the complex UITableViewCell causes the code to be messy.
You can see that the framework itself is not the problem, the problem is that the people using it do not know or improper design ideas caused the problem. When something goes wrong, the first thing we should do is to reflect on ourselves instead of blaming others. (This chicken soup is really bad!!) How to solve the problems mentioned above that lead to the expansion of C layer? This is also the focus of this article.
Different code build times
The controller class is a functional control room for scheduling, and it also provides us with processing callbacks when events occur at various stages of the controller’s life cycle through the design pattern of the template method. Such as controller build time (init), View is built (loadView), view is built (viewDidLoad), view is about to appear in front of the window (viewWillAppear), View is already presented to the window (viewDidAppear), View is about to be deleted from the window (viewWillDisappear), view The window has been deleted (viewDidDisappear), the view has been destroyed (viewDidUnload, this method will not work after iOS6.0), and the controller has been destroyed (dealloc). To implement the functionality, we may need to add the corresponding processing code somewhere above. How do I add code? And what code to add to the template methods described above? It’s very critical. Here I want to emphasize is that although the controller has a view of the root view properties, but the controller’s life cycle generally longer than the root view of the life cycle, and it may be a function in the view of different scenarios is completely different, or have done by rebuilding views may be some change skin function. In iOS6 controllers after that they only provide a view build and a template method after the build is done, but they don’t provide any template methods before or after the view is destroyed, so we have to take that into account when we add code to loadView and viewDidLoad, Because it does not provide reciprocal processing like the other methods.
-
If your business model object has the same life cycle as your controller, then it is recommended to put the building of the business model object in the initialization code of your controller, provided that your business model object is a lightweight object. If your business model objects are particularly time consuming to build then it is not recommended to build them during initialization of the controller but through lazy loading or when a touch event occurs. If your controller consists of multiple child controllers, this is also where initialization of the child controllers is best done. We can also initialize and create other lightweight properties during controller initialization that have the same life cycle as the controller.
-
If your view is created by SB or XIB, then congratulations, you can omit this part of the code. If you are building your view from code, it is necessary to add your view building and layout code here. You need to override the loadView method and do all the view building and layout preferably here. If you want to reuse the default root view as your root view, you need to call the loadView method of the base class before building your other subviews. If you want to completely build your root view and subview system, you don’t need to call the loadView method of the base class. A lot of people like to build a view inside viewDidLoad, which is not the best solution, because literally what you’re adding to viewDidLoad is some processing logic after the view is built and loaded. How to construct interface layout code in loadView more elegantly and reasonably, I will give a specific solution later.
-(void)loadView {/* Custom root view builds without calling base class methods. You can also use UIScrollView or UITableView as the root view directly here. This eliminates the need to create scroll views or list subviews on top of the default root view. */ self.view = [[UIView alloc] initWithFrame: [UIScreen mainScreen].bounds]; / /... Create additional subviews. }Copy the code
-
The code for the event binding (viewDidLoad) that’s going to be called when the view is built. So here you should do some business logic initialization actions, initial requests for business model service interfaces, event handling binding actions for some controls, and setting up the view’s delegate and dataSource. This is used to associate views with controllers and controllers with business models. The best thing to do in viewDidLoad is to bind between the view and the controller and bind between the controller and the business model. It is not recommended to do view building, or anything related to the entire controller life cycle.
-
View appear and disappear (viewWill/DidAppear, viewWill/DidDisappear) view of the present and may be called repeatedly. It is recommended that the timer and notification observer be added and destroyed here. In general, timers and observers are only active when the interface is present and not when the interface is gone, so adding timers and notifying observers is most appropriate. There is also the benefit of implementing timers and observers here without creating circular references that would cause the controller to be unable to be released.
-
Controller Destruction (Dealloc) When a controller is destroyed, it indicates that the controller life cycle has ended. There is no need to add special code, but it is important to set the delegate and dataSource of the various control views to nil! Be sure to set the delegate and dataSource in the various control views to nil here! Be sure to set the delegate and dataSource in the various control views to nil here!
Important things say three times! Whether these delegates are assigned or weak.
Lazy loading
Lazy loading is designed to address scenarios of on-demand and optional use as well as time-consuming creation. Lazy loading can speed up the presentation in some cases, and lazy loading can delay the creation of certain objects. Do you want to create all objects lazily? The answer is no. Many students like to create and layout all views in the controller through lazy loading, as shown in the following code snippet:
@interface XXXViewController() @property(strong) UILabel *label; @property(strong) UITableView *tableView; @end @implementation XXXViewController -(UILabel*)label { if (_label == nil) { _label = [UILabel new]; [self.view addSubview:_label]; } return _label;} return _label; } -(UITableView*)tableView { if (_tableView == nil) { _tableView = [UITableView new]; [self.view addSubview:_tableView]; _tableView.delegate = self; } return _label;} return _label; } -(void)viewDidLoad { [super viewDidLoad]; self.label.text = @"hello"; [self.tableView reloadData]; } @endCopy the code
It looks pretty neat and clean, at least in viewDidLoad. But there may be some pitfalls:
-
View hierarchies are scrambled and code is scattered because views are lazily loaded and scattered, so you can’t see the view hierarchies as a whole and what the order is. This makes it difficult for us to read the code as well as debug and maintain it.
-
Lazy loading delays creation, but the overwriting of view attributes goes beyond simply creating a view. In addition to creating a view, it also implements the function of adding the view to the parent view and layout, and may even implement other more complex logic. This can result in an implementation of a GET attribute carrying more functionality than a method can handle. We simply use it as a read property and have some code duplication problems.
-
Inexplicable problems and crashes lazy loading of views made our view properties have to be set to strong, and the code implementation was created only once. If for some reason all the views in our controller need to be recreated (such as a skin change) then it is possible that the lazy view will not be created again, resulting in an unexplained interface problem. Even worse, too much code was implemented in lazy loading, resulting in crashes when accessing properties in some places.
Therefore, lazy loading is not recommended for all view builds in a controller. View builds and layouts should be handled uniformly in loadView. Lazy loading is not to be abused, especially in view building code. We should build lazy-loaded objects only for optional objects and those that might affect performance, not all objects. Also need to be aware of is if must be lazy loading is used to realize the object of building, in the lazy loading code should also be simplified as far as possible, you just need to create part of the functions, and don’t put some unnecessary logic code in to the realization of lazy loading, the implementation of the logic, the more the more would be to use the restrictions and uncertain factors. In the example above, the user calls self.label or self.tableView as normal attributes, without thinking about all the setup and processing that goes into them (such as layout and adding to the superview). This can lead to the misuse of these attributes with disastrous consequences. Another view while you build is done through lazy loading form, but if you’re in such as a large number of access these properties in the viewDidLoad creates a view of building operation, actually so directly create a view object is the same, and didn’t have any performance optimization effect, and it is also and the original intent of lazy loading is against.
An example of this in our project is the lazy loading of a UITableView, which in addition to creating a real exception to the UITableView sets the delegate value and other code logic. And this UITableView happens to be an optional display view. And we also set the delegate of this UITableView to nil in the dealloc of the view controller. As a result, the code ended up crashing online.
Simplifies view building in controllers
There are two ways to build a view: visually through a Storyboard or XIB; One is to do it in program code. Both methods have their advantages and disadvantages. Both iOS and Android provide powerful visual interface layout systems, and both use XML files to describe the layout. This approach is very consistent with the DEFINITION of V in MVC, where the view part is separate and hierarchical. Building your view in this way will not pollute the code in your controller and cause the code in your controller to bloat to a certain extent. With the use of SB and XIB, we can simplify the construction of the view part. In practice, you’ll find that if you’re building and laying out your view in code, that code will probably exceed 50% of your controller’s lines of code. So one solution to layer C bloatiness is to unify your interface layout code through SB or XIB. Some students may say that using SB or XIB is not conducive to collaborative development, and it is easy to cause code conflicts when merging. This is actually a false statement. Typically, our functionality is split up into view controllers, with one person responsible for each controller. If you use a XIB to implement the interface layout of the controller you are responsible for how can there be code merge conflicts? Even if you build your interface in SB, which puts most of the interface in a single file, in practice your application can build multiple SBS. From the perspective of function similarity, we can put the same function in one SB, and different large modules create different SB files. In this way, one SB can be decomposed into multiple small SB according to the application module. As long as the separation is reasonable, it will minimize the occurrence of conflicts during collaborative development. With the update of XCODE version, SB has more and more powerful functions. In addition to interface layout, SB can realize the logical jump and page switch without writing a line of code. We can actually take a moment to really study it, rather than just reject and reject it. Android developers still prefer to do their interface layout through XML and mostly by writing it.
Maybe the above approach doesn’t apply to you, but you still build the layout side in code. That’s ok, this article is about fixing controller code bloat, not stirring up factions. What if I just want to code the layout? After all, it is more flexible and configurable through code layout (at the expense of WYSIWYG). We know that the default implementation logic of loadView in iOS is to first go to SB or XIB and search for a matching view layout file based on the type of view controller. If so, the view layout file is parsed and the corresponding view hierarchy tree is built and those socket variables (IBOutlets) in the view controller are set and the event handlers (IBActions) associated with the binding view controls are set. A blank root view (self.view) is created if no corresponding layout file is found. The main purpose of visible loadView is to complete the construction and layout of the view. So when you’re creating a view and laying it out in code you should put that logic in there as well not in viewDidLoad. View building and layout should be done in one place rather than through lazy loading of code scattered over the view properties. Here I offer two ways to implement view building and layout separately or categorically from the controller.
First, adopt the method of classification extension
As the name suggests, the approach to using categorical extensions is to create a categorical extension of view construction and layout specifically for the view controller. To separate this part of the code from the rest of the code in the controller, we can implement the classification extension code for the view build in a separate new file.
// Create a controller name for each controller +CreateView header //XXXXViewController+ createView.h#import "XXXXViewController.h"// Define an extension that defines view properties that all controllers may use in the same way that you would with SB or XIB. @interface XXXXViewController () @property(nonatomic, weak) IBOutlet UILabel *label; @property(nonatomic, weak) IBOutlet UIButton *button; @property(nonatomic, weak) IBOutlet UITableView *tableView; / /... @end .................................. // The implementation of the code layout //XXXXViewController+ createView.m#import "XXXXViewController+CreateView.h"@implementation ViewController(CreateView) -(void)loadView {[super loadView]; // If you want to completely customize the root view, you can do the same as the code I listed above without calling the parent class's methods. // This completes the construction and layout of all subviews. Because the code for building a view is written together, it's easy to read the code to see how the view is laid out. UILabel *label = [UILabel new]; label.textColor = [UIColor redColor]; label.font = .... [self.view addSubview:label]; _label = label; UIButton *button = [UIButton new]; [self.view addSubview:button]; _button = button; UITableView *tableView = [UITableView new]; [self.view addSubview:tableView]; _tableView = tableView; / /... // You can either autolayout all the subviews here, or you can write the code layout as soon as each view is created. There is no limit. } @endCopy the code
As you can see from the above code, we created a separate extension to define all view properties, created a classification and overloaded loadView to create and layout the view. In the code, we just do the build and layout and nothing else. For example, the UIButton event binding and the UITableView delegate and dataSource Settings are not in there. This category is a very concise code construction and interface layout code. This makes the main implementation of the controller very clean.
//XXXXViewController.h @interface XXXXViewController @end .............................. // xxxxviewController.m // Import categories here to access view properties within them#import XXXXViewController+CreateView.h@implementation XXXXViewController -(void)viewDidLoad { [super viewDidLoad]; // We're binding events to buttons, assigning delegates and data sources to tableView, and we can see that the best thing to do in viewDidLoad is to set up associations and bindings between the view and the controller. [self.button addTarget:self action:@selector(handleClick:)forControlEvents:UIControlEventTouchUpInside];
self.tableView.delegate = self;
self.tableView.dataSource = self;
}
@end
Copy the code
Extending by category does not reduce the code of the controller, but it does break down specific logic into categories, making the code more readable and maintainable. Because the view-building and layout parts of the code are split into separate places, the main implementation part of our controller can focus on writing the control logic. Even this split method can split the work in two: one person is responsible for the interface layout, and one person is responsible for writing the control logic.
Two. Interface and message forwarding
The view controller splits the view build by extending the classification, and the code remains part of the view controller. If we want to fully implement the V in MVC that stands alone and can be reused, we can abstract the view build and layout into a single view class, and establish the connection between the controller and the view through interface definition and message forwarding. Remember the forwarding technology I mentioned in my last post? In order to achieve the separation of view and controller we can still use this method to achieve the separation of hierarchy.
- 1. Define the view property interface and view layout classes
// Define a protocol and implementation class that starts with the controller name and adds View. //XXXXViewControllerView.h @protocol XXXXViewControllerView @optional @property(nonatomic, weak) UILabel *label; @property(nonatomic, weak) UIButton *button; @property(nonatomic, weak) UITableView *tableView; / /... @end // Your layout root view can inherit from UIView or UIScrollView or any other view. @interface XXXXViewControllerView:UIView<XXXXViewControllerView> @property(nonatomic, weak) IBOutlet UILabel *label; @property(nonatomic, weak) IBOutlet UIButton *button; @property(nonatomic, weak) IBOutlet UITableView *tableView; @end ................................ //XXXXViewControllerView.m @implementation XXXXViewControllerView -(id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame];if(self ! = nil) { self.backgroundColor = [UIColor whiteColor]; UILabel *label = [UILabel new]; [self.view addSubview:label]; _label = label; UIButton *button = [UIButton new]; [self.view addSubview:button]; _button = button; UITableView *tableView = [UITableView new]; [self.view addSubview:tableView]; _tableView = tableView; // If you are using AutoLayout then you can add the code for layout constraints here. If you are using frames for layout, then use layoutSubviews for the layout of subviews. }returnself; } -(void)layoutSubviews { [super layoutSubviews]; // If you set the layout with frame, you can refresh the layout here. } @endCopy the code
You can see that the code above has no relationship to the controller and is independent of the controller. The view layout class is only used for the layout, construction and presentation of the view, which is very consistent with the definition and implementation of V in MVC. After a view is constructed, you need to lay out the view. You can use AutoLayout or Frame to lay out the view. An AutoLayout layout is a way to implement layouts by setting constraints between views, while a Frame layout is an early apple layout. AutoLayout code layout, the amount of code is very large and complex, this problem in iOS9 after a lot of simplification. Thankfully, there are a number of third-party layout libraries such as Mansory that can be very helpful in minimizing the difficulty of a layout. If your layout is performance oriented and you want to make it easier to complete your layout, you can use MyLayout, which is my open source interface layout.
- 2. Binding of view controller and layout view class.
//XXXXViewController.h
@interface XXXXViewController:UIViewController
@end
................................
//XXXXViewController.m
#import "xxxxviewControllerView. h" // Import the layout view class here// View controllers also need to implement the XXXXViewControllerView interface. This gives you direct access to some properties of the view in your view controller. @interface XXXXViewController ()<XXXXViewControllerView> @end@implementation XXXXViewController // Override loadView to complete the viewview construction. -(void)loadView { self.view = [[ViewControllerView alloc] initWithFrame:[UIScreen mainScreen].bounds]; } // This section is the implementation key to distribute controller access to the view property protocol to the layout view. -(id)forwardingTargetForSelector:(SEL)aSelector { struct objc_method_description omd = protocol_getMethodDescription(@protocol(ViewControllerView), aSelector, NO, YES);if(omd.name ! = NULL) {return self.view;
}
return[super forwardingTargetForSelector:aSelector]; } - (void)viewDidLoad { [super viewDidLoad]; Task 1: To load the view, task 1 can be used to load the view. [self.button addTarget:self action:@selector(handleClick:)forControlEvents:UIControlEventTouchUpInside];
}
-(void)handleClick:(id)sender
{
}
@end
Copy the code
You can see through the above loadView and forwardingTargetForSelector method overloading to achieve binding between the view controller and view. Then we can access properties in the view interface from anywhere. Binding is the same for all view controller classes, so you can do this with a macro definition:
// Define the following macro somewhere public#define BINDVIEW(viewclass) \-(void)loadView \ {\ self.view = [[viewclass alloc] initWithFrame:[UIScreen mainScreen].bounds]; \ }\ -(id)forwardingTargetForSelector:(SEL)aSelector\ {\ struct objc_method_description omd = protocol_getMethodDescription(@protocol(viewclass), aSelector, NO, YES); \if(omd.name ! = NULL)\ {\returnself.view; \} \return[super forwardingTargetForSelector:aSelector]; \} \... //XXXXViewController.m#import "XXXXViewControllerView.h"// View controllers also need to implement the XXXXViewControllerView interface. This gives you direct access to some properties of the view in your view controller. @interface XXXXViewController ()<XXXXViewControllerView> @end@implementation XXXXViewController BINDVIEW(XXXXViewControllerView) //... Add additional code here. @endCopy the code
Both of the above approaches to decomposing view build and layout solve the bloat problem caused by view code build in the controller. The first approach essentially just does some code splitting and doesn’t completely separate the controller from the view; The second approach completely separates the view from the controller. The view is built and laid out without the presence of a controller, and you can even reuse the view, which means you can have multiple controller classes reuse code from a view class. These controllers perform the same or slightly different functions, but the event handling logic can be completely different. The implementation mechanism of the second method more reflects the hierarchical relationship in MVC and the independence of V layer construction. So whether you build your view from SB or XIB or build your view layout from code, designing it properly can be very effective in reducing view-dependent code in your view controller.
Sinking of business logic
The problem with the build part of the view has been solved successfully. Let’s look at the thin service layer APIService. At the beginning I said that many architects design a service layer API based on all the apis that interact with the server, let’s call it APIService. APIService generates a simple encapsulation for each interface that interacts with the server. This encapsulation only completes the packaging of the data requested from the server and the encapsulation of the URL link, and directly returns the message returned by the server to the view controller through block callback after de-sequencing the packet.
@interface APIService:NSObject +(void)requestXXXWithDict:(NSDictionary*)input callback:(void (^)(XXXXModel *model, NSError *error))callback; +(void)requestYYYYWithDict:(NSDictionary*)input callback:(void (^)(YYYYModel *model, NSError *error))callback; / /... @endCopy the code
Each network request in our view controller directly invokes the corresponding request method and processes the returned Model data Model, such as interface view data refresh, file processing, some logic adjustments, etc. In this process, the controller assumes the invisible implementation of the business logic, thus increasing the burden of the code in the controller. For example, the following code example:
@implementation XXXXViewController // some event handler code for a controller. -(void)handleClick:(id)sender {// this part of the code needs to request different services according to different states. Suppose the state value is saved to the controllerif(self.status == 1) {// Pop loading... [APIService requestXXX:^(XXXModel* model, NSError *error){// Destroy loadingif(error == nil) {// Write model to a file. // Update the model data to a view. self.status = 2; // Update the status. // Other logic. }else{/ /.. Error handling. }}]; }else if(status == 2) {// Pop loading... Wait box and request another service, return the same data model. [APIService requestYYY:^(XXXModel *model, NSError *error){// Destroy loadingif(error == nil) {// Write model to file or update model to database. // Update the model data to a view. self.status = 1; // Update the status. // Other logic. }else{/ /.. Error handling. }}]; } } @endCopy the code
As can be seen from the above code, in addition to saving some states, the controller also makes different network service requests, file read and write, state update, view refresh operation and other logic according to different states, which leads to the controller code is very bloated and difficult to maintain. What went wrong? It is a misunderstanding of the model layer and a misuse of the definition of the service layer.
The M model layer in real MVC represents the business model rather than the data model, and the role of the business model is to complete the concrete implementation of business logic. What the M layer does is encapsulate things that have nothing to do with the presentation of the view and nothing to do with the controller, and just provide a very simple interface for the controller to call. APIService packaging is illogical and wrong packaging! We know that any system has a complete business implementation architecture, which exists not only on the server side but also on the client side, and the two are consistent. You can think of the business implementation architecture as a proxy of the server-side implementation architecture, and the communication link between the proxy and the server service is the interface message. We can not simply understand the client’s proxy implementation as a simple encapsulation of interface messages, but should be designed as a business logic implementation layer with complete architectural system as the server, which I think is the essence of the M layer. So we in the design of the client’s M layer must be in line with the idea to design, also can’t simply as an interface message encapsulation, and inside the controller to achieve some business logic, but should be the implementation of business logic, network request, the processing of a message to an abstract unity and other things related to the business scenario in M model layer. This concept and design approach is explained in great detail in my other two articles on model layer construction. We should somehow sink and decompose the logic that belongs in the controller to move the implementation part of the logic down to the model layer, so that we are not simply implementing one APIService method after another at design time. Instead, a complete business model framework is constructed for the controller to use. Using the above example, the solution is to design a business model class such as XXXXService, which internally encapsulates the state and unused network requests, as well as some file read and write implementations:
//XXXXService.h
@interface XXXXService
-(void)request:(void (^)(XXXModel *model, NSError *error))callback;
@end
..........................
//XXXXService.m
@ implementation XXXXService
{
int status = 1;
}
-(void)request:(void (^)(XXXModel *model, NSError *error))callback
{
if (self.status == 1)
{
[network get:@"URL1" complete:^(id obj, NSError *error){
XXXModel *retModel = nil;
if(error ! = nil) {XXXModel *retModel = obj --> XXXModel self.status = 2; // Update the status here. } callback(retModel, error); }]; }else if (self.status == 2)
{
[network get:@"URL2" complete:^(id obj, NSError *error){
XXXModel *retModel = nil;
if(error ! = nil) {XXXModel *retModel = obj --> XXXModel; self.status = 1; // Update the status here. } callback(retModel, error); }]; } } @endCopy the code
The business model code above is purely a logical implementation independent of the specific controller and view. So how do we use this business model in the controller?
//XXXXViewController.m
#import "XXXXService.h"@interface XXXXViewController() @property(strong) XXXXService *service; // Store the business model as an object, where we no longer see singletons or flat service requests, but as a normal object. And a real object!! @end@implementation XXXXViewController // The service can be created when the controller is initialized or lazy-loaded. Here we create by lazy loading. Best practice for lazy loading -(XXXService*)service {if (_service == nil){
_service = [XXXService new];
}
return_service; } -(void)handleClick:(id)sender {// [self.service request^(XXXModel* model, NSError *error){// Destroy loadingif(error == nil){// Update model data to a view. }else{/ /.. Error handling. }}]; } @endCopy the code
As you can see, the code in our view controller is very concise. The controller no longer holds state and does no business implementation-related processing. It simply calls the services provided by the business model and updates the view with the data in the data model in a callback. The controller no longer makes different requests based on the state, no longer deals with the business implementation of the task, and the business model is no longer provided to the controller in the form of a singleton or class method, but an object! A real object! An object defined in object-oriented to call the controller. The encapsulation of the business model layer makes it very easy to use the services provided by the business model in other view controllers. This simplifies the code and logic in the controller. In the above example, it can be clearly seen that M in MVC is responsible for the implementation of the business logic, V is responsible for the layout and presentation of the view, and C is responsible for connecting the two.
Control the separation of logic
Through the encapsulation and decoupling of view class, the problem that the view occupies the code of the controller is solved. Through the correct definition of M layer, the problem that the controller handles too much business logic realization is solved. The code in our controllers will be greatly improved and streamlined. We’ve solved 80% of the problems. But even then our controller might have a lot of logic in it.
A very important reason for code bloat in a view controller we build may be that the logic of the functionality is very complex or the interface presentation is very complex:
- An interface integrates many small function points at the same time, and some interfaces or small function points need to be displayed under special conditions. Some small functional interfaces are optional.
- An interface is divided into several blocks for presentation, each of which is relatively independent but for some reason integrated into the same page.
- An interface controlled by a certain state may display completely different interfaces and realize completely different functions under different states.
For these functions with complex logic, the logic in the controller can be very complex and large if not properly designed. How to solve these problems? The answer is decomposition. As for how to decompose this will be specific problem specific analysis, this is a very test of the architecture designer’s technical and business skills. We will not discuss how to split the service here, but the ability of the controller to support the separation. When the logic in a controller is too large and complex, it can be considered to split the function into multiple sub-controllers
After iOS5, the system provides the ability to support the child controller. The child controller and the parent controller have similar callback processing mechanism of various methods in the life cycle. The introduction of child controllers in addition to the ability to split the layout of the view can also split the processing logic. In this case we call the superview controller the container controller. The container controller is more about scheduling and controlling the whole. It may no longer be responsible for specific services, but the specific services are completed by the child controllers. In each of the three scenarios listed above we can split some logic into child controllers in the form of functional split. I’ll use code to illustrate the implementation of the second and third scenarios above, respectively:
- This is an implementation scenario of a complex interface consisting of multiple areas
//ContainerVC.m
#import "SubVC1.h"
#import "SubVC2.h"
#import "SubVC3.h"@interface ContainerVC() // This function is divided into 3 separate areas for display and processing. @property(nonatomic, strong) SubVC1 *vc1; @property(nonatomic, strong) SubVC2 *vc2; @property(nonatomic, strong) SubVC3 *vc3; @end @implementation ContainerVC - (void)viewDidLoad { [super viewDidLoad]; // Build of child view controllers, which you can handle either in init of container view controller or in viewDidLoad. / / it is first remove the whole interface in order to prevent possible interface view be re-initialized happen [self.'s vc1 removeFromParentViewController]; [self.vc2 removeFromParentViewController]; [self.vc3 removeFromParentViewController]; self.vc1 = [[SubVC1 alloc] init]; self.vc2 = [[SubVC2 alloc] init]; self.vc3 = [[SubVC3 alloc] init]; / / add child view controller [self addChildViewController: self.'s vc1]; [self addChildViewController:self.vc2]; [self addChildViewController:self.vc3]; // Add the views in the child view controller to different locations in the container view controller. Of course, you can also use Autolayout tolayout [self.view addSubview:self.vc1]; self.vc1.view.frame = CGRectMake(x, x, x, x); [self.view addSubview:self.vc2.view]; self.vc2.view.frame = CGRectMake(x, x, x, x); [self.view addSubview:self.vc3.view]; self.vc3.view.frame = CGRectMake(x, x, x, x); } @endCopy the code
- This is a scenario where a complex interface is driven by different state changes
//ContainerVC.m
#import "SubVC1.h"
#import "SubVC2.h"
#import "SubVC3.h"@interface ContainerVC() @property(nonatomic, assign) int status; @property(nonatomic, strong) UIViewController *currentVC; @end@implementation ContainerVC - (void)viewDidLoad {[super viewDidLoad]; self.status = 1; // Set the current state. } -(void)setStatus:(int)status
{
if (_status == status)
return;
[self.currentVC.view removeFromSuperview];
[self.currentVC removeFromParentViewController];
self.currentVC = nil;
Class cls = nil;
switch (_status) {
case 1:
cls = [SubVC1 class];
break;
case 2:
cls = [SubVC2 class];
break;
case 3:
cls = [SubVC3 class];
break;
default:
NSAssert(0, @"oops!");
break; } self.currentVC = [[cls alloc] init]; / / this can bring the inside of the container view state or other business model parameters to initialize the [self addChildViewController: self. CurrentVC]; [self.view addSubview:self.currentVC.view]; self.currentVC.view.frame = self.view.bounds; }Copy the code
Both of the above scenarios use apis for child view controllers. Let’s take a look at all the relevant apis for child view controllers in iOS:
@ interface UIViewController (UIContainerViewControllerProtectedMethods) / / get a parent view controller inside of all child view controller @ property (nonatomic,readonly) NSArray<__kindof UIViewController *> *childViewControllers; // addChildViewController - (void)addChildViewController:(UIViewController *)childController; / / to remove from the parent view controller - (void) removeFromParentViewController; / / if we want to add a child view controller and delete a child view controller executes and have the animation effects at the same time this method can be used - (void) transitionFromViewController: [UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion; / / if the container of the controller to control child view controller to render the callback to shouldAutomaticallyForwardAppearanceMethods overloaded container controller and returns NO. // Then call the following two methods of the child view controller when appropriate to implement the rendered custom control handling. // These two methods are calls to child view controllers and should be executed in pairs. - (void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated; - (void)endAppearanceTransition; // Override toreturn a child view controller or nil. If non-nil, that view controller's status bar appearance attributes will be used. If nil, self is used. Whenever the return values from these methods change, -setNeedsUpdatedStatusBarAttributes should be called. @property(nonatomic, readonly, nullable) UIViewController *childViewControllerForStatusBarStyle; @property(nonatomic, readonly, nullable) UIViewController *childViewControllerForStatusBarHidden; // Call to modify the trait collection for child view controllers. - (void)setOverrideTraitCollection:(nullable UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController; - (nullable UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController; // Override to return a child view controller or nil. If non-nil, that view controller's preferred user interface style will be used. If nil, self is used. Whenever the preferredUserInterfaceStyle for a view controller has changed setNeedsUserInterfaceAppearanceUpdate should be called.
@property (nonatomic, readonly, nullable) UIViewController *childViewControllerForUserInterfaceStyle; @end @interface UIViewController (UIContainerViewControllerCallbacks) / / container controller child view controller can override this method to control the view when added to the window and remove from the window a child view controller will automatically call viewWillAppear/viewDidAppear/viewWillDisappear/viewDidDisappear this several The default is YES. // If the container controller overloads this method and returns NO then the container controller can manually tell the child view controller to execute the corresponding render callback method. @property(nonatomic,readonly) BOOL shouldAutomaticallyForwardAppearanceMethods / / child view controller will be moved to the parent view controller and has moved to the parent view controller calls, Child view controller can override these two methods - (void) willMoveToParentViewController: (nullable UIViewController *) parent; - (void)didMoveToParentViewController:(nullable UIViewController *)parent; @endCopy the code
Derivation of controller
The design pattern used for the separation of control logic is the so-called composite design pattern, whose essence is to disperse the function into various sub-modules and then combine them to achieve a complete large function. Not all scenarios are suitable for solving problems through splitting and combining. Consider the following two business scenarios:
- Two functions with similar interfaces but different processing logic or different interfaces but similar processing logic in general because they are two different functions will be implemented by two different controllers, especially if the two functions belong to different modules. Although there are many similarities between the two functions, it is still possible to do this simply by copying code. This is not the best solution, however, because of the possibility of inconsistent updates through code replication. We can also solve this problem through composition, but the use of composition can increase the amount of code and transfer between shared parameters to some extent, so the best solution is to use class inheritance. For example, when two view controllers with the same interface have different processing logic, we simply derive a new class and override the processing logic methods of the base class.
//VC1.h
@interface VC1:UIViewController
@end
......................
//VC1.m
@interface VC1()
@property(nonatomic, weak) UIButton *button;
@end
@implementation VC1
-(void)viewDidLoad
{
[super viewDidLoad];
[self.button addTarget:self action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside]; // Call a method internally [self fn1]; } -(void)handleClick:(id)sender { //... VC1 event handling logic. } -(void)fn1 {// Logic of VC1. } @endCopy the code
The handleClick and fn1 methods in the base class are designed to handle the logic and events of VC1. Now we will construct a derived class of VC1, VC2, which has the same interface but completely different event handling logic and methods. We can override the corresponding methods of the base class to implement logic changes.
/ / VC2. H / / VC2 derived from's VC1 @ interface VC2:'s VC1 @ the end... @interface VC1() @property(nonatomic, weak) UIButton *button; @end @implementation VC2 -(void)handleClick:(id)sender { //... VC2 event handling logic. } -(void)fn1 {//VC2 logic. Since the base class self.button is declared here, the derived class has access to the self.button attribute. } @endCopy the code
Instead of building two different view controllers through code copy, we can use different view controllers for different scenarios. Of course, we can also have one view controller in two different scenarios. Using one controller requires you to do if and else judgments in your code according to different scenarios. Using two controllers can avoid these problems and make your controller code clearer and simpler.
- One of the two interfaces has some additional functionality in addition to implementing all the capabilities of the other, and the same is true for the new capability scenario. We simply add the corresponding additional interface and processing logic to the derived class. Consider a real-world scenario: in a typical e-commerce application, each item has an item detail page, which is typically entered from the item list. When a user does not log in, the commodity details he sees are just ordinary commodity details display page, and once he enters the commodity details page after logging in, there may be a part of the commodity details such as the purchase record information of the user for the commodity at the bottom. The purchase record is user-specific and optional, whereas the item details are user-independent. When we design the architecture, it is possible to design the goods module and the user module. The item details is the item module, which is independent of the user. It is not possible to put some interface and logic with user attributes in the item Details view controller. The solution is to create a derived class of an item detail view controller, and then add things to the derived class that have user attributes such as the user’s purchase history. This design idea can also reduce the coupling degree between each module.
/ / GoodsVC. H / / commodity details view controller @ interface GoodsVC: UIViewController @ the end... / / GoodsVC. M @ implementation GoodsVC / / the logic is goods related logic, it does not involve any user related things @ the end... // goodswrappervc. h @interface GoodsWrapperVC:GoodsVC -(id)initWithUser:(User*) User; @end ..................................... // goodswrappervc.m@interface GoodsWrapperVC() @property(weak) UITableView *userRecordTableView; @property(strong) User *user; @property(strong) NSArray<Record*> records; @end @implementation GoodsWrapperVC -(id)initWithUser:(User*)user { self = [self init];if(self ! = nil) { _user = user; }returnself; } -(void)viewDidLoad { [super viewDidLoad]; // Add the request logic to get the user purchase record. __weak GoodsWrapperVC *weakSelf = self; [self.user getRecords:^(NSArray<Record*> *records, NSError *error{ [weakSelf reloadRecordList:records]; }]; } -(void)reloadRecordList:(NSArray<Record*>) *records { In this way, when there is no user purchase record for the product details and the base class interface to keep the same.if (records.count > 0)
{
self.records = records;
if( _userRecordTableView == nil) { UITableView *userRecordTableView = [[UITableView alloc] initWithFrame:CGRectMake(x,x,x,x)]; userRecordTableView.delegate = self; userRecordTableView.dataSource = self; [self.view addSubview:userRecordTableView]; _userRecordTableView = userRecordTableView; } } [self.userRecordTableView reloadData]; } @end ....................................... -(void)handleShowGoodsDetail:(id)sender {GoodsVC * GoodsVC = nil;if(serviceSystem.user ! = nil && serviceSystem.user.isLogin) { goodsVC = [[GoodsWrapperVC alloc] initWithUser:serviceSystem.user]; }else
{
goodsVC =[ [GoodsVC alloc] init];
}
[self.navigationController pushViewController:goodsVC animated:YES];
}
Copy the code
The above event processing into the commodity details is generally carried out in the commodity list, then we will face the same problem, that is, the commodity list is actually irrelevant to the user, but the user object does appear in the code, so there is a coupling problem between the commodity module and the user module. How to solve this problem? The answer is routing, which means that when we handle interface jumps we don’t build the target view controller directly but instead use a mediator route to make interface jumps. There are already many open source libraries or implementations on the web, so I won’t go into details here.
View updates and interaction with the data model
Finally, there’s the vexing update method for UITableViewCell. UITableView is currently one of the most used controls in the App. UITableViewCell is an object that belongs to the view hierarchy. Typically the data presented in a UITableViewCell comes from the data model in the business model layer. All you need to do to update a UITableViewCell is feed back changes to the data model into the view, which involves coupling between the view and the model. We know that M and V are independent of each other in MVC, and they are associated by C, so updating the UITableViewCell above is done by the view controller. But in practice, it’s possible that the UITableViewCell has a lot of things to display, and the logic to display is complicated, and if all of that code is handled by the view controller, then the controller code is bloated. How to solve this problem is also what we should think about in this section. I’ll list six different solutions to the problem of view data updates:
- The view provides properties and this method is the default method in UITableViewCell, in UITableViewCell
ImageVew, textLabel, detailTextLabel
A few default view properties. Normally if we don’t customize the UITableViewCell then we can directly set the data model’s data to these properties in the UITableView’s delegate or dataSource callback. Similarly, if we want to customize the UITableViewCell we can also make the derived classes of the UITableViewCell expose the view properties to solve the problem. This scenario is generally used when the interface is not complex and the logic is simple.
//XXXTableViewCell.h
@interface XXXTableViewCell:UITableViewCell
@property(weak) UILabel *nameLabel;
@property(weak) UILabel *ageLabel;
@property(weak) UILabel *addressLabel;
@end
Copy the code
- View exposure methods In some application scenarios, in addition to updating the contents of the view properties in the UITableViewCell, the display effects such as font colors may also be updated. If this section of logic is too much, we can solve the problem by providing a method to update the view for the derived classes of UITableViewCell. By providing the form of a method, our UITableViewCell does not need to expose the view hierarchy and view properties inside of it to the outside world. The method parameters provided are just some data. All view updates and style Settings are done inside the method, thus reducing the amount of code in the view controller. So this is essentially moving the update logic from the view controller to the view.
/ / XXXTableViewCell. H @ interface XXXTableViewCell: UITableViewCell / / no longer exposure to view properties, But provide a method to update the view -(void)update:(NSString*)name age:(int)age address:(NSString*)address; @end ...................................... XXXTableViewCell.m @interface XXXTableViewCell() @property(weak) UILabel *nameLabel; @property(weak) UILabel *ageLabel; @property(weak) UILabel *addressLabel; @end @implementation XXXTableViewCell -(void)update:(NSString*)name age:(int)age address:(NSString*)address { // This updates the content of the parameter to the corresponding subview, and updates the display style of the view and so on. self.nameLabel.text = name; self.ageLabel.text = [NSString stringWithFormat:@"%d", age];
self.addressLabel.text = address;
}
@end
..........................................
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
XXXTableViewCell *cell = ( XXXTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"test" forIndexPath:indexPath]; // It reads the data in the data model and calls the update view to update the interface. XXXDataModel *data = .... [cell update:data.name age:data.age address:data.address];return cell;
}
Copy the code
Exposing the update method through the view can effectively reduce the code in the view controller, and can hide the implementation of the view update, but the disadvantage is that when the UI of the UITableViewCell has too many interface elements, the method’s parameters can be too many. This approach is therefore suitable for scenarios where there are not many interface elements.
- If the interface has a lot of elements, but we don’t want the view to be associated with the data model, we can modify the Update method in the UITableViewCell to accept only one parameter: a dictionary parameter
-(void)update:(NSDictionary*)params;
Copy the code
Passing data in the form of dictionaries can reduce the number of parameters in a method, and there are plenty of solutions for turning data models into dictionaries. Taking the dictionary as an argument adds data conversion steps, and the UPDATE method in the UITableViewCell must know what the dictionary contains, as well as external calls to it. To some extent, the introduction of dictionaries makes code less maintainable.
- Using interfaces through method parameters and dictionaries are two different ways of passing data. The disadvantage is that once the interface changes, you need to manually adjust the position and number of parameters. When there are a lot of interface elements to update, we can also use the interface form in the update method to solve the problem:
// a separate interface definition file // xxxxitf.h@protocol xxxxitf@property NSString *name; @property int age; @property NSString *address; @end .............................. Data model implementation interface // xxxDatamodel.h#import "XXXXItf.h"@interface XXXXDataModel:NSObject<XXXXItf> @property NSString *name; @property int age; @property NSString *address; @end .................................. The definition of XXXXTableViewCell#import "XXXXItf.h"@ interface XXXXTableViewCell: which UITableViewCell / / here into the parameter is an interface protocol. -(void)update:(id<XXXXItf>)data; @endCopy the code
It can be seen that the form of interface protocol can solve the problem of too many method parameters and dictionary as a parameter of difficult maintenance. The method defined through interface can also decouple the strong association between view layer and model layer. The disadvantage of using the interface approach is that you need to define an additional interface protocol.
- View holding model can solve the coupling between view and data model through the interface protocol. In fact, in practice, some of our UITableView Cells are dedicated to showing certain data model. In some sense, they are actually very strong coupling between them. So in this case we can make the UITableViewCell hold the data model and it’s not a bad solution!! Although MVC emphasizes separation of layers, it is possible to allow some coupling scenarios in some practical situations. When we use the view to hold the data model, instead of providing an update method, we can directly assign the data model to the view. In the view, we can override the set method of the data model properties to implement the interface update.
//XXXXTableViewCell.h
#import "XXXXDataModel.h"
@interface XXXXTableViewCell:UITableViewCell
@property XXXXDataModel *data;
@end
...........................
//XXXXTableViewCell.m
#import "XXXXTableViewCell.h"
@implementation XXXXTableViewCell
-(void)setXXXXDataModel:(XXXXDataModel*)data { _data = data; / /... Here update the content of the interface} @ the end... - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { XXXTableViewCell *cell = ( XXXTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"test" forIndexPath:indexPath]; cell.data = ... This is where the data model is assigned to the view.return cell;
}
Copy the code
6. Create intermediate binding classes
In all of the above solutions, either the code logic is handled in the view controller, or the code logic is ported to the view, and it is possible that the view will hold the data model. We can also take this updated logic out of the view and make it neither view nor view controller but provide a new data binding class to solve the problem. The interaction between the view and the data model is implemented through the data binding class, which is what the VM class in MVVM does a lot these days.
/ / XXXXTableViewCell. H @ interface XXXXTableViewCell: UITableViewCell / / exposed view of view attributes. @property UILabel *nameLabel; @property UILabel *addressLabel; @end ............................................... //XXXXDataModel.h @interface XXXXDataModel:NSObject @property NSString *name; @property NSString *address; @end ............................................. //XXXXViewModel.h @interface XXXXViewModel:NSObject -(id)initView:(XXXXTableViewCell*)cell withData:(XXXXDataModel*)data; @end ....................................... //XXXXViewModel.m @implementation XXXXViewModel -(id)initView:(XXXXTableViewCell*)cell withData:(XXXXDataModel*)data { self = [self init];if(self ! = nil) { cell.nameLabel.text = data.name; cel.addressLabel.text = data.address; }returnself; } @end ................................................... // a view controller - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { XXXTableViewCell *cell = ( XXXTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"test" forIndexPath:indexPath]; // Assume the data model is data XXXXDataModel *data =.... // Build a viewmodel binding class. XXXXViewModel *vm = [ [XXXXViewModel alloc] initView:cell withData:data];return cell;
}
Copy the code
In the above example, we just implemented a simple ViewModel class whose purpose is to update and bind data to the view. This makes the code in the view part, and in the view controller, much more concise and simple. The disadvantage is that the introduction of intermediate classes increases the code and maintenance costs.
That’s all I have to say about view controller building, which is another very long article, divided into two parts that you may not have the patience to read. But I expect these things will give you a new perspective on view controllers and MVC after you read them. Before coding, no matter how much work is done, we should have an idea and thinking in advance. How to reduce coupling, and how to make our programs more robust and easy to maintain, is the focus of our thinking. In the field of mobile development, iOS and Android provide developers with MVC framework system. For so many years, this framework system has not been changed, which proves that its life is still relatively tenacious and very suitable for the current mobile development. While open source frameworks are numerous and easy to introduce for a company, it is important to recognize that the introduction of unofficial third-party libraries must be as minimally replaceable and intrusive as possible in your entire system. And the lower level must be the least dependent on third parties. So substitutability and standardization should be a key consideration when designing the architecture of the entire application.
Welcome to visit my Github address and follow Ouyang Big Brother 2013