The paper contains 7044 words and is expected to last 14 minutes

Credit: unsplash.com/@max_duz

Swift 5.1 adds a number of new features, some of which promise to revolutionize the way Swift code is written and built. So how do YOU use Swift 5.1 Property Wrappers to cut your dependency injection code in half?

This article discusses Swift Property Wrappers and demonstrates a method that can greatly simplify your code.

background

Modern software development is an exercise in project management complexity, and architecture is one of the ways we try to achieve it. Architecture, in turn, is really just a term to describe how complex software is broken down into easy-to-understand layers and components.


Therefore, we broke the software down into simplified components that we could easily write, do only one thing (single responsibility principle, SRP), and easily test.

However, once you have a bunch of parts, you have to rewire them all together to form a working application.

By wiring parts together in the right way, you can have a clean architecture of loosely coupled components.

But if the wiring goes wrong, you end up with a tightly coupled jumble in which most of the parts contain information about how many of the child components are built and operate internally.

This makes component sharing nearly impossible, and it is equally impossible to easily swap one component layer for another.

It’s a quandary, and the tools and techniques we use to try to simplify code end up making our lives more complicated.

Fortunately, another technique can be used to manage this additional layer of complexity, called dependency injection, based on a principle called inversion of control.

Dependency injection

This article cannot provide a complete and exhaustive explanation of dependency injection, which simply means that it allows a given component to ask the system to connect to everything it needs to do its job.

These dependencies are returned to the component that is fully formed and ready for use.

For example, a ViewController might need a ViewModel. The ViewModel might need an API component to get some data, which in turn needs access to the authentication system and the current session manager. The ViewModel also needs a data transformation service with dependencies.

A ViewController doesn’t do these things, nor should it, but simply talks to the components it needs to do its job.

To demonstrate the techniques involved, this article uses a powerful lightweight dependency injection system called Resolver. This works if you use any other DI framework.

For more information, see the Dependency Injection guide on the Resolver GitHub repository, as well as the Documentation for Resolver itself.

Portal: https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page————————- —

Simple example

A very basic view model using dependency injection looks like this:


class XYZViewModel {private var fetcher: XYZFetching    
private var service: XYZServiceinit(fetcher: XYZFetching, service: XYZService) {        
self.fetcher = fetcher        
self.service = service    }func load() -> Image {        
let data = fetcher.getData(token)        
return service.decompress(data)   }}Copy the code

Listed above are the components required by the View Model, along with an initialization function that basically assigns any components passed to the model to instance variables of the model.

This is called constructor injection and is used to ensure that a given component is not given everything it needs if it cannot be instantiated.

Now that I have the View Model, how do I get the View Controller?

Resolver solves this problem automatically in several modes, the simplest of which is to use a pattern called Service Locator…… This is basically code that knows how to locate and request a service.

class XYZViewController: UIViewController {    
private let viewModel: XYZViewModel = Resolver.resolve()    
override func viewDidLoad() {... }}Copy the code

So the viewModel requires that the Resolver “resolve” dependency. The parser uses the supplied type information to find the instance factory used to create the object of the requested type.

Note that the viewModel needs a fetcher and a service provided to it, but the View Controller doesn’t need those things at all and just relies on the dependency injection system to take care of all those messy little details.

There are other benefits as well. For example, you can run a “Mock” scheme where the data layer is replaced with Mock data from a JSON file embedded in the application, which is easy to run during development, debugging, and testing.

Dependent systems can easily handle this kind of thing in the background, and all view Controllers know that they still have the required viewmodel.

Resolver document Sample portal: https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page—————————

Finally, note that in dependency injection terminology, dependencies are often referred to as services.

registered

In order for a typical dependency injection system to work, services must be registered and factory methods associated with each type the system might want to create need to be provided.

In some systems, the dependency is named, while in others, the dependency type must be specified. However, the parser can usually infer the type information needed.

Therefore, a typical registry block in a parser might look like this:

