preface

What are the most important things that influence your decision when choosing a programming language?

Some people say that the less you write, the better your language. (No, PHP is the best language.)

Well, that may be true. But writing less is not a quantifiable indicator of the same results all the time and everywhere. The number of lines of code will fluctuate depending on your task.

I think the best way is to see how many primitives there are in the programming language.

For some older programming languages, they don’t have multidimensional arrays. This means that arrays cannot contain themselves. This limits the ability of some developers to invent data structures that are recursive in nature, and also limits the expressiveness of the language. The expressiveness of language, formally speaking, is the computational power of language.

But the array example I just mentioned is only about runtime computing power. What about compile time computing power?

good Languages like C++ that have the ability to show the compilation process and some “code template” facilities have the ability to do some compile-time calculations. They usually take pieces of source code and organize them into a new piece of code. You’ve probably heard the big word: “metaprogramming.” Yes, this is metaprogramming (but at compile time). These languages also include C and Swift.

C++ metaprogramming relies on templates. In C, metaprogramming relies on a special header file called metamacros.h from libobjcext. In Swift, metaprogramming relies on generics.

Although you can do compile-time metaprogramming in all three languages, the capabilities are different. Since much has been written about how C++ templates are turing-complete (a measure of computing power that you can simply think of as “everything”), I don’t want to waste my practice on them. I’m going to discuss generic metaprogramming in Swift and give a brief introduction to Metamacros.h in C. Both languages have less compile-time metaprogramming capability than C++. They can only implement a DFA (deterministic automata, another measure of computing power). You can simply think of it as a compile-time computing facility with a “finite mode”) ceiling.


Case study: VFL secure at compile time

We have many Auto Layout libraries: Cartography, navigation, SnapKit… But are they really good? What if there was a Swift version of VFL that was compile-correct and interacted with Xcode’s code completion?

To be honest, I’m a VFL fan. You can lay out many views in one line of code. Cartography or SnapKit would have been “long and smelly”.

Since the original VFL has a bit of a problem with modern iOS design support, mainly with layout Guide, you may want the API we are going to implement to support Layout Guide as well.

Finally, in my production code, I built the following API that is secure at compile time and supports layout Guide.

// Create layout constraints and install them into the view

constrain {
    withVFL(H: view1 - view2)
    
    withVFL(H: view.safeAreaLayoutGuide - view2)
    
    withVFL(H: |-view2)
}

// Just create layout constraints

let constraints1 = withVFL(V: view1 - view2)

let constraints2 = withVFL(V: view3 - view4, options: .alignAllCenterY)
Copy the code

Imagine how many lines of code it would take to build the equivalent in Cartography or SnapKit? Want to know how I built it?

Let me tell you.

Grammar deformation

If we import the original VFL syntax into Swift source code and remove quotes from string literals, you’ll quickly see that some of the characters used in the original VFL like [,], @, (and) cannot be used for operator overloading in Swift. So I made some variations on the original VFL syntax:

/ / the original VFL: @ "| - [the view1] - [view2]." "
withVFL(H: |-view1 - view2)

// VFL: @"[view1(200@20)]"
withVFL(H: view1.where(200 ~ 20))

// VFL: @"V:[view1][view2]"
withVFL(V: view1 | view2)

/ / the original VFL: @ "V: | [the view1] - [view2] |"
withVFL(V: |view1 - view2|)

/ / the original VFL: @ "V: | [the view1] - (> = 4 @ 200) - [view2] |"
withVFL(V: |view1 - (> =4 ~ 200) - view2|)
Copy the code

To explore the implementation

How to achieve our design?

One intuitive answer is to use operator overloading.

Yes. I have implemented our design with operator overloading in my production code. But how does operator overloading work here? I mean, why can operator overloading carry our design?

Before we answer that question, let’s look at some examples.

withVFL(H: |-view1 - view2 - 4)
Copy the code

The above example is an invalid input that should not be accepted by the compiler. The corresponding original VFL is as follows:

@"|-[view1]-[view2]-4"
Copy the code

We can find that after 4 missing a view, or a – |.

We want our system to be able to control good input by making the compiler accept a piece of input and bad input by making the compiler reject a piece of input (because that’s the implication of being safe at compile time). The secret behind this is not dark magic cast by a mysterious engineer whose header is “Senior Software Development Engineer”, but simply accepting user input by matching it with defined functions and rejecting it by mismatching it with defined functions.

