Best vs. Worst Coding Practices in Swift: 20 Key Examples

Kalidoss Shanmugam
7 min readJul 15, 2024

--

When it comes to Swift development, coding style can significantly impact the readability, maintainability, and quality of your code. Understanding the best and worst practices can help you write cleaner and more efficient Swift code. This article explores the best and worst coding styles in Swift through 20 examples, providing clear comparisons and explanations.

Table of Contents

  1. Consistent Naming Conventions
  2. Code Readability and Formatting
  3. Error Handling
  4. Use of Optionals
  5. Best Practices for Closures
  6. Using guard Statements
  7. Avoiding Force Unwrapping
  8. Effective Use of Protocols
  9. Choosing the Right Data Structures
  10. Avoiding Redundant Code
  11. Best Practices for Unit Testing
  12. Proper Use of Extensions
  13. Avoiding Hard-Coded Values
  14. Memory Management
  15. Following SOLID Principles
  16. Adopting Swift’s Modern Features
  17. Code Documentation
  18. Using defer Statements
  19. Implementing Dependency Injection
  20. Adhering to Swift’s API Design Guidelines

1. Consistent Naming Conventions

Best Case:

// Good: Descriptive and follows Swift naming conventions
struct UserProfile {
var userName: String
var userEmail: String
var userAge: Int
}

Worst Case:

// Bad: Vague and inconsistent naming
struct User {
var name: String
var email: String
var age: Int
}

Explanation: Good naming conventions help in understanding the purpose of a variable or type. Descriptive names like userName, userEmail, and userAge are preferred over ambiguous names like name, email, and age.

2. Code Readability and Formatting

Best Case:

// Good: Well-formatted and readable
func calculateDiscount(for price: Double, discountPercentage: Double) -> Double {
let discountAmount = price * (discountPercentage / 100)
return price - discountAmount
}

Worst Case:

// Bad: Poor formatting and hard to read
func calculateDiscount(for price:Double,discountPercentage:Double)->Double{let discountAmount=price*(discountPercentage/100);return price-discountAmount}

Explanation: Proper indentation and spacing improve code readability. Clear formatting makes it easier to understand and maintain the code.

3. Error Handling

Best Case:

// Good: Using `do-catch` for error handling
func fetchData() throws -> Data {
let data = try performNetworkRequest()
return data
}

Worst Case:

// Bad: Force unwrapping without error handling
let data = try! performNetworkRequest()

Explanation: Always handle errors gracefully using do-catch blocks instead of force unwrapping, which can lead to runtime crashes.

4. Use of Optionals

Best Case:

// Good: Safe unwrapping of optionals
if let userName = userProfile.userName {
print("User's name is \(userName)")
}

Worst Case:

// Bad: Force unwrapping optionals
print("User's name is \(userProfile.userName!)")

Explanation: Use optional binding (if let) to safely handle optionals and avoid potential crashes due to force unwrapping.

5. Best Practices for Closures

Best Case:

// Good: Using trailing closure syntax for better readability
performTask {
print("Task completed")
}

Worst Case:

// Bad: Using verbose closure syntax
performTask({
print("Task completed")
})

Explanation: The trailing closure syntax improves readability when the closure is the last argument of a function.

6. Using guard Statements

Best Case:

// Good: Using `guard` for early exits
func processOrder(order: Order?) {
guard let validOrder = order else {
print("Order is nil")
return
}
// Process the order
}

Worst Case:

// Bad: Using nested if-let statements
func processOrder(order: Order?) {
if let validOrder = order {
// Process the order
} else {
print("Order is nil")
}
}

Explanation: guard statements help to handle invalid conditions early and keep the main code logic unindented.

7. Avoiding Force Unwrapping

Best Case:

// Good: Optional chaining and safe unwrapping
if let userProfile = fetchUserProfile() {
print("User profile fetched successfully")
}

Worst Case:

// Bad: Force unwrapping optionals
let userProfile = fetchUserProfile()!
print("User profile fetched successfully")

Explanation: Force unwrapping should be avoided as it can lead to unexpected crashes. Always use optional binding or optional chaining.

8. Effective Use of Protocols

Best Case:

// Good: Protocols for abstraction
protocol PaymentMethod {
func processPayment(amount: Double)
}
class CreditCard: PaymentMethod {
func processPayment(amount: Double) {
// Process credit card payment
}
}

Worst Case:

// Bad: Direct implementation without protocols
class PaymentProcessor {
func processCreditCardPayment(amount: Double) {
// Process credit card payment
}
}

Explanation: Protocols allow for abstraction and flexibility, enabling different implementations of the same behavior.

9. Choosing the Right Data Structures

Best Case:

// Good: Using a dictionary for key-value pairs
let userScores: [String: Int] = ["Alice": 90, "Bob": 85]

Worst Case:

// Bad: Using arrays for key-value pairs
let userScores = [("Alice", 90), ("Bob", 85)]

Explanation: Use dictionaries for key-value relationships, which offer more efficient lookups compared to arrays.

10. Avoiding Redundant Code

Best Case:

// Good: Reusing common functionality
func calculateArea(of shape: Shape) -> Double {
return shape.area()
}

Worst Case:

// Bad: Repeating the same logic
func calculateRectangleArea(width: Double, height: Double) -> Double {
return width * height
}
func calculateCircleArea(radius: Double) -> Double {
return Double.pi * radius * radius
}

