Chapters

Hide chapters

Expert Swift

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

14. API Design Tips & Tricks
Written by Shai Mishali

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Welcome to the last chapter of the book!

You’ve spent most of your time in this book diving into specific topics, learning how they work, writing code to sharpen your instincts around them and working through real-life examples.

Although using Swift and all its incredible capabilities is a wonderful skill to have, it doesn’t help much without actually shipping code with it. More often than not, though, you’ll find yourself creating code that’s used not only by you but also by your team, or even other teams if you’re creating an open-source project.

In those cases, just knowing Swift as a language isn’t enough, and neither is just practicing a specific language feature. That’s why this chapter is going to be a bit different.

In this chapter, you’ll explore a few different topics. Each of these isn’t directly related to the previous one, but they all tie into enhancing your skillset and intuition for designing great APIs. You may freely explore each of these individual topics based on your interest:

  • What developers consider a good API.
  • How to separate and encapsulate your implementation details from your public API using access levels.
  • Powerful language features with examples you can leverage for your APIs, including examples from Swift itself: Literals, Dynamic Member Lookup, Dynamic Callable, Property Wrappers and others.
  • Documenting your APIs using Swift’s powerful markup syntax.
  • Finally, a few important concepts and ideas related to the process of shipping your API to the world.

This chapter will also be less code-heavy than previous ones and won’t require you to copy-paste code or run any project. It’s more of a philosophical and exploratory chapter that doesn’t cover one specific topic. You’re welcome to stop at any point to experiment with a specific idea in Xcode. We’re opening a discussion together, me and you, to hopefully help inspire you with some fresh new ideas and ways of thinking about API design.

Note: API design is a highly opinionated topic. As such, you should take everything in this chapter with a grain of salt and mainly as an inspiration rather than a single truth. Take the portions that make sense for your use case and taste, and discard the ones that don’t.

What do developers want?

Wow, that’s a tough question. I wish I knew, really; it might’ve helped bring this book even further! And yet, some things are obvious and universal for developers.

The first time a developer interacts with a new piece of code, they have certain hopes and expectations. It doesn’t matter if that code is your app and the person is a new developer on your team, or if it’s an open-source library you shared with the community and the person is a new consumer of it.

In essence, developers look for some characteristics that make an API “feel good”:

  • It should be obvious: This means using the API “makes sense” for a developer and that your API’s design aligns with their expectations. For example, a Zip class might have an expected method called unzip or extract rather than pullContentsTo(path:), which is not common or obvious.

  • It should be well-documented: People often say Swift is a self-documenting language and, as such, good APIs don’t need documentation. I personally disagree with that statement. Even though Swift is a very expressive language, documenting the public portions of your API is language-agnostic and crucial to help self-exploration, reduce ambiguity and make sure your intention is clear to the consumer. It would be good for internal APIs to also be documented well, but public-facing documentation is the bare minimum.

  • Reduces mental load: This ties to being obvious but is a bit broader and more open to interpretation. Some things that fall into this category include trying to use the minimum obvious naming for APIs, using prior art if a convention exists in the domain you’re developing for (you might use View instead of Screen if it makes sense in that domain, for example) and using abstractions that are simple to the consumer. As the Swift API guidelines sharply note: “Don’t surprise an expert. Don’t confuse a beginner.”

  • Being modern: This point touches a wide range of topics. Using proper language-specific conventions, leveraging the correct language features a consumer would expect to see and inspiring proper usage and creativity from the consumer are all small parts of this point.

What is the core of your API?

When the raywenderlich.com team works on a tutorial or a book, it always asks: “What is the most important 80 percent of this topic?”

Using access levels properly

Access levels define which entities of your code are exposed and to which scopes they’re exposed. Swift specifically provides a relatively fine-grained set of five access levels (from most permissive to most restrictive): open, public, internal, fileprivate and private.

Internal by default

Every piece of code that doesn’t have an explicit access level set is internal by default. This means other files in the same module can access it, but files outside of the module can’t.

// In Module2
func getThings() {

}

// In Module1
Module2.getThings()
// Error: Module 'Module2' has no member named 'getThings'

The public world

