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
TaskandProjectin a single array under oneUserDefaultskey, while maintaining type safety. - Custom Codable implementation: The enum's manual encoding/decoding adds a
typetag to each entry, so we know which struct to parse when retrieving data. - Scalability: If you add more entry types later, just extend the
Entryenum 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

