TinyVFL: A safer virtual format language for Auto Layout

Galvin Li
8 min readFeb 3, 2019

--

此文章同时提供中文版本:TinyVFL: 更安全的Auto Layout Virtual Format Language实现

Virtual Format Language(later referred VFL) is a native way to set up Auto Layout constraints. If you not familiar with it you can checkout the Apple Document to learn more about it. Some developer feel the VFL is a bit hard to understand. But when you understand it and use it, it provide an easier way to layout multiple views at the same time. Also the code provide a visual cue of layout. The VFL code in Objective-C look like this:

Usually code will become simpler when transform from OC to Swift. But VFL is an exception. Because Swift not provide a method like NSDictionaryOfVariableBindings to help us build the dictionary of views. Finally the VFL code in Swift look like this:

Maybe it looks no that complex. But every time you need to manage the view dictionary will make it not so convenient. Also Swift is a static language, we should prevent to use raw string to link an instance, it’s not safe. That’s why I create a library name TinyVFL to replace the exist API in UIKit and provide safer and easier usage. Use TinyVFL to create same layout would look like this:

The vertical layout finally could code in vertical style. It can help people to visual the real layout. And I remove all unsafe string so no more typo crash. Also no need to manage the view dictionary, would be much easier when you need to add or remove view in the layout, and never afraid to rename the instance.

If you feel interest, let’s found out how TinyVFL work in this way.

Handling dynamic view dictionary

The underlying implementation of TinyVFL actually calls the native VFL method directly. Although you can create a separate NSLayoutConstraint directly, but if two different layout systems are connected together, you need a lot of work during the test to ensure that each constraint is set correctly. Obviously it is not necessary, TinyVFL's main job is to provide a better usage way to replace the original one. So using original VFL for underlying implementation is obviously the best choice.

But the biggest challenge here is to implement a similar method like NSDictionaryOfVariableBindings. This method is not provided by Swift and I can't go beyond the limitations of the language itself. However, it is not necessary. The advantage of NSDictionaryOfVariableBindings is that it automatically forms easy-to-use strings and object mappings. But we don't need to use the string manually, so the mapping relationship doesn't need to be so dynamic. The most important thing is that the VFL string created by the underlying layer can correspond to the appropriate view dictionary. There are many ways to do this, but the best part is that each view has a unique string that represents itself. The word "unique" reminds me that Set is a type that holds unique objects, and Set determines that the object's uniqueness with hashValue, so I created such an extension:

Now each view can output a unique view string, and our NSDictionaryOfVariableBindings becomes an implementation like this:

Of course we still can’t use view1 directly under our VFL format string. We need to output view1.vflName, but because it is only used by the underlying implementation, this implementation is good enough.

Implement enum-like method

The main feature of TinyVFL is that you can set the layout object directly using .view(view1). Most people may think at first glance that it is an enum case object. Swift's enum is really powerful, but I did not use enum here, but use struct with static methods, and the internal is actually corresponding to enum to store. Why won't I use enum directly? Because enum has two minor drawbacks:

  • You can’t use default parameter.
  • You can’t declare a case value with the same name, even with different parameters.

For a more detailed explanation and solution for enum restrictions, please refer to this article: TinySolution: Resolve the restrictions of enum

So if you use enum, you can't implement both .space(10) and .space(20, priority: 250) ash the same time. Only can with .space(10, priority: nil) which is obviously unacceptable. But if you use the static method of struct, you can achieve similar results, as follows:

There is another advantage to using struct. VFL has two similar formats: the value of space and the size of view. Both are numbers plus optional priority, so the basic format is the same, but the initialization name needs To maintain differentiation, we can use the same enum to implement the struct at the internal side, but the outer struct uses different initialization methods.

But here is a point to note, the VFL space can have no width value, the system will use the default separation distance. But if we set the space directly be optional, we can’t restrict the user write an invalid code like .space(nil, priority: 250). Of course, I believe that no one will do this, but we can do better, just add another one method with only optional space parameter, so it is safe to call directly.

Friendly exception

The very unfriendly thing about the original VFL is that when you make some wrong configuration, such as the layout string is wrong, or the wrong alignment information is set in the options, the crash will be positioned to AppDelegate. Then you need to figure out which part of code went wrong. Actually we can optimize it.

Although we can use the initialization method to control the creation of objects, we can’t restrict users from passing objects incorrectly, such as putting .superView in the middle or putting two .spaces together. For these problems, we can add a verification method when initializing the object, verify the situation of the problem, and give the corresponding crash prompt content. Normally we won't force a method to crash, but the wrong layout will still crash when it is finally executed, so it is more suitable for crash processing. The judgment is as follows:

For options, it is actually more problematic, because the options for vertical and horizontal are not that obvious. Some of wrong configurations will crash, some will not crash but not have any effect. And actually the verification of options is very simple, can be directly added to the native api, I do not understand why UIKit does not directly add this part of the verification. The specific verification method is as follows:

Make code more readable

The code effect of the vertical layout can be optimized, but the horizontal layout is a bit too long and it becomes less clear. We don’t need such a long initialization method when we get used to it, so we can reduce it. .view(...) is very simple to reduce to .v(...), .space(...) to .s(...), priority: to the p:, such prompt content reduces interference and retains the hint. The implementation is also very simple, we only need to add the corresponding static method, while retaining the original method.

We can’t reduce .superView() to .s(), because this conflicts with the .space(). I thought about replacing it with side, but the side is still abbreviated to s and the semantics are not so clear. Another consideration is the use of emoji, I think ◀️▶️🔼🔽 is actually very suitable for representing the boundary, but unfortunately Swift supports most emoji as a method name, but there are still a few parts that are not supported, and these arrows are one of them. In the end, I decided to use the direction to directly represent the four sides .left, .right, .top, .bottom, although the bottom layer is actually .superView, but the semantics are more clear.

Make code more versatility

Our code does not add any restrictions compared to the native code, so where can we improve versatility? The answer is the platform. Auto Layout can be used with macOS in addition to iOS. The UIView we use is dependent on UIKit, while the macOS dependency is AppKit, but Appkit doesn't have UIView, only NSView, but we don't actually use UIView-specific properties or methods. We just need to make sure this is for Auto Layout's View only, so we can handle this problem with typealias, with a uniform name for View. The implementation is as follows:

  • This article not including all code of TinyVFL, just some introduction of key implementation and optimization direction. The complete code can be found in the GitHub project.
  • If you have questions or suggestions, welcome to leave comment for discuss.
  • If you feel this article is valuable, please forward it so more people can see it.
  • If you like this type of content, welcome to follow my Medium and Twitter, I will keep posting useful content for everyone.

--

--

Galvin Li

A Tiny iOS developer who love to solve problems and make things better.