Swift Defining Classes: Syntax, Properties, Methods, and Init
Defining classes is one of the core skills in Swift because classes let you group related data and behavior into reusable custom types. In this article, you will learn how to declare a class, add properties and methods, write initializers, understand how classes differ from structs, and avoid common beginner mistakes when creating your own class types.
Quick answer: In Swift, you define a class with the class keyword, followed by the class name and a body in braces. Inside the class, you usually add stored properties, methods, and one or more initializers to describe what the class stores and what it can do.
Difficulty: Beginner
Helpful to know first: You'll understand this better if you already know basic Swift syntax, variables and constants, functions, and simple types like String, Int, and Bool.
1. What Is Defining Classes?
Defining a class means creating a new custom reference type in Swift. A class acts like a blueprint for objects. That blueprint describes what data each object stores and what actions it can perform.
When you define a class, you are usually answering questions like these:
- What information should this type store?
- What operations should this type perform?
- How should a new instance of this type be created?
- Should this type support inheritance?
A simple way to think about a class is that it combines state and behavior:
- State is stored in properties.
- Behavior is written in methods.
- Creation rules are handled by initializers.
In Swift, classes are reference types. That means when you assign a class instance to another variable, both variables can refer to the same underlying object in memory. This is one of the most important differences between classes and structs.
A class definition creates the blueprint. An instance is a real object created from that blueprint.
2. Why Defining Classes Matters
You define classes when you need a custom type that represents something with identity, shared mutable state, or behavior that naturally belongs together.
Classes matter because they let you model real program concepts clearly. For example:
- A UserAccount can store a username, email, and login state.
- A BankAccount can store a balance and perform deposits or withdrawals.
- A Car can store speed and fuel level and provide methods for driving or refueling.
They are especially useful when:
- Multiple parts of a program should reference the same object.
- You need inheritance.
- You want to model identity, not just value.
You should not automatically use a class for every custom type. In Swift, many types are better modeled as structs. A struct is often the better choice for simple data that should be copied by value instead of shared by reference. This class-vs-struct decision is a common Swift design question, and we will return to it later in the article.
3. Basic Syntax or Core Idea
The smallest useful class definition uses the class keyword and a name. Inside the braces, you can place properties, methods, and initializers.
Basic class declaration
This example defines a class named Person with two stored properties, one initializer, and one method.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func introduce() {
print("Hi, I'm \(name) and I'm \(age) years old.")
}
}
This class has three major parts:
- var name and var age are stored properties.
- init(...) is the initializer used to create a new instance.
- introduce() is a method that belongs to the class.
Creating an instance
After defining a class, you create an instance by calling its initializer.
let person = Person(name: "Maya", age: 28)
person.introduce()
This creates one Person object and calls its method. Even though person is declared with let, the instance itself is still a reference type, so some internal mutable properties may still be changed if they are declared with var.
Using default property values
If a property has a default value, you do not always need to assign it inside an initializer.
class LightSwitch {
var isOn = false
func toggle() {
isOn.toggle()
}
}
Here, isOn starts as false, so Swift does not require a custom initializer for that property.
4. Step-by-Step Examples
The following examples show how class definitions become more useful as you add properties, methods, and initialization logic.
Example 1: A simple class with one property
This first example keeps things minimal. It defines a class that stores a title.
class Book {
var title: String
init(title: String) {
self.title = title
}
}
This is a complete class. It stores one piece of information and ensures every new Book has a title.
Example 2: A class with multiple properties and a method
Most classes represent more than one piece of state and some behavior.
class BankAccount {
var owner: String
var balance: Double
init(owner: String, balance: Double) {
self.owner = owner
self.balance = balance
}
func deposit(amount: Double) {
balance += amount
}
}
This class groups account data and account behavior together. That makes your code easier to understand and reuse.
Example 3: A class with a computed property
Classes can include computed properties as well as stored properties. A computed property calculates a value instead of storing it directly.
class Rectangle {
var width: Double
var height: Double
var area: Double {
width * height
}
init(width: Double, height: Double) {
self.width = width
self.height = height
}
}
The area property is always based on the current width and height. You do not need to store a separate area value.
Example 4: A class showing reference behavior
This example shows why classes are different from structs. Two variables can point to the same class instance.
class Counter {
var value = 0
}
let firstCounter = Counter()
let secondCounter = firstCounter
secondCounter.value = 10
print(firstCounter.value)
The printed value is 10, not 0. That happens because both variables refer to the same object.
5. Practical Use Cases
Defining a class is useful in many realistic situations:
- Modeling users, accounts, sessions, or profiles that have ongoing identity.
- Representing shared managers such as a download manager or cache controller.
- Creating types whose instances should be shared across parts of an app.
- Building inheritance hierarchies when a base type and specialized subtypes make sense.
- Encapsulating mutable state together with the methods that control it.
- Representing long-lived objects whose state changes over time.
If your type is mostly just data and should be copied safely, a struct is often a better fit. If it needs shared identity or inheritance, a class is often the better choice.
6. Common Mistakes
Beginners often understand the basic class syntax but still run into problems with initialization, property access, and reference semantics.
Mistake 1: Forgetting to initialize all stored properties
Every stored property in a class must have a value before initialization finishes, unless it is optional or has a default value.
Problem: This class leaves one stored property without a value, so Swift reports an initialization error such as Class 'User' has no initializers or complains that a stored property is not initialized.
class User {
var name: String
var age: Int
init(name: String) {
self.name = name
}
}
Fix: Make sure every stored property gets a value either from a default value, from the initializer, or by making the property optional when that design is actually appropriate.
class User {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
The corrected version works because all stored properties are initialized before the initializer ends.
Mistake 2: Confusing parameter names with property names when using self
It is common for an initializer parameter to have the same name as a property. In that case, you must use self to refer to the property.
Problem: This code assigns the parameter to itself instead of assigning the parameter to the property, so the stored property remains uninitialized.
class Product {
var name: String
init(name: String) {
name = name
}
}
Fix: Use self.name for the property and name for the parameter.
class Product {
var name: String
init(name: String) {
self.name = name
}
}
The corrected version works because self clearly refers to the instance property.
Mistake 3: Expecting class assignment to create a copy
Because classes are reference types, assigning one instance to another variable does not make a separate independent object.
Problem: This code assumes copiedDog is a different object, but it actually points to the same instance as originalDog.
class Dog {
var name: String
init(name: String) {
self.name = name
}
}
let originalDog = Dog(name: "Fido")
let copiedDog = originalDog
copiedDog.name = "Buddy"
print(originalDog.name)
Fix: If you need separate independent values, consider using a struct or write explicit copy logic for the class.
struct Dog {
var name: String
}
var originalDog = Dog(name: "Fido")
var copiedDog = originalDog
copiedDog.name = "Buddy"
print(originalDog.name)
The corrected version works because structs are value types, so assignment creates an independent copy.
Mistake 4: Trying to use a property before all properties are initialized
Swift initialization rules are strict for safety. During initialization, you must fully initialize stored properties before using self in ways Swift does not allow.
Problem: This code tries to call an instance method before the instance is fully initialized, which causes an error because Swift prevents use of self too early.
class Greeting {
var message: String
init() {
printMessage()
self.message = "Hello"
}
func printMessage() {
print(message)
}
}
Fix: Initialize all stored properties first, then use instance methods or other property-dependent behavior.
class Greeting {
var message: String
init() {
self.message = "Hello"
printMessage()
}
func printMessage() {
print(message)
}
}
The corrected version works because the object is fully initialized before the method uses its state.
7. Best Practices
Good class definitions are clear, predictable, and intentional. The following practices help you create classes that are easier to use and maintain.
Practice 1: Give every class a clear responsibility
A class should represent one main idea. When a class handles too many unrelated tasks, it becomes difficult to understand and reuse.
Less-preferred approach:
class AppManager {
var username = ""
var cartItems: [String] = []
func login() {}
func addToCart(item: String) {}
func sendNotification() {}
}
Preferred approach:
class UserSession {
var username: String
init(username: String) {
self.username = username
}
}
class ShoppingCart {
var items: [String] = []
func add(item: String) {
items.append(item)
}
}
This practice matters because focused classes are easier to test, reason about, and extend safely.
Practice 2: Use constants for properties that should not change
Inside a class, not every property needs to be mutable. If a property should stay fixed after initialization, declare it with let.
Less-preferred approach:
class Employee {
var employeeID: Int
init(employeeID: Int) {
self.employeeID = employeeID
}
}
Preferred approach:
class Employee {
let employeeID: Int
init(employeeID: Int) {
self.employeeID = employeeID
}
}
This makes your intent clearer and reduces accidental state changes.
Practice 3: Prefer meaningful initializers over half-configured objects
A good initializer leaves the object in a valid, usable state immediately.
Less-preferred approach:
class Article {
var title: String = ""
var author: String = ""
}
Preferred approach:
class Article {
let title: String
let author: String
init(title: String, author: String) {
self.title = title
self.author = author
}
}
This practice matters because it prevents invalid or incomplete objects from spreading through your code.
8. Limitations and Edge Cases
Classes are powerful, but there are some important constraints and behaviors to keep in mind:
- Classes are reference types, so assignment and parameter passing share the same instance unless you implement copying yourself.
- Classes do not get a memberwise initializer automatically the way structs often do. You usually need to write your own initializer.
- All stored properties must be initialized before an instance is fully usable.
- A let constant that stores a class instance prevents reassignment of the reference, but does not automatically make the instance immutable.
- Inheritance is supported for classes, but not for structs. That flexibility can also add complexity if overused.
- If you expected copying behavior, classes may feel like they are "not working" because changes in one place appear in another. That is normal reference semantics.
- When designing simple data models, using a class can be unnecessarily heavy compared with a struct.
9. Practical Mini Project
Let’s build a small but complete example that uses a class to model a bank account. This mini project shows a realistic class definition with properties, methods, and an initializer.
class BankAccount {
let accountNumber: String
let ownerName: String
private(set) var balance: Double
init(accountNumber: String, ownerName: String, balance: Double) {
self.accountNumber = accountNumber
self.ownerName = ownerName
self.balance = balance
}
func deposit(amount: Double) {
guard amount > 0 else {
return
}
balance += amount
}
func withdraw(amount: Double) {
guard amount > 0, amount <= balance else {
return
}
balance -= amount
}
func displaySummary() {
print("Account: \(accountNumber)")
print("Owner: \(ownerName)")
print("Balance: $\(balance)")
}
}
let account = BankAccount(accountNumber: "AC-1001", ownerName: "Jordan Lee", balance: 500.0)
account.deposit(amount: 150.0)
account.withdraw(amount: 100.0)
account.displaySummary()
This mini project demonstrates several good class-design ideas:
- Identity fields such as account number and owner name are set during initialization.
- balance is mutable, but only the class itself can change it directly because of private(set).
- Methods enforce simple rules such as preventing negative deposits or overdrawing.
- The object stays valid because the initializer gives every stored property a value.
10. Key Points
- You define a class in Swift with the class keyword.
- A class can contain stored properties, computed properties, methods, and initializers.
- Classes are reference types, so multiple variables can refer to the same instance.
- Stored properties must be initialized before initialization finishes.
- You often need to write a custom initializer for classes.
- Use self to distinguish properties from parameters with the same name.
- Classes are useful when identity, shared mutable state, or inheritance matters.
- For simple value-like data, a struct may be a better choice.
11. Practice Exercise
Create a class named Student with these requirements:
- It should store a student name as a String.
- It should store a grade level as an Int.
- It should have an initializer that sets both properties.
- It should have a method named describe() that prints a sentence about the student.
- Create one instance and call the method.
Expected output: A sentence similar to Student Nina is in grade 6.
Hint: Follow the same pattern used earlier: properties at the top, then init, then a method.
class Student {
var name: String
var gradeLevel: Int
init(name: String, gradeLevel: Int) {
self.name = name
self.gradeLevel = gradeLevel
}
func describe() {
print("Student \(name) is in grade \(gradeLevel).")
}
}
let student = Student(name: "Nina", gradeLevel: 6)
student.describe()
12. Final Summary
Defining classes in Swift means creating custom reference types with properties, methods, and initializers. A class gives you a way to model data and behavior together in a single reusable type. To define one correctly, you usually choose a clear class name, declare stored properties, initialize them properly, and then add methods that describe what the object can do.
You also saw the most important practical idea behind classes: they are reference types. That affects how assignment works, how objects are shared, and when a class is a better choice than a struct. If you are still deciding between the two, your best next step is to study Swift classes versus structs and then move on to topics like inheritance, access control, and deinitialization.