Chapters

Hide chapters

Swift Apprentice: Beyond the Basics

First Edition · iOS 16 · Swift 5.8 · Xcode 14.3

Section I: Beyond the Basics

Section 1: 13 chapters
Show chapters Hide chapters

1. Access Control, Code Organization & Testing
Written by Eli Ganim

You declare Swift types with properties, methods, initializers and even other nested types. These elements make up the interface to your code or the API (Application Programming Interface).

As code grows in complexity, controlling this interface becomes an essential part of software design. You may wish to create methods that serve as “helpers” to your code or properties that keep track of internal states you don’t want as part of your code’s interface.

Swift solves these problems with a feature area known as access control, which lets you control your code’s viewable interface. Access control enables you, the library author, to hide implementation complexity from users.

This hidden internal state is sometimes called the invariant, which your public interface should always maintain. Preventing direct access to the internal state and keeping the invariant valid is a fundamental software design concept known as encapsulation. In this chapter, you will learn what access control is, the problems it solves, and how to apply it.

Problems Introduced by Lack of Access Control

Imagine for a moment you are writing a banking library. This library serves as the foundation for your customers (other banks) to write their banking software.

In a playground, start with the following protocol:

/// A protocol describing core functionality for an account
protocol Account {
  associatedtype Currency

  var balance: Currency { get }
  func deposit(amount: Currency)
  func withdraw(amount: Currency)
}

This code contains Account, a protocol that describes what any account should have — the ability to deposit, withdraw, and check the balance of funds.

Now add a conforming type with the code below:

typealias Dollars = Double

/// A U.S. Dollar based "basic" account.
class BasicAccount: Account {

  var balance: Dollars = 0.0

  func deposit(amount: Dollars) {
    balance += amount
  }

  func withdraw(amount: Dollars) {
    if amount <= balance {
      balance -= amount
    } else {
      balance = 0
    }
  }
}

This conforming class, BasicAccount, implements deposit(amount:) and withdraw(amount:) by simply adding or subtracting from the balance (typed in Dollars, an alias for Double). Although this code is very straightforward, you may notice a slight issue. The balance property in the Account protocol is read-only — in other words, it only has a get requirement.

However, BasicAccount implements balance as a variable that is both readable and writeable.

Nothing can prevent other code from directly assigning new values for balance:

// Create a new account
let account = BasicAccount()

// Deposit and withdraw some money
account.deposit(amount: 10.00)
account.withdraw(amount: 5.00)

// ... or do evil things!
account.balance = 1000000.00

Oh no! Even though you carefully designed the Account protocol to only be able to deposit or withdraw funds, the implementation details of BasicAccount make it possible for outside to change the internal state arbitrarily.

Fortunately, you can use access control to limit the scope at which your code is visible to other types, files or even software modules!

Note: Access control is not a security feature protecting your code from malicious hackers. Instead, it lets you express intent by generating helpful compiler errors if a user attempts directly access implementation details that may compromise the invariant and, therefore, correctness.

Introducing Access Control

You can add access modifiers by placing a modifier keyword in front of a property, method or type declaration.

Add the access control modifier private(set) to the definition of balance in BasicAccount:

private(set) var balance: Dollars

The access modifier above is placed before the property declaration and includes an optional get/set modifier in parentheses. In this example, the setter of balance is made private.

You’ll cover the details of private shortly, but you can see it in action already: your code no longer compiles!

By adding private to the property setter, the property becomes inaccessible to the consuming code.

This example demonstrates the fundamental benefit of access modifiers: they restrict access to code that needs or should have access to and prevent access to code that doesn’t need it. Effectively, access control allows you to control the code’s accessible interface while defining whatever properties, methods or types you need to implement the behavior you want.

The private modifier used in the brief example above is one of several access modifiers available to you in Swift:

  • private: Accessible only to the defining type, all nested types and extensions on that type within the same source file.
  • fileprivate: Accessible from anywhere within the source file in which it’s defined.
  • internal: Accessible from anywhere within the module in which it’s defined. This level is the default access level. If you don’t write anything, this is what you get.
  • public: Accessible from anywhere that imports the module.
  • open: The same as public, with the additional ability granted to override the code in another module.

Next, you will learn more about these modifiers, when to use them, and how to apply them to your code.

private

The private access modifier restricts access to the entity it is defined in and any nested type within it — also known as the “lexical scope”. Extensions on the type within the same source file can also access the entity.

To demonstrate, continue with your banking library by extending the behavior of BasicAccount to make a CheckingAccount:

