INTRODUCTION: BRIDGING TWO WORLDS
If you are reading this article, you are probably a Java developer who has heard about Swift and wants to explore what Apple’s modern programming language has to offer. Perhaps you are building an iOS or macOS application, or maybe you are just curious about how Swift compares to the language you know and love. Whatever your motivation, you are in the right place.
Swift and Java share many conceptual similarities. Both are object-oriented languages with strong type systems. Both support protocols or interfaces, generics, and modern programming paradigms. However, Swift brings some unique features to the table that might surprise you. Features like optionals, value types, protocol extensions, and a more concise syntax can make Swift feel both familiar and refreshingly different.
This tutorial will take you on a journey through Swift from a Java developer’s perspective. We will compare and contrast the two languages, highlighting what is similar and what is different. By the end of this guide, you will have a solid foundation in Swift and will be ready to write your own applications with confidence.
GETTING STARTED WITH SWIFT
Before we dive into code, you need to set up your Swift development environment. If you are on a Mac, you can download Xcode from the Mac App Store. Xcode includes everything you need: the Swift compiler, debugger, and an excellent IDE. If you are not on a Mac, you can use Swift on Linux or Windows, though the ecosystem is more mature on macOS.
For quick experimentation, Xcode provides Playgrounds, which are interactive environments where you can write Swift code and see results immediately. Think of them as a sophisticated REPL. For this tutorial, you can use a Playground or create a simple command-line Swift project.
THE BASICS: SYNTAX AND STRUCTURE
Let us start with the most fundamental difference you will notice: Swift has no semicolons required at the end of statements. Coming from Java, this might feel strange at first, but you will quickly appreciate the cleaner look.
In Java, you might write:
String greeting = "Hello, World";
System.out.println(greeting);
In Swift, the equivalent is:
let greeting = "Hello, World"
print(greeting)
Notice that we used “let” instead of a type declaration. Swift has powerful type inference, which means the compiler can figure out that greeting is a String without you explicitly stating it. However, you can still specify types when you want to be explicit.
Another immediate difference is how Swift handles its entry point. In Java, you need a main method inside a public class. In Swift, if you are writing a simple script or using a Playground, you can write code at the top level without any ceremony. For a full application, you would use the @main attribute or AppDelegate/App struct depending on whether you are building a command-line tool or a GUI application.
VARIABLES AND CONSTANTS: LET VERSUS VAR
One of Swift’s core philosophies is immutability by default. In Swift, you declare constants with “let” and variables with “var”. The language encourages you to use “let” whenever possible, making your code safer and more predictable.
In Java, you might write:
final String name = "Alice";
int age = 30;
age = 31;
In Swift:
let name = "Alice"
var age = 30
age = 31
The “let” keyword declares an immutable constant. Once you assign a value, you cannot change it. The “var” keyword declares a mutable variable. Swift’s compiler is smart enough to warn you if you declare something as “var” but never actually mutate it, suggesting you use “let” instead.
This might seem like a small change, but it has profound implications for how you write code. In Java, everything is mutable by default unless you explicitly use “final”. In Swift, you must consciously choose mutability by using “var”. This leads to fewer bugs and makes your intentions clearer.
TYPE SYSTEM: STRONG BUT FLEXIBLE
Swift has a strong, static type system, just like Java. However, Swift’s type inference makes the code feel more dynamic while maintaining all the safety of static typing. Let me show you some examples:
let integer = 42 // Inferred as Int
let decimal = 3.14 // Inferred as Double
let text = "Hello" // Inferred as String
let isValid = true // Inferred as Bool
You can also be explicit about types:
let integer: Int = 42
let decimal: Double = 3.14
let text: String = "Hello"
let isValid: Bool = true
Swift has several built-in types that correspond to Java types. Int is like Java’s int (though Swift’s Int is architecture-dependent). Double and Float are like their Java counterparts. String works similarly to Java’s String class. Bool is like Java’s boolean.
One significant difference is that Swift’s basic types are actually structs, not primitives. This means they have methods and properties, just like objects. You can write:
let number = 42
let doubled = number.advanced(by: number)
Swift does not have primitive types versus reference types in the same way Java does. Instead, it has value types and reference types, which we will explore in detail later.
THE ELEPHANT IN THE ROOM: OPTIONALS
If there is one feature that defines Swift and differentiates it most from Java, it is optionals. In Java, any reference type can be null, which is the source of countless NullPointerException errors. Swift takes a radically different approach.
In Swift, a variable cannot be nil unless you explicitly declare it as optional. An optional is a type that can either contain a value or be nil. You declare an optional by adding a question mark after the type:
let name: String = "Alice" // Cannot be nil
let nickname: String? = nil // Can be nil
If you try to assign nil to a non-optional variable, Swift will not compile. This catches entire categories of bugs at compile time rather than runtime.
Working with optionals requires unwrapping them to access the underlying value. Swift provides several ways to do this safely. The most straightforward is optional binding with if-let:
let possibleName: String? = "Bob"
if let actualName = possibleName {
print("The name is \(actualName)")
} else {
print("No name available")
}
This is roughly equivalent to Java’s pattern:
String possibleName = getPossibleName();
if (possibleName != null) {
System.out.println("The name is " + possibleName);
} else {
System.out.println("No name available");
}
But Swift’s approach is more explicit and type-safe. You can also use guard statements for early returns:
func greet(person: String?) {
guard let name = person else {
print("Cannot greet without a name")
return
}
print("Hello, \(name)!")
}
Swift also provides the nil-coalescing operator for providing default values:
let nickname: String? = nil
let displayName = nickname ?? "Anonymous"
And optional chaining for safely accessing properties and methods on optional values:
let possiblePerson: Person? = getPerson()
let uppercaseName = possiblePerson?.name.uppercased()
This is far more elegant than the verbose null checks you would need in Java. Optional chaining returns nil if any part of the chain is nil, preventing crashes.
There is also force unwrapping with the exclamation mark, but this should be used sparingly:
let definitelyAString: String? = "Hello"
let unwrapped = definitelyAString! // Force unwrap
Force unwrapping is similar to assuming a value is not null in Java. If you are wrong, your program will crash. Use it only when you are absolutely certain a value exists.
FUNCTIONS: MORE THAN JUST METHODS
Functions in Swift are first-class citizens and have some interesting characteristics that differ from Java methods. Let us start with a basic function:
func greet(name: String) -> String {
return "Hello, \(name)!"
}
let greeting = greet(name: "Alice")
The syntax is different from Java. You use “func” instead of specifying a return type first, and the return type comes after an arrow. The parameter label “name” is part of the function signature, making calls more readable.
Swift functions support external and internal parameter names:
func greet(person name: String) -> String {
return "Hello, \(name)!"
}
let greeting = greet(person: "Bob")
Here, “person” is the external name (used at the call site) and “name” is the internal name (used inside the function). This makes function calls read like natural language.
You can also make parameters optional by providing default values:
func greet(name: String, enthusiastically: Bool = false) -> String {
let greeting = "Hello, \(name)"
return enthusiastically ? greeting + "!!!" : greeting + "."
}
print(greet(name: "Charlie"))
print(greet(name: "Diana", enthusiastically: true))
Swift functions can return multiple values using tuples:
func getMinMax(numbers: [Int]) -> (min: Int, max: Int)? {
guard let first = numbers.first else {
return nil
}
var currentMin = first
var currentMax = first
for number in numbers {
if number < currentMin {
currentMin = number
}
if number > currentMax {
currentMax = number
}
}
return (currentMin, currentMax)
}
if let bounds = getMinMax(numbers: [3, 1, 4, 1, 5, 9, 2, 6]) {
print("Min: \(bounds.min), Max: \(bounds.max)")
}
This is much cleaner than Java’s approach of creating a custom class just to return multiple values.
CONTROL FLOW: FAMILIAR YET DIFFERENT
Swift’s control flow statements will look familiar to Java developers, but with some nice enhancements. The basic if statement works as you would expect:
let temperature = 72
if temperature > 80 {
print("It's hot!")
} else if temperature > 60 {
print("It's nice.")
} else {
print("It's cold.")
}
Notice that parentheses around the condition are optional. Braces, however, are always required, even for single-line statements. This prevents the infamous “goto fail” bug.
Swift’s switch statement is much more powerful than Java’s. It does not fall through by default, supports pattern matching, and can switch on any type:
let character = "a"
switch character {
case "a":
print("The first letter")
case "z":
print("The last letter")
default:
print("Some other letter")
}
You can switch on ranges:
let score = 85
switch score {
case 90...100:
print("Grade: A")
case 80..<90:
print("Grade: B")
case 70..<80:
print("Grade: C")
default:
print("Grade: F")
}
And you can use where clauses for additional conditions:
let point = (x: 1, y: 1)
switch point {
case (0, 0):
print("Origin")
case (let x, 0):
print("On x-axis at \(x)")
case (0, let y):
print("On y-axis at \(y)")
case (let x, let y) where x == y:
print("On diagonal at (\(x), \(y))")
default:
print("Somewhere else")
}
Loops in Swift are similar to Java but with cleaner syntax. The for-in loop is the primary way to iterate:
let names = ["Alice", "Bob", "Charlie"]
for name in names {
print("Hello, \(name)")
}
for i in 0..<5 {
print("Iteration \(i)")
}
Swift also has while and repeat-while loops (equivalent to Java’s while and do-while):
var count = 0
while count < 5 {
print(count)
count += 1
}
repeat {
print("This executes at least once")
} while false
CLASSES: THE OBJECT-ORIENTED FOUNDATION
Classes in Swift work similarly to Java classes, but with some syntactic differences. Let us create a simple class:
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.")
}
}
let alice = Person(name: "Alice", age: 30)
alice.introduce()
Several things to note here. Swift uses “init” instead of a constructor with the class name. You use “self” instead of “this”. Properties must be initialized either with a default value or in the initializer.
Swift supports inheritance with the same single-inheritance model as Java:
class Student: Person {
var studentId: String
init(name: String, age: Int, studentId: String) {
self.studentId = studentId
super.init(name: name, age: age)
}
override func introduce() {
super.introduce()
print("My student ID is \(studentId).")
}
}
You use “override” to explicitly mark methods that override parent methods. This prevents accidental overrides and makes your intentions clear.
Swift classes are reference types, just like Java objects. When you assign a class instance to a new variable, both variables reference the same object:
let person1 = Person(name: "Bob", age: 25)
let person2 = person1
person2.age = 26
print(person1.age) // Prints 26
STRUCTS: THE VALUE TYPE ALTERNATIVE
Here is where Swift really diverges from Java. Swift has structs, which are similar to classes but are value types instead of reference types. When you assign a struct to a new variable, it creates a copy:
struct Point {
var x: Double
var y: Double
func distanceFromOrigin() -> Double {
return (x * x + y * y).squareRoot()
}
}
var point1 = Point(x: 3, y: 4)
var point2 = point1
point2.x = 10
print(point1.x) // Still prints 3
Structs get a free memberwise initializer, so we did not need to define init. Structs cannot inherit from other structs or classes, but they can adopt protocols (more on that later).
When should you use a class versus a struct? Apple’s general guidance is to prefer structs unless you need reference semantics, inheritance, or the ability to deinitialize resources. Many of Swift’s built-in types, including String, Array, and Dictionary, are actually structs.
Structs can have computed properties:
struct Rectangle {
var width: Double
var height: Double
var area: Double {
return width * height
}
var perimeter: Double {
return 2 * (width + height)
}
}
let rect = Rectangle(width: 5, height: 3)
print("Area: \(rect.area)")
print("Perimeter: \(rect.perimeter)")
You can also have property observers that run code when a property changes:
struct StepCounter {
var totalSteps: Int = 0 {
willSet {
print("About to set totalSteps to \(newValue)")
}
didSet {
print("Added \(totalSteps - oldValue) steps")
}
}
}
PROTOCOLS: SWIFT’S INTERFACES
Protocols in Swift are similar to Java interfaces, but with some additional capabilities. They define a blueprint of methods, properties, and other requirements:
protocol Identifiable {
var id: String { get }
func displayInfo()
}
class User: Identifiable {
let id: String
let username: String
init(id: String, username: String) {
self.id = id
self.username = username
}
func displayInfo() {
print("User: \(username) (ID: \(id))")
}
}
Both classes and structs can adopt protocols. You use a colon to indicate protocol adoption, just like inheritance:
struct Product: Identifiable {
let id: String
let name: String
func displayInfo() {
print("Product: \(name) (ID: \(id))")
}
}
Protocols can have optional requirements and can be extended with default implementations. This is similar to Java’s default methods in interfaces but more powerful:
protocol Greetable {
var name: String { get }
func greet()
}
extension Greetable {
func greet() {
print("Hello, I'm \(name)")
}
}
struct Employee: Greetable {
let name: String
}
let emp = Employee(name: "Sarah")
emp.greet() // Uses the default implementation
This feature enables protocol-oriented programming, a paradigm that Swift strongly encourages. You can compose behavior by adopting multiple protocols, creating flexible and reusable code.
ENUMS: MORE THAN JUST CONSTANTS
If you think Swift enums are like Java enums, prepare to be amazed. Swift enums can have associated values, making them incredibly powerful for modeling complex data:
enum Result {
case success(String)
case failure(Error)
}
enum NetworkError: Error {
case timeout
case noConnection
case serverError(code: Int)
}
func fetchData() -> Result {
let success = Bool.random()
if success {
return .success("Data loaded successfully")
} else {
return .failure(NetworkError.noConnection)
}
}
let result = fetchData()
switch result {
case .success(let message):
print("Success: \(message)")
case .failure(let error):
print("Error: \(error)")
}
Notice how each case can carry associated data. This makes enums perfect for representing states, results, or any situation where you have a fixed set of possibilities with varying data.
Enums can also have raw values like Java enums:
enum Direction: String {
case north = "N"
case south = "S"
case east = "E"
case west = "W"
}
let dir = Direction.north
print(dir.rawValue) // Prints "N"
And they can have methods and computed properties:
enum Compass {
case north, south, east, west
func opposite() -> Compass {
switch self {
case .north:
return .south
case .south:
return .north
case .east:
return .west
case .west:
return .east
}
}
}
ERROR HANDLING: DO, TRY, CATCH
Swift has a sophisticated error handling model that is cleaner than Java’s checked exceptions. Functions that can throw errors are marked with “throws”:
enum FileError: Error {
case notFound
case permissionDenied
case invalidFormat
}
func readFile(named filename: String) throws -> String {
guard filename.hasSuffix(".txt") else {
throw FileError.invalidFormat
}
if filename == "missing.txt" {
throw FileError.notFound
}
return "File contents here"
}
To call a throwing function, you use try within a do-catch block:
do {
let contents = try readFile(named: "data.txt")
print(contents)
} catch FileError.notFound {
print("File not found")
} catch FileError.invalidFormat {
print("Invalid file format")
} catch {
print("Unknown error: \(error)")
}
You can also use try? to convert the result to an optional:
if let contents = try? readFile(named: "data.txt") {
print(contents)
} else {
print("Could not read file")
}
Or try! if you are certain the call will not throw:
let contents = try! readFile(named: "data.txt")
Unlike Java, Swift does not have checked exceptions in the method signature. You do not need to list every possible error type, giving you more flexibility while still maintaining explicit error handling.
COLLECTIONS: ARRAYS, DICTIONARIES, AND SETS
Swift’s collection types are similar to Java’s but with value semantics. Arrays are ordered collections:
var numbers: [Int] = [1, 2, 3, 4, 5]
numbers.append(6)
numbers.insert(0, at: 0)
numbers.remove(at: 1)
for number in numbers {
print(number)
}
let doubled = numbers.map { $0 * 2 }
let evens = numbers.filter { $0 % 2 == 0 }
let sum = numbers.reduce(0, +)
Dictionaries are key-value pairs:
var ages: [String: Int] = ["Alice": 30, "Bob": 25]
ages["Charlie"] = 35
if let aliceAge = ages["Alice"] {
print("Alice is \(aliceAge) years old")
}
for (name, age) in ages {
print("\(name) is \(age) years old")
}
Sets are unordered collections of unique values:
var uniqueNumbers: Set<Int> = [1, 2, 3, 4, 5]
uniqueNumbers.insert(3) // Already exists, no effect
uniqueNumbers.insert(6)
let isPresent = uniqueNumbers.contains(4)
let set1: Set = [1, 2, 3, 4]
let set2: Set = [3, 4, 5, 6]
let intersection = set1.intersection(set2)
let union = set1.union(set2)
All Swift collections are value types, meaning they are copied on assignment. However, Swift optimizes this with copy-on-write, so copies are cheap until you actually modify them.
CLOSURES: SWIFT’S LAMBDAS
Closures in Swift are similar to Java lambdas but with a more flexible syntax. A closure is a self-contained block of functionality:
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map({ (number: Int) -> Int in
return number * 2
})
Swift allows you to simplify this syntax in several ways. If the types can be inferred:
let doubled = numbers.map({ number in
return number * 2
})
If the closure is a single expression, the return is implicit:
let doubled = numbers.map({ number in number * 2 })
If the closure is the last argument, you can use trailing closure syntax:
let doubled = numbers.map { number in number * 2 }
And Swift provides shorthand argument names:
let doubled = numbers.map { $0 * 2 }
This progression from verbose to concise is one of Swift’s design principles. You can be explicit when clarity matters and concise when the intent is obvious.
Closures capture values from their surrounding context:
func makeIncrementer(increment: Int) -> () -> Int {
var total = 0
let incrementer: () -> Int = {
total += increment
return total
}
return incrementer
}
let incrementByTwo = makeIncrementer(increment: 2)
print(incrementByTwo()) // 2
print(incrementByTwo()) // 4
print(incrementByTwo()) // 6
EXTENSIONS: ADDING FUNCTIONALITY TO EXISTING TYPES
Extensions allow you to add functionality to existing types, even types you do not own. This is more powerful than Java’s extension methods:
extension String {
func withPrefix(_ prefix: String) -> String {
return "\(prefix)\(self)"
}
var isValidEmail: Bool {
return self.contains("@") && self.contains(".")
}
}
let greeting = "World".withPrefix("Hello, ")
print(greeting)
let email = "test@example.com"
if email.isValidEmail {
print("Valid email")
}
You can extend your own types, built-in types, and even types from frameworks. Extensions can add methods, computed properties, and protocol conformances:
extension Int {
func squared() -> Int {
return self * self
}
var isEven: Bool {
return self % 2 == 0
}
}
print(5.squared())
print(4.isEven)
Extensions are a key part of protocol-oriented programming, allowing you to provide default implementations for protocol methods.
MEMORY MANAGEMENT: AUTOMATIC REFERENCE COUNTING
Swift uses Automatic Reference Counting (ARC) instead of garbage collection. This is similar to Java’s garbage collection in that you do not manually allocate and deallocate memory, but the mechanisms are different.
In most cases, ARC just works and you do not need to think about it. However, you need to be aware of strong reference cycles, which occur when two objects hold strong references to each other:
class Department {
let name: String
var manager: Employee?
init(name: String) {
self.name = name
}
}
class Employee {
let name: String
weak var department: Department?
init(name: String) {
self.name = name
}
}
The “weak” keyword breaks the reference cycle by creating a weak reference that does not increase the reference count. When the referenced object is deallocated, the weak reference automatically becomes nil.
There is also “unowned” for cases where you know the reference will never be nil during its lifetime:
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
}
class CreditCard {
let number: String
unowned let customer: Customer
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
}
For closures that capture self, you need to be careful about reference cycles. Use capture lists to specify weak or unowned captures:
class NetworkManager {
var data: String = ""
func fetchData(completion: @escaping () -> Void) {
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
self.data = "Downloaded data"
completion()
}
}
}
GENERICS: TYPE-SAFE FLEXIBILITY
Swift’s generics work similarly to Java’s, allowing you to write flexible, reusable functions and types:
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 5
var y = 10
swap(&x, &y)
print("x: \(x), y: \(y)")
The “inout” keyword allows you to modify parameters, similar to passing by reference. You use an ampersand when calling the function.
You can create generic types:
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
return items.isEmpty ? nil : items.removeLast()
}
func peek() -> Element? {
return items.last
}
var isEmpty: Bool {
return items.isEmpty
}
}
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop() ?? 0)
You can constrain generic types using protocols:
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
PUTTING IT ALL TOGETHER: A COMPLETE RUNNING EXAMPLE
Now that we have covered the fundamentals of Swift, let us build a complete, production-ready application that demonstrates these concepts in action. We will create a Library Management System that handles books, members, lending operations, and provides a clean interface for managing a library.
This system will demonstrate classes, structs, protocols, enums, error handling, collections, optionals, and other Swift features we have discussed. The code follows clean architecture principles with clear separation of concerns.
// MARK: - Core Domain Models
/// Represents a unique identifier for any entity in the system
struct EntityID: Hashable, CustomStringConvertible {
let value: String
init() {
self.value = UUID().uuidString
}
init(value: String) {
self.value = value
}
var description: String {
return value
}
}
/// Represents the genre of a book
enum BookGenre: String, CaseIterable {
case fiction
case nonFiction
case science
case history
case biography
case mystery
case romance
case fantasy
case technology
var displayName: String {
switch self {
case .nonFiction:
return "Non-Fiction"
default:
return self.rawValue.capitalized
}
}
}
/// Represents the condition of a book
enum BookCondition: String, Comparable {
case excellent
case good
case fair
case poor
static func < (lhs: BookCondition, rhs: BookCondition) -> Bool {
let order: [BookCondition] = [.poor, .fair, .good, .excellent]
guard let lhsIndex = order.firstIndex(of: lhs),
let rhsIndex = order.firstIndex(of: rhs) else {
return false
}
return lhsIndex < rhsIndex
}
}
/// Errors that can occur in the library system
enum LibraryError: Error, CustomStringConvertible {
case bookNotFound
case memberNotFound
case bookAlreadyBorrowed
case bookNotBorrowed
case memberHasOverdueBooks
case memberBorrowLimitReached
case invalidOperation(String)
var description: String {
switch self {
case .bookNotFound:
return "The requested book was not found in the library"
case .memberNotFound:
return "The requested member was not found in the system"
case .bookAlreadyBorrowed:
return "This book is currently borrowed by another member"
case .bookNotBorrowed:
return "This book is not currently borrowed"
case .memberHasOverdueBooks:
return "Member has overdue books and cannot borrow more"
case .memberBorrowLimitReached:
return "Member has reached the maximum borrowing limit"
case .invalidOperation(let message):
return "Invalid operation: \(message)"
}
}
}
/// Protocol for entities that can be identified
protocol Identifiable {
var id: EntityID { get }
}
/// Protocol for entities that can provide a detailed description
protocol Describable {
func detailedDescription() -> String
}
/// Represents a book in the library
struct Book: Identifiable, Describable, Hashable {
let id: EntityID
let title: String
let author: String
let isbn: String
let genre: BookGenre
let publicationYear: Int
var condition: BookCondition
var isAvailable: Bool
var currentBorrowerId: EntityID?
init(title: String, author: String, isbn: String, genre: BookGenre,
publicationYear: Int, condition: BookCondition = .good) {
self.id = EntityID()
self.title = title
self.author = author
self.isbn = isbn
self.genre = genre
self.publicationYear = publicationYear
self.condition = condition
self.isAvailable = true
self.currentBorrowerId = nil
}
func detailedDescription() -> String {
let status = isAvailable ? "Available" : "Borrowed"
return """
Book Details:
Title: \(title)
Author: \(author)
ISBN: \(isbn)
Genre: \(genre.displayName)
Publication Year: \(publicationYear)
Condition: \(condition.rawValue.capitalized)
Status: \(status)
"""
}
static func == (lhs: Book, rhs: Book) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
/// Represents a library member
struct Member: Identifiable, Describable, Hashable {
let id: EntityID
let name: String
let email: String
let phoneNumber: String
var borrowedBooks: Set<EntityID>
let maxBorrowLimit: Int
var membershipDate: Date
init(name: String, email: String, phoneNumber: String,
maxBorrowLimit: Int = 5) {
self.id = EntityID()
self.name = name
self.email = email
self.phoneNumber = phoneNumber
self.borrowedBooks = []
self.maxBorrowLimit = maxBorrowLimit
self.membershipDate = Date()
}
var canBorrowMore: Bool {
return borrowedBooks.count < maxBorrowLimit
}
func detailedDescription() -> String {
return """
Member Details:
Name: \(name)
Email: \(email)
Phone: \(phoneNumber)
Books Borrowed: \(borrowedBooks.count)/\(maxBorrowLimit)
Member Since: \(formatDate(membershipDate))
"""
}
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter.string(from: date)
}
static func == (lhs: Member, rhs: Member) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
/// Represents a borrowing transaction
struct BorrowingRecord: Identifiable {
let id: EntityID
let bookId: EntityID
let memberId: EntityID
let borrowDate: Date
var returnDate: Date?
let dueDate: Date
init(bookId: EntityID, memberId: EntityID, borrowDate: Date = Date(),
lendingPeriodDays: Int = 14) {
self.id = EntityID()
self.bookId = bookId
self.memberId = memberId
self.borrowDate = borrowDate
self.returnDate = nil
self.dueDate = Calendar.current.date(
byAdding: .day,
value: lendingPeriodDays,
to: borrowDate
) ?? borrowDate
}
var isOverdue: Bool {
guard returnDate == nil else { return false }
return Date() > dueDate
}
var daysOverdue: Int {
guard isOverdue else { return 0 }
let calendar = Calendar.current
let components = calendar.dateComponents(
[.day],
from: dueDate,
to: Date()
)
return components.day ?? 0
}
func detailedDescription() -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
var description = """
Borrowing Record:
Borrow Date: \(formatter.string(from: borrowDate))
Due Date: \(formatter.string(from: dueDate))
"""
if let returnDate = returnDate {
description += "\n Return Date: \(formatter.string(from: returnDate))"
} else {
description += "\n Status: Currently Borrowed"
if isOverdue {
description += " (OVERDUE by \(daysOverdue) days)"
}
}
return description
}
}
// MARK: - Repository Protocol and Implementation
/// Protocol defining repository operations
protocol Repository {
associatedtype Entity: Identifiable
func add(_ entity: Entity) throws
func remove(id: EntityID) throws
func find(id: EntityID) -> Entity?
func findAll() -> [Entity]
func update(_ entity: Entity) throws
}
/// Generic in-memory repository implementation
class InMemoryRepository<T: Identifiable>: Repository {
typealias Entity = T
private var storage: [EntityID: T] = [:]
func add(_ entity: T) throws {
storage[entity.id] = entity
}
func remove(id: EntityID) throws {
guard storage[id] != nil else {
throw LibraryError.invalidOperation("Entity not found")
}
storage.removeValue(forKey: id)
}
func find(id: EntityID) -> T? {
return storage[id]
}
func findAll() -> [T] {
return Array(storage.values)
}
func update(_ entity: T) throws {
guard storage[entity.id] != nil else {
throw LibraryError.invalidOperation("Entity not found")
}
storage[entity.id] = entity
}
func clear() {
storage.removeAll()
}
var count: Int {
return storage.count
}
}
// MARK: - Library Service
/// Main library service that manages all operations
class LibraryService {
private let bookRepository: InMemoryRepository<Book>
private let memberRepository: InMemoryRepository<Member>
private let borrowingRepository: InMemoryRepository<BorrowingRecord>
init() {
self.bookRepository = InMemoryRepository<Book>()
self.memberRepository = InMemoryRepository<Member>()
self.borrowingRepository = InMemoryRepository<BorrowingRecord>()
}
// MARK: - Book Management
func addBook(_ book: Book) throws {
try bookRepository.add(book)
}
func removeBook(id: EntityID) throws {
guard let book = bookRepository.find(id: id) else {
throw LibraryError.bookNotFound
}
if !book.isAvailable {
throw LibraryError.invalidOperation(
"Cannot remove a borrowed book"
)
}
try bookRepository.remove(id: id)
}
func findBook(id: EntityID) -> Book? {
return bookRepository.find(id: id)
}
func findBooks(by criteria: (Book) -> Bool) -> [Book] {
return bookRepository.findAll().filter(criteria)
}
func getAllBooks() -> [Book] {
return bookRepository.findAll()
}
func searchBooks(byTitle title: String) -> [Book] {
let lowercasedTitle = title.lowercased()
return findBooks { book in
book.title.lowercased().contains(lowercasedTitle)
}
}
func searchBooks(byAuthor author: String) -> [Book] {
let lowercasedAuthor = author.lowercased()
return findBooks { book in
book.author.lowercased().contains(lowercasedAuthor)
}
}
func searchBooks(byGenre genre: BookGenre) -> [Book] {
return findBooks { $0.genre == genre }
}
func getAvailableBooks() -> [Book] {
return findBooks { $0.isAvailable }
}
// MARK: - Member Management
func registerMember(_ member: Member) throws {
try memberRepository.add(member)
}
func removeMember(id: EntityID) throws {
guard var member = memberRepository.find(id: id) else {
throw LibraryError.memberNotFound
}
if !member.borrowedBooks.isEmpty {
throw LibraryError.invalidOperation(
"Cannot remove member with borrowed books"
)
}
try memberRepository.remove(id: id)
}
func findMember(id: EntityID) -> Member? {
return memberRepository.find(id: id)
}
func getAllMembers() -> [Member] {
return memberRepository.findAll()
}
func searchMembers(byName name: String) -> [Member] {
let lowercasedName = name.lowercased()
return memberRepository.findAll().filter { member in
member.name.lowercased().contains(lowercasedName)
}
}
// MARK: - Borrowing Operations
func borrowBook(bookId: EntityID, memberId: EntityID) throws {
guard var book = bookRepository.find(id: bookId) else {
throw LibraryError.bookNotFound
}
guard var member = memberRepository.find(id: memberId) else {
throw LibraryError.memberNotFound
}
guard book.isAvailable else {
throw LibraryError.bookAlreadyBorrowed
}
guard member.canBorrowMore else {
throw LibraryError.memberBorrowLimitReached
}
if hasOverdueBooks(memberId: memberId) {
throw LibraryError.memberHasOverdueBooks
}
let record = BorrowingRecord(bookId: bookId, memberId: memberId)
try borrowingRepository.add(record)
book.isAvailable = false
book.currentBorrowerId = memberId
try bookRepository.update(book)
member.borrowedBooks.insert(bookId)
try memberRepository.update(member)
}
func returnBook(bookId: EntityID) throws {
guard var book = bookRepository.find(id: bookId) else {
throw LibraryError.bookNotFound
}
guard !book.isAvailable else {
throw LibraryError.bookNotBorrowed
}
guard let borrowerId = book.currentBorrowerId else {
throw LibraryError.invalidOperation("No borrower recorded")
}
guard var member = memberRepository.find(id: borrowerId) else {
throw LibraryError.memberNotFound
}
let activeRecords = borrowingRepository.findAll().filter { record in
record.bookId == bookId && record.returnDate == nil
}
guard var record = activeRecords.first else {
throw LibraryError.invalidOperation(
"No active borrowing record found"
)
}
record.returnDate = Date()
try borrowingRepository.update(record)
book.isAvailable = true
book.currentBorrowerId = nil
try bookRepository.update(book)
member.borrowedBooks.remove(bookId)
try memberRepository.update(member)
}
func getBorrowingHistory(bookId: EntityID) -> [BorrowingRecord] {
return borrowingRepository.findAll().filter {
$0.bookId == bookId
}.sorted { $0.borrowDate > $1.borrowDate }
}
func getMemberBorrowingHistory(memberId: EntityID) -> [BorrowingRecord] {
return borrowingRepository.findAll().filter {
$0.memberId == memberId
}.sorted { $0.borrowDate > $1.borrowDate }
}
func getActiveBorrowings(memberId: EntityID) -> [BorrowingRecord] {
return borrowingRepository.findAll().filter { record in
record.memberId == memberId && record.returnDate == nil
}
}
func hasOverdueBooks(memberId: EntityID) -> Bool {
let activeRecords = getActiveBorrowings(memberId: memberId)
return activeRecords.contains { $0.isOverdue }
}
func getOverdueRecords() -> [BorrowingRecord] {
return borrowingRepository.findAll().filter { $0.isOverdue }
}
// MARK: - Statistics
func getStatistics() -> LibraryStatistics {
let totalBooks = bookRepository.count
let availableBooks = getAvailableBooks().count
let borrowedBooks = totalBooks - availableBooks
let totalMembers = memberRepository.count
let activeRecords = borrowingRepository.findAll().filter {
$0.returnDate == nil
}
let overdueRecords = getOverdueRecords()
return LibraryStatistics(
totalBooks: totalBooks,
availableBooks: availableBooks,
borrowedBooks: borrowedBooks,
totalMembers: totalMembers,
activeBorrowings: activeRecords.count,
overdueBooks: overdueRecords.count
)
}
}
/// Statistics about the library
struct LibraryStatistics: CustomStringConvertible {
let totalBooks: Int
let availableBooks: Int
let borrowedBooks: Int
let totalMembers: Int
let activeBorrowings: Int
let overdueBooks: Int
var description: String {
return """
Library Statistics:
Total Books: \(totalBooks)
Available: \(availableBooks)
Borrowed: \(borrowedBooks)
Total Members: \(totalMembers)
Active Borrowings: \(activeBorrowings)
Overdue Books: \(overdueBooks)
"""
}
}
// MARK: - Demo Usage
func demonstrateLibrarySystem() {
print("=== Library Management System Demo ===\n")
let library = LibraryService()
// Create some books
let book1 = Book(
title: "The Swift Programming Language",
author: "Apple Inc.",
isbn: "978-0-13-468599-1",
genre: .technology,
publicationYear: 2024,
condition: .excellent
)
let book2 = Book(
title: "Clean Architecture",
author: "Robert C. Martin",
isbn: "978-0-13-449416-6",
genre: .technology,
publicationYear: 2017,
condition: .good
)
let book3 = Book(
title: "The Pragmatic Programmer",
author: "Andrew Hunt and David Thomas",
isbn: "978-0-13-595705-9",
genre: .technology,
publicationYear: 2019,
condition: .excellent
)
// Add books to library
do {
try library.addBook(book1)
try library.addBook(book2)
try library.addBook(book3)
print("Successfully added \(library.getAllBooks().count) books\n")
} catch {
print("Error adding books: \(error)\n")
}
// Register members
let member1 = Member(
name: "Alice Johnson",
email: "alice@example.com",
phoneNumber: "555-0101"
)
let member2 = Member(
name: "Bob Smith",
email: "bob@example.com",
phoneNumber: "555-0102"
)
do {
try library.registerMember(member1)
try library.registerMember(member2)
print("Successfully registered \(library.getAllMembers().count) members\n")
} catch {
print("Error registering members: \(error)\n")
}
// Demonstrate borrowing
print("Alice borrows 'The Swift Programming Language':")
do {
try library.borrowBook(bookId: book1.id, memberId: member1.id)
print("Success!\n")
if let updatedBook = library.findBook(id: book1.id) {
print(updatedBook.detailedDescription())
print()
}
} catch {
print("Error: \(error)\n")
}
// Try to borrow the same book again
print("Bob tries to borrow the same book:")
do {
try library.borrowBook(bookId: book1.id, memberId: member2.id)
print("Success!\n")
} catch {
print("Error: \(error)\n")
}
// Bob borrows a different book
print("Bob borrows 'Clean Architecture':")
do {
try library.borrowBook(bookId: book2.id, memberId: member2.id)
print("Success!\n")
} catch {
print("Error: \(error)\n")
}
// Search for books by author
print("Searching for books by 'Martin':")
let martinBooks = library.searchBooks(byAuthor: "Martin")
for book in martinBooks {
print(" - \(book.title) by \(book.author)")
}
print()
// Get available books
print("Available books:")
let availableBooks = library.getAvailableBooks()
for book in availableBooks {
print(" - \(book.title)")
}
print()
// Return a book
print("Alice returns 'The Swift Programming Language':")
do {
try library.returnBook(bookId: book1.id)
print("Success!\n")
if let member = library.findMember(id: member1.id) {
print(member.detailedDescription())
print()
}
} catch {
print("Error: \(error)\n")
}
// Show statistics
let stats = library.getStatistics()
print(stats.description)
print()
// Show borrowing history
print("Bob's borrowing history:")
let bobHistory = library.getMemberBorrowingHistory(memberId: member2.id)
for record in bobHistory {
print(record.detailedDescription())
print()
}
}
// Run the demonstration
demonstrateLibrarySystem()
This complete example demonstrates all the key concepts we covered in this tutorial. The Library Management System is production-ready code that handles real-world scenarios including error handling, state management, search functionality, and comprehensive statistics.
The code follows clean architecture principles with clear separation between domain models, repository layer, and service layer. Every function has a single responsibility, making the code maintainable and testable. The use of protocols allows for future extensibility, such as swapping the in-memory repository for a persistent database.
Notice how Swift’s features make the code safer and more expressive than equivalent Java code would be. Optionals prevent null pointer errors. Structs with value semantics prevent unintended mutations. Enums with associated values model complex states cleanly. Error handling is explicit and type-safe.
CONCLUSION: YOUR JOURNEY WITH SWIFT
You have now completed a comprehensive tour of Swift from a Java developer’s perspective. We covered the fundamental differences in syntax and structure, explored Swift’s unique features like optionals and value types, and built a complete application demonstrating these concepts in practice.
Swift is a modern language that learns from decades of programming language evolution. It combines the safety and performance of compiled languages with the expressiveness typically associated with dynamic languages. Features like type inference, optionals, powerful enums, and protocol-oriented programming make Swift code safer, more concise, and more maintainable than traditional object-oriented approaches.
As you continue your Swift journey, remember that the best way to learn is by building real applications. Start with small projects and gradually increase complexity. Explore Apple’s frameworks like SwiftUI for user interfaces, Combine for reactive programming, and Swift Concurrency for asynchronous code.
The Swift community is vibrant and welcoming. Take advantage of resources like the official Swift documentation, Swift forums, and open-source Swift projects on GitHub. Apple’s WWDC videos are excellent for learning advanced topics and best practices.
Most importantly, embrace Swift’s philosophy of safety and clarity. Use immutability by default with let. Leverage the type system to catch errors at compile time. Write expressive, self-documenting code that clearly communicates intent. These practices will make you not just a Swift developer, but a better programmer overall.
Welcome to the Swift community. Happy coding!
No comments:
Post a Comment