In cases where internal doesn’t suffice, you’ll want to use either open or public. These levels mean the same thing in essence: This entity is available to every piece of code inside or outside the module it was defined in.

// In Module1
public class AmazingClass {
  public init() { }
}

open class WonderfulClass {
  public init() { }
}

// In Module2
AmazingClass() // OK
WonderfulClass() // OK
class AmazingSubclass: AmazingClass { } // Error: Cannot inherit from non-open class 'AmazingClass' outside of its defining module

class WonderfulSubclass: WonderfulClass { } // OK

Keeping it private

With public and open representing the more permissive side of the possible access levels, it’s also critical to properly limit access to the private portions of your code. These pieces of code are often implementation details and don’t concern consumers of your public interface or even your internal interfaces.

struct Encrypter<Encrypted> {
  let base: Encrypted
}
struct Person {
  let id: UUID
  let name: String
  private let password: String
}
extension Encrypter where Encrypted == Person {
  func encrypt() -> String {
    sha256(base.password)
  }
}

Finally…

No, this isn’t the end of the chapter, but final has a different meaning you should know. I mentioned that public means an entity is public outside of a module but can’t be overridden and subclassed.

final public class Network {
  // Code here...
}

class SpecializedNetwork: Network { } // Error: Inheritance from a final class 'Network'

Exploring your interface

A great feature built into Xcode is the ability to view the generated interface of source files. You used this a bit earlier in this book, in the “Objective-C Interoperability” chapter, but you can use the same capability for Swift files, too.

import Foundation

public struct Student {
  public let id: UUID
  public let name: String
  public let grade: Int
  let previousTests: [Test]
  
  public func sendMessage(_ message: String) throws -> Bool {
    // Implementation
  }
  
  private func expel() throws -> Bool {
    // Implementation
  }
}

struct Test {
  let id: UUID
  let name: String
  let topic: String
}

public struct Student {
  public let id: UUID
  public let name: String
  public let grade: Int
  internal let previousTests: [Test]
  public func sendMessage(_ message: String) throws -> Bool
}

internal struct Test {
  internal let id: UUID
  internal let name: String
  internal let topic: String
}

Language features

This section will focus on some interesting language features you can leverage to improve your API surface, and provides short examples of how API designers and developers commonly use them.

Literals

Literals are a great abstraction to let consumers initialize your types using typed literals, such as String, Bool, Array and many others.

public struct Path: ExpressibleByStringLiteral {
  private let path: String
    
  public init(stringLiteral value: StringLiteralType) {
    self.path = value
  }
    
  public func relativePath(to path: Path) -> Path {
    // Implementation ...
  }
}
// Option 1
Path("/Users/freak4pc/Work/")

// Option 2
let path: Path = "/Users/freak4pc/Work/"
public struct AlphabeticArray<Element: Comparable>: Collection, ExpressibleByArrayLiteral {
  // Additional collection boilerplate here
  let values: [Element]
    
  public init(arrayLiteral elements: Element...) {
    self.values = elements.sorted(by: <)
  }
}

public func presentContacts(_ contacts: AlphabeticArray<String>) {
  print(contacts)
}

presentContacts(["Shai", "Elia", "Ethan"]) // Prints Elia, Ethan, Shai
public struct Headers {
  private let headers: [String: String]
  // Many other pieces of headers-specific functionality
}

extension Headers: ExpressibleByDictionaryLiteral {
  public init(dictionaryLiteral elements: (Header, String)...) {
    self.headers = Dictionary(uniqueKeysWithValues: elements.map { ($0.rawValue, $1) })
  }
  
  public enum Header: String {
    case accept = "Accept"
    case contentType = "Content-Type"
    case authorization = "Authorization"
    case language = "Accept-Language"
    // Additional headers
  }
}
class HTTPRequest {
  func addingHeaders(_ headers: Headers) -> Self {
    // Implementation ...
  }
}

let request = HTTPRequest(...)
  .addingHeaders([.accept: "text/html",
                  .authorization: "Basic freak4pc:b4n4n4ph0n3"])
extension Headers: ExpressibleByArrayLiteral {
  public init(arrayLiteral elements: TypedHeader...) {
    self.headers = Dictionary(uniqueKeysWithValues: 
      elements.map(\.value))
  }

