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:

A simple way to think about a class is that it combines state and behavior:

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:

They are especially useful when:

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:

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:

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:

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:

10. Key Points

11. Practice Exercise

Create a class named Student with these requirements:

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.