Persistent Load Workflow

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In this segment, you’ll implement file-based persistence using JSON serialization. By the end, your task manager will load tasks from disk automatically on startup, proving that data survives app restarts. You’ll learn:

  • How Swift’s Codable protocol enables automatic JSON serialization
  • File I/O with FileManager across iOS and Android platforms
  • The Result type pattern for explicit, type-safe error handling
  • StateFlow synchronization between Swift and Kotlin

You’ll get started by understanding how JSON serialization works.

Understanding JSON Serialization with Codable

Before you can save tasks to disk, you need a way to convert Swift objects into a format that can be written to a file. That format is JSON (JavaScript Object Notation), a human-readable text format that’s widely supported across programming languages and platforms.

What is Codable?

Codable is actually a combination of two protocols:

Task Model Review

Your Task struct is already set up for JSON serialization. Open Task.swift and you’ll see:

public struct Task: Codable {
  let id: String
  let title: String
  let description: String
  let priority: Priority
  var isCompleted: Bool
  let photoFilename: String?
}
public enum Priority: String, Codable {
  case low, medium, high
}

How Encoding and Decoding Work

To convert objects to JSON, you use JSONEncoder:

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted  // Makes JSON human-readable
let data = try encoder.encode(tasks)       // [Task] → Data
let data = try Data(contentsOf: fileURL)            // Read file
let decoder = JSONDecoder()
let tasks = try decoder.decode([Task].self, from: data)  // Data → [Task]

Creating TaskStorage for Load Operations

The TaskStorage class is responsible for all file I/O operations. It handles reading tasks from disk, writing tasks to disk, and managing the file path across iOS and Android platforms.

Creating the StorageError Enum

First, you’ll define custom error types that describe what went wrong during file operations.

import Foundation

enum StorageError: Error {
  case fileNotFound
  case corruptedData
  case encodingFailed
  case decodingFailed
  case writeFailed(Error)
  case readFailed(Error)
}

Creating the TaskStorage Class

Next, you’ll create the class structure and set up the file path.

// 1
class TaskStorage {
  
  // 2
  private let fileURL: URL
  
  // 3
  public init(filename: String = "tasks.json") {
    #if os(Android)
    // Android: Use app's files directory
    let documentsPath = "/data/data/com.kodeco.android.swiftsdkforandroid.taskmanager/files"
    self.fileURL = URL(fileURLWithPath: documentsPath).appendingPathComponent(filename)
    #else
    // iOS/macOS: Use documents directory
    let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    self.fileURL = documentsDirectory.appendingPathComponent(filename)
    #endif
  }
}

Implementing the loadTasks() Method

Now for the core functionality: reading tasks from disk.

public func loadTasks() -> Result<[Task], StorageError> {
  guard FileManager.default.fileExists(atPath: fileURL.path) else {
    return .failure(.fileNotFound)
  }
  
  do {
    let data = try Data(contentsOf: fileURL)
    
    guard !data.isEmpty else {
      return .success([])
    }
    
    let decoder = JSONDecoder()
    let tasks = try decoder.decode([Task].self, from: data)
    
    return .success(tasks)
  } catch let error as DecodingError {
    return .failure(.corruptedData)
  } catch {
    return .failure(.readFailed(error))
  }
}

Using the Result Type Pattern

Notice the return type: Result<[Task], StorageError>. This is Swift’s built-in Result type, which represents either success or failure:

Integrating TaskStorage into TaskManager

Now that TaskStorage can load tasks from disk, you need to wire it up to TaskManager so tasks are loaded automatically when the app starts.

Adding the Storage Property

Open taskmanager-lib/Sources/TaskManagerKit/TaskManager.swift. Find the class declaration and add the storage property:

public class TaskManager: @unchecked Sendable {
  private var tasks: [Task] = []
  private let storage = TaskStorage()  // NEW

Loading Tasks on Initialization

Find the private init() method and modify it to load tasks:

private init() {
  if case .success(let loadedTasks) = storage.loadTasks() {
    self.tasks = loadedTasks
  }
}
./gradlew taskmanager-lib:clean 
cd taskmanager-lib
swift build

Adding getAllTasksJSON() to TaskManager

For Kotlin to access the loaded tasks, you need to add a method that returns tasks as a JSON string. This method will be automatically exposed to Kotlin through swift-java.

public static func getAllTasksJSON() -> String {
  let encoder = JSONEncoder()
  guard let jsonData = try? encoder.encode(shared.tasks),
        let jsonString = String(data: jsonData, encoding: .utf8) else {
    return "[]"
  }
  return jsonString
}
cd taskmanager-lib
swift build

Kotlin Repository Integration

The Swift side now loads tasks from disk, but your Kotlin app doesn’t know about them yet. In this section, you’ll connect the Kotlin TaskRepository to Swift’s TaskManager so tasks flow from the JSON file → Swift → Kotlin → UI.

Understanding the Architecture

Before diving into code, let’s clarify the data flow:

Implementing loadTasks() in Kotlin

Open app/src/main/java/com/kodeco/android/swiftsdkforandroid/taskmanager/repository/TaskRepository.kt. Add this method inside the TaskRepository object:

private fun loadTasks() {
  try {
    // Get tasks as JSON from Swift
    val jsonString = TaskManager.getAllTasksJSON()
    val jsonArray = JSONArray(jsonString)
    
    val loadedTasks = mutableListOf<Task>()
    for (i in 0 until jsonArray.length()) {
      val jsonTask = jsonArray.getJSONObject(i)
      
      // Parse priority string
      val priorityString = jsonTask.getString("priority")
      val priority = when (priorityString.lowercase()) {
        "low" -> Priority.low(arena)
        "medium" -> Priority.medium(arena)
        "high" -> Priority.high(arena)
        else -> Priority.medium(arena)
      }
      
      val task = Task.init(
        jsonTask.getString("id"),
        jsonTask.getString("title"),
        jsonTask.getString("description"),
        priority,
        jsonTask.getBoolean("isCompleted"),
        Optional.empty(),  // Photo handling in next segment
        arena
      )
      loadedTasks.add(task)
    }
    
    _tasks.value = loadedTasks
  } catch (e: Exception) {
    e.printStackTrace()
    _tasks.value = emptyList()
  }
}

Adding the Init Block

Now you need to call loadTasks() automatically when the repository is initialized.

object TaskRepository {
  private val _tasks = MutableStateFlow<List<Task>>(emptyList())
  val tasks: StateFlow<List<Task>> = _tasks
  
  private val arena = SwiftArena.ofAuto()
  private val manager = TaskManager.getShared(arena)
  
  // Add this init block:
  init {
    loadTasks()
  }
./gradlew :app:assembleDebug

Verifying the Persistence Layer

Now that you’ve got the code wired up, you’ll go through and see it in action!

Verify Persistence Layer
Wenaxz Jeqbeyzumdi Vasod

# View the tasks.json file contents
adb shell cat /data/data/com.kodeco.android.swiftsdkforandroid.taskmanager/files/tasks.json
[
  {
    "id": "uuid-here",
    "title": "Buy groceries",
    "description": "Milk, eggs, bread",
    "priority": "high",
    "isCompleted": false,
    "photoFilename": null
  },
  ...
]

Key Takeaways

You’ve built a robust persistence layer that loads tasks from disk automatically. Here’s what you learned:

See forum comments
Download course materials from Github
Previous: Introduction Next: CRUD Operations & Validation