For example, as shown in the view1-view2 section above, we can design the following function to control it.

func - (lhs: UIView.rhs: UIView) -> BinarySyntax {
    // Do something really combine these two views together.
}
Copy the code

If we treat UIView and BinarySyntax in the above code block as two states, then we can introduce state transitions in our system by operator overloading.

Naive state transitions

Knowing that introducing state transitions through operator overloading might solve our problem, we can breathe a sigh of relief.

But… How many types do we create for this solution?

What you may not know is that VFL can be expressed as a DFA.

Yes. Because recursive texts such as [,], (and) are not really recursive texts in a VFL (in the correct VFL they can only occur in one layer and cannot be nested), a DFA can represent the entire set of possible inputs in a VFL.

So I drew a DFA to simulate the state transitions in our design. Be careful. I didn’t put the layout guide in this picture. Adding a Layout Guide only complicates the DFA.

For a more down-to-earth introduction to recursion and DFA, you can check out the Book The Nature of Computing: An In-depth Look at Programs and Computers

Above, a prefix | | the pre said operators, in the same way, a suffix | | post said operators. Two circles indicate acceptance and a single circle indicates acceptance.

Counting the number of types we want to create is a complex task. With binocular operator | and – and a unary operator | — – | to | prefix and | postfix, counting methods are different in the two operators.

A binocular operator consumes two state transitions, while a monocular operator consumes one. Each operator creates a new type.

Because the counting method itself is so complicated, I’d rather think of something else…

Multi-state state transitions

I drew the DFA diagram above by desperately testing possible input characters to see if a state accepts them. This maps everything to one dimension at a time. Perhaps we can create a cleaner expression by abstracting the problem from multiple dimensions.

Before we dive in, we have to pick up some basics about the associativity of the Swift operator.

Associativity is an operator (strictly speaking, the binocular operator). A property that determines which side the compiler chooses to build the syntax tree on at compile time. Swift’s default operator associativity is left. This means that the compiler prefers to build the syntax tree on the left-hand side of an operator. We can then see that a syntax tree generated by a leftward combined operator is visually tilted to the left.

First let’s look at some of the simplest expressions:

// Should be accepted
withVFL(H: view1 - view2)

// Should be accepted
withVFL(H: view1 | view2)

// Should be accepted
withVFL(H: |view1|)

// Should be accepted
withVFL(H: |-view1-|)
Copy the code

Their syntax tree is as follows:

Then we can divide the situation into two categories:

  • Like the view1 – view2, the view1 | view2 the binocular expression.

  • Like | the view1, the view1 – | the monocular expression.

This led us intuitively to create two types:

struct Binary<Lhs.Rhs> { . }

func - <Lhs.Rhs> (lhs: Lhs.rhs: Rhs) -> Binary { . }

func | <Lhs.Rhs> (lhs: Lhs.rhs: Rhs) -> Binary { . }

struct Unary<Operand> { . }

prefix func | <Operand> (operand: Operand) -> Unary { . }

postfix func | <Operand> (operand: Operand) -> Unary { . }

prefix func |- <Operand> (operand: Operand) -> Unary { . }

postfix func -| <Operand> (operand: Operand) -> Unary { . }
Copy the code

But is it enough?

Syntax Attribute

As you will soon see, we can substitute anything into Binary’s Lhs or Rhs, or Unary’s Operand. We need to make some restrictions.

Typically, says like | — – | | the prefix, | postfix fore and aft ends this input should only appear in the expression. Because we also want to support layout Guides (like safeAreaLayoutGuide), and layout Guides should only appear at the beginning and end of expressions, we also need to restrict these things to ensure that they only appear at both ends of expressions.

|-view-|
|view|
Copy the code

In addition, inputs like 4, >=40 should only appear with a precursor and successor view/parent view or layout Guide.

view - 4 - safeAreaLayoutGuide

view1 - (> =40) - view2
Copy the code

The above study of expressions prompts us to group all the things involved in expressions into three groups: Layout ‘Ed object (view), confinement (layout guides and be | — – | the | prefix and | postfix wrapped), and constant.

Now we will change our design to:

protocol Operand {
    associatedtype HeadAttribute: SyntaxAttribute
    
    associatedtype TailAttribute: SyntaxAttribute
}

protocol SyntaxAttribute {}

struct SyntaxAttributeLayoutedObject: SyntaxAttribute {}

struct SyntaxAttributeConfinment: SyntaxAttribute {}

