markbattistella/swiftdatacounter
SwiftDataCounter is a Swift package that provides live tracking of SwiftData model counts with optional per-model or default limits. It listens for ModelContext save notifications and automatically refreshes counts, while also logging changes and limit crossings using OSLog
Why Use This Package?
SwiftDataCounter is designed for apps that need to track usage of SwiftData models and enforce limits. Common use cases:
- Free vs Paid Limits: Enforce "up to 10 users on the free plan."
- Feature Gating: Track counts of posts, projects, or files to unlock or disable features.
- Analytics: Log model usage over time without writing boilerplate counting logic.
- Debugging: Automatically observe when models exceed their limits.
By centralising count tracking and limit checks, you can keep app logic clean and consistent.
[!NOTE] Tracked models must conform to
FetchablePersistentModeland define afetchDescriptorso thatEntityCountercan query their counts.
Features
- Live Counts: Automatically refreshes when the
ModelContextsaves. - Per-Model Limits: Specify limits individually or apply a default to all.
- Aggregate Queries: Get combined totals and remaining capacity across all tracked models.
Installation
Swift Package Manager
Add SwiftDataCounter using Swift Package Manager:
dependencies: [
.package(
url: "https://github.com/markbattistella/SwiftDataCounter",
from: "26.3.8"
)
]Alternatively, add it in Xcode:
- Open your project in Xcode.
- Go to
File > Add Packages. - Enter the repository URL:
``url https://github.com/markbattistella/SwiftDataCounter ``
- Click Add Package.
Requirements
- Swift 6.0+
- iOS 17.0+
- macOS 14.0+
- Mac Catalyst 17.0+
- tvOS 17.0+
- watchOS 10.0+
- visionOS 1.0+
Usage
Setup
Tracked models must conform to FetchablePersistentModel and implement a static fetchDescriptor:
// Extend your existing model
extension User: FetchablePersistentModel {
static var fetchDescriptor: FetchDescriptor<User> {
FetchDescriptor<User>()
}
}Track models with a shared default limit:
import SwiftDataCounter
let counter = EntityCounter(
context: modelContext,
for: User.self, Post.self,
defaultLimit: 100
)Or with per-model limits:
let counter = EntityCounter(
context: modelContext,
for: (User.self, 10), (Post.self, nil) // nil = unlimited
)Per-Model Queries
let users = counter.count(for: User.self)
// eg. 6
let remainingUsers = counter.remaining(for: User.self)
// eg. 4 (10 limit - 6 used)
let userLimit = counter.limit(for: Post.self)
// nil when Post is set to unlimited
if counter.isOverLimit(for: User.self) {
print("User count is over the limit!")
}Aggregate Queries
let total = counter.grandCount
// Combined limit including unlimited models - can return nil
let combined = counter.combinedLimit(scope: .all)
// Combined limit excluding unlimited models
let finite = counter.combinedLimit(scope: .excludingUnlimited)
// Remaining total capacity
let remaining = counter.combinedRemaining(scope: .all)
if counter.isOverAnyLimit {
print("At least one model is over its limit.")
}Convenience Extensions
For cleaner code, extend EntityCounter with typed accessors:
extension EntityCounter {
var userCount: Int { count(for: User.self) }
var userRemaining: Int? { remaining(for: User.self) }
var userLimit: Int? { limit(for: User.self) }
var isUserOverLimit: Bool { isOverLimit(for: User.self) }
}Then use:
if isUserOverLimit {
print("Too many users. Remaining: \(userRemaining)")
}Logging
SwiftDataCounter uses OSLog via SimpleLogger.
It automatically logs:
- Model initialisation counts.
- Count changes (old → new).
- Limit crossings (exceeded / back under).
Example log in Console.app:
EntityCounter initialised. Tracking 2 models, defaultLimit = 10
Tracking User with limit 10
Tracking Post with no limit
User count changed from 3 to 5
User exceeded limit 10. Current count: 11Warnings
[!WARNING] Limited and unlimited models have different capacity semantics:
remaining(for:)andlimit(for:)returnnilfor unlimited models.isOverLimit(for:)always returnsfalsefor unlimited models.- Treat
nilas "no limit" rather than as zero remaining capacity.
Licence
SwiftDataCounter is released under the MIT licence. See LICENCE for details.
[Shield1]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmarkbattistella%2FSwiftDataCounter%2Fbadge%3Ftype%3Dswift-versions
[Shield2]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmarkbattistella%2FSwiftDataCounter%2Fbadge%3Ftype%3Dplatforms
[Shield3]: https://img.shields.io/badge/Licence-MIT-white?labelColor=blue&style=flat
Package Metadata
Repository: markbattistella/swiftdatacounter
Default branch: main
README: README.md