Skip to content

Commit

Permalink
Post abound dependency injection and boundaries
Browse files Browse the repository at this point in the history
  • Loading branch information
sergihernanz committed Sep 6, 2024
1 parent ca8dcf7 commit 8ff0d51
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: post
title: "Decoupling Views from ViewModels in SwiftUI"
date: 2024-09-04 21:31:41 +0200
date: 2024-09-03 21:31:41 +0200
categories: swiftui
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
---
layout: post
title: "Injection of External/Environmental Dependencies and Boundaries in Swift iOS Development"
date: 2024-09-04 21:31:41 +0200
categories: swift dependency injection
---

## **Injection of External/Environmental Dependencies and Boundaries in Swift iOS Development**

When developing iOS applications with Swift, it’s crucial to understand the boundaries between your business logic and
external dependencies, such as system libraries and third-party services. By properly injecting these dependencies,
you can enhance your code’s flexibility, testability, and resilience. This article will guide you through the concept
of dependency injection, the importance of separating external dependencies, and best practices for setting up boundaries
in your Swift code.

### **Why Inject External Dependencies?**

External dependencies include system libraries, environmental factors, and third-party services that your app relies on, such as:

- **System Locale and Date Formatting**: Handling dates, times, and locales based on user settings.
- **Network Responses**: Making API calls and handling responses.
- **File System Access**: Reading from or writing to files.
- **User Defaults or Keychain**: Storing and retrieving user-specific data.

These dependencies are essential but often unpredictable in behavior, especially during testing. Injecting these dependencies rather
than hardcoding them into your business logic has several advantages:

1. **Mocking and Stubbing**: You can easily mock or stub dependencies during testing, simulating different scenarios without
relying on actual system behavior or network conditions.
2. **Decoupling Business Logic**: By decoupling business logic from third-party libraries and system dependencies, you keep
your core application logic clean and focused.
3. **Increased Testability**: Dependencies can be swapped out for controlled versions in unit tests, ensuring that tests
are isolated and reliable.
4. **Improved Flexibility and Scalability**: Your code becomes more adaptable to changes, such as switching to a different
library or adjusting behavior based on testing needs.

### **Identifying Boundaries: Business Logic vs. External Dependencies**

A critical aspect of managing dependencies is identifying the boundaries between what constitutes your business logic and what belongs
to external dependencies.

- **Business Logic**: The core algorithms, rules, and data manipulations specific to your application. This is the code you
write and control.
- **External Dependencies**: Code and libraries that interact with the outside world, such as date formatting, networking, or
accessing hardware features. These are often pre-tested and maintained by their providers.

**Boundary Principle**: Set up boundaries at the interface where your business logic meets external dependencies. The goal is to
inject dependencies rather than letting them bleed into your business logic.

### **Examples of Dependency Injection and Decoupling**

Let’s explore a few common examples of dependency injection in Swift iOS development.

#### **1. Injecting Date Providers**

Directly using `Date()` and `DateFormatter()` inside your business logic couples your code to system date behavior, making testing
difficult. Instead, inject a date provider.

**Provider Definition:**

Define a struct to abstract the date functionality:

```swift
struct DateProvider {
var currentDate: () -> Date
}

// The Providers enum is used as a namespace
enum Providers {}
extension Providers {
static var defaultDateProvider = DateProvider(
currentDate: Date.init
)
}
```

*You may use a protocol for this, but we love using value types as much as possible*

**Using the DateProvider in Business Logic:**

Inject the `DateProvider` into your class instead of directly using `Date()`:

```swift
struct EventScheduler {
private let dateProvider: DateProvider
func scheduleEvent() -> String {
let currentDate = dateProvider.currentDate()
// Your business logic here using currentDate
return "Event scheduled at \(currentDate)"
}
}
```

**Testing with Mocked Date Providers:**

Inject a mock or stub provider during testing to simulate specific date scenarios.

```swift
extension Providers {
static var mockDateProviderFirstJan1970 = DateProvider(
currentDate: {
// Return a fixed date for testing
return Date(timeIntervalSince1970: 0)
}
)
}

// Unit Test Example
class DependenciesAndBoundariesTests: XCTestCase {
func testInjectionDate() {
let scheduler = EventScheduler(dateProvider: Providers.mockDateProviderFirstJan1970)
XCTAssertEqual(scheduler.scheduleEvent(), "Event scheduled at 1970-01-01 00:00:00 +0000")
}
}
```

