If you've worked with SwiftUI you've most likely come across property wrappers, @State, @ObservableObject are both implementations of property wrappers which are heavily used within the framework.

The @ syntax is likely all to you familiar to you (more so if you've worked with objc); we use @IBAction and @IBOutlet to reference our UI in code, things like @discardableResult to ignore return types, now there's one more @ implementation to keep track of: property wrappers, but what are they, what do they do and how are they useful? In this article I hope to answer these questions and have you writing your own!


Property wrappers allow us to apply generalised functionality when defining our properties in swift. In practice this means we can perform operations on our types as they're defined, removing the need to call extra functions in our code to perform common tasks.

Let's look at an example: We have an app that loads user generated images from the server, we want to display these as a 300x300 square and place them inside a UIImageView.


Defining a Property Wrapper

Property wrappers are simply classes or structs defined using the @propertyWrapper attribute. The only requirement for a property wrapper is that it implements the wrappedValue variable.

To keep this property wrapper generic we want to be able to pass in the size for our cropped image, just in case our designer decides to change the size from 300 to 100. When defining a property wrapper, swift will pass the variable being initialised into the wrappedValue argument of our initialiser, so if we create an initialiser init(wrappedValue: UIImage, sqSize: Int) we only need to pass in the sqSize argument when we instantiate a variable implementing this functionality. Hopefully this will be clearer as we go.

Add a new file called Crop.swift with the following code.

// 1
@propertyWrapper
struct Crop {
    
    // 2
    private var value: UIImage = UIImage()
    private let sqSize: Int
    
    // 3
    var wrappedValue: UIImage {
        get { value }
        
        set {value = newValue}
    }
    
    // 4
    init(wrappedValue: UIImage, sqSize: Int) {
        self.sqSize = sqSize
        self.wrappedValue = wrappedValue
    }
}
  1. We tell swift our struct is a property wrapper using the @propertyWrapper attribute.
  2. Define our variables, in this case the image we want to process and the crop size.
  3. Remember our requirement to implement wrappedValue? Define wrappedValue here, it can be any type you need it to be. We'll make it a computed property so we can perform our image processing later.
  4. Define out initialiser and set our variables.

This is enough to get our property wrapper up and running, we can now define an image like so

@Crop(sqSize: 300) var image = UIImage(named:"my-img")

but nothing will happen.

Notice the function signature when using our property wrapper doesn't include wrappedValue even though we require it in our init method. When our variable is defined wrappedValue is automatically passed in, so the value to the right of the = operator is our wrappedValue parameter.

Now let's do something with it! Replace the code in set{} with the following

        set {
            let centerX = Int(newValue.size.width / 2) - (sqSize / 2)
            let centerY = Int(newValue.size.height / 2) - (sqSize / 2)
            
            let imageRect = CGRect(x: centerX, 
            	y: centerY, 
		width: sqSize, 
            	height: sqSize)
            if let cgImage = newValue.cgImage {
                let cgImageRef = cgImage.cropping(to: imageRect)!
                value = UIImage(cgImage: cgImageRef)
            }
        }

That's all we need for the property wrapper; when the setter is called on our wrappedValue we perform the crop by finding the center x and center y of the image,  then we create a new rect using the new coordinates (plus our sqSize parameter) and call the CGImage function cropping(to: CGRect) to perform our crop.  Now we have this let's use it in practice. Download the below image (right click -> Save As) and add it to Assets.xcassets in your project.

Create a reference to this in ViewController.swift

class ViewController: UIViewController { 

    @Crop(sqSize: 400) 
    var image = UIImage(named: "swift-compiled-logo")!
    ...

We're going to add a UIImageView in code  and add it to our view object, you can also do this in an xib or Storyboard if you'd prefer. Add the following function to ViewController.swift.

    func loadImageView() {
        let imageView = UIImageView(image: self.image)
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .center
        self.view.addSubview(imageView)
        
        NSLayoutConstraint.activate([
            imageView.centerYAnchor.
	        constraint(equalTo: view.centerYAnchor),
            imageView.centerXAnchor.
            	constraint(equalTo: view.centerXAnchor)
        ])
        
        self.view.layoutIfNeeded()
    }

Now replace viewDidLoad() with the following.

    override func viewDidLoad() {
        super.viewDidLoad()
        loadImageView()
    }

That's it! If you build and run the app you should see the following.

Pretty cool, with the addition of property wrappers we've processed an image as we define it, we can now use this generic functionality for any image we have in our app.


That's it for now, if you haven't already i'd recommend looking at the swift documentation on property wrappers to find out what else they're capable of, this was just a small taste but should be everything you need to get started, the code for this blog post can be found on github.

Thanks for reading! ✌️