class CheckingAccount: BasicAccount {
  private let accountNumber = UUID().uuidString

  class Check {
    let account: String
    var amount: Dollars
    private(set) var cashed = false

    func cash() {
      cashed = true
    }

    init(amount: Dollars, from account: CheckingAccount) {
      self.amount = amount
      self.account = account.accountNumber
    }
  }
}

CheckingAccount has an accountNumber declared as private. CheckingAccount also has a nested type Check that can read the private value of accountNumber in its initializer.

Note: In this example, the UUID type provides a unique account number. This class is part of the Foundation module, so don’t forget to import it!

Checking accounts should be able to write and cash checks as well. Add the following methods to CheckingAccount:

func writeCheck(amount: Dollars) -> Check? {
  guard balance > amount else {
    return nil
  }

  let check = Check(amount: amount, from: self)
  withdraw(amount: check.amount)
  return check
}

func deposit(_ check: Check) {
  guard !check.cashed else {
    return
  }

  deposit(amount: check.amount)
  check.cash()
}

While CheckingAccount can still make necessary deposits and withdrawals, it can now also write and deposit checks! The method writeCheck(amount:) verifies a sufficient balance before withdrawing the amount and creating a check. deposit(_:) will not deposit an already cashed check.

Give this code a try in your playground by having John write a check to Jane:

// Create a checking account for John. Deposit $300.00
let johnChecking = CheckingAccount()
johnChecking.deposit(amount: 300.00)

// Write a check for $200.00
let check = johnChecking.writeCheck(amount: 200.0)!

// Create a checking account for Jane, and deposit the check.
let janeChecking = CheckingAccount()
janeChecking.deposit(check)
janeChecking.balance // 200.00

// Try to cash the check again. Of course, it had no effect on
// Jane’s balance this time :]
janeChecking.deposit(check)
janeChecking.balance // 200.00

Of course, this code works great; the real story is what this code can’t do. Remember that access control lets you control the interface to your code. Look at what the autocomplete window shows as the interface for CheckingAccount:

The accountNumber is treated as an implementation detail of CheckingAccount and isn’t visible to client code. Likewise, Check makes the setter for cashed private and requires consumers to use cash() instead:

This interface gives Check a way for consumers to mark a check as deposited, but not the other way around! In other words, it is not possible to un-cash a check.

Finally, even though accountNumber was not visible on CheckingAccount, the number is made accessible by anyone holding a Check:

While the account property got its value from the CheckingAccount, that’s another implementation detail. The important thing is that access modifiers let the code shape its interface regardless of the code used to implement it.

Playground Sources

Before jumping into the rest of this chapter, you’ll need to learn a new Swift playground feature: source files.

In Xcode, make sure the Project Navigator is visible by going to View\Navigators\Show Project Navigator. Under the playground tree, look for a slightly dimmed folder named Sources:

Right-click on the folder, select New File, and name the file Account.swift. Move the Account protocol, the BasicAccount class, and the Dollars typealias to this file.

Create one more source file and name it Checking.swift. Move CheckingAccount into this file.

That’s it! The critical thing to note about the Sources folder is that Xcode treats the code in it as a separate module.

You can comment out the rest of the code in your playground for now. It won’t be able to “see” the code you just moved until later in this chapter.

fileprivate

Closely related to private is fileprivate, which permits access to any code written in the same file as the entity instead of the same lexical scope and extensions within the same file that private provides. You’ll use the two new files you just created to try this out!

Right now, nothing prevents a haphazard coder who doesn’t read the documentation from creating a Check on their own. In your safe code, you want a Check to only originate from CheckingAccount so that it can keep track of balances.

In the Check class, try adding the private modifier to the initializer:

