You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中用UserDefaults单键存储两类结构体的编解码实现求助

Got it, let's walk through this solution step by step. You need to store both Task and Project structs under a single UserDefaults key, then handle their display in a table view—showing task details directly in cells and navigating to a project's task list when tapping a project entry. Here's a complete, production-ready implementation:

1. Define Core Structs

First, create your Task and Project models, both conforming to Codable so they can be serialized to/from data:

import Foundation

struct Task: Codable {
    let id: UUID
    let title: String
    let isCompleted: Bool
    // Add other properties like dueDate, notes, etc., as needed
}

struct Project: Codable {
    let id: UUID
    let name: String
    let tasks: [Task]
    // Add other project properties like description, creationDate, etc.
}

2. Create a Unified Enumeration

Since UserDefaults can only store a single type of collection under one key, we'll wrap both structs in an enum. This enum will handle the type differentiation during encoding/decoding:

enum Entry: Codable {
    case task(Task)
    case project(Project)
    
    // Internal keys for encoding/decoding type metadata
    private enum CodingKeys: String, CodingKey {
        case type
        case data
    }
    
    private enum EntryType: String, Codable {
        case task
        case project
    }
    
    // Custom decoding to determine which type to parse
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let entryType = try container.decode(EntryType.self, forKey: .type)
        let entryData = try container.decode(Data.self, forKey: .data)
        
        switch entryType {
        case .task:
            let task = try JSONDecoder().decode(Task.self, from: entryData)
            self = .task(task)
        case .project:
            let project = try JSONDecoder().decode(Project.self, from: entryData)
            self = .project(project)
        }
    }
    
    // Custom encoding to tag each entry with its type
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        switch self {
        case .task(let task):
            try container.encode(EntryType.task, forKey: .type)
            let taskData = try JSONEncoder().encode(task)
            try container.encode(taskData, forKey: .data)
        case .project(let project):
            try container.encode(EntryType.project, forKey: .type)
            let projectData = try JSONEncoder().encode(project)
            try container.encode(projectData, forKey: .data)
        }
    }
}

3. UserDefaults Extension for Easy Access

Wrap the UserDefaults logic in an extension to clean up your code and avoid repeated encoding/decoding boilerplate:

extension UserDefaults {
    private enum StorageKeys: String {
        case allEntries
    }
    
    var allEntries: [Entry] {
        get {
            guard let data = self.data(forKey: StorageKeys.allEntries.rawValue) else {
                return []
            }
            do {
                return try JSONDecoder().decode([Entry].self, from: data)
            } catch {
                print("Failed to decode entries: \(error.localizedDescription)")
                return []
            }
        }
        set {
            do {
                let data = try JSONEncoder().encode(newValue)
                self.set(data, forKey: StorageKeys.allEntries.rawValue)
            } catch {
                print("Failed to encode entries: \(error.localizedDescription)")
            }
        }
    }
}

4. Table View Implementation

Now integrate this with your table view to handle display and navigation:

import UIKit

class MainEntryTableViewController: UITableViewController {
    private var entries: [Entry] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Tasks & Projects"
        loadEntries()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "EntryCell")
    }
    
    private func loadEntries() {
        entries = UserDefaults.standard.allEntries
        tableView.reloadData()
    }
    
    // MARK: - Table View Data Source
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        entries.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "EntryCell", for: indexPath)
        let entry = entries[indexPath.row]
        
        switch entry {
        case .task(let task):
            cell.textLabel?.text = task.title
            cell.accessoryType = task.isCompleted ? .checkmark : .none
            // Style completed tasks with strikethrough
            if task.isCompleted {
                let strikethroughAttr = NSAttributedString(
                    string: task.title,
                    attributes: [.strikethroughStyle: NSUnderlineStyle.single.rawValue]
                )
                cell.textLabel?.attributedText = strikethroughAttr
            } else {
                cell.textLabel?.attributedText = nil
            }
        case .project(let project):
            cell.textLabel?.text = project.name
            cell.accessoryType = .disclosureIndicator
            cell.detailTextLabel?.text = "\(project.tasks.count) tasks"
        }
        
        return cell
    }
    
    // MARK: - Table View Delegate
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        let entry = entries[indexPath.row]
        
        switch entry {
        case .task(var task):
            // Toggle task completion status and save
            task.isCompleted.toggle()
            entries[indexPath.row] = .task(task)
            UserDefaults.standard.allEntries = entries
            tableView.reloadRows(at: [indexPath], with: .automatic)
        case .project(let project):
            // Navigate to project's task list
            let projectTaskVC = ProjectTaskListViewController()
            projectTaskVC.project = project
            navigationController?.pushViewController(projectTaskVC, animated: true)
        }
    }
}

// Project Task List View Controller
class ProjectTaskListViewController: UITableViewController {
    var project: Project!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        title = project.name
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ProjectTaskCell")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        project.tasks.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ProjectTaskCell", for: indexPath)
        let task = project.tasks[indexPath.row]
        cell.textLabel?.text = task.title
        cell.accessoryType = task.isCompleted ? .checkmark : .none
        return cell
    }
}

5. Key Notes

  • Why the enum? It lets us store both Task and Project in a single array under one UserDefaults key, while maintaining type safety.
  • Custom Codable implementation: The enum's manual encoding/decoding adds a type tag to each entry, so we know which struct to parse when retrieving data.
  • Scalability: If you add more entry types later, just extend the Entry enum and update the encoding/decoding logic—no major refactoring needed.
  • UUID usage: Unique IDs for tasks and projects make it easy to update or delete specific entries later if needed.

内容的提问来源于stack exchange,提问作者Anton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:29:41