Explanation: Consolidate redundant code into common functions or methods to promote code reuse and reduce duplication.

11. Best Practices for Unit Testing

Best Case:

// Good: Writing clear and isolated unit tests
func testCalculateDiscount() {
let discount = calculateDiscount(for: 100, discountPercentage: 10)
XCTAssertEqual(discount, 90)
}

Worst Case:

// Bad: Combining multiple tests in one
func testCalculateDiscount() {
let discount1 = calculateDiscount(for: 100, discountPercentage: 10)
XCTAssertEqual(discount1, 90)
let discount2 = calculateDiscount(for: 200, discountPercentage: 20)
XCTAssertEqual(discount2, 160)
}

Explanation: Write isolated tests for different scenarios and avoid combining multiple assertions in a single test.

12. Proper Use of Extensions

Best Case:

// Good: Using extensions to add functionality
extension String {
func reversed() -> String {
return String(self.reversed())
}
}

Worst Case:

// Bad: Adding methods to classes or structs inappropriately
class UserProfile {
func reversedUserName() -> String {
return String(userName.reversed())
}
}

Explanation: Use extensions to add functionality to existing types without modifying their original implementation.

13. Avoiding Hard-Coded Values

Best Case:

// Good: Using constants for values
let apiBaseURL = "https://api.example.com"

Worst Case:

// Bad: Hard-coding values
let apiBaseURL = "https://api.example.com/v1/users"

Explanation: Use constants for values that might change, making it easier to update and maintain the code.

14. Memory Management

Best Case:

// Good: Using weak references to avoid retain cycles
class ViewController: UIViewController {
var completion: (() -> Void)?
}

Worst Case:

// Bad: Strong reference causing a retain cycle
class ViewController: UIViewController {
var completion: (() -> Void)?
}

Explanation: Use weak references in closures and delegate properties to avoid retain cycles and memory leaks.

15. Following SOLID Principles

Best Case:

// Good: Single Responsibility Principle
class OrderProcessor {
func processOrder(order: Order) {
// Process the order
}
}

Worst Case:

// Bad: Violating Single Responsibility Principle
class Order {
func processOrder() {
// Process the order
}
func calculateDiscount() {
// Calculate discount
}
}

Explanation: Follow SOLID principles to ensure your code is maintainable, flexible, and easy to understand.

16. Adopting Swift’s Modern Features

Best Case:

// Good: Using Swift’s modern syntax
let names = ["Alice", "Bob", "Charlie"]
let uppercasedNames = names.map { $0.uppercased() }

Worst Case:

// Bad: Using outdated syntax
let names = ["Alice", "Bob", "Charlie"]
var uppercasedNames = [String]()
for name in names {
uppercasedNames.append(name.uppercased())
}

Explanation: Use modern Swift features like map for concise and expressive code.

17. Code Documentation

Best Case:

// Good: Adding documentation comments
/// Calculates the discount for a given price.
/// - Parameters:
/// - price: The original price.
/// - discountPercentage: The discount percentage.
/// - Returns: The discounted price.
func calculateDiscount(for price: Double, discountPercentage: Double) -> Double {
let discountAmount = price * (discountPercentage / 100)
return price - discountAmount
}

Worst Case:

// Bad: No documentation
func calculateDiscount(for price: Double, discountPercentage: Double) -> Double {
let discountAmount = price * (discountPercentage / 100)
return price - discountAmount
}

Explanation: Document your code to explain its functionality and usage, making it easier for others (and yourself) to understand.

18. Using defer Statements

Best Case:

// Good: Using `defer` to ensure cleanup
func openFile() {
let file = open("file.txt")
defer {
close(file)
}
// Work with the file
}

Worst Case:

// Bad: Missing cleanup code
func openFile() {
let file = open("file.txt")
// Work with the file
close(file)
}

Explanation: Use defer to ensure that cleanup code runs regardless of how the function exits.

19. Implementing Dependency Injection

Best Case:

// Good: Injecting dependencies
class OrderService {
private let apiClient: APIClient

init(apiClient: APIClient) {
self.apiClient = apiClient
}
}

Worst Case:

// Bad: Hard-coding dependencies
class OrderService {
private let apiClient = APIClient()
}

Explanation: Inject dependencies to promote testability and flexibility.

20. Adhering to Swift’s API Design Guidelines

Best Case:

// Good: Following API design guidelines
func loadUserProfile(userID: String) async throws -> UserProfile

Worst Case:

// Bad: Ignoring API design guidelines
func fetchProfile(id: String) -> UserProfile

Explanation: Follow Swift’s API Design Guidelines for clarity, consistency, and usability.

Conclusion

By adopting the best practices and avoiding the worst practices outlined above, you can write more robust, maintainable, and readable Swift code. These examples demonstrate how small changes in your coding style can have a significant impact on your projects. Embrace the best practices to improve your Swift programming skills and produce high-quality software.

If you enjoyed this article and would like to support my work, consider buying me a coffee. Your support helps keep me motivated and enables me to continue creating valuable content. Thank you!

--

--

Kalidoss Shanmugam

Experienced mobile app developer with 11 years of expertise, focused on creating innovative solutions that elevate user experiences in today's digital landscap.