I used my spare time last week to sort out this specification. I will use this specification as the code specification of our iOS team, and I will update it from time to time according to the feedback of readers, the practice of the project and the in-depth research. I also hope that you can see a lot of corrections and criticism.

This specification is divided into three parts:

  1. Core Principles: Describes the core principles followed by this code specification.
  2. Generic specification: generic code specification that is not limited to iOS (using C and Swift).
  3. IOS specification: Code specification for iOS only (in Objective-C).

I. Core Principles

Rule 1: Code should be simple and understandable, with clear logic

Because software needs people to maintain it. That person probably won’t be you in the future. So programming for people first, and for computers second:

  • Don’t be too technical and reduce the readability of your program.
  • Clean code can leave bugs nowhere to hide. Write code that is obviously bug-free, not bug-free.

Principle two: Program for change, not for requirements.

Need is temporary, change is permanent. In this iteration, we should not only write programs with strong expansibility and easy modification for the current needs, but also be responsible for ourselves and the company.

Principle 3: First ensure the correctness of the program, prevent over engineering

Over-engineering: Over-complicating engineering by overthinking extension and reuse before properly usable code has been written. To quote from The Immensity: The Wisdom of Programming:

  1. Solve the immediate problem first, solve it, and then consider the expansion problem in the future.
  2. Write usable code first, iterate over it, and then consider whether you need to reuse it.
  3. Write usable, simple, and obviously bug-free code first, and then worry about testing.

Ii. General specifications

The operator


1. Interval between operator and variable

1.1 No Spaces between unary operators and variables:

! bValue ~iValue ++iCount *strSource &fSumCopy the code

1.2 There must be Spaces between binary operators and variables

fWidth = 5 + 5;
fLength = fWidth * 2;
fHeight = fWidth + fLength;
for(int i = 0; i < 10; i++)
Copy the code

2. Use parentheses to specify precedence when multiple different operators exist at the same time

Parentheses should be used properly when multiple different operators exist at the same time, and do not blindly rely on operator precedence. Because sometimes there’s no guarantee that the person reading your code will know the precedence of all the operators in the algorithm you’re writing.

Look at this: 2 << 2 + 1 * 3-4

The << is a shift operation, but intuitively it is easy to think of it as a high priority, so it is mistaken as :(2 << 2) + 1 * 3-4

But in fact, it has a lower priority than addition and subtraction, so it should be the same as: 2 << 2 + 1 * 3-4. So in future, when writing a more complex equation, try to add more parentheses to avoid giving others the wrong impression (or even yourself).

variable


1. A variable has one and only one function. Try not to use a variable for more than one purpose

2. Initialize variables before using them to prevent uninitialized variables from being referenced

3. Local variables should be as close to where they are used as possible

It is recommended to write:

func someFunction(a) {
 
  letindex = ... ;//Do something With index. .let count = ...;
  //Do something With count
  
}
Copy the code

Not recommended:

func someFunction(a) {
 
  letindex = ... ;let count = ...;
  //Do something With index. .//Do something With count
}
Copy the code

If statement


1. All branches must be listed (to name all cases), and each branch must give a clear result.

It is recommended to write:

var hintStr;
if (count < 3) {
  hintStr = "Good";
} else {
  hintStr = "";
}
Copy the code

Not recommended:

var hintStr;
if (count < 3) {
 hintStr = "Good";
}
Copy the code

2. Don’t use too many branches, and be good at using returns to return errors ahead of time

It is recommended to write:

- (void)someMethod { 
  if(! goodCondition) {return;
  }
  //Do something
}
Copy the code

Not recommended:

