Structs and classes are two of the primary building blocks of Swift programming. They are both means of grouping together related data and functions into self-contained units of functionality. And when you define a struct or class, you are defining a new type to be used within Swift, just as you used the types you've already worked with, like Int
and String
.
There are many similarities between structs and classes in Swift. Among other similarities, both are able to store values in properties and provide functionality through the use of methods.
They each provide some additional functionality, which is out of scope for this exercise.
Both structs and classes are defined in roughly the same way. They start with the appropriate keyword followed by the type name that is being defined, then the body of the struct/class follows, placed between curly braces.
The body may consist of stored properties, which are defined and behave just like regular constants or variables.
struct CharacterStats {
var health = 0.0
var speed = 0
var strength = 0
}
class GameCharacter {
var stats = CharacterStats()
var characterClass: String?
var name: String?
var active = false
let id = makeRandomID()
}
As noted above, defining a struct or class is just defining a new type. It is just the blueprint for what the values of that type will look like, but it does not actually create any values of that type for you to work with.
In order to create an instance of that type, you need to write the name of the type followed by a pair of parentheses.
let someStats = CharacterStats()
let someCharacter = GameCharacter()
This will create values of these types, where the properties are populated with the default values supplied in the definition. Note that in optional cases like GameCharacter's name
property, unless a value is provided, the property will default to nil, just like defining regular optional types where a value is not immediately provided.
With structs, Swift automatically provides something called a memberwise initializer, where values for the structs properties may be provided inside the parentheses which will override the default values in the definition.
let differentStats = CharacterStats(health: 100.0, speed: 6, strength: 18)
Struct and class properties can be accessed using dot notation where the name of the value is followed by a dot (.
) and the name of the property. If a property of a struct or class has properties of its own, this dot notation can be used to access these nested properties as well.
This notation can be used both to retrieve the property's value and, where allowed, to change it.
someStats.health
// => 0
someCharacter.name
// => nil
someStats.health = 87.3
someStats.health
// => 87.3
someCharacter.name = "Luther"
someCharacter.name
// => "Luther"
someCharacter.id = "new id"
// Error: Cannot assign to property: 'id' is a 'let' constant
Like properties, which store data in your structs and classes, you may also define methods which store functions in your struct or class.
Methods are defined in the same way as a regular function, only inside the body of the struct or class. Note that if a function changes the value of a property in a struct, it must be preceded by the mutating
keyword. Additionally, if a property can be changed by a method, that property must be defined using var
rather than let
, just like regular variables.
struct CharacterStats {
var health = 0.0
var speed = 0
var strength = 0
mutating func takeHit(_ damage: Double) {
health = max(0.0, health - damage)
}
func canLift(_ weight: Int) -> Bool {
weight < strength * 100
}
}
class GameCharacter {
var stats = CharacterStats()
var characterClass: String?
var name: String?
var active = false
let id: String = makeRandomID()
func takesDamage(_ damage: Double) {
stats.takeHit(damage)
if stats.health <= 0 {
active = false
}
}
func sayName() -> String {
return "My name is \(name ?? "no one"), my class is \(characterClass ?? "undetermined")"
}
func lift(_ weight: Int) -> String {
if stats.canLift(weight) {
return "No problem!"
} else {
return "Ooof! No way."
}
}
}
These methods can be called using dot notation, just like properties.
var myChar = GameCharacter()
myChar.stats = CharacterStats(health: 72.8, speed: 19, strength: 6)
myChar.active = true
myChar.lift(750)
// => "Ooof! No way."
myChar.takesDamage(80)
myChar.active
// => false
Instances of structs and classes each have an implicit value named self
which refers to the instance itself. There are multiple uses for self
, but it is most commonly used to disambiguate the names of properties and methods of the struct/class when there may be some confusion.
struct MySelf {
var x = 0
mutating func move(x: Int) {
// here if we just say x = x it is unclear if we mean
// the property x or the method parameter x, so we use
// self for clarity
self.x = x
}
}
In this exercise, you will be simulating a windowing based computer system. You will create some windows that can be moved and resized and display their contents. The following image is representative of the values you will be working with below.
<--------------------- screenSize.width --------------------->
^ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| β β
| β position.x,_ β
| β position.y \ β
| β \<----- size.width -----> β
| β ^ *βββββββββββββββββββββββ β
| β | β title β β
| β | ββββββββββββββββββββββββ€ β
screenSize.height β | β β β
| β size.height β β β
| β | β contents β β
| β | β β β
| β | β β β
| β v ββββββββββββββββββββββββ β
| β β
| β β
v ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Define a struct named Size
with two Int
properties, width
and height
that store the window's current width and height, respectively. The initial width and height should be 80 and 60, respectively. Include a method resize(newWidth:newHeight:)
that takes new width and height parameters and changes the properties to reflect the new size.
let size1080x764 = Size(width: 1080, height: 764)
// => Size
var size1200x800 = size1080x764
// => Size
size1200x800.resize(newWidth: 1200, newHeight: 800)
size1200x800.height
// => 800
Define a struct named Position
with two Int
properties, x
and y
that store the current horizontal and vertical position, respectively, of the window's upper left corner. The initial values of x and y should each be 0. The position (0, 0) is the upper left corner of the screen with x
values getting larger as you move right and y
values getting larger as you move down.
Include a method moveTo(newX:newY:)
that takes new x and y parameters and changes the properties to reflect the new position.
var point = Position(x: 10, y: 20)
// => Position
point.moveTo(newX: 100, newY: -100)
point.y
// => -100
Define a window class with the following properties:
title
: String
, Initial value is "New Window"screenSize
: Size
, constant value with width
= 800 and height
= 600size
: Size
, initial value is the default value of the Size
structposition
: Position
, initial value is the default value of the Position
structcontents
: String?
, initial value is nil
resize(to:)
: (Size) -> ()
- This method takes a Size
struct as input and attempts to resize the window to the specified size. However, the new size cannot exceed certain bounds. - The minimum allowed height or width is 1. Requested heights or widths less than 1 will be clipped to 1. - The maximum height and width depends on the current position of the window, the edges of the window cannot move past the edges of the screen. Values larger than these bounds will be clipped to the largest size they can take. E.g. if the window's position is at x
= 400, y
= 300 and a resize to height
= 400, width
= 300 is requested, then the window would be resized to height
= 300, width
= 300 as the screen is not large enough in the y
direction to fully accommodate the request.move(to:)
: (Position) -> ()
- This is similar to resize(to:)
, however, this method adjusts the position of the window to the requested value, rather than the size. As with resize
the new position cannot exceed certain limits. - The smallest position is 0 for both x
and y
. - The maximum position in either direction depends on the current size of the window; the edges cannot move past the edges of the screen. Values larger than these bounds will be clipped to the largest size they can take. E.g. if the window's size is at x
= 250, y
= 100 and a move to x
= 600, y
= 200 is requested, then the window would be moved to x
= 550, y
= 200 as the screen is not large enough in the x
direction to fully accommodate the request.update(title:)
: (String) -> ()
- This method sets the title
property to the value of the string that was passed in.update(text:)
: (String?) -> ()
- This method sets the contents
property to the value of the optional string that was passed in.display()
: () -> String
- This method returns a string describing the current state of the window. For example, if the window has the title
"My First Window" with position: x = 10, y = 100; size: width = 200, height = 150; and contents: "I π my window", it should return the string: "My First Window\nPosition: (10, 100), Size: (200 x 150)\nI π my window\n"
- If contents
is nil, the last line should read "[This window intentionally left blank]"Create an instances of the Window class and modify it via their methods as follows:
mainWindow
.Sign up to Exercism to learn and master Swift with 35 concepts, 103 exercises, and real human mentoring, all for free.