  public enum TypedHeader {
    case accept(AcceptType)
    case jwtAuthorization(Token)
    case basicAuthorization(user: String, password: String)
    
    var value: (String, String) {
      switch self {
      case .accept(let type):
        return ("Accept", type)
      case .jwtAuthorization(let token):
        return ("Authorization", "Bearer \(token)")
      case let .basicAuthorization(user, password):
        return ("Authorization", "Basic \(user):\(password)")
      }
    }
  }
}
let request = HTTPRequest(...)
  .addingHeaders([.jwtAuthorization("AmazingToken"),
                  .basicAuthorization(user: "freak4pc",
                                      password: "b4n4n4ph0n3"),
                  .accept("text/html")])
let headersFromDict: Headers = [
  .accept: "text/html",
  .authorization: "Basic freak4pc:b4n4n4ph0n3"
]

let headersFromArray: Headers = [
  .jwtAuthorization("AmazingToken"),
  .basicAuthorization(user: "freak4pc",
                      password: "b4n4n4ph0n3"),
  .accept("text/html")
]

Dynamic member lookup

Dynamic member lookup was initially shipped in Swift 4.2 (SE-0195) and meant to provide a somewhat type-safe way to access arbitrary string keys for a type. This was relatively helpful to bridge dynamic languages, such as Python, or create proxy APIs. Unfortunately, it lacked real type-safety when it came to abstracting existing Swift code as well as providing actual runtime safety.

Wrapping types naturally

It’s quite common to create types that would wrap existing types. An example of this might be trying to create your own SearchBar view that wraps a regular UITextField:

class SearchBar: UIControl {
  private let textField: UITextField
}
extension SearchBar {  
  var isEnabled: Bool {
    get { textField.isEnabled }
    set { textField.isEnabled = newValue }
  }
  
  var keyboardType: UIKeyboardType {
    get { textField.keyboardType }
    set { textField.keyboardType = newValue }
  }
  
  // About 20 more of these ...
}
@dynamicMemberLookup
class SearchBar: UIControl {
  private var textField: UITextField

  subscript<T>(
    dynamicMember keyPath: WritableKeyPath<UITextField, T>
  ) -> T {
    get { textField[keyPath: keyPath] }
    set { textField[keyPath: keyPath] = newValue }
  }
}
let searchBar = SearchBar(...)
searchBar.isEnabled = true
searchBar.returnKeyType = .go
searchBar.keyboardType = .emailAddress
// etc, etc...

Enriching key paths

Exposing or mirroring the key paths of a linked object is extremely useful, but you can return anything you want from the dynamic member subscript method.

@dynamicMemberLookup
struct Reactive<Base> {
  // Additional implementation details...

  subscript<Property>(
    dynamicMember keyPath: WritableKeyPath<Base, Property>
  ) -> Binder<Property> where Base: AnyObject {
    Binder(base) { base, value in
      base[keyPath: keyPath] = value
    }
  }
}
myView.isEnabled // Bool
myView.rx.isEnabled // Binder<Bool>

Dynamic callable

Dynamic callable was introduced in Swift 5 (SE-0216) to provide syntactic sugar when creating wrappers around dynamic languages/calls inside Swift and allows to naturally invoke values as if they’re functions.

@dynamicCallable
struct Command {
  let base: String
  
  init(_ base: String) {
    self.base = base
  }
  
  func dynamicallyCall(withArguments args: [String]) {
    print(#line, base, args.joined(separator: " "))
  }
}

struct Shell {
  static let swift = Command("swift")
}
Shell.swift("--version")
swift --version
subscript(dynamicMember member: String) -> Command {
  Command("\(base) \(member)")
}
Shell.swift.build("--verbose")
swift build --verbose

Property wrappers

Property wrappers, introduced in Swift 5.1 (SE-0258), provide a way to abstract the handling of the get/set accessor portions of properties. Some of the common built-in ones are @Published, @State and @Binding, which you used in the Functional Reactive Programming chapter.

Reusing accessor logic

A property wrapper’s primary goal is encapsulating the get/set accessors for properties, both internally for you as a developer and for other people contributing to your codebase. But also, if this sort of abstraction is powerful outside your module, you might want to make it public.

@propertyWrapper
struct AppStorage<Value> {
  var wrappedValue: Value {
    get { defaults.object(forKey: key) as? Value ?? fallback }
    set { defaults.setValue(newValue, forKey: key) }
  }
  