struct SyntaxAttributeConstant: SyntaxAttribute {}
Copy the code

Then for combinations like view1-4-view2, we can create the following expression types:

// connect to 'view-4'
struct LayoutableToConstantSpacedSyntax<Lhs: Operand.Rhs: Operand> :Operand where// check whether the left-hand operand ends in onelayouted object
    Lhs.TailAttribute= =SyntaxAttributeLayoutedObject/// check whether the head of the right-hand operand is the sameconstant
    Rhs.HeadAttribute= =SyntaxAttributeConstant
{
     typealias HeadAttribute = Lhs.HeadAttribute
     typealias TailAttribute = Lhs.TailAttribute
}

func - <Lhs.Rhs> (lhs: Lhs.rhs: Rhs) -> LayoutableToConstantSpacedSyntax<Lhs.Rhs> { . }

// link '(view-4) -view2'
struct ConstantToLayoutableSpacedSyntax<Lhs: Operand.Rhs: Operand> :Operand where// check whether the left-hand operand ends in oneconstant
    Lhs.TailAttribute= =SyntaxAttributeConstant/// check whether the head of the right-hand operand is the samelayouted object
    Rhs.HeadAttribute= =SyntaxAttributeLayoutedObject
{
     typealias HeadAttribute = Lhs.HeadAttribute
     typealias TailAttribute = Lhs.TailAttribute
}

func - <Lhs.Rhs> (lhs: Lhs.rhs: Rhs) -> ConstantToLayoutableSpacedSyntax<Lhs.Rhs> { . }
Copy the code

By following the Operand protocol, a type actually gets two compile-time containers named HeadAttribute and TailAttribute; The value is the type of the SyntaxAttribute. By calling the function – (either of the above code blocks), The compiler will check whether the left-hand operand and the right-hand operand and function return value (ConstantToLayoutableSpacedSyntax or LayoutableToConstantSpacedSyntax) of generic constraint. If it succeeds, we can say that the state has been successfully transferred to another state.

We can see that because we have set HeadAttribute = lhs. HeadAttribute and TailAttribute = lhs.tailattribute in the body of the above type, Now the Lhs and Rhs header and tail attributes have been moved from Lhs and Rhs to this newly synthesized type. The value is stored in its HeadAttribute and TailAttribute.

We then managed to get the compiler to accept inputs like view1-4-view2, view1-10-view2-19… Wait a minute! view1 – 10 – view2 – 19??? View1-10-view2-19 should be an invalid input rejected by the compiler!

Syntax Boundaries

In fact, we just ensured that a view followed by a number, and a number followed by a view, regardless of whether the expression starts or ends with a view (or layout Guide).

In order to make the expression with a view, layout guide or | — – | the | prefix and | postfix beginning, TailAttribute == SyntaxAttributeConstant and rhs.headAttribute == SyntaxAttributeLayoutedObject. There are two groups of confinement and layout’ed object. In order for an expression to always start or end with one of these two sets of expressions, we must implement it using compile-time or logic. We write it in run-time code:

if (lhs.tailAttribute = = .isLayoutedObject || lhs.tailAttribute  = = .isConfinment) &&
    (rhs.headAttribute = = .isLayoutedObject || rhs.headAttribute = = .isConfinment)
{ . }
Copy the code

But this logic cannot be simply implemented in Swift compile time, and the only logic that Swift compile time evaluates is the and logic. Because we can only be used in the type constraints in Swift and logic (through the use of Lhs. TailAttribute = = SyntaxAttributeLayoutedObject and Rhs HeadAttribute = = In SyntaxAttributeConstant, symbol), We can only in the above code block (LHS) tailAttribute = =) isLayoutedObject | | LHS. TailAttribute. = = isConfinment) and (RHS. HeadAttribute = = . IsLayoutedObject | | RHS. HeadAttribute = =. Container isConfinment) blend together into a compile time value, and then use logic to link them.

In fact, Lhs. TailAttribute = = SyntaxAttributeLayoutedObject or Rhs. HeadAttribute = = SyntaxAttributeConstant the = = and = = in most programming languages Operator equivalent. In addition, Swift compile-time calculations also have an operator equivalent to >= :

Consider the following code:

protocol One {}
protocol Two: One {}
protocol Three: Two {}

struct Foo<T> where T: Two {}
Copy the code

Now T in Foo can only be “greater than Two”.

Then we can change our design to:

protocol Operand {
    associatedtype HeadAttribute: SyntaxAttribute

