Modern Swift API Design

Description: Every programming language has a set of conventions that people come to expect. Learn about the patterns that are common to Swift API design, with examples from new APIs like SwiftUI, Combine, and RealityKit. Whether you're developing an app as part of a team, or you're publishing a library for others to use, find out how to use new features of Swift to ensure clarity and correct use of your APIs.

The most important goal as an API designer: Clarity at the point of use.

@DynamicMemberLookup

It can be used to expose all the properties of a hidden object as if they belonged to the main object.

@dynamicMemberLookup
struct Material { 
  public var roughness: Float
  public var color: Color 

  private var _texture: Texture 

  public subscript<T>(
  	dynamicMember keyPath: ReferenceWritableKeyPath<Texture, T>
  ) -> T { 
  	get { _texture[keyPath: keyPath] }
  	set { 
      if !isKnownUniquelyReferenced(&_texture) { 
      	_texture = Texture (copying: _texture) 
      }
      _texture[keyPath: keyPath] = newValue 
    }
  }
}

@PropertyWrapper

  • Provides similar benefits to the built-in lazy
  • Eliminates boilerplate.
  • Documents semantics at the point of definition.

Property Wrappers can be seen as generic getters and setters:

// Implementing a Property Wrapper 

@propertyWrapper 
public struct LateInitialized<Value> { 
	private var storage: Value?

  public init() { 
    storage = nil 
  }

  public var value: Value {
  	get { 
      guard let value = storage else {
      	fatalError("value has not yet been set!") 
      } 
      return value 
    }
    set { 
      storage = newValue 
    }
  }
}
// Uses of property wrappers expand into a stored property and a computed property public 

public struct MyType {
  @LateInitialized public var text: String 
}

// Compiler-synthesized code 👇🏻
public struct MyType {
  var $text: LateInitialized<String> = LateInitialized<String>()

  public var text: String { 
  	get { $text. value } 
  	set { $text. value = newValue }
  }
}

We can even pass values to the property initializer:

// Defensive Copying 

@propertyWrapper 
public struct DefensiveCopying<Value: NSCopying> {
	private var storage: Value 

  public init(initialValue value: Value) {
	  storage = value. copy () as! Value 
  }

  public var value: Value {
  	get { storage } 
  	set { 
      storage = newValue.copy() as! Value 
    }
  }
}
// Initializing the backing storage property: 

public struct MyType { 
  @DefensiveCopying(withoutCopying: UIBezierPath())
  public var path: UIBezierPath 
}

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.