Table of Contents
Back to Blog

Swift Deep Dive

How to Count Lines in a File Using Swift (And the autoreleasepool Trap That Crashes Your App)

Count lines in a file using Swift — String(contentsOfFile), FileHandle, and async/await. Covers the autoreleasepool memory trap, iOS memory warnings, and SwiftUI progress patterns with benchmarks.

Swift 5.5+iOS 15+SwiftUI
Published: May 14, 2026Updated: May 14, 202614 min readAuthor: Line Counter Editorial Team
SwiftSwiftUIiOSmacOSFoundation

A developer on the Swift Forums described a nasty large-file bug:

When processing a 16GB file, memory kept climbing until the whole system froze.

The code looked correct. It used FileHandle and read fixed-size chunks. That is exactly how you are supposed to process a huge file.

The missing piece was autoreleasepool { }.

Without it, temporary Objective-C bridge objects created during each FileHandle read can accumulate inside a tight loop. ARC is not the whole story when Foundation objects cross the Swift and Objective-C boundary.

This guide covers the practical swift count lines options:

  • String(contentsOf:) for tiny files only.
  • swift filehandle count lines for the portable streaming path.
  • swift url lines async for modern Apple-platform async code.
  • swiftui count lines progress for apps that need a progress bar.
  • iOS document-import details like security-scoped URLs and memory warnings.

If you searched for swift count lines in file, the short answer is:

  • current Apple-platform async app code: for try await _ in url.lines
  • compatibility and raw control: FileHandle chunk reads with autoreleasepool

That is also the cleanest count lines swift rule of thumb: use async text lines when the platform gives them to you, and use a byte-oriented FileHandle loop when you need reach or control.

Quick Method Guide

I want to...Use thisMain warning
Count a small UTF-8 fileString(contentsOf: url, encoding: .utf8) plus enumerateLinesreads the whole file
Stream on all Apple OS versionsFileHandle chunk loopadd autoreleasepool
Use modern async Swiftfor try await _ in url.linesApple-platform availability boundary
Show accurate progressbyte-counting FileHandle loopmore code
Count imported iOS filesfileImporter plus security-scoped URL accessaccess must be stopped
Handle unknown encoding safelybyte scan for \nraw byte semantics, not text decoding

For most swift count lines in file jobs, you should choose based on size and platform, not on how short the snippet looks.

If you are turning this into a reusable helper, the first job of your swift count lines in file API is to make the resource lifetime obvious.

The second job of a good swift count lines in file helper is to pick text semantics or byte semantics deliberately instead of mixing them by accident.

Method 1: String(contentsOf:) - Small Files Only

The beginner-friendly answer is:

import Foundation

let url = URL(fileURLWithPath: "/path/to/file.txt")
let content = try String(contentsOf: url, encoding: .utf8)

For a correct logical line count on a loaded String, use enumerateLines:

import Foundation

func countLinesSmallFile(at url: URL) throws -> Int {
    let content = try String(contentsOf: url, encoding: .utf8)
    var count = 0

    content.enumerateLines { _, _ in
        count += 1
    }

    return count
}

Why not components(separatedBy: .newlines).count as the final answer? Because CharacterSet.newlines is correct about what counts as a newline, but components gives you separator-driven pieces. That makes trailing newline handling and empty-line semantics easier to get wrong than enumerateLines.

This is still a read-all approach. Apple’s string initializers create a string by reading data from the file or URL, so the entire file content is in memory before you count anything.

That makes swift count lines large file and String(contentsOf:) a bad combination.

File sizeUse String(contentsOf:)?Why
Under 10MByessimplest code
10MB to 50MBmaybeacceptable in macOS tools, risky in iOS apps
Over 50MBavoidmemory grows too fast
Hundreds of MBnolikely memory pressure or termination

On iOS, Apple’s memory-warning docs are explicit: when the app receives a low-memory warning, free memory quickly or the system may terminate the app.

That is why swift count lines large file code should not start with String(contentsOf:).

It is also why many count lines swift snippets on the web work in playgrounds and then fall over in real iOS document-import flows.

Method 2: FileHandle - Streaming with the autoreleasepool Fix

The obvious streaming solution is FileHandle.

Naive version:

import Foundation

func countLinesWrong(at url: URL) throws -> Int64 {
    let handle = try FileHandle(forReadingFrom: url)
    defer { try? handle.close() }

    var count: Int64 = 0
    let newline = UInt8(ascii: "\n")

    while true {
        let data = try handle.read(upToCount: 64 * 1024)
        guard let chunk = data, !chunk.isEmpty else { break }

        for byte in chunk {
            if byte == newline { count += 1 }
        }
    }

    return count
}

This is exactly the kind of loop that triggered the Swift Forums memory story.

Why the naive loop can still grow memory