    associatedtype TailAttribute: SyntaxAttribute

    associatedtype HeadBoundary: SyntaxBoundary

    associatedtype TailBoundary: SyntaxBoundary
}

protocol SyntaxBoundary {}

struct SyntaxBoundaryIsLayoutedObjectOrConfinment: SyntaxBoundary {}

struct SyntaxBoundaryIsConstant: SyntaxBoundary {}
Copy the code

This time we added two compile-time containers: HeadBoundary and TailBoundary, whose values are of the type of SyntaxBoundary. To view or layout guide objects, for their part, provided the fore and aft two SyntaxBoundaryIsLayoutedObjectOrConfinment types of boundaries. When the – function is called, the boundary information for the view or layout Guide is passed into the newly synthesized type.

// connect to 'view-4'
struct LayoutableToConstantSpacedSyntax<Lhs: Operand.Rhs: Operand> :Operand where/ / / confirmationLhsTailAttributeSyntaxAttributeLayoutedObject
    Lhs.TailAttribute= =SyntaxAttributeLayoutedObject, / / / confirmationRhsHeadAttributeSyntaxAttributeConstant
    Rhs.HeadAttribute= =SyntaxAttributeConstant
{
    typealias HeadBoundary = Lhs.HeadBoundary
    typealias TailBoundary = Rhs.TailBoundary
    typealias HeadAttribute = Lhs.HeadAttribute
    typealias TailAttribute = Lhs.TailAttribute
}

func - <Lhs.Rhs> (lhs: Lhs.rhs: Rhs) -> LayoutableToConstantSpacedSyntax<Lhs.Rhs> { . }
Copy the code

Now we can change the function signature of our withVFL family of functions to:

func withVFL<O: Operand> (V: O)- > [NSLayoutConstraint] where
    O.HeadBoundary = = SyntaxBoundaryIsLayoutedObjectOrConfinment.O.TailBoundary = = SyntaxBoundaryIsLayoutedObjectOrConfinment
{ . }
Copy the code

Then, only boundaries are accepted as views or layout guide expressions.

Syntax Associativity

But the syntax of the concept of boundaries or stop accepting such as the view1 cannot help compiler – | | view2 or view2 input – | – view2. This is because even if boundaries are ensured for an expression, you cannot guarantee that the expression is associable.

So we’ll introduce a third pair of associatedTypes in our design:

protocol Operand {
    associatedtype HeadAttribute: SyntaxAttribute

    associatedtype TailAttribute: SyntaxAttribute

    associatedtype HeadBoundary: SyntaxBoundary

    associatedtype TailBoundary: SyntaxBoundary

    associatedtype HeadAssociativity: SyntaxAssociativity

    associatedtype TailAssociativity: SyntaxAssociativity
}

protocol SyntaxAssociativity {}

struct SyntaxAssociativityIsOpen: SyntaxAssociativity {}

struct SyntaxAssociativityIsClosed: SyntaxAssociativity {}

Copy the code

Like | — – | expression or an expression such as layout of the guide, we will be able to in the process of the synthesis of new type to turn off their associativity.

Is that enough?

Yes. Actually, I cheated here. You might be surprised why I was able to quickly identify problems by giving examples, so I didn’t hesitate to say “yes” to the above question. The reason is, I’ve already enumerated all the syntax tree configurations on paper. Planning on paper is a good habit for a good software engineer.

The core concepts of syntax tree design are now very close to my production code. You can check them out here.

Generate an instance of NSLayoutConstraint

All right, come back. We still have things to achieve. This is important for our overall work — generating layout constraints.

Since what we get in the arguments of the withVFL(V:) family of functions is a syntax tree, we can simply build an environment to evaluate this syntax tree.

I’m refraining from using big words, so I’m talking about “building an environment.” But I can’t help but tell you that we are now going to start building a virtual machine!

By looking at a syntax tree, we can see that each layer of the syntax tree is or is not a monocular operation node, binocular operation node, or operand node. We can abstract the calculation of the NSLayoutConstraint into small fragments, which are then generated by these three types of nodes.

That sounds good. But how do you make this abstraction? How do you design those little pieces?

For those who have experience in virtual machine design or compiler construction, they may know that this is a problem of “process abstraction” and “instruction set design”. But I didn’t want to scare readers like you who might not be knowledgeable enough about this, so I called them “abstracting the calculation of NSLayoutConstraint into” “little pieces.”