private init(amount: Dollars, from account: CheckingAccount) { //...

While this prevents incorrect code from creating a Check, you’ll notice it also prevents CheckingAccount from creating one as well. private entities can be accessed from anything within lexical scope. Still, in this case, CheckingAccount is one step outside the scope of Check. Fortunately, this is where fileprivate is very useful.

Replace the initializer instead with fileprivate:

fileprivate init(amount: Dollars, from account: CheckingAccount) { //...

Great! Now CheckingAccount can still write checks, but you can’t create them from anywhere else.

The fileprivate modifier is ideal for code that is “cohesive” within a source file; that is, code that is closely related or serves enough of a common purpose to have shared but protected access. Check and CheckingAccount are examples of two cohesive types.

internal, public and open

With private and fileprivate, you could protect code from being accessed by other types and files. These access modifiers modified access from the default access level of internal.

The internal access level means that an entity can be accessed from anywhere within the software module in which it’s defined. So far, you’ve written all of your code in a single playground file, which means it’s all been in the same module.

When you added code to the Sources directory in your playground, you effectively created a module that your playground consumed. The way playgrounds work in Xcode, all files in the Sources directory are part of one module, and everything in the playground is another module that consumes the module in the Sources folder.

internal

Back in your playground, uncomment the code that handles John writing checks to Jane:

// Create a checking account for John. Deposit $300.00
let johnChecking = CheckingAccount()
johnChecking.deposit(amount: 300.00)
// ...

CheckingAccount has no specified access modifier and is treated as internal, making it inaccessible to the playground.

The result is that Swift displays a build error when trying to use the CheckingAccount type.

To remedy this, you must learn about the public and open access modifiers.

Note: Because internal is the default access level, you never need to explicitly declare your code internal. Whether you use the internal keyword in your definitions is a matter of style and preference.

public

To make CheckingAccount visible to your playground, you must change the access level from internal to public. An entity that is public can be seen and used by code outside the module in which it’s defined.

Add the public modifier to class CheckingAccount:

public class CheckingAccount: BasicAccount {

You’ll also need to add public to BasicAccount since CheckingAccount subclasses it:

public class BasicAccount: Account

The playground will now recognize CheckingAccount, yet you’re still unable to instantiate it.

While the type itself is now public, its members are still internal and unavailable outside the module. You’ll need to add public modifiers to all the entities you want to be part of your module’s interface.

Start by adding a public initializer to BasicAccount and CheckingAccount:

// In BasicAccount:
public init() { }

// In CheckingAccount:
public override init() { }

Next, in BasicAccount, add public to balance, deposit(amount:) and withdraw(amount:). You’ll also need to make the Dollars typealias public, as this typealias appears in public methods.

Finally, in CheckingAccount, add public to writeCheck(amount:), deposit(_:) and class Check. Save all files. You’ll find that everything builds and runs!

Note: Even though BasicAccount adopts Account, you may notice that the playground can’t see Account, nor does it know that BasicAccount conforms to it. Protocol conformance will be invisible to consuming modules if the protocol itself is not accessible.

open

Now that CheckingAccount and its public members are visible to the playground, you can use your banking interface as designed.

Well — almost! The banking library should provide a set of standard accounts such as checking accounts and be open to extensibility for any special kind of account a bank may have.

In your playground, create an interest-accumulating SavingsAccount that subclasses BasicAccount:

class SavingsAccount: BasicAccount {
  var interestRate: Double

  init(interestRate: Double) {
    self.interestRate = interestRate
  }

  func processInterest() {
    let interest = balance * interestRate
    deposit(amount: interest)
  }
}

While BasicAccount is declared public and is accessible to the playground, Swift will show a build error when trying to subclass BasicAccount:

You can override a class, method, or property from another model by declaring it open. Go to the file Account.swift and replace the public access modifier for class BasicAccount with open:

open class BasicAccount: Account { //..

Do you see it all coming together? The interfaces you’ve crafted using public and open permit subclassing of BasicAccount to provide new types of accounts. withdraw(amount:) and deposit(amount:), because they’re public, can be used by those subclasses. The implementations of withdraw(amount:) and deposit(amount:) are safe from being overridden because they’re only public, not open!

Imagine if you could override withdraw(amount:) and deposit(amount:):

override func deposit(amount: Dollars) {
    // LOL
    super.deposit(amount: 1_000_000.00)
}

This capability presents a problem!

If you’re creating a library, you often want to restrict the ability to override methods and properties so you can avoid otherwise surprising behavior. The open access modifier lets you explicitly control what other modules do to your code.

Mini-exercises

  1. Create a struct Person in a new Sources file. This struct should have first, last and fullName properties readable but not writable by the playground.
  2. Create a similar type, except make it a class and call it ClassyPerson. In the playground, subclass ClassyPerson with class Doctor and make a doctor’s fullName print the prefix "Dr.".

Organizing Code Into Extensions

A theme of access control is the idea that your code should be loosely coupled and highly cohesive. Loosely coupled code limits how much one entity knows about another, making different parts of your code less dependent on others. As you learned earlier, highly cohesive code helps closely related code work together to fulfill a task.

Swift features such as access modifiers, when used with extensions, can help you both organize your code and encourage good software design.

Extensions by Behavior

An effective strategy in Swift is to organize your code into extensions by behavior. You can even apply access modifiers to extensions themselves, which will help you categorize entire code sections as public, internal or private.

Begin by adding some basic fraud protection to CheckingAccount. Add the following properties to CheckingAccount:

private var issuedChecks: [Int] = []
private var currentCheck = 1

These will keep track of checks written by the checking account.

Next, add the following private extension:

private extension CheckingAccount {
  func inspectForFraud(with checkNumber: Int) -> Bool {
    issuedChecks.contains(checkNumber)
  }

  func nextNumber() -> Int {
    let next = currentCheck
    currentCheck += 1
    return next
  }
}

CheckingAccount can use these two methods to determine the check number and confirm that the account issued it.

Notably, this extension is marked private. A private extension implicitly denotes all its members as private. These fraud protection tools are features of CheckingAccount only — you don’t want other code incrementing the currentCheck number! Putting these two methods together also connects two related, cohesive methods. It’s clear to yourself and anyone else maintaining the code that these two are cohesive and help solve a common task.

Extensions by Protocol Conformance

Another effective technique is to organize your extensions based on protocol conformance. You’ve already seen this technique used in “Swift Apprentice: Fundamentals — Chapter 17: Protocols”. As an example, make CheckingAccount conform to CustomStringConvertible by adding the following extension:

extension CheckingAccount: CustomStringConvertible {
  public var description: String {
    "Checking Balance: $\(balance)"
  }
}

This extension implements CustomStringConvertible and, more importantly:

  • Makes it obvious description is part of CustomStringConvertible.
  • Doesn’t help conform to other protocols.
  • Can easily be removed without doing collateral damage to the rest of CheckingAccount.
  • It’s easy to understand!

available()

If you look at SavingsAccount, you’ll notice that you can abuse processInterest() by calling it multiple times and repeatedly adding interest to the account. You can add a PIN to the account to make this function more secure.

Add a pin property to SavingsAccount, and ensure the initializer and processInterest() method take this PIN as a parameter. The class should look like this:

class SavingsAccount: BasicAccount {
  var interestRate: Double
  private let pin: Int

  init(interestRate: Double, pin: Int) {
    self.interestRate = interestRate
    self.pin = pin
  }

  func processInterest(pin: Int) {
    if pin == self.pin {
      let interest = balance * interestRate
      deposit(amount: interest)
    }
  }
}

You’re thrilled with the new layer of security. However, after you send this updated code to the bank, you get angry phone calls. The bank’s code now doesn’t compile because it used your old SavingsAccount class.

To prevent breaking code that uses the old implementation, you need to deprecate the code rather than replace it. Luckily, Swift has built-in support for this.

Bring back the old implementation of the initializer and processInterest(), and add this line of code before the initializer:

@available(*, deprecated, message: "Use init(interestRate:pin:) instead")

And this line of code before processInterest():

@available(*, deprecated, message: "Use processInterest(pin:) instead")

Now, these methods still work as expected; however, Xcode generates a warning with your custom message when someone tries to use them:

The asterisk in the parameters denotes which platforms are affected by this deprecation. It accepts the values *, iOS, iOSMac, tvOS or watchOS. The second parameter details whether this method is deprecated, renamed or unavailable.

Opaque Return Types

Imagine you need to create a public API for users of your banking library. You must make a function called createAccount to create and return a new account. One of the requirements of this API is to hide implementation details so that clients are encouraged to write generic code. It means you shouldn’t expose the type of account you’re creating, be it a BasicAccount, CheckingAccount or SavingsAccount. Instead, you’ll return some instance that conforms to the protocol Account.

To enable that, you must first make the Account protocol public. Open Account.swift and add the public modifier before protocol Account. Now go back to your playground and insert this code:

func createAccount() -> Account {
  CheckingAccount()
}

You’ll notice you get an error:

To solve this, you can add the keyword some before the return type so that it would look like this:

func createAccount() -> some Account {
  CheckingAccount()
}

This code is an opaque return type, and it lets the function decide what kind of Account it wants to return without exposing the class type.

You’ll learn more about this feature in “Chapter 11: Advanced Protocols & Generics”.

Swift Package Manager

Another powerful way to organize your code is to use Swift Package Manager, or SwiftPM for short. SwiftPM lets you “package” your module so that you or other developers can easily use it in their code. For example, a module that implements the logic of downloading images from the web is useful in many projects. Instead of copying & pasting the code to all your projects needing image downloading functionality, you could import and reuse this module.

Swift Package Manager is out of scope for this book; however, you can read more about it here: https://swift.org/package-manager/.

Testing

Imagine new engineers join your team to work on your banking library. These engineers are tasked with updating the SavingsAccount class to support taking loans. For that, they will need to update the basic functionality of the code you’ve written. This change is risky since they’re unfamiliar with the code, and their changes might introduce bugs to the existing logic. An excellent way to prevent this from happening is to write unit tests.

Unit tests are pieces of code that test existing code to ensure it works as expected. For example, you might write a test that deposits $100 to a new account and then verifies the balance is indeed $100.

It might sound like overkill at first, but when many engineers are working on a codebase or going back to make changes to the code you wrote long ago, unit tests help you verify that you don’t break anything.

Creating a Test Class

To write unit tests, you first need to import the XCTest framework. Add this at the top of the playground:

import XCTest

Next, you need to create a new class that’s a subclass of XCTestCase:

class BankingTests: XCTestCase {
}

Writing Tests

Once you have your test class ready, it’s time to add some tests. Tests should cover the core functionality of your code and some edge cases. The acronym FIRST describes a concise set of criteria for useful unit tests. Those criteria are:

  • Fast: Tests should run quickly.
  • Independent/Isolated: Tests should not share state.
  • Repeatable: You should obtain the same results every time you run a test.
  • Self-validating: Tests should be fully automated, and the output should be either “pass” or “fail”.
  • Timely: Ideally, write tests before writing the code they test (Test-Driven Development).

Adding tests to a test class is super easy - just add a function that starts with the word test, takes no arguments and returns nothing.

func testSomething() {
}

Congratulations! You’ve just written your first test.

To run your tests in the playground, add this at the bottom, outside of the BankingTests class.

BankingTests.defaultTestSuite.run()

Now run the playground, and you’ll see something similar to this printed to the console:

Test Suite 'BankingTests' started at ...
Test Case '-[__lldb_expr_2.BankingTests testSomething]' started.
Test Case '-[__lldb_expr_2.BankingTests testSomething]' passed (0.837 seconds).
Test Suite 'BankingTests' passed at ...
   Executed 1 test, with 0 failures (0 unexpected) in 0.837 (0.840) seconds

The test passed, which is unsurprising since it currently does nothing.

XCTAssert

XCTAssert functions ensure your tests meet certain conditions. For example, you can verify that a certain value is greater than zero or that an object isn’t nil. Here’s an example of how to check that a new account starts with a zero balance. Replace the testSomething method with this:

func testNewAccountBalanceZero() {
  let checkingAccount = CheckingAccount()
  XCTAssertEqual(checkingAccount.balance, 0)
}

The method XCTAssertEqual verifies that the two parameters are equal, or else it fails the test. Note how the name of the test explicitly states what it tests.

If you run your playground now, this should appear in your console:

Test Case '-[__lldb_expr_4.BankingTests testNewAccountBalanceZero]' started.
Test Case '-[__lldb_expr_4.BankingTests testNewAccountBalanceZero]' passed (0.030 seconds).

Awesome, your test is passing! If someone makes changes that inadvertently cause new accounts to start with a balance other than zero, the test will fail. Why not test it? Open the file Account.swift and find this line:

public private(set) var balance: Dollars = 0.0

Replace the 0.0 with 1.0. Now run the test in your playground, and you should see this printed to the console:

error: -[BankingTests testNewAccountBalanceZero] : XCTAssertEqual failed: ("1.0") is not equal to ("0.0")

You can see the test fails, and it even tells you why it failed! This functionality is the real power of unit tests. From now on, tests will protect your accounts code from this mistake.

Now go ahead and return the variable balance to 0.0 and then add one more test:

func testCheckOverBudgetFails() {
    let checkingAccount = CheckingAccount()
    let check = checkingAccount.writeCheck(amount: 100)
    XCTAssertNil(check)
}

Can you figure out what this test does? It creates a new account and then tries to write a check for $100. The account balance is zero, so this test verifies that writing a check fails and returns nil.

XCTFail and XCTSkip

If a certain pre-condition isn’t met, you can opt to fail the test. For example, suppose you’re writing a test to verify an API only available on iOS 15 and above. In that case, you can fail the test for iOS simulators running older versions with an informative message:

func testNewAPI() {
    guard #available(iOS 15, *) else {
      XCTFail("Only available in iOS 15 and above")
      return
    }
    // perform test
}

Alternatively, instead of failing the test, you can skip it. XCTSkip is a type of Error a test can throw.

func testNewAPI() throws {
    guard #available(iOS 15, *) else {
      throw XCTSkip("Only available in iOS 15 and above")
    }
    // perform test
}

Making Things @testable

When you import Foundation, Swift brings in the public interface for that module. You might create a Banking module for your banking app that imports the public interface. But you might want to check the internal state with XCTAssert. Instead of making things public that really shouldn’t be, you can do this in your test code:

@testable import Banking

This attribute makes your internal interface visible. (Note: Private API remains private.) This technique is an excellent tool for testing, but you should never do this in production code. Always stick to the public API there.

The setUp and tearDown Methods

You’ll notice that both test methods start by creating a new checking account, and it’s likely that many of the tests you’d write will do the same. Luckily there’s a setUp method. This method executes before each test, and its purpose is to initialize the needed state for the tests to run.

Add this at the top of your BankingTests class:

var checkingAccount: CheckingAccount!

override func setUp() {
  super.setUp()
  checkingAccount = CheckingAccount()
}

and remove the line let checkingAccount = CheckingAccount() from both tests.

Just as setUp executes before each test, tearDown runs after every test. It doesn’t matter whether the test passes or fails. It’s good when you need to release resources you acquired or when you need to reset the state of an object. For example, you could reset the balance of the CheckingAccount instance to zero. This code is unnecessary since setUp will initialize new accounts, but you can add it for the sake of the example.

Add this below the setUp method:

override func tearDown() {
  checkingAccount.withdraw(amount: checkingAccount.balance)
  super.tearDown()
}

You can read more about unit tests at https://developer.apple.com/documentation/xctest.

Challenges

Before moving on, here are some challenges to test your knowledge of access control and code organization. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.

Challenge 1: Singleton Pattern

A singleton is a design pattern that restricts the instantiation of a class to one object.

Use access modifiers to create a singleton class Logger. This Logger should:

  • Provide shared, public, global access to the single Logger object.
  • Not be able to be instantiated by consuming code.
  • Have a method log() that will print a string to the console.

Challenge 2: Stack

Declare a generic type Stack. A stack is a LIFO (last-in-first-out) data structure that supports the following operations:

  • peek: returns the top element on the stack without removing it. Returns nil if the stack is empty.
  • push: adds an element on top of the stack.
  • pop: returns and removes the top element on the stack. Returns nil if the stack is empty.
  • count: returns the size of the stack.

Ensure that these operations are the only exposed interface. In other words, additional properties or methods needed to implement the type should not be visible.

Challenge 3: Character Battle

Utilize something called a static factory method to create a game of Wizards vs. Elves vs. Giants.

Add a file Characters.swift in the Sources folder of your playground.

To begin:

  • Create an enum GameCharacterType that defines values for elf, giant and wizard.
  • Create a protocol GameCharacter that inherits from AnyObject and has properties name, hitPoints and attackPoints. Implement this protocol for every character type.
  • Create a struct GameCharacterFactory with a single static method make(ofType: GameCharacterType) -> GameCharacter.
  • Create a global function battle that pits two characters against each other — with the first character striking first! If a character reaches 0 hit points, they have lost.

Hints:

  • The playground should not be able to see the concrete types that implement GameCharacter.
  • Elves have 3 hit points and 10 attack points. Wizards have 5 hit points and 5 attack points. Giants have 10 hit points and 3 attack points.
  • The playground should know none of the above!

In your playground, you should use the following scenario as a test case:

let elf = GameCharacterFactory.make(ofType: .elf)
let giant = GameCharacterFactory.make(ofType: .giant)
let wizard = GameCharacterFactory.make(ofType: .wizard)

battle(elf, vs: giant) // Giant defeated!
battle(wizard, vs: giant) // Giant defeated!
battle(wizard, vs: elf) // Elf defeated!

Key Points

  • Access control modifiers are private, fileprivate, internal, public and open. The internal access level is the default.
  • Modifiers control your code’s visible interface and can hide complexity.
  • private and fileprivate protect code from being accessed by code in other types or files, respectively.
  • public and open allow code access from another module. The open modifier additionally lets you override from other modules.
  • When you apply access modifiers to extensions, all members of the extension receive that access level.
  • Extensions that mark protocol conformance cannot have access modifiers.
  • The keyword available can be used to evolve a library by deprecating APIs.
  • You use unit tests to verify your code works as expected.
  • @testable import lets you test internal API.
  • Adding methods beginning with test to your XCTestCase derived test class will automatically add the method to your suite of unit tests.
  • XCTAssertEqual lets you check invariants in your unit tests.
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.