  private let key: String
  private let defaults: UserDefaults
  private let fallback: Value
  
  init(wrappedValue fallback: Value,
       _ key: String,
       store: UserDefaults = .standard) {
    self.key = key
    self.defaults = store
    self.fallback = fallback
    
    if defaults.object(forKey: key) == nil {
      self.wrappedValue = fallback
    }
  }
}
@AppStorage("counter") var counter = 4
@AppStorage("thing", store: customDefaults) var thing = "hello"
init<R: RawRepresentable>(
  wrappedValue fallback: Value,
  _ key: R,
  store: UserDefaults = .standard
) where R.RawValue == String {
  self.init(wrappedValue: fallback,
            key.rawValue,
            store: store)
}
enum Key: String {
  case counter
  case thing
}

@AppStorage(Key.counter) var counter = 4
@AppStorage(Key.thing, store: customDefaults) var thing = "hi"
@propertyWrapper
struct Clamped<T: Comparable> {
  var wrappedValue: T {
    get { storage }
    set { 
      storage = min(max(range.lowerBound, newValue),
                        range.upperBound)
    }
  }
  
  private var storage: T
  private let range: ClosedRange<T>
  
  init(wrappedValue: T, _ range: ClosedRange<T>) {
    assert(range.contains(wrappedValue))
    self.storage = wrappedValue
    self.range = range
  }
}
struct Patient {
  let id = UUID()
  let name: String
  @Clamped(35...42) var temperature = 37.5
}

var p = Patient(name: "Shai")
p.temperature = 39
// Temperature is unmodified as 39, since it's within range

p.temperature = 100
// Temperature is 42, the maximum value in the range

p.temperature = 20
// Temperature is 35, the minimum value in the range

Layering with projection

A somewhat hidden superpower of property wrappers is their projected value. It’s an auxiliary value you can access for the wrapped property using the $ prefix. This feature is heavily used in Combine and SwiftUI.

@Published var counter = 1
counter // Int
$counter // Publisher<Int, Never>
@propertyWrapper
struct MyPublished<Value> {
  var wrappedValue: Value {
    get { storage.value }
    set { storage.send(newValue) }
  }
  
  var projectedValue: AnyPublisher<Value, Never> {
    storage.eraseToAnyPublisher()
  }

  private let storage: CurrentValueSubject<Value, Never>
  