- (void)someMethod { 
  if (goodCondition) {
    //Do something}}Copy the code

A typical example I encountered in JSONModel is:

- (id)initWithDictionary:(NSDictionary*)dict error:(NSError)err{
   // Method 1. Argument nil
   if(! dict) {if (err) *err = [JSONModelError errorInputIsNil];
     return nil;
    }

    // Method 2. Arguments are not nil, but neither are dictionaries
    if(! [dict isKindOfClass:[NSDictionary class]]) {
        if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];
        return nil;
    }

    // Method 3. Initialize
    self = [self init];
    if (!self) {
        // Initialization failed
        if (err) *err = [JSONModelError errorModelIsInvalid];
        return nil;
    }

    Check if the set of attributes in the user-defined model is greater than the set of keys in the dictionary passed in (if so, return NO).
    if(! [self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {
        return nil;
    }

    // Method 5. Core method: mapping dictionary keys to model attributes
    if(! [self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {
        return nil;
    }

    We can override the [self validate:err] method and return NO, allowing the user to define the error and blocking the return of model
    if(! [self validate:err]) {
        return nil;
    }

    // Method 7. Finally passed! Return model successfully
    return self;
}
Copy the code

As you can see, in this case, we’re going to first identify the various wrong cases and then go back ahead of time, and put the most correct case back in the end.

3. Conditional expressions that are long need to be extracted and assigned to a BOOL

It is recommended to write:

let nameContainsSwift = sessionName.hasPrefix("Swift")
let isCurrentYear = sessionDateCompontents.year == 2014
let isSwiftSession = nameContainsSwift && isCurrentYear
if (isSwiftSession) { 
   // Do something
}
Copy the code

Not recommended:

if ( sessionName.hasPrefix("Swift") && (sessionDateCompontents.year == 2014)) {// Do something
}
Copy the code

4. Conditional statements should be judged by variables on the left and constants on the right

It is recommended to write:

if ( count == 6) {}Copy the code

or

if ( object == nil) {}Copy the code

or

if ( !object ) {
}
Copy the code

Not recommended:

if ( 6 == count) {
}
Copy the code

or

if ( nil == object ) {
}
Copy the code

5. The implementation code for each branch must be surrounded by curly braces

It is recommended to write:

if(! error) {return success;
}
Copy the code

Not recommended:

if(! error)return success;
Copy the code

or

if(! error)return success; 
Copy the code

6. Break lines when conditions are too long

It is recommended to write:

if (condition1() && 
    condition2() && 
    condition3() && 
    condition4()) {
  // Do somethingCopy the code

Not recommended:

if (condition1() && condition2() && condition3() && condition4()) {
  // Do something
}
Copy the code

For statement


1. Do not modify loop variables within the for loop to prevent the for loop from getting out of control.

for (int index = 0; index < 10; index++){
   ...
   logicToChange(index)
}
Copy the code

2. Avoid using continue and break.

Continue and break describe “when not to do what,” so in order to read the code in which they are located, we need to mentally invert them.

It’s best to keep these two things out of the way, because our code just needs to be “what when”, and with the right approach, they can be eliminated:

2.1 If “continue” appears, just reverse the “continue” condition

var filteredProducts = Array<String> ()for level in products {
    if level.hasPrefix("bad") {
        continue
    }
    filteredProducts.append(level)
}
Copy the code

We can see that some values are filtered out by determining if the string contains the prefix “bad”. We can avoid using continue by inverting it:

for level in products {
    if! level.hasPrefix("bad") {
      filteredProducts.append(level)
    }
}
Copy the code

#### 2.2 Eliminate break in while: reverse the break condition and merge it into the main loop

A block in a while is essentially “non-existent”, and since it does not exist, it can be excluded from the initial conditional statement.

Break in while:

while (condition1) {
  ...
  if (condition2) {
    break; }}Copy the code

Invert and merge into the main condition:

while(condition1 && ! condition2) { ... }Copy the code

#### 2.3 Eliminate break in methods that return: convert break to return immediately

Some friends like to return a value after a break in a method that returns a value. You could have just returned on the break line.

func hasBadProductIn(products: Array<String>) -> Bool
{

    var result = false    
    for level in products {
        if level.hasPrefix("bad") {
            result = true}}return result
}
Copy the code

Error condition returns:

func hasBadProductIn(products: Array<String>) -> Bool 
{
    for level in products {
        if level.hasPrefix("bad") {
            return true}}return false
}
Copy the code

This way you don’t have to declare a variable to hold the value that needs to be returned, so it looks very concise and readable.

A Switch statement


1. Each branch must be enclosed in braces

It is recommended to write:

switch (integer) {  
  case 1:  {
    / /...
   }
    break;  
  case 2: {  
    / /...
    break;  
  }  
  case 3: {
    / /...
    break; 
  }
  default: {/ /...
    break; }}Copy the code

2. When enumeration types are used, there must be no default branch. Except enumeration types, there must be default branches

RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;  
switch (menuType) {  
  case RWTLeftMenuTopItemMain: {
    / /...
    break; 
   }
  case RWTLeftMenuTopItemShows: {
    / /...
    break; 
  }
  case RWTLeftMenuTopItemSchedule: {
    / /...
    break; }}Copy the code

If the default branch is used when the Switch statement uses enumeration types, the compiler will not be able to check the new enumeration types in the future.

function


1. A function must be limited to 50 lines

In general, when reading a function, it is very difficult to read the code if you have to cross a long vertical distance. If you have to scroll your eyes or code back and forth to see a method, it will affect your consistency of thinking and speed of reading code. The best case scenario is to be able to see the entire code of the method at a glance without scrolling the eyeballs or code.

2. A function does one thing (single principle)

The responsibilities of each function should be clearly delineated (just like classes).

It is recommended to write:

dataConfiguration()
viewConfiguration()
Copy the code

Not recommended:

void dataConfiguration(a)
{... viewConfiguration() }Copy the code

3. For functions (methods) that return a value, each branch must have a return value

It is recommended to write:

int function(a)
{
    if(condition1){
        return count1
    }else if(condition2){
        return count2
    }else{
       return defaultCount
    } 
}
Copy the code

Not recommended:

int function(a)
{
    if(condition1){
        return count1
    }else if(condition2){
        return count2
    }
}
Copy the code

4. Check whether the input parameters are correct and valid. If any parameter is incorrect, the system returns immediately

It is recommended to write:

void function(param1,param2)
{
      if(param1 is unavailable){
           return;
      }
    
      if(param2 is unavailable){
           return;
      }

     //Do some right thing
}

Copy the code

5. If there is the same function inside different functions, the same function should be extracted as a separate function

Original call:

voidlogic() { a(); (b);if (logic1 condition) {
    c();
  } else{ d(); }}Copy the code

I’m going to extract the A and B functions as separate functions

void basicConfig() 
{
  a();
  b();
}
  
void logic1() 
{
  basicConfig();
  c();
}

void logic2() 
{
  basicConfig();
  d();
}
Copy the code

6. Extract the complex logic inside the function as a separate function

The code in a function that is not clear (more logical judgment, more lines) can often be extracted to form a new function and then called in the original place so that you can use meaningful function names instead of comments to make the program more readable.

Here’s an example of sending an email:

openEmailSite();
login();

writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);

send();
Copy the code

The middle parts are slightly longer and we can extract them:

void writeEmail(title, content,receiver,attachment)
{
  writeTitle(title);
  writeContent(content);
  writeReceiver(receiver);
  addAttachment(attachment); 
}
Copy the code

Then look at the original code again:

openEmailSite();
login();
writeEmail(title, content,receiver,attachment)
send();
Copy the code

8. Avoid using global variables and class members to pass information. Use local variables and parameters instead.

Within a class, it is common to pass some variable. If the variable to be passed is a global variable or attribute, some friends do not like it as a parameter, but directly access the method inside:

 class A {
   var x;

   func updateX(a) 
   {
      ...
      x = ...;
   }

   func printX(a) 
   {
     updateX();
     print(x); }}Copy the code

As you can see, there is no value passing between the updateX and print methods in printX. At first glance, we may not know where x comes from, which makes the program less readable.

If you use local variables instead of class members to pass information, the two functions don’t depend on a single class, and are easier to understand and less error-prone:

func updateX() -> String { x = ... ;return x;
 }

 func printX() 
 {
   String x = updateX();
   print(x);
 }
Copy the code

annotation


Good code is largely self-describing, and we can use the code itself to express what it is doing without the help of comments.

This is not to say that you must not write comments. There are three situations for writing comments: 1. Public interface (comments tell the person reading the code what the current class can do). 2. Code that involves deep expertise (comments that reflect implementation principles and ideas). 3. Ambiguous code (but strictly speaking, ambiguous code is not allowed).

In addition to these three cases, if someone has to rely on comments to understand your code, think about what’s wrong with your code.

Finally, the content of a comment should be more about “why” than “what”.

Code Review


Line breaks, comments, method length, code duplication, etc. These are problems that are checked out by machines, not by humans.

In addition to reviewing the extent to which requirements are implemented and whether bugs are nowhere to hide, you should also focus on the design of the code. Such as the degree of coupling between classes, the extensibility of the design, reusability, whether certain methods can be pulled out as interfaces, and so on.

Iii. IOS specifications

variable


1. Variable names must be humped

Class, protocol using a large hump:

HomePageViewController.h
<HeaderViewDelegate>
Copy the code

Local variables such as objects use small humps:

NSString *personName = @ "";
NSUInteger totalCount = 0;
Copy the code

2. The name of a variable must contain both function and type

UIButton *addBtn // Add button
UILabel *nameLbl // Name tag
NSString *addressStr// Address string
Copy the code

3. Add suffixes to common class instance variable declarations

type The suffix
UIViewController VC
UIView View
UILabel Lbl
UIButton Btn
UIImage Img
UIImageView ImagView
NSArray Array
NSMutableArray Marray
NSDictionary Dict
NSMutableDictionary Mdict
NSString Str
NSMutableString Mstr
NSSet Set
NSMutableSet Mset

constant


1. Constants are prefixed with the name of the related class

It is recommended to write:

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
Copy the code

Not recommended:

static const NSTimeInterval fadeOutTime = 0.4;
Copy the code

2. Use type constants instead of #define preprocessing

Let’s start by comparing the two declared constants:

  • Preprocessing commands: simple text replacements that do not include type information and can be modified at will.
  • Type constants: Contain type information and can be scoped and cannot be modified.

Although preprocessing can achieve the purpose of replacing text, it still has its limitations:

  • No type information is available.
  • It can be modified at will.

3. Make a constant public:

If we need to send a notification, we need to get the notification’s “channel” string (the name of the notification) in a different place, which obviously cannot be easily changed and can be retrieved in a different place. At this point, you need to define a visible string constant.

It is recommended to write:

/ / header files
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
Copy the code
// Implementation file
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
Copy the code

Not recommended:

#define CompanyName @"Apple Inc." 
#define magicNumber 42
Copy the code

The macro


1. Use uppercase letters for macro and constant names and separate words with the underscore ‘_’.

#define URL_GAIN_QUOTE_LIST @"/v1/quote/list"
#define URL_UPDATE_QUOTE_LIST @"/v1/quote/update"
#define URL_LOGIN  @"/ v1 / user/login"Copy the code

2. If the macro definition contains expressions or variables, the expressions and variables must be enclosed in parentheses.

#define MY_MIN(A, B)  ((A)>(B)?(B):(A))
Copy the code

CGRect function


In iOS, there are some functions to get the parts of CGRect. They are very readable and short.

It is recommended to write:

CGRect frame = self.view.frame; 
CGFloat x = CGRectGetMinX(frame); 
CGFloat y = CGRectGetMinY(frame); 
CGFloat width = CGRectGetWidth(frame); 
CGFloat height = CGRectGetHeight(frame); 
CGRect frame = CGRectMake(0.0.0.0, width, height);
Copy the code

Rather than

CGRect frame = self.view.frame;  
CGFloat x = frame.origin.x;  
CGFloat y = frame.origin.y;  
CGFloat width = frame.size.width;  
CGFloat height = frame.size.height;  
CGRect frame = (CGRect){ .origin = CGPointZero, .size = frame.size };
Copy the code

paradigm


It is recommended to use generics when defining NSArray and NSDictionary to ensure program security:

NSArray<NSString *> *testArr = [NSArray arrayWithObjects:@"Hello".@"world".nil];
NSDictionary<NSString *, NSNumber *> *dic = @{@"key": @ (1), @"age": @ (10)};
Copy the code

Block


Create a typedef for commonly used Block types

If we need to repeatedly create a block (same argument, return value) variable, we can use a typedef to define a block of its own new type

Such as:

int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value)
{
     // Implementation
     return someInt;
}

Copy the code

This Block takes a bool and an int and returns an int. We can define a type for it:

typedef int(^EOCSomeBlock)(BOOL flag, int value);

When redefined, this can be done with a simple assignment:

EOCSomeBlock block = ^(BOOL flag, int value){
     // Implementation
};

Copy the code

Define blocks as arguments:

- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;

Copy the code

The Block here has an NSData parameter, an NSError parameter that has no return value

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error); - (void) startWithCompletionHandler (EOCCompletionHandler) completion;"Copy the code

The advantage of defining a Block signature through a typedef is that if you want to add parameters to a Block, you can only modify the line of code that defines the signature.

Literal grammar


Use literal values to create immutable objects such as NSString, NSDictionary, NSArray, and NSNumber:

It is recommended to write:

NSArray *names = @[@"Brian".@"Matt".@"Chris".@"Alex".@"Steve".@"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate".@"iPad" : @"Kamal".@"Mobile Web" : @"Bill"}; 
NSNumber *shouldUseLiterals = @YES;NSNumber *buildingZIPCode = @10018Copy the code

Not recommended:

NSArray *names = [NSArray arrayWithObjects:@"Brian".@"Matt".@"Chris".@"Alex".@"Steve".@"Paul".nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate".@"iPhone".@"Kamal".@"iPad".@"Bill" ];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018]; 
Copy the code

attribute


1. Attributes are named with small humps

It is recommended to write:

@property (nonatomic.readwrite.strong) UIButton *confirmButton;
Copy the code

2. Attribute keywords are recommended in order of atomicity, read/write, and memory management

It is recommended to write:

@property (nonatomic.readwrite.copy) NSString *name;
@property (nonatomic.readonly.copy) NSString *gender;
@property (nonatomic.readwrite.strong) UIView *headerView;
Copy the code

3. The Block attribute should use the copy keyword

It is recommended to write:

typedef void (^ErrorCodeBlock) (id errorCode,NSString *message);
@property (nonatomic.readwrite.copy) ErrorCodeBlock errorBlock;// Copy blocks to the heap
Copy the code

4. Getters for BOOL attributes should be prefixed with is

It is recommended to write:

@property (assign.getter=isEditable) BOOL editable;
Copy the code

5. Use the getter method for lazy loading

Instantiating an object is expensive. If the instantiation of a property in the object requires a lot of configuration and calculation, it needs to be lazy and instantiated just before it is used:

- (NSDateFormatter *)dateFormatter 
{
    if(! _dateFormatter) { _dateFormatter = [[NSDateFormatter alloc] init];
           NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
           [_dateFormatter setLocale:enUSPOSIXLocale];
           [_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];
    } 
    return _dateFormatter;
}
Copy the code

But there is controversy: getter methods can have side effects, such as hard-to-troubleshoot errors if it modifies global variables.

6. It is recommended to use dot syntax to access properties except for the init and dealloc methods

Advantages of using dot syntax:

Setter:

  1. Setters adhere to memory management semantics (strong, copy, weak).
  2. It helps to debug bugs by setting breakpoints internally.
  3. You can filter some external incoming values.
  4. Capture KVO notifications.

Getter:

  1. Subclassing is allowed.
  2. It helps to debug bugs by setting breakpoints internally.
  3. Implement lazy initialization.

Note:

  1. Lazy-loaded properties that must be read by point syntax. Because lazy loading initializes an instance variable by overwriting the getter method, if the instance variable is not read through a property, it will never be initialized.
  2. The consequence of using dot syntax in the init and dealloc methods is that there can be many other operations in the setter and getter because the setter and getter are not bypassing. And if its subclass overrides its setter and getter methods, it may cause that subclass to call other methods.

7. Don’t abuse dot syntax. Distinguish between method calls and property access

It is recommended to write:

view.backgroundColor = [UIColor orangeColor]; 
[UIApplication sharedApplication].delegate; 
Copy the code

Not recommended:

[view setBackgroundColor:[UIColor orangeColor]]; 
UIApplication.sharedApplication.delegate; 
Copy the code

8. Use immutable objects whenever possible

It is recommended that the properties published to the public be set as read-only as possible, and inside the implementation file as read/write. The specific approach is:

  • In the header file, set the object property toreadonly.
  • Set to in the implementation filereadwrite.

This way, the data can only be read externally, not modified, making the data held by instances of the class more secure. Also, for objects of the collection class, you should carefully consider whether you can make them mutable.

If you can only set it as a read-only property in the public part, store a variable in the private part. So when we get this property externally, we only get an immutable version of the internal variant, for example:

In the public API:

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends // public immutable set - (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName; - (void)addFriend:(EOCPerson*)person; - (void)removeFriend:(EOCPerson*)person; @endCopy the code

Here, we set the Friends property to an immutable set. Public interfaces are then provided to add and remove elements from the set.

In the implementation file:

@interface EOCPerson () @property (nonatomic, copy, readwrite) NSString *firstName; @property (nonatomic, copy, readwrite) NSString *lastName; @end @implementation EOCPerson { NSMutableSet *_internalFriends; } - (NSSet*)friends {return[_internalFriends copy]; // The get method always returns a variableset} - (void)addFriend (EOCPerson*)person {[_internalFriends addObject:person]; // Add a collection element externallydosomething when add element } - (void)removeFriend:(EOCPerson*)person { [_internalFriends removeObject:person]; // Remove an element externally //do something when remove element
}

- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName 
{

     if ((self = [super init])) {
        _firstName = firstName;
        _lastName = lastName;
        _internalFriends = [NSMutableSet new];
    }
 return self;
}

Copy the code

We can see that in the implementation file, a mutable set is kept to record external additions and deletions.

The most important code here is:

- (NSSet*)friends 
{
   return [_internalFriends copy];
}
Copy the code

This is how to get the Friends property: it copies the currently saved mutable set to an immutable set and returns it. Therefore, externally read sets will be immutable versions.

methods


1. Do not use and in the method name, and the signature must be highly consistent with the corresponding parameter name

It is recommended to write:

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
Copy the code

Not recommended:

- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
Copy the code
- (instancetype)initWith:(int)width and:(int)height;
Copy the code

2. When the method is implemented, if the parameters are too long, make each parameter occupy a line aligned with colons.

- (void)doSomethingWith:(NSString *)theFoo
                   rect:(CGRect)theRect
               interval:(CGFloat)theInterval
{
   //Implementation
}
Copy the code

3. Private methods should be declared in the implementation file.

@interface ViewController(a)
- (void)basicConfiguration;
@end

@implementation ViewController
- (void)basicConfiguration
{
   //Do some basic configuration
}
@end
Copy the code

4. The method name is a combination of words beginning with a lowercase letter

- (NSString *)descriptionWithLocale:(id)locale;
Copy the code

5. Method name prefix

  • The name of the method that refreshes the view should berefreshLed.
  • The name of the method that updates the data should beupdateLed.

It is recommended to write:

- (void)refreshHeaderViewWithCount:(NSUInteger)count;
- (void)updateDataSourceWithViewModel:(ViewModel*)viewModel;
Copy the code

Protocol oriented programming


If certain functions (methods) are reusable, we need to extract them into an abstract interface file (in iOS, an abstract interface is a protocol) so that different types of objects follow the protocol and have the same functionality.

Because a protocol is independent of an object, we can use a protocol to decouple two objects from each other. How do you understand that? Let’s take a look at this example:

Now there is a requirement to pull the feed in a UITableViewController and display it.

Solution a:

ZOCFeedParser defines a class that pulls feeds. This class has some proxy methods that implement feed-related functions:

@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info; 
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedItem:(ZOCFeedItemDTO *)item; 
- (void)feedParserDidFinish:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didFailWithError:(NSError *)error;@end 

@interface ZOCFeedParser : NSObject
@property (nonatomic.weak) id <ZOCFeedParserDelegate> delegate; 
@property (nonatomic.strong) NSURL *url; 

- (id)initWithURL:(NSURL *)url; 
- (BOOL)start; 
- (void)stop; 
@end 
Copy the code

Then pass ZOCFeedParser into ZOCTableViewController and follow its proxy method to implement the pull function of feed.

@interface ZOCTableViewController : UITableViewController<ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser; 
@end 
Copy the code

Specific application:

NSURL *feedURL = [NSURL URLWithString:@"http://bbc.co.uk/feed.rss"]; 
ZOCFeedParser *feedParser = [[ZOCFeedParser alloc] initWithURL:feedURL]; 
ZOCTableViewController *tableViewController = [[ZOCTableViewController alloc] initWithFeedParser:feedParser]; 
feedParser.delegate = tableViewController; 
Copy the code

OK, now we have the requirement: a ZOCFeedParser object is stored in the ZOCTableViewController to handle the feed pull.

But there is a serious coupling problem: The ZOCTableViewController can only handle feed pulling through the ZOCFeedParser object. So let’s revisit this requirement: All we really need is the ZOCTableViewController to pull the feed, and the ZOCTableViewController doesn’t care which object pulls the feed.

In other words, we need to provide ZOCTableViewController with a more generic object that can pull feeds, not just a specific object (ZOCFeedParser). Therefore, the design needs to be modified again:

Scheme 2:

The first step is to define an abstract protocol for pulling feeds in an interface file called zocFeedParserProtocol. h:

@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info; 
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedItem:(ZOCFeedItemDTO *)item; 
- (void)feedParserDidFinish:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didFailWithError:(NSError *)error;@end 

@protocol ZOCFeedParserProtocol <NSObject>
@property (nonatomic.weak) id <ZOCFeedParserDelegate> delegate; 
@property (nonatomic.strong) NSURL *url;

- (BOOL)start;
- (void)stop;

@end
Copy the code

The original ZOCFeedParser pulls feeds only by following this protocol:

@interface ZOCFeedParser : NSObject <ZOCFeedParserProtocol
- (id)initWithURL:(NSURL *)url;// Just pass in the URL and leave everything else to ZOCFeedParserProtocol@end
Copy the code

Also, ZOCTableViewController doesn’t depend directly on the ZOCFeedParser object, we just pass it an object that follows the

.

@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(id<ZOCFeedParserProtocol>)feedParser;
@end
Copy the code

Thus, there is no direct relationship between ZOCTableViewController and ZOCFeedParser. In the future, if we want to:

  • Add new functionality to the feed puller: just change itZOCFeedParserProtocol.hFile.
  • Replace a Feed puller instance: Create a new type to followZOCFeedParserProtocol.hCan.

Delegate design in iOS


1. Distinguish between proxy and data source

Delegation patterns in iOS development include a delegate and a datasource. Although both belong to the delegation pattern, they are different. The difference is that information flows in different directions:

  • Delegate: The delegate needs to notify the agent when an event occurs. (Information flow from principal to agent)
  • Datasource: the delegate needs to pull data from the datasource. (Information flows from the data source to the consignor)

Yet even Apple has failed to set a good example of how to distinguish them thoroughly. UITableView, for example, has a method in its delegate method:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
Copy the code

This method correctly represents what the proxy does: the proxy (TableView) tells the proxy (controller) that “one of my cells was clicked.” However, UITableViewDelegate also has this method in its list of methods:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
Copy the code

The function of this method is for the controller to tell the tabievlew the row height, that is, its information flow is from the controller (data source) to the delegate (TableView). Specifically, it should be a data source method, not a proxy method.

In UITableViewDataSource, there are standard data source methods:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
Copy the code

And what this method does is it tells the TableView to pull one section of data from the controller.

Therefore, when we design a view control proxy and data source, we must distinguish the difference between the two, and reasonably divide which methods belong to the proxy method, which methods belong to the data source method.

2. The first argument to the proxy method must be the proxy

Proxy methods must take the delegate as the first argument (see UITableViewDelegate). The purpose is to distinguish between instances of different delegates. Because the same controller can be a proxy for multiple TableViews. In order to distinguish which tableView cell is clicked, you need to be in ‘ ‘

  • (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath.

When sending a message to a broker, you need to determine whether it implements this method

Finally, when the delegate sends a message to the broker, we need to determine whether the delegate implements the broker method:

if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) { 
 [self.delegate signUpViewControllerDidPressSignUpButton:self]; 
} 
Copy the code

3. Follow too many agents when line feed alignment is displayed

@interface ShopViewController()"UIGestureRecognizerDelegate.
                                  HXSClickEventDelegate,
                                  UITableViewDelegate.UITableViewDataSource>
Copy the code

4. The proxy approach needs to be clear about what is required and what is not

Proxy methods are mandatory by default. However, when designing a set of proxy methods, some methods may not be mandatory (because of default configurations), and these methods need to be decorated with the @optional keyword:

@protocol ZOCServiceDelegate <NSObject> @optional- (void)generalService: (ZOCGeneralService *)service didRetrieveEntries: (NSArray *)entries
@end 
Copy the code

class


1. Class names should be prefixed with three uppercase letters; When creating a subclass, place the section that represents the characteristics of the subclass between the prefix and the name of the parent class

It is recommended to write:


/ / parent class
ZOCSalesListViewController

/ / subclass
ZOCDaySalesListViewController
ZOCMonthSalesListViewController
Copy the code

2. initializer && dealloc

Recommendation:

  • Place the dealloc method at the top of the implementation file
  • Place the init method after the dealloc method. If there are multiple initializers, you should place the specified initializer first and the other initializers second.

2.1 The dealloc method should access instance variables directly, not with dot syntax

2.2 Writing method of init:

– Init method return type must be instanceType, not ID.

  • You must implement [super init] first.
- (instancetype)init 
{ 
    self = [super init]; // call the designated initializer 
    if (self) { 
        // Custom initializationreturn self; 
} 
Copy the code

2.3 Specifying the initialization method

Designated Initializer is an initializer that provides all (most) parameters. Secondary initializer is an initializer that has one or some parameters.

Note 1: Indirect initializer methods must call specified initializer methods.

@implementation ZOCEvent 

// Specify the initialization method
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date 
location:(CLLocation *)location
{ 
    self = [super init]; 
      if (self) {
         _title = title; 
         _date = date; 
         _location = location; 
      } 
    return self; 
} 

// Indirect initialization method
-  (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date
{ 
    return [self initWithTitle:title date:date location:nil];
}

// Indirect initialization method
-  (instancetype)initWithTitle:(NSString *)title 
{ 
    return [self initWithTitle:title date:[NSDate date] location:nil];
}
 @end 
Copy the code

Note 2: If the immediate parent class has a specified initialization method, its specified initialization method must be called

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 
    if (self) {}return self; 
}
Copy the code

Note 3: If you want to customize a new omnipotent initialization method in the current class, you need to do the following steps

  1. Define a new designated initialization method and ensure that the initialization method of the immediate parent class is called.
  2. Overrides the initialization method of the immediate parent class and internally calls the newly defined specified initialization method.
  3. Document the new specified initialization method.

Look at a standard example:

@implementation ZOCNewsViewController

// New specified initialization method
- (id)initWithNews:(ZOCNews *)news 
{
    self = [super initWithNibName:nil bundle:nil]; 
    if (self) {
        _news = news;
    }
    return self;
} 

// Override the initialization method of the parent class
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
    return [self initWithNews:nil]; 
}
@end 
Copy the code

Here, the reason for overloading the parent class’s initializer and calling the newly defined designated initializer internally is that you can’t be sure that the caller is calling the new designated initializer you defined and not the one you inherited from the parent class.

Suppose you don’t override the parent’s designated initializer, but the caller actually calls the parent’s initializer. The caller may never be able to call the newly specified initialization method that you define.

If you succeed in defining a new designated initializer and ensure that the caller can call it, it is best to document which one is the new initializer you defined. Or you can flag it with the compiler directive __attribute__((objc_designated_initializer)).

3. All methods that return class objects and instance objects should use InstanceType

Using the instanceType keyword as a return value allows the compiler to do type checking, as well as subclass checking. This ensures that the return type is correct (it must be the current class object or instance object).

It is recommended to write:

@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name; 
@end 
Copy the code

Not recommended:

@interface ZOCPerson
+ (id)personWithName:(NSString *)name; 
@end 
Copy the code

### 4. Minimize references to other headers in class header files

Sometimes class A needs instance variables of class B as attributes of its public API. At this point, we should not introduce headers of class B, but rather use the class keyword forward declaration and reference B’s header in A’s implementation.

// EOCPerson.h
#import <Foundation/Foundation.h>

@class EOCEmployer;

@interface EOCPerson : NSObject

@property (nonatomic.copy) NSString *firstName;
@property (nonatomic.copy) NSString *lastName;
@property (nonatomic.strong) EOCEmployer *employer;// use EOCEmployer as an attribute

@end

// EOCPerson.m
#import "EOCEmployer.h"

Copy the code

What are the advantages of this:

  • If you don’t include B’s header in A’s header file, you don’t include all of B’s content at the same time, which reduces compilation time.
  • Circular references can be avoided: if two classes import each other’s header files in their own header files, one of them will not compile properly.

Occasionally, however, we must introduce headers from other classes:

There are two main cases:

  1. If the class inherits from a class, the parent class’s header file should be imported.
  2. If the class complies with a protocol, the header file for that protocol should be imported. And it’s best to keep the protocol in a separate header file.

5. Class layout


#pragma mark - Life Cycle Methods
- (instancetype)init
- (void)dealloc

- (void)viewWillAppear:(BOOL)animated
- (void)viewDidAppear:(BOOL)animated
- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated

#pragma mark - Override Methods

#pragma mark - Intial Methods

#pragma mark - Network Methods

#pragma mark - Target Methods

#pragma mark - Public Methods

#pragma mark - Private Methods

#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate

#pragma mark - Lazy Loads

#pragma mark - NSCopying

#pragma mark - NSObject Methods
Copy the code

classification


1. Add prefix and underscore (_) by category

It is recommended to write:

@interface NSDate (ZOCTimeExtensions)
 - (NSString *)zoc_timeAgoShort;
@end 
Copy the code

Not recommended:

@interface NSDate (ZOCTimeExtensions
- (NSString *)timeAgoShort;
@end 
Copy the code

2. Divide the implementation code of a class into manageable categories

A class may have many common methods, and these methods can often be grouped with some peculiar logic. We can use the classification mechanism of Objecctive-C to logically divide these methods into several partitions.

An 🌰 :

Let’s look at a class that doesn’t use unclassified:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;

/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;

/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;

@end

Copy the code

After classification:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString*)firstName

andLastName:(NSString*)lastName;

@end

@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end

@interface EOCPerson (Work)

- (void)performDaysWork;
- (void)takeVacationFromWork;

@end

@interface EOCPerson (Play)

- (void)goToTheCinema;
- (void)goToSportsGame;

@end

Copy the code

The realization code of FriendShip classification can be written as follows:


// EOCPerson+Friendship.h
#import "EOCPerson.h"

@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end

// EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"

@implementation EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person 
{
   /* ... */
}

- (void)removeFriend:(EOCPerson*)person 
{
   /* ... */
}

- (BOOL)isFriendsWith:(EOCPerson*)person 
{
   /* ... */
}

@end

Copy the code

Note: When creating a new classification file, be sure to introduce the classified class file.

The classification mechanism allows class code to be divided into manageable functional areas and easy to debug. Because the method name of the classification contains the name of the classification, you can immediately see which category the method belongs to.

To take advantage of this, we can create a class called Private and put all the Private methods in it. That way, we can justify calling private by where it occurs, and it’s a way to write self-documenting code.

The singleton


1. Singletons cannot be used as container objects

A singleton should not expose any properties, that is, it should not be used as a container for storing objects externally. It should be a tool that handles specific tasks, such as GPS and acceleration sensors in iOS. We can only get certain data from them.

2. Use dispatch_once to generate singletons

It is recommended to write:

+ (instancetype)sharedInstance 
{ 
 static id sharedInstance = nil; 
 static dispatch_once_t onceToken = 0;
       dispatch_once(&onceToken, ^{ 
  sharedInstance = [[self alloc] init];
  }); 
 return sharedInstance; 
} 
Copy the code

Not recommended:

+ (instancetype)sharedInstance 
{ 
 static id sharedInstance; 
 @synchronized(self) { 
 if (sharedInstance == nil) { sharedInstance = [[MyClass alloc] init]; }}return sharedInstance; 
} 
Copy the code

The judgment of equality


A reasonable way to determine if two Person classes are equal:

-  (BOOL)isEqual:(id)object 
{

     if (self == object) {  
            return YES; // Determine the memory address
    } 
  
    if(! [object isKindOfClass:[ZOCPersonclass]]) { 
     return NO; // Whether the class is current or derived
     } 

     return [self isEqualToPerson:(ZOCPerson *)object]; 
 
}

// A custom method for determining equality
-  (BOOL)isEqualToPerson:(Person *)person 
{ 
        if(! person) {return NO;
        } 
        BOOL namesMatch = (!self.name && ! person.name) || [self.name isEqualToString:person.name]; 
        BOOL birthdaysMatch = (!self.birthday && ! person.birthday) || [self.birthday isEqualToDate:person.birthday]; 
        return haveEqualNames && haveEqualBirthdays; 
} 
Copy the code

Methods the document


A function (method) must have a string document to explain it unless it:

  • Non-public, private functions.
  • Is short.
  • It’s obvious.

The rest, including exposed interfaces, important methods, classes, and protocols, should be accompanied by documentation (comments) :

  • Begin with /
  • The second line is the concluding statement
  • The third line is always blank
  • Write the rest of the comment in line with the beginning of the second line.

Suggest writing:

/This comment serves to demonstrate the format of a doc string.

Note that the summary line is always at most one line long, and after the opening block comment,
and each line of text is preceded by a single space.
*/
Copy the code

Look at a comment specifying the initialization method:

/ 
  *  Designated initializer. *
  *  @param store The store for CRUD operations.
  *  @param searchService The search service used to query the store. 
  *  @return A ZOCCRUDOperationsStore object.
  */ 
- (instancetype)initWithOperationsStore:(id<ZOCGenericStoreProtocol>)store searchService:(id<ZOCGenericSearchServiceProtocol>)searchService;
Copy the code

Use more queues and less synchronous locks to avoid resource snatching


When multiple threads execute the same piece of code, it is likely that data will be out of sync. It is recommended to use GCD to lock the code around this problem.

#### Solution 1: Use serial synchronization queue to arrange read and write operations in the same queue:

_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL); // Read string - (NSString*)someString {__block NSString*localSomeString;
         dispatch_sync(_syncQueue, ^{
            localSomeString = _someString;
        });
         return localSomeString; } // Set string - (void)setSomeString:(NSString*)someString 
{

     dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}

Copy the code

In this way, read and write operations are performed in serial queues, which are less prone to errors.

However, there is another way to make performance even better:

#### Scheme 2: Put write operations into the fence and let them execute separately; The read operation is executed concurrently.

_syncQueue = dispatch_queue_create("com.custom.queue", DISPATCH_QUEUE_CONCURRENT); // Read string - (NSString*)someString {__block NSString*localSomeString;
     dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
     return localSomeString;
}
Copy the code
// Set string - (void)setSomeString:(NSString*)someString 
{

     dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });

}