func setupMyRegistrations {    
register { XYZViewModel(fetcher: resolve(), service: resolve()) }    
register { XYZFetcher(session: resolve()) as XYZFetching }    
register { XYZService() }    
register { XYZSessionManager()}Copy the code

Note that the first registration function registers the XYZViewModel and provides a factory function to create a new instance. The registered type is automatically inferred from the return type of the factory.

Each parameter required by XYZViewModel to initialize the function can also be resolved by infering the type signature again and parsing it in turn.

The second function registers the Xyzfetch protocol and satisfies that protocol by building an instance of XYZFetcher with its own dependencies.

The process repeats recursively until all parts have all the parts needed to initialize and perform the actions they need.



The problem

Reduce complexity to simplicity

However, most real-world programs are complex, so initialization functions can start to get out of hand.

class MyViewModel {var userStateMachine: UserStateMachine var keyValueStore: KeyValueStore var bundle: BundleProviding var touchIdService: TouchIDManaging var status: SystemStatusProviding? init(userStateMachine: UserStateMachine, bundle: BundleProviding, touchID: TouchIDManaging, status: SystemStatusProviding? , keyValueStore: KeyValueStore) {self.userStateMachine = userStateMachine self.bundle = bundle self.touchIdService = touchID self.status = status self.keyValueStore = keyValueStore }... }Copy the code

There is quite a bit of code in the initialization function, which is required, but all the code is boilerplate. How can this be avoided?

Swift 5.1 and Property Wrappers

Fortunately Swift 5.1 gave us a new tool called Property Wrappers (officially “Property delegates”) which was presented on the Swift Forum as part of Proposal SE-0258, And added to Swift 5.1 and Xcode 11.

The new feature of the Property Wrapper enables automatic wrapping of Property values using a custom GET/set implementation, hence the name.

Note that you can use custom getters and setters on property values to perform some of these operations, but the downside is that you have to write almost the same code on each property, more boilerplate. It’s even worse if every attribute requires some sort of internal support variable. (There’s more boilerplate.)

@Injected Property Wrapper

So automatically wrapping properties in get/SET pairs doesn’t sound exciting, but property wrappers will have a major impact on our Swift code.

To demonstrate, we will create a Property Wrappers named @injected and add it to the code base.

Now, back to the “out of control” example, see what the brand new property packaging does for us.

class MyViewModel {@Injected var userStateMachine: UserStateMachine    
@Injected var keyValueStore: KeyValueStore    
@Injected var bundle: BundleProviding    
@Injected var touchIdService: TouchIDManaging    
@Injected var status: SystemStatusProviding?...}Copy the code

That’s it. Just mark the properties as @injected and each property will be automatically resolved (Injected) as needed, thus all the boilerplate code in the initialization function is gone!

In addition, it is now clear from the @injected comment what services are provided by the DI system.

This particular type of annotation scheme is applied in other languages, most notably programming in Kotlin on Android and using the Dagger 2 dependency injection framework.

To perform

The property wrapper implementation is simple. We define a generic structure using the Service type and mark it as @propertyWrapper.


@propertyWrapperstruct Injected<Service> {    private var service: Service?    
public var container: Resolver?    
public var name: String?    
public var value: Service {        
mutating get {           
 if service == nil {              
 service = (container ?? Resolver.root).resolve(                    
Service.self,                    
 name: name                )            }            return service!        }       
 mutating set {            service = newValue        }    }}Copy the code

All attribute wrappers must implement a variable named value.

Value provides getter and setter implementations used by property wrappers when requesting or assigning values from variables.

In this case, when the service is requested, our value “getter” checks if this is the first time it has been called. If so, when accessing the wrapper code, the request Resolver parses an instance of the required service based on the generic type, stores the result in a private variable for later use, and returns the service.

We also provide a setter when we want to assign services manually. This can come in handy in certain situations, most notably when unit testing.

The implementation also exposes some additional parameters, such as the name and container, and is implemented in less than a second.

More instances

The implementation of the property wrapper is simple. Define a generic structure using the service type and mark it as @propertyWrapper.

class XYZViewController: UIViewController {   
 @Injected private var viewModel: XYZViewModel    
override func viewDidLoad() {... }}Copy the code

The ViewModel is reduced to its most basic code:

class XYZViewModel {   
 @Injected private var fetcher: XYZFetching    
@Injected private var service: XYZService    
func load() -> Image {        
let data = fetcher.getData(token)        
return service.decompress(data)   }}Copy the code

The registration code is also simplified because the constructor arguments are removed left and right……

func setupMyRegistrations { register { XYZViewModel() } register { XYZFetcher() as XYZFetching } register { XYZService()  } register { XYZSessionManager()}Copy the code

Named Service type

The parser supports named types, which allow programs to distinguish between services or protocols of the same type.

This also shows an interesting property wrappers property, so let’s take a look at this.

A common use case might require a View Controller in two different view models, depending on whether it has already passed data, so it should operate in add or Edit mode.

The registry might look something like this, with both models conforming to the XYZViewModel protocol or base class.

func setupMyRegistrations {    
register(name: "add") { NewXYZViewModel() as XYZViewModel }    
register(name: "edit") { EditXYZViewModel() as XYZViewModel }}Copy the code

Then in the View Controller:

class XYZViewController: UIViewController {@Injected private var viewModel: 
XYZViewModelvar myData: MyData?    
override func viewDidLoad() {        
$viewModel.name = myData == nil ? "add" : "edit"       
 viewModel.configure(myData)        ...    }}Copy the code

Note the $viewModel.name referenced in viewDidLoad.

In most cases, we want Swift to pretend that the wrapped value is the actual value of the property. However, prefixing the property wrapper with the dollar sign allows us to reference the property wrapper itself to gain access to any public variables or functions that may be exposed.

In this case, set the name parameter, which is passed to the Resolver on the first attempt to use the viewmodel. The parser passes the name when it resolves the dependency.

In short, using the $prefix on an attribute wrapper lets us manipulate and/or reference the wrapper itself. You’ll see a lot of this in SwiftUI.

Why “inject”?

Many people ask: why use the word “injection”? Since the code uses Resolver, why not mark it @resolve?

The reason is simple. We’re using Resolver now, mainly because we wrote it. But we might want to share or use some of my model or service code in another application, and that application might use a different system to manage dependency injection. For example, Swinject Storyboard.

“Injected” becomes a more neutral term, and all that needs to be done is to provide a new version of the @injected property wrapper, using Swinject as a backend, which can be immobilized once used.

Other use cases

More uses for Property Wrappers are on Swift in the future.

SwiftUI makes extensive use of dependency injection, and in addition to that, it’s not surprising that standard classes in Cocoa and UIKit provide some additional wrappers.

We think of common wrappers around user defaults and keystring access. Imagine wrapping any property with the following code:

@Keychain(key: "username") var username: String?Copy the code

And automatically pull data from your keystring to support you.

Excessive use of

However, as with any cool new hammer, we run the risk of overusing it because every problem looks like a nail.

Once everything becomes a protocol, start to understand when you can best use the protocol (such as data layer code), and then quit. Before that, C ++ added the custom operator, and we suddenly tried to figure out what the result of user1 + user2 might be?

The key question when implementing Property Wrappers is to ask yourself: Would I use this wrapper extensively in all code bases? If so, Property Wrappers might be a good choice.

Or at least reduce the space it takes up. If you create an @keychain wrapper like the one shown above, you can implement it as Fileprivate in the same file as the KeychainManager class, avoiding arbitrary interlapping throughout the code.

After all, using it is now as simple as:

@Injected var keychain: KeychainManagerCopy the code

We don’t want a version where every model looks like this:

class MyModel {    
@Injected private var fetcher: XYZFetching   
 @Injected private var service: XYZService    
@Error private var error: String    
@Constrain private var myInt: Int   
 @Status private var x = 0   
 @Status private var y = 0  }Copy the code

Then leave the next developer to look at the code scrambling to figure out what each wrapper does.

Complete block

Property Wrappers are just one of many features introduced in Swift5.1 and Xcode 11 that promise to revolutionize the way Swift applications are written.

SwiftUI and Combine get a lot of media attention, but especially before SwiftUI and Combine are actually used, Property Wrappers will drastically reduce the amount of boilerplate code written in everyday programming.

Unlike SwiftUI and Combine, property wrappers can be used on earlier versions of iOS! It’s not just iOS 13.

Leave a comment like follow

We share the dry goods of AI learning and development. Welcome to pay attention to the “core reading technology” of AI vertical we-media on the whole platform.



(Add wechat: DXSXBB, join readers’ circle and discuss the freshest artificial intelligence technology.)