Another reason why I don’t talk about this in terms of “process abstraction” and “instruction set design” is that instruction set design is the front end of the solution: you’re going to get something called OpCode (operation Code, I don’t know why they abbreviated the term that way). But instruction set design can seriously affect the final shape of process abstraction, and if you don’t think about process abstraction before doing instruction set design, it’s hard to fathom the concept behind the instruction set.

The initialization of abstract NSLayoutConstraint

Since we want to support layout Guide, the old API:

convenience init(
    item view1: Any.attribute attr1: NSLayoutConstraint.Attribute.relatedBy relation: NSLayoutConstraint.Relation.toItem view2: Any?.attribute attr2: NSLayoutConstraint.Attribute.multiplier: CGFloat.constant c: CGFloat
)
Copy the code

It becomes unavailable. You can’t make a Layout Guide work with this API. Yes, I have.

Then we might think of layout anchors.

Yes, it works. My production code uses the Layout anchors. But why layout Anchors work?

In fact, we can check the documentation to see that the base class NSLayoutAnchor for Layout anchors has a set of apis that generate NSLayoutConstraint. If we can get all the parameters of this set of apis in certain steps, then we can abstract a formal model for the calculation process.

Can we get all the parameters of this set of apis in the defined steps?

The answer is clearly “yes”.

Syntax tree evaluation at a glance

In Swift, the syntax tree is evaluated depth-first. The following diagram shows the traversal order of view1-bunchofviews in the following code block.

let bunchOfViews = view2 - view3
view1 | bunchOfViews
Copy the code

But while the root node is the first to be accessed in the entire evaluation process, it will generate the NSLayoutConstraint instance last, since it needs the evaluation of its left-hand and right-hand children to complete the evaluation process.

The calculation of abstract NSLayoutConstraint

By looking at the illustration above of the Swift syntax tree evaluation, we can see that the node view1 will be evaluated in the second place, but the result will be used last. So we need a data structure that can hold the evaluation results of each node. You may remember to use the stack. Yes. I use this stack in my production code. But you should know why we use a stack: a stack can transform a recursive structure into a linear one, and that’s what we want. You might have guessed that I was going to use the stack, but intuition isn’t always there.

With this stack, we can put all the computing resources that initialize an instance of NSLayoutConstraint into it.

In addition, we want the stack to remember the first and last nodes of the syntax tree that has been evaluated.

Why is that? Take a look at the syntax tree below:

The syntax tree is generated by the following expression.

let view2_3 = view2 - view3
let view2_4 = view2_3 - view4
view1 | view2_4
Copy the code

When we evaluate the – node at the second level of the tree (counting from the root), we must select the “inside” of view3 to create an instance of NSLayoutConstraint. In fact, generating an NSLayoutConstraint instance always requires picking a node that looks “inside” from the node being evaluated. But for | with node, “inside” becomes the view1 node and view2. So we have to let the stack remember the first and last nodes of the evaluated syntax tree.

About “Return value”

Yes, we had to devise a mechanism for each node in the syntax tree to return the evaluation.

I don’t want to talk about how a real computer passes a return value between stacks because it varies depending on the size of the data returned. In the Swift world, since everything is secure, this means that being able to bind a piece of memory to another type of API is very difficult to use, and processing data at a fragmented pace is not a good option (at least not for coding efficiency).

All we need to do is use a local variable in the evaluation context to hold the result of the last bounce of the stack, and then generate the instruction to fetch data from that variable, and we are done with the design of the “return value” system.

Creating a VM

Once process abstraction is complete, the design of the instruction set is just one step away.

In effect, we just need the directive to do the following:

  • Get back view, Layout Guide, constraint relationship, constraint constant, constraint priority.

  • Generate the information for which Layout Anchor to select.

  • Create layout constraints.

  • Stack, spring stack.

The finished production code is here

assessment

We have completed our conceptual design for a compile-time secure VFL.

The question is what do we get?

VFL is secure for our compile time

The advantage here is that the correctness of the expression is guaranteed. Such as withVFL (H: 4 – view) or withVFL (H: the view – | – 4 – view) of expression will be rejected at compile time.

Then, we’ve got the Layout Guide working with our VFL Swift implementation.

Third, since we are executing instructions generated by the syntax tree organized at compile time, the overall computational complexity is O(N), where N is the number of instructions generated by the syntax tree. But because the syntax tree is not built at compile time, it must be built at run time. The good news is that in my production code, the syntax trees are all of type struct, which means that the syntax trees are built in stack memory rather than heap memory.