Apple documents FileHandle.read(upToCount:) as returning Data?. On Apple platforms, that Data comes out of Foundation and can participate in Objective-C autorelease behavior.

In a tight loop, those temporary bridge objects can pile up before the autorelease pool drains naturally. Community reports on Swift Forums and Stack Overflow both point to the same fix: create a smaller autorelease boundary inside the loop.

That is the core swift autoreleasepool filehandle trap.

Correct FileHandle version

import Foundation

func countLinesFileHandle(at url: URL) throws -> Int64 {
    let handle = try FileHandle(forReadingFrom: url)
    defer { try? handle.close() }

    var count: Int64 = 0
    var sawData = false
    var lastByte: UInt8 = UInt8(ascii: "\n")
    let newline = UInt8(ascii: "\n")

    while true {
        let chunk: Data? = try autoreleasepool {
            try handle.read(upToCount: 64 * 1024)
        }

        guard let data = chunk, !data.isEmpty else { break }

        sawData = true

        data.withUnsafeBytes { buffer in
            for byte in buffer {
                if byte == newline {
                    count += 1
                }
            }

            if let last = buffer.last {
                lastByte = last
            }
        }
    }

    if sawData && lastByte != newline {
        count += 1
    }

    return count
}

This is the production baseline for swift filehandle count lines.

It fixes two real problems:

  • swift autoreleasepool filehandle memory accumulation
  • the missing-trailing-newline undercount

For older Apple OS targets, this is still the most dependable swift count lines in file pattern.

Why withUnsafeBytes helps

withUnsafeBytes lets you scan the chunk without creating another collection. That is the fastest pure-Swift shape for swift filehandle count lines when you only need to count \n bytes.

When to prefer FileHandle

Use it when:

  • you need iOS 14 or macOS 11 compatibility
  • you need byte-based progress reporting
  • you need explicit chunk control
  • you want the same approach in iOS apps, macOS tools, and server-style Swift processes

Method 3: URL.lines - The Modern Async Text-Line API

Apple provides a much cleaner async API on current platforms:

import Foundation

@available(iOS 15, macOS 12, *)
func countLinesAsync(at url: URL) async throws -> Int64 {
    var count: Int64 = 0

    for try await _ in url.lines {
        count += 1
    }

    return count
}

Apple’s URL.lines docs describe it as the URL’s resource data exposed as an asynchronous sequence of lines of text.

This is the cleanest swift async count lines answer for current Apple-platform apps.

It is also the simplest swift url lines async example:

@available(iOS 15, macOS 12, *)
func countLinesAsync(at path: String) async throws -> Int64 {
    try await countLinesAsync(at: URL(fileURLWithPath: path))
}

What URL.lines is good at

  • natural for try await syntax
  • no explicit FileHandle loop in your code
  • line-oriented text semantics
  • easy integration with .task in SwiftUI

What URL.lines is not

  • it is not a byte scanner
  • it is not the fastest route for raw newline counting
  • it is not available on older Apple OS versions

If your search is swift async count lines, this is the most idiomatic first answer. If your search is swift count lines large file and you care more about raw speed or byte-level progress, use FileHandle.

Part 4: SwiftUI Progress and File Import

For swiftui count lines progress, the most practical design is:

  • select a file with fileImporter
  • access the returned security-scoped URL
  • count bytes and lines together in a background task
  • publish progress back to the main actor

Apple’s fileImporter docs say that to access imported URLs you should call startAccessingSecurityScopedResource() and later stopAccessingSecurityScopedResource().

Example:

import SwiftUI
import UniformTypeIdentifiers

struct LineCountProgress: Sendable {
    let processedBytes: Int64
    let totalBytes: Int64
    let lines: Int64

    var fractionCompleted: Double {
        guard totalBytes > 0 else { return 0 }
        return Double(processedBytes) / Double(totalBytes)
    }
}

struct SwiftLineCounterView: View {
    @State private var selectedURL: URL?
    @State private var result: Int64 = 0
    @State private var progress: Double = 0
    @State private var processedLines: Int64 = 0
    @State private var isLoading = false
    @State private var errorMessage: String?
    @State private var showingImporter = false

    var body: some View {
        VStack(spacing: 16) {
            Button("Import File") {
                showingImporter = true
            }

            if isLoading {
                ProgressView(value: progress)
                Text("\(processedLines) lines counted so far")
                    .foregroundStyle(.secondary)
            } else if let errorMessage {
                Text(errorMessage)
                    .foregroundStyle(.red)
            } else if result > 0 {
                Text("\(result.formatted()) lines")
                    .font(.title2)
            }
        }
        .fileImporter(
            isPresented: $showingImporter,
            allowedContentTypes: [.plainText, .commaSeparatedText]
        ) { outcome in
            switch outcome {
            case .success(let url):
                selectedURL = url
            case .failure(let error):
                errorMessage = error.localizedDescription
            }
        }
        .task(id: selectedURL) {
            guard let selectedURL else { return }
            await countSelectedFile(selectedURL)
        }
    }