  init(wrappedValue: Value) {
    self.storage = CurrentValueSubject(wrappedValue)
  }
}
@MyPublished var count = 1

count // Int
$count // AnyPublisher<Int, Never>
class MyViewModel: ObservableObject {
  @Published var counter = 5
}

// In a different class
@ObservedObject var viewModel = MyViewModel()

viewModel // MyViewModel
$viewModel // MyViewModel.Wrapper (which has @dynamicMemberLookup)

viewModel.counter // Int
$viewModel.counter // Binding<Int>

Documenting your code

As mentioned earlier in this chapter, documenting at least the public-facing portions of your code is crucial for new consumers of your code. Documenting your internal code is just as important because a different kind of consumer (developers) will use it later on.

Symbol documentation

Back in Objective-C days, Apple used a variation of Headerdoc for documentation. Luckily, with Swift, you can now use (almost) full-blown markdown to write documentation. Apple calls this Swift-flavored markdown Markup.

/// This method does a thing.
///
/// This can easily become a multi-line comment as well,
/// and span as many lines as possible.
func performThing() { 
  // Implementation
}

/**
 This method does a different thing.

 Multi-line works using these Javadoc-styled delimiters as 
 well, and it's mainly a matter of taste and preference.
 */
func performOtherThing() { 
  // Implementation
}
/// This method does a thing.
///
/// This can easily become a multi-line comment as well,
/// and span as many lines as possible
///
/// - parameter userId: Identifier of user to fetch things for
///
/// - throws: `User.Error` if user doesn't exist or
///           `id` is invalid
///
/// - returns: An array of `Thing`s for the user with
///            the provided ID
func fetchThings(for userId: UUID) throws -> [Thing] {
  // Implementation
}

/// Represents a single node in a linked list
indirect enum LinkedNode<T> {
  /// A node with a value of type `T`
  case value(T)
  
  /// A node with a value of type `T`, linked
  /// to the next node in a linked list
  ///
  /// - note: `next` is simply another case of the
  ///         same indirect `Node` enum
  case link(value: T, next: LinkedNode)
  
  /// The value associated with the current node
  var value: T {
    switch self {
    case .link(let value, _),
         .value(let value):
      return value
    }
  }

  /// The next node, if one exists
  var next: LinkedNode? {
    if case .link(_, let next) = self {
      return next
    }
    
    return nil
  }
}

/// This is a function
///
/// A proper usage of this method is:
///
///     myFunction(
///       a: 1,
///       b: "Hello"
///     )
///
func myFunction(a: Int, b: String) -> Bool {

}
/// This is a function
///
/// - parameter a: A number
/// - parameter b: A String
///
/// A proper usage of this method is:
/// 
/// ```
/// myFunction(
///   a: 1,
///   b: "Hello"
/// )
/// ```
func myFunction(a: Int, b: String) -> Bool {

}

/// This is a function
///
/// It might have a single paragraph, with **bold**
/// words or even _italic text_
/// 
/// - parameters:
///     - a: A number
///     - b: A string
///
/// - returns: A boolean value
///
/// But it might have more points to discuss, such as:
///
///   * Item 1
///   * Item 2
///
/// # H1 header
/// Some descriptive text with ordered list:
///   1. First item
///   2. [Second link item](https://raywenderlich.com)
func myFunction(a: Int, b: String) {
  // Implementation
}

Additional metadata fields

Like parameters, returns or even note, Xcode supports a wide range of metadata fields you can use in your documentation:

uorkic eizhuqz zehrxewcx zovo xioivji jenca wizvoay iwzabriek nih ucrevanizc movu totahl hihu remzmegigh ibnuceeyx amdekzugd blehewwaqoaq kaqmtiqjadeep jicuomot kowyutd MOXOCIP GOGUWAQU FAWXOKI EZJU

Code markers

Aside from symbol-specific documentation, you can also divide your code using code markers. Xcode supports three of these: MARK, TODO and FIXME:

// TODO: - Finalize this class later on
class MyClass {
  // MARK: - Properties
  var a = 33
  var b = "Hello"
  
  // FIXME: - There's some issue here that needs attention
  func badMethod() {
    
  }
}

Publishing to the world

In this section, you’ll explore some ideas and important tidbits about how to release a library or other piece of code to the outside world. It doesn’t matter if you’re open-sourcing your code to the entire world or publishing it as an internal library in your company, some guidelines exist that you should follow to make fellow developers’ lives easier.

Versioning

When writing code for yourself, versioning doesn’t matter much. But as soon as a piece of code is bundled into some reusable dependency and consumed by other developers, versioning becomes quite critical in ensuring consistency and expectability.

Deprecation

In the lifetime of every piece of code, you might need to retire some classes or methods. First, you should almost always bump the major version of your framework if you deprecate a piece of code because it means you’re making a breaking change to your API contract with your consumer.

@available(*, deprecated)
func myOldMethod(value: Int) { 
  // implementation
}

@available(*, deprecated, message: "Don't use me anymore")
func myOldMethod(value: Int) { 
  // implementation
}

@available(*, deprecated, renamed: "myNewMethod(number:)")
func myOldMethod(value: Int) { 
  // implementation
}

Key points

Great work on finishing this chapter!

Where to go from here?

As mentioned at the beginning of this chapter, API design is the subject of many opinions. As such, the best way to gain intuition as to what you like is to learn as much as possible about different perspectives, combined with the official guidelines from Apple, and experiment with different creative options! There is usually more than a single way to expose functionality, and the process of finding the right API is part of the fun of designing code!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now