Copy the code

Obviously, the correctness of the data depends on the write operation, so as long as the thread is safe at the time of the write, the data can be guaranteed to be synchronized even if the read operation is concurrent.

The dispatch_barrier_async method here makes the operation “ordered” in the synchronous queue, ensuring that the write operation is in the serial queue.

Implement the description method to print information about user-defined objects


When we print an instance object of our own class, the console output usually looks like this:

object = <EOCPerson: 0x7fd9a1600600>
Copy the code

This contains only the class name and memory address, and its information is obviously not specific, far from the requirements of debugging.

But! If we override the Description method in our own class, we can print the information we want when we print an instance of the class.

Such as:


- (NSString*)description 
{
     return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}

Copy the code

Here, the memory address is displayed, along with all the attributes of the class.

Furthermore, it is more readable if we print these attribute values in a dictionary:

- (NSString*)description 
{

     return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,
   
    @{    @"title":_title,
       @"latitude":@(_latitude),
      @"longitude":@(_longitude)}
    ];
}
Copy the code

Output result:

location = <EOCLocation: 0x7f98f2e01d20, {

    latitude = "51.506"; longitude = 0; title = London; } >Copy the code

As we can see, rewriting the description method allows us to know more about the object, facilitating later debugging and saving development time.

NSArray& NSMutableArray