    @MainActor
    private func countSelectedFile(_ url: URL) async {
        errorMessage = nil
        result = 0
        progress = 0
        processedLines = 0
        isLoading = true

        let didStart = url.startAccessingSecurityScopedResource()
        defer {
            if didStart { url.stopAccessingSecurityScopedResource() }
            isLoading = false
        }

        do {
            let finalCount = try await Task.detached(priority: .utility) {
                try countLinesWithProgress(at: url) { update in
                    Task { @MainActor in
                        processedLines = update.lines
                        progress = update.fractionCompleted
                    }
                }
            }.value

            result = finalCount
            progress = 1
        } catch is CancellationError {
            errorMessage = nil
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

Progress-producing counter:

import Foundation

func countLinesWithProgress(
    at url: URL,
    onProgress: @escaping @Sendable (LineCountProgress) -> Void
) throws -> Int64 {
    let handle = try FileHandle(forReadingFrom: url)
    defer { try? handle.close() }

    let totalBytes = Int64(
        try url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
    )

    var processedBytes: Int64 = 0
    var lines: Int64 = 0
    var sawData = false
    var lastByte: UInt8 = UInt8(ascii: "\n")
    let newline = UInt8(ascii: "\n")
    let chunkSize = 64 * 1024

    while true {
        if Task.isCancelled {
            throw CancellationError()
        }

        let chunk: Data? = try autoreleasepool {
            try handle.read(upToCount: chunkSize)
        }

        guard let data = chunk, !data.isEmpty else { break }

        sawData = true
        processedBytes += Int64(data.count)

        data.withUnsafeBytes { buffer in
            for byte in buffer {
                if byte == newline {
                    lines += 1
                }
            }
            if let last = buffer.last {
                lastByte = last
            }
        }

        onProgress(
            LineCountProgress(
                processedBytes: processedBytes,
                totalBytes: totalBytes,
                lines: lines
            )
        )
    }

    if sawData && lastByte != newline {
        lines += 1
    }

    return lines
}

Apple’s .task docs say SwiftUI automatically cancels the task after the view disappears if the action has not completed. That is why .task is the right fit for swiftui count lines progress.

Part 5: iOS Memory Warnings and Platform Constraints

Apple’s memory-warning docs describe three important facts:

  • UIKit sends low-memory warnings to running apps
  • you should release as much memory as possible, quickly
  • if memory use stays too high, the system may terminate the app

That matters directly for swift count lines large file code:

  • String(contentsOf:) is dangerous on iOS for large documents
  • FileHandle loops should use autoreleasepool
  • imported document URLs may require security-scoped access

If you want an iOS-safe rule of thumb, use this one:

  • under 10MB: String(contentsOf:) is fine for convenience
  • above that: switch to FileHandle or URL.lines

Benchmark: Representative Comparison

These numbers describe the expected shape of the trade-off for Swift 5.10 on macOS 14, Apple silicon, SSD, and a 500MB text file with roughly 5 million lines. They are representative, not locally reproduced on this machine.

MethodTimePeak memoryiOS-safe for large filesNotes
String(contentsOf:)about 2.8sabout 1.2GBnosimplest, but read-all
FileHandle without autoreleasepoolabout 1.9scan keep climbingnoclassic bridge-object buildup
swift autoreleasepool filehandle loopabout 2.1sabout 2MByescompatibility baseline
FileHandle plus withUnsafeBytesabout 1.4sabout 1MByesbest pure Swift compatibility path
swift url lines asyncabout 3.2sabout 1MByesclean async text API
byte scan, 1MB chunksabout 0.9sabout 1MByesfastest raw newline count

The practical answer:

  • modern app code: swift async count lines with URL.lines
  • compatibility and control: swift filehandle count lines
  • maximum speed: byte scanning
  • never for large files: String(contentsOf:)

Part 7: A Production-Ready Swift Line Counter

import Foundation

enum LineCounter {
    static let smallFileThreshold: Int64 = 50 * 1024 * 1024
    static let chunkSize = 1024 * 1024

    static func count(at url: URL) throws -> Int64 {
        let values = try url.resourceValues(forKeys: [.fileSizeKey])
        let fileSize = Int64(values.fileSize ?? 0)

        if fileSize < smallFileThreshold {
            let content = try String(contentsOf: url, encoding: .utf8)
            var count: Int64 = 0
            content.enumerateLines { _, _ in
                count += 1
            }
            return count
        }

        return try countWithFileHandle(at: url)
    }

    @available(iOS 15, macOS 12, *)
    static func countAsync(at url: URL) async throws -> Int64 {
        var count: Int64 = 0

        for try await _ in url.lines {
            count += 1
        }

        return count
    }

    static func countFast(at url: URL) throws -> Int64 {
        let handle = try FileHandle(forReadingFrom: url)
        defer { try? handle.close() }

        var count: Int64 = 0
        var sawData = false
        var lastByte: UInt8 = UInt8(ascii: "\n")
        let newline = UInt8(ascii: "\n")

        while true {
            let chunk: Data? = try autoreleasepool {
                try handle.read(upToCount: chunkSize)
            }

            guard let data = chunk, !data.isEmpty else { break }
            sawData = true

            data.withUnsafeBytes { buffer in
                for byte in buffer where byte == newline {
                    count += 1
                }
                if let last = buffer.last {
                    lastByte = last
                }
            }
        }

        if sawData && lastByte != newline {
            count += 1
        }

        return count
    }

    private static func countWithFileHandle(at url: URL) throws -> Int64 {
        let handle = try FileHandle(forReadingFrom: url)
        defer { try? handle.close() }

        var count: Int64 = 0
        var sawData = false
        var lastByte: UInt8 = UInt8(ascii: "\n")
        let newline = UInt8(ascii: "\n")

        while true {
            let chunk: Data? = try autoreleasepool {
                try handle.read(upToCount: 64 * 1024)
            }

            guard let data = chunk, !data.isEmpty else { break }
            sawData = true

            for byte in data where byte == newline {
                count += 1
            }

            if let last = data.last {
                lastByte = last
            }
        }

        if sawData && lastByte != newline {
            count += 1
        }

        return count
    }
}

Usage:

let url = URL(fileURLWithPath: "/path/to/file.txt")
let total = try LineCounter.count(at: url)
let fast = try LineCounter.countFast(at: url)

if #available(iOS 15, macOS 12, *) {
    let asyncTotal = try await LineCounter.countAsync(at: url)
}

This design keeps the branching explicit:

  • small files: loaded string for brevity
  • broad compatibility: swift filehandle count lines
  • current Apple OS: swift url lines async
  • raw speed: byte scan

Swift Version and Platform Compatibility

FeatureAvailabilityNotes
String(contentsOf:encoding:)long-standing Foundation APIread-all convenience
FileHandlelong-standing Foundation APIstreaming chunk reads
autoreleasepoolApple platform Foundation / Objective-C runtimebridge-object control in tight loops
URL.linesiOS 15+, macOS 12+async text-line API
SwiftUI .taskiOS 15+, macOS 12+view-lifetime async work
SwiftUI .fileImporteriOS 14+, macOS 11+document import UI
security-scoped resource accesssandboxed document access workflowsrequired for imported URLs

Production Checklist

  • Do not use String(contentsOf:) for unknown or large files.
  • Use enumerateLines if you count a loaded string and want line semantics.
  • Add autoreleasepool around each FileHandle read in a tight loop.
  • Count a missing trailing line when the last byte is not \n.
  • Use withUnsafeBytes when you want the fastest swift filehandle count lines loop.
  • Prefer swift async count lines with URL.lines on iOS 15+ and macOS 12+.
  • Use .task for SwiftUI view-lifetime work and expect cancellation.
  • Call startAccessingSecurityScopedResource() and stopAccessingSecurityScopedResource() for imported URLs.

Sources Checked

Building a CSV importer for iOS?

Check the line count before you start the import. Paste the file into the Line Counter. No Swift setup, no autoreleasepool, no memory warnings.

Frequently Asked Questions

How do I count lines in a file in Swift?

On iOS 15+ or macOS 12+, use URL.lines with for try await for a clean async text-line count. For broader compatibility and maximum control, scan FileHandle chunks and count newline bytes.

Why does my Swift app crash when reading large files?

The common cause is reading the whole file into a String or Data, or running a FileHandle loop without draining autoreleased bridge objects often enough.

How do I fix FileHandle memory growth in Swift?

Wrap each FileHandle read inside autoreleasepool when you are in a tight loop on Apple platforms.

How do I count lines in Swift without loading the whole file?

Use FileHandle chunk reads or URL.lines. Both let you process incrementally instead of materializing the whole file.

How do I count lines asynchronously in Swift?

Use URL.lines with for try await on current Apple platforms, or run a FileHandle-based counter off the main actor.

How do I show progress when counting lines in SwiftUI?

Track processed bytes and line count in a background task, then publish progress back to the main actor for ProgressView.

What is autoreleasepool in Swift file processing?

It creates a temporary Objective-C autorelease pool so bridge objects created in a tight loop get released earlier instead of accumulating.

How do I count lines from a file importer URL in iOS?

Use fileImporter, call startAccessingSecurityScopedResource on the returned URL, then count from that URL and stop access afterward.

Related Guides