Snippet
Open and save UTF-8 plain-text files safely
A complete Swift file service for .txt and .md documents with strict UTF-8 decoding, atomic saves, and a 10 MB warning.
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#
Opens only .txt and .md files, refuses invalid UTF-8 instead of silently corrupting it,
warns for files above 10 MB, and writes saves atomically. The file service is intentionally
boring. Boring is excellent when documents are involved.
You need#
- macOS 15 or later
- Xcode 27 or Swift 6.4
- Foundation only
- A caller that shows the large-file warning and asks the user before reopening with
allowLargeFile: true
Where it goes#
Save it as UTF8DocumentFileService.swift in a Swift package, app target, or shared core
module.
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Sarang Datye
import Foundation
let largeFileWarningThresholdBytes = 10 * 1024 * 1024
enum PlainTextDocumentType: String, Codable, Equatable, Sendable {
case txt
case md
case unknown
init(url: URL?) {
guard let ext = url?.pathExtension.lowercased(), !ext.isEmpty else {
self = .unknown
return
}
self = PlainTextDocumentType(rawValue: ext) ?? .unknown
}
var displayName: String {
switch self {
case .txt: return ".txt"
case .md: return ".md"
case .unknown: return "Plain text"
}
}
}
enum PlainTextFileError: Error, Equatable, LocalizedError, Sendable {
case unsupportedExtension(String)
case invalidUTF8
case invalidFileName(String)
case readFailed(String)
case writeFailed(String)
var errorDescription: String? {
switch self {
case .unsupportedExtension(let ext):
"Only UTF-8 .txt and .md files are supported. Unsupported extension: \(ext)."
case .invalidUTF8:
"This file is not valid UTF-8, so it was not opened to avoid silent corruption."
case .invalidFileName(let name):
"Save As names must be single file names, not paths. Invalid name: \(name)."
case .readFailed(let message):
"Could not read file: \(message)"
case .writeFailed(let message):
"Could not save file: \(message)"
}
}
}
struct PlainTextDocument: Equatable, Sendable {
var title: String
var text: String
var fileURL: URL?
var documentType: PlainTextDocumentType
var isDirty: Bool
}
struct OpenPlainTextDocumentResult: Equatable, Sendable {
var document: PlainTextDocument
var shouldWarnLargeFile: Bool
}
struct UTF8DocumentFileService {
var fileManager: FileManager
init(fileManager: FileManager = .default) {
self.fileManager = fileManager
}
func open(url: URL, allowLargeFile: Bool = false) throws -> OpenPlainTextDocumentResult {
let type = PlainTextDocumentType(url: url)
if type == .unknown {
throw PlainTextFileError.unsupportedExtension(url.pathExtension)
}
let attrs = try? fileManager.attributesOfItem(atPath: url.path)
let size = (attrs?[.size] as? NSNumber)?.int64Value ?? 0
let shouldWarn = size > Int64(largeFileWarningThresholdBytes)
if shouldWarn && !allowLargeFile {
return OpenPlainTextDocumentResult(
document: PlainTextDocument(
title: url.lastPathComponent,
text: "",
fileURL: url,
documentType: type,
isDirty: false
),
shouldWarnLargeFile: true
)
}
let data: Data
do {
data = try Data(contentsOf: url)
} catch {
throw PlainTextFileError.readFailed(error.localizedDescription)
}
guard let text = String(data: data, encoding: .utf8) else {
throw PlainTextFileError.invalidUTF8
}
return OpenPlainTextDocumentResult(
document: PlainTextDocument(
title: url.lastPathComponent,
text: text,
fileURL: url,
documentType: type,
isDirty: false
),
shouldWarnLargeFile: shouldWarn
)
}
func save(document: PlainTextDocument, to url: URL) throws -> PlainTextDocument {
let type = PlainTextDocumentType(url: url)
if type == .unknown {
throw PlainTextFileError.unsupportedExtension(url.pathExtension)
}
guard let data = document.text.data(using: .utf8) else {
throw PlainTextFileError.invalidUTF8
}
do {
try data.write(to: url, options: [.atomic])
return PlainTextDocument(
title: url.lastPathComponent,
text: document.text,
fileURL: url,
documentType: type,
isDirty: false
)
} catch {
throw PlainTextFileError.writeFailed(error.localizedDescription)
}
}
func defaultSaveAsURL(in folder: URL, proposedName: String = "Untitled") throws -> URL {
let name = proposedName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty,
!name.contains("/"),
!name.contains("\\"),
URL(fileURLWithPath: name).lastPathComponent == name else {
throw PlainTextFileError.invalidFileName(proposedName)
}
let baseName = (name as NSString).deletingPathExtension
let safeName = baseName.isEmpty ? "Untitled" : baseName
return folder.appendingPathComponent(safeName).appendingPathExtension("txt")
}
}
Check that it works#
Create a temporary .txt file, open it, save it to a second file, try opening a binary
file, and call defaultSaveAsURL with ../escape. The binary file should fail with
invalidUTF8, the extensionless open should fail with unsupportedExtension, and the
traversal name should fail with invalidFileName.