1. Make a non-null judgment before addObject.

2. Determine whether the subscript is out of bounds.

3. Use firtstObject and lastObject when fetching the first or last element

NSCache


1. Use NSCache instead of NSDictionary to build the cache

If we use caching properly, the response time of the application will improve. Only data that is “cumbersome to recalculate is worth caching,” such as data that needs to be fetched from the network or read from disk.

Many people are used to using NSDictionary or NSMutableDictionary when building caches, but the authors recommend using NSCache, which as a class for managing caches has many advantages over dictionaries because it is designed for managing caches.

2. The advantages of NSCache over NSDictionary:

  • When system resources are about to be used up, NSCache automatically deletes buffers. It also removes the “most unused” objects first.
  • NSCache does not copy keys, but preserves them. Not all keys comply with the copy protocol (dictionary keys must support the copy protocol, so there are limitations).
  • NSCache is thread-safe: Multiple threads can access NSCache at the same time without writing locking code.

NSNotification


1. Name of the notice

It is recommended that the name of the notification be kept as a constant in a special class:

// Const.h
extern NSString * const ZOCFooDidBecomeBarNotification

// Const.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";
Copy the code

2. Notification removal

Notifications must be removed before the object is destroyed.

other


1. The physical path of the Xcode project file must be consistent with the logical path.

2. Ignore compile warnings that do not use variables

For some temporary variables that are not used for the time being and may be used in the future, we can use the following method to eliminate the warning:

- (NSInteger)giveMeFive 
{ 
 NSString *foo; 
 #pragma unused (foo)
 return 5; 
} 
Copy the code

3. Manually indicate warnings and errors

Manually clear an error:

- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor 
{ 
 #error Whoa, buddy, you need to check for zero here!
 return (dividend / divisor); 
} 
Copy the code

Manually specify a warning:

- (float)divide:(float)dividend by:(float)divisor 
{ 
 #warning Dude, don't compare floating point numbers like this!
     if(divisor ! =0.0) { 
        return (dividend / divisor); 
     } elsereturnNAN; }}Copy the code

References:

  1. Wang Yin: Programming wisdom
  2. Talk about Clean Code
  3. Zen and the Art of Objective-C programming
  4. J_Knight’s Anthology: iOS – Effective Objective-C 2.0
  5. Angel of Butterfly dreams: iOS Code Programming specification – summary based on project experience
  6. Objective-c high quality code reference specification

This post has been synchronized to my blog: Portal

— — — — — — — — — — — — — — — — — — — — — — — — — — — — on July 17, 2018 update — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Pay attention!!

The author recently opened a personal public account, mainly to share programming, reading notes, thinking articles.

  • Programming articles: including selected technical articles published by the author before, and subsequent technical articles (mainly original), and gradually away from iOS content, will shift the focus to improve the direction of programming ability.
  • Reading notes: Share reading notes on programming, thinking, psychology, and career books.
  • Thinking article: to share the author’s thinking on technology and life.

Because the number of messages released by the official account has a limit, so far not all the selected articles in the past have been published on the official account, and will be released gradually.

And because of the various restrictions of the major blog platform, the back will also be released on the public number of some short and concise, to see the big dry goods article oh ~

Scan the qr code of the official account below and click follow, looking forward to growing with you