#### **2. Injecting Network Clients**

Networking is another area where tightly coupling your code with libraries like `URLSession` can make testing and maintenance difficult. Inject a network client instead.

**Provider Definition:**

Define a provider for networking:

```swift
struct NetworkProvider {
let fetchData: (URLRequest) async throws -> Data
}
extension Providers {
static var defaultNetworkProvider = NetworkProvider(
fetchData: {
try await URLSession.shared.data(for: $0).0
}
)
}
```

Let's make the provider smarter, though we'll need ot get sure these lines of code are tested as part of our business logic:

```swift
extension NetworkProvider {
func fetchData<T: Decodable>(from: URLRequest) async throws -> T {
try await JSONDecoder().decode(T.self, from: fetchData(from))
}
}
```

**Injecting the NetworkClient in Business Logic:**

Inject the `NetworkClient` instead of directly using `URLSession` in your code:

```swift
struct DataFetcher {
private let networkClient: NetworkProvider
init(networkClient: NetworkProvider) {
self.networkClient = networkClient
}

struct DTO: Codable {
let message: String
}
func fetchRemoteData(from url: URLRequest) async throws -> DTO {
try await networkClient.fetchData(from: url) as DTO
}
}
```

**Testing with Mocked Network Clients:**

For testing, use a mock implementation of `NetworkClient` to simulate network responses.

```swift
extension DependenciesAndBoundariesTests {
func testInjectionNetwork() async throws {
let fetcher = DataFetcher(
networkClient: NetworkProvider(
fetchData: { _ in
// Return a fixed string for testing
return "{\"message\": \"Hello world\"}".data(using: .utf8)!
}
)
)
let dummyURL = URLRequest(url: URL(string: "about:blank")!)
let resultString = try await fetcher.fetchRemoteData(from: dummyURL)
XCTAssertEqual(resultString.message, "Hello world")
}
}
```

### **Best Practices for Setting Up Dependency Injection Boundaries**

1. **Use Providers to Define Boundaries**: Providers serve as boundaries that separate your business logic from external dependencies, allowing you to inject different implementations based on the context.

2. **Inject Dependencies at the Highest Level**: Dependencies should be injected at the topmost level where they are used, such as during the initialization of a view model or a service class.

3. **Minimize Direct Access**: Avoid directly accessing system or third-party APIs within your business logic. Instead, encapsulate them behind providers or service layers.

4. **Design for Testability**: Always design your classes and services with testing in mind. Ask yourself: Can this code be tested in isolation without relying on actual system behavior?

5. **Leverage Dependency Injection Frameworks**: Consider using dependency injection frameworks like Resolver or Swinject to manage the injection and lifecycle of dependencies in your app.

### **Conclusion**

By properly injecting external dependencies and defining clear boundaries between your business logic and third-party libraries, you can create a more maintainable, testable, and flexible codebase. Decoupling these dependencies allows you to focus on building robust business logic while safely leveraging external functionality.
4 changes: 2 additions & 2 deletions about.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ title: About
permalink: /about/
---

This is the technical blog posting from Inqbarna. You can find more info about our company at [inqbarna.com](https://inqbarna.com/)
This is the technical blog posting from Inqbarna. You can find more info about our company at [inqbarna.com](http://inqbarna.com/)

You can find the source code of this site built with Jekyl at [github][https://github.com/InQBarna] /
You can find the source code of this site built with Jekyl at [github](https://github.com/InQBarna) /

You can find the source code for Jekyll at GitHub:
[jekyll][jekyll-organization] /
Expand Down
2 changes: 1 addition & 1 deletion index.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

layout: home
---
Hello world
After countless lines of code, late-night debugging, and way too much coffee, we finally found time to share our experiences. Dive into the world of mobile app development with the iqb-tech-blog, where real-world lessons, hard-earned insights, and tech tips come to life!

0 comments on commit 8ff0d51

Please sign in to comment.