Snippet
Persist tab sessions without storing document text
A complete Swift session store that remembers tab metadata and selected tab while keeping document contents out of the session file.
AI-generated, human-reviewed: the first draft was generated by an AI agent; I fact-checked, tested, and edited it. AI use policy
What it does#
Persists tab metadata such as title, file path, dirty state, selection, and recovery record ID,
but intentionally excludes document text. On decode, it also ignores legacy text values if
an older session file contains them. Privacy by omission: not glamorous, oddly effective.
You need#
- macOS 15 or later
- Xcode 27 or Swift 6.4
- Foundation only
- A separate recovery-copy flow for unsaved text, such as the recovery-copy service
Where it goes#
Save it as PrivateSessionStore.swift in your core layer. Call save when tabs change and
load during app startup.
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Sarang Datye
import Foundation
struct SessionTab: Identifiable, Codable, Equatable, Sendable {
var id: UUID
var title: String
var filePath: String?
var isDirty: Bool
var selectionLocation: Int
var selectionLength: Int
var recoveryRecordID: UUID?
var transientText: String
private enum CodingKeys: String, CodingKey {
case id
case title
case filePath
case isDirty
case selectionLocation
case selectionLength
case recoveryRecordID
}
init(
id: UUID = UUID(),
title: String = "Untitled",
filePath: String? = nil,
isDirty: Bool = false,
selectionLocation: Int = 0,
selectionLength: Int = 0,
recoveryRecordID: UUID? = nil,
transientText: String = ""
) {
self.id = id
self.title = title
self.filePath = filePath
self.isDirty = isDirty
self.selectionLocation = max(0, selectionLocation)
self.selectionLength = max(0, selectionLength)
self.recoveryRecordID = recoveryRecordID
self.transientText = transientText
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
self.title = try container.decodeIfPresent(String.self, forKey: .title) ?? "Untitled"
self.filePath = try container.decodeIfPresent(String.self, forKey: .filePath)
self.isDirty = try container.decodeIfPresent(Bool.self, forKey: .isDirty) ?? false
self.selectionLocation = max(0, try container.decodeIfPresent(Int.self, forKey: .selectionLocation) ?? 0)
self.selectionLength = max(0, try container.decodeIfPresent(Int.self, forKey: .selectionLength) ?? 0)
self.recoveryRecordID = try container.decodeIfPresent(UUID.self, forKey: .recoveryRecordID)
self.transientText = ""
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(title, forKey: .title)
try container.encodeIfPresent(filePath, forKey: .filePath)
try container.encode(isDirty, forKey: .isDirty)
try container.encode(selectionLocation, forKey: .selectionLocation)
try container.encode(selectionLength, forKey: .selectionLength)
try container.encodeIfPresent(recoveryRecordID, forKey: .recoveryRecordID)
}
}
struct SessionState: Codable, Equatable, Sendable {
var schemaVersion: Int
var tabs: [SessionTab]
var selectedTabID: UUID?
init(schemaVersion: Int = 1, tabs: [SessionTab], selectedTabID: UUID?) {
self.schemaVersion = schemaVersion
self.tabs = tabs
self.selectedTabID = selectedTabID
}
}
struct PrivateSessionStore {
var fileManager: FileManager
var encoder: JSONEncoder
var decoder: JSONDecoder
init(fileManager: FileManager = .default) {
self.fileManager = fileManager
self.encoder = JSONEncoder()
self.encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
self.decoder = JSONDecoder()
}
func save(_ state: SessionState, to url: URL) throws {
try fileManager.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try encoder.encode(state).write(to: url, options: [.atomic])
}
func load(from url: URL) -> SessionState? {
guard let data = try? Data(contentsOf: url) else { return nil }
return try? decoder.decode(SessionState.self, from: data)
}
func reconciled(_ state: SessionState) -> SessionState {
let tabs = state.tabs.isEmpty ? [SessionTab()] : state.tabs.map { tab in
var copy = tab
copy.transientText = ""
if tab.filePath == nil && tab.isDirty && tab.recoveryRecordID == nil {
copy.isDirty = false
}
return copy
}
let selectedTabID = if let savedID = state.selectedTabID, tabs.contains(where: { $0.id == savedID }) {
savedID
} else {
tabs.first?.id
}
return SessionState(
schemaVersion: state.schemaVersion,
tabs: tabs,
selectedTabID: selectedTabID
)
}
}
Check that it works#
Encode a SessionState whose tab has transientText set. The resulting JSON should not
contain the text or a transientText property. Decode a legacy session and the text comes
back empty. Reconcile an empty tab list with a stale selected ID and verify the selected ID
points at the newly created tab.