[Swift] Property wrappers to the rescue!

ganeshrajugalla
5 min readJul 8, 2023

--

property wrapper

  • Types that wrap specific values ​​to add logic
  • A separate layer defining how properties are stored or computed during reading.
  • Types that are familiar within the SwiftUI framework, such as @State , @Binding, @StateObject etc.

implementation method

  • Both structs and classes can be supported
  • @propertyWrappermust be defined as an attribute
  • wrappedValuemust have properties

Implementation of Capitalized propertyWrapper

@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
set {
value = newValue.capitalized
}
get {
return value
}
}

init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
  • wrappedValueof get, adjust the setgiven throughvalue
  • capitalizedReturns the given value
import Foundation

struct UserModel {
@Capitalized var firstName: String
@Capitalized var lastName: String
}
  • To use the property, @just declare that it follows the property through (declarative type)
import Foundation

struct CapitalizedWrapper {
private var _value: String = ""
var value: String {
set {
_value = newValue.capitalized
}

get {
return _value
}
}

init(value: String) {
self._value = value.capitalized
}
}
  • @propertyWrapperThe logic to implement that method without
import Foundation

struct WrappedUserModel {
var firstName: CapitalizedWrapper
var lastName: CapitalizedWrapper
}
  • In actual use, one more step valueis needed to find
let wrappedUser = WrappedUserModel(firstName: CapitalizedWrapper(value: "ganesh"), lastName: CapitalizedWrapper(value: "raju"))
wrappedUser.firstName.value // ganesh

let user = UserModel(firstName: "ganesh", lastName: "raju")
user.firstName // ganesh
@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
set {
value = newValue.capitalized
}
get {
return value
}
}
var projectedValue: Capitalized {
return self
}

init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
  • projectedValueIf it returned itself, $it can be accessed as
user.$firstName
  • $You can access that address through

Implementation of PlainUserDefaults propertyWrapper

@propertyWrapper
struct PlainUserDefaultsBacked<T> {
let key: String
let defaultValue: T
var storage: UserDefaults = .standard
var wrappedValue: T {
get {
let value = storage.value(forKey: key) as? T
return value ?? defaultValue
}
set {
storage.setValue(newValue, forKey: key)
}
}
}
  • It is also possible to utilize property wrappers to store data such as user defaults.
  • The struct declared as generic follows the property wrapper
  • Keys, default values, storage, etc. are included in the structure

Implementation of CodableUserDefaults propertyWrapper

import Foundation

@propertyWrapper
struct CodableUserDefaultsBacked<T: Codable> {
let key: String
let defaultValue: T
var storage: UserDefaults = .standard

var wrappedValue: T {
get {
guard
let data = storage.value(forKey: key) as? Data,
let value = try? JSONDecoder().decode(T.self, from: data) else {
return defaultValue
}
return value
}
set {
let data = try? JSONEncoder().encode(newValue)
storage.setValue(data,forKey: key)
}
}
}
  • CodableAfter receiving a generic that conforms to the protocol, encoding and decoding can be done automatically within the structure.
import Foundation

struct NoteModel: Codable {
let title: String
}
  • A data model that follows a simple Codableprotocol
import Foundation

class UserDefaultsDataSource {
@PlainUserDefaultsBacked(key: "is_first_launch", defaultValue: true)
static var isFirstLaunch: Bool
@PlainUserDefaultsBacked(key: "user_name", defaultValue: "unknown")
static var userName: String
@PlainUserDefaultsBacked(key: "counter", defaultValue: 0, storage: .standard)
static var counter: Int

@CodableUserDefaultsBacked(key: "notes", defaultValue: nil)
static var notes: [NoteModel]?
}
  • A data source class that uses a structure that utilizes the user defaults above.
  • staticYou can access or modify the current user default value through the variable declared as
import UIKit

class PropertyWrapperController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
print(UserDefaultsDataSource.isFirstLaunch) // true
print(UserDefaultsDataSource.userName) // unknown
print(UserDefaultsDataSource.counter) // 0

UserDefaultsDataSource.isFirstLaunch = false
UserDefaultsDataSource.userName = "ganesh"
UserDefaultsDataSource.counter += 1

print(UserDefaultsDataSource.isFirstLaunch) // false
print(UserDefaultsDataSource.userName) // ganesh
print(UserDefaultsDataSource.counter) // 1

print(UserDefaultsDataSource.notes) // nil
UserDefaultsDataSource.notes = [NoteModel(title: "First Note")]
print(UserDefaultsDataSource.notes)
// [NoteModel(title: "First Note")]
UserDefaultsDataSource.notes?.append(NoteModel(title: "Second Note"))
print(UserDefaultsDataSource.notes)
// [NoteModel(title: "First Note"), NoteModel(title: "Second Note")]
}
}
  • In fact, the user default values ​​are continually added each time the view is initialized.

Implementation of ImageAsset propertyWrapper

  • Property wrappers can be applied to use image assets
  • Easy to centrally manage and automate multiple codes
@propertyWrapper
struct ImageAsset {
let key: String
init(_ key: String) {
self.key = key
}

func image(for name: String) -> UIImage {
UIImage(named: name) ?? .init()
}

var projectedValue: String {
return key
}

var wrappedValue: UIImage {
self.image(for: key)
}
}
  • The corresponding image asset structure following the property wrapper receives the key value as an initialization parameter and initializes the key.
  • projectedValuereturns the key
  • wrappedValueimagereturns the given image using the built-in function via the key used for initialization.
enum Asset {
@ImageAsset("menu_icon") static var menuIcon: UIImage
@ImageAsset("search_icon") static var searchIcon: UIImage
@ImageAsset("settings_icon") static var settingsIcon: UIImage
@ImageAsset("plus_icon") static var plusIcon: UIImage
}
  • staticEasily access asset images through Enum that has the structure as
import UIKit

class AssetViewController: UIViewController {
private let imageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = Asset.menuIcon
imageView.image = Asset.plusIcon
imageView.image = Asset.searchIcon
imageView.image = Asset.settingsIcon

}

}

Implementation of Colors propertyWrapper

  • Like images, colors can be easily centrally managed through enum
enum Colors {
static var mainRed: UIColor {
UITraitCollection.current.userInterfaceStyle == .dark ? UIColor.black : UIColor.red
}
}
  • To support the color scheme through enum (without using the light/dark mode of the asset), return the corresponding value through the operation property.
@propertyWrapper
struct Color {
var light: UIColor
var dark: UIColor

var isDark: Bool {
UITraitCollection.current.userInterfaceStyle == .dark
}

var projectedValue: Color { return self }

var wrappedValue: UIColor {
if isDark {
return dark
} else {
return light
}
}
}
  • It’s cleaner if you use a color wrapped in a property wrapper
enum Colors {
@Color(light: .red, dark: .black) static var mainRed2
}
import UIKit

class ColorViewController: UIViewController {
private let imageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Colors.mainRed2
let lightColor: UIColor = Colors.$mainRed2.light
let darkColor: UIColor = Colors.$mainRed2.dark
let usingDarkColor: Bool = Colors.$mainRed2.isDark
}
}
  • projectedValueSince it returns as , $it can be accessed through
  • ColorYou can access the value entered as a parameter when declaring .

organize

  • Wrappers: add functionality to properties
  • Properties: Can be used like unwrapped ( unwrapped) properties

Limit

  • A property can have only one wrapper attribute
  • Property wrappers cannot be overridden in subclasses
  • Cannot be declared in a protocol

characteristic

  • Ease of code reusability and generalization
  • code clean
  • simple to use
  • projectedValueIf the stored value is very transparent: $can be accessed through the mark → can be dangerous because the value itself can be accessed
  • Don’t use it in the same context as a DSL (Domain Specific Language): it hides too much logic inside property wrappers and makes it difficult to communicate with other developers. It also defeats the purpose for which property wrappers were originally created.

If you only encounter given @State, @Bindingetc., you can customize and declare the property wrapper yourself, of course, but you can work on it from the outside, but I admired the function that can reduce unnecessary code and make the code clean.

Source Code : https://github.com/GaneshRajuGalla/Swift/tree/main/PropertyWrapper

By XCoder | LinkedIn | Medium | GitHub

--

--