In fact, after a full day of optimization, my production code surpassed all existing alternatives (including Cartography and SnapKit). This, of course, includes the original VFL. I will place some optimization tips later in this article.

For VFL

In theory, the original VFL had some performance advantages over our design. The VFL string is actually stored as a C string in the data section of the executable (the Mach-O file). The operating system loads them directly into memory and does nothing to initialize them before starting to use them. Once these VFL strings are loaded, the target platform’s UI framework is ready to parse the VFL strings. Because the VFL syntax is so simple, it is easy to build a parser with O(N) time complexity. But I don’t know why VFL is the slowest of all the constraints for helping developers build Auto Layout layouts.

The performance test

The following results were measured by measuring 10K layout constraint builds on the iPhone X.


Further reading

Swift optimization

The price of an Array

An Array in Swift spends a lot of time deciding whether its internal container is objective-C or a Swift implementation. Use a ContiguousArray to get your code to think in Swift alone.

The price of the Collection. The map

Collection.map in Swift is well optimized — it preallocates each element before it is added, which eliminates frequent allocation overhead.

However, if you want to map arrays into multi-dimensional arrays and flatten them into low-dimensional arrays, it is better to create a new Array at the beginning, pre-allocate all the space, and then traditionally call Array’s append(_:) function.

The cost of an unnamed type

Do not use anonymous types (tuples) for writing purposes.

When writing an unnamed type, Swift needs access to the runtime to ensure code security. This will take a lot of time, and you should use a named type, or struct, instead.

Subscript. modify the cost of the function

In Swift, a subscript([key] in self[key]) has three potential pairing functions.

  • getter

  • setter

  • modify

What is the modify?

Consider the following code:

struct StackLevel {
    var value: Int = 0
}

let stack: Array<StackLevel> = [.init()]

/ / use subscript. Setter
stack[0] = StackLevel(value: 13)

/ / use subscript. The modify
stack[0].value = 13
Copy the code

Subscript. modify is a function that modifies the value of a member of an element inside a container. But it seems to be doing more than just changing the value.

I can’t even understand where malloc and free come from in my evaluation tree.

I replaced the evaluation stack from Array with my own implementation, and implemented a function called modifyTopLevel(with:) to modify the top of the stack.

internal class _CTVFLEvaluationStack {
    internal var _buffer: UnsafeMutablePointer<_CTVFLEvaluationStackLevel>

    .

    internal func modifyTopLevel(with closure: (inout _CTVFLEvaluationStackLevel) -> Void) {
        closure(&_buffer[_count - 1])}}Copy the code

The price of OptionSet

The convenience of OptionSet in Swift is not free.

As you can see, OptionSet uses a very deep evaluation tree to obtain a value that can be evaluated by manual bit masking. I don’t know if this exists in release builds, but I’m currently using manual bit masking in production code.

Exclusivity Enforcement costs

Exclusivity Enforcement also has an impact on performance. You can see a lot of swift_beginAcces and swift_endAccess calls in your evaluation stack. If you are confident in your code, I recommend turning off exclusivity Enforcement at runtime. Search “exclusivity” in Build Settings to see the options.

In the release build of Swift 5, Exclusivity Enforcement is enabled by default.

Compile-time calculation of C

I also implemented an interesting syntax in one of my frameworks: adding an automatic synthesizer for @dynamic property via metamacros.h. Here’s an example:

@ObjCDynamicPropertyGetter(id, WEAK) {
    // Use _prop to access the property name
    // The rest is the same as an atomic Weak Objective-C getter.
}

@ObjCDynamicPropertyGetter(id, COPY) {
    // Use _prop to access the property name
    // The rest is the same as an atomic copy Objective-C getter.
}

@ObjCDynamicPropertyGetter(id, RETAIN, NONATOMIC) {
    // Use _prop to access the property name
    // The rest is the same as a nonatomic retain Objective-C getter.
};
Copy the code

The implementation file is here.

For C programmers, metamacros.h is a very useful scaffolding for creating macros to lighten the load.


Thank you for reading such a long article. I have to apologize: I lied about the headline. This article is not a “shallow talk” about Swift generic metaprogramming at all, but rather more about computing in depth. But I think it’s the basics of being a good programmer.

Finally, may Swift generic metaprogramming not be part of the interview for iOS engineers.


Originally published on my blog (English)

This paper uses OpenCC for simplified conversion