[Swift] Property wrappers to the rescue!
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
@propertyWrapper
must be defined as an attributewrappedValue
must 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
}
}
wrappedValue
ofget
, adjust theset
given throughvalue
capitalized
Returns 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
}
}
@propertyWrapper
The logic to implement that method without
import Foundation
struct WrappedUserModel {
var firstName: CapitalizedWrapper
var lastName: CapitalizedWrapper
}
- In actual use, one more step
value
is 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
}
}
projectedValue
If 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)
}
}
}
Codable
After 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
Codable
protocol
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.
static
You 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.
projectedValue
returns the keywrappedValueimage
returns 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
}
static
Easily 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
}
}
projectedValue
Since it returns as ,$
it can be accessed throughColor
You 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
projectedValue
If 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
, @Binding
etc., 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