Ios data layer
Skill almasumdev/awesome-ios-agent-skills/.github/skills/architecture/ios-data-layer
Curated agent skills, conventions, and workflows for building iOS apps (Swift, SwiftUI, UIKit) with AI coding agents.
npx -y skills add almasumdev/awesome-ios-agent-skills --skill ios-data-layerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Expert guidance on designing an iOS data layer using the repository pattern, SwiftData or Core Data for persistence, URLSession async for networking, and offline-first strategies. Use when designing persistence, caching, or sync.
SKILL.md
5.1 KB, as published. Nobody here has run it
iOS Data Layer & Offline-First
Instructions
The data layer is the only place that knows about the network, disk, DTOs, and mapping. Presentation code depends on repository protocols defined in the domain layer.
1. Repository Protocol in Domain
public struct Article: Sendable, Identifiable, Hashable {
public let id: String
public let title: String
public let body: String
public let updatedAt: Date
}
public protocol ArticleRepository: Sendable {
func latest() async throws -> [Article]
func observeLatest() -> AsyncStream<[Article]>
func refresh() async throws
}
2. DTOs, Mappers, Remote Source
Never leak JSON shapes past the data layer.
struct ArticleDTO: Decodable {
let id: String
let title: String
let body: String
let updated_at: Date
}
extension ArticleDTO {
func toDomain() -> Article {
Article(id: id, title: title, body: body, updatedAt: updated_at)
}
}
actor ArticleRemoteSource {
private let session: URLSession
private let decoder: JSONDecoder
private let baseURL: URL
init(session: URLSession = .shared, baseURL: URL) {
self.session = session
self.baseURL = baseURL
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
self.decoder = decoder
}
func latest() async throws -> [ArticleDTO] {
let url = baseURL.appending(path: "articles")
let (data, response) = try await session.data(from: url)
try HTTPError.ensureOK(response)
return try decoder.decode([ArticleDTO].self, from: data)
}
}
3. Local Persistence with SwiftData (iOS 17+)
import SwiftData
@Model
final class ArticleEntity {
@Attribute(.unique) var id: String
var title: String
var body: String
var updatedAt: Date
init(id: String, title: String, body: String, updatedAt: Date) {
self.id = id; self.title = title; self.body = body; self.updatedAt = updatedAt
}
}
actor ArticleLocalSource {
private let context: ModelContext
init(context: ModelContext) { self.context = context }
func upsert(_ dtos: [ArticleDTO]) throws {
for dto in dtos {
let entity = ArticleEntity(id: dto.id, title: dto.title,
body: dto.body, updatedAt: dto.updated_at)
context.insert(entity)
}
try context.save()
}
func all() throws -> [Article] {
let descriptor = FetchDescriptor<ArticleEntity>(
sortBy: [SortDescriptor(\.updatedAt, order: .reverse)]
)
return try context.fetch(descriptor).map {
Article(id: $0.id, title: $0.title, body: $0.body, updatedAt: $0.updatedAt)
}
}
}
Use Core Data instead when deployment targets predate iOS 17 or you need mature migration tooling.
4. Offline-First Repository
Serve cache first, refresh in the background, publish updates as a stream:
actor DefaultArticleRepository: ArticleRepository {
private let remote: ArticleRemoteSource
private let local: ArticleLocalSource
private let continuation: AsyncStream<[Article]>.Continuation
nonisolated let stream: AsyncStream<[Article]>
init(remote: ArticleRemoteSource, local: ArticleLocalSource) {
self.remote = remote
self.local = local
var cont: AsyncStream<[Article]>.Continuation!
self.stream = AsyncStream { cont = $0 }
self.continuation = cont
}
func latest() async throws -> [Article] {
let cached = try await local.all()
if !cached.isEmpty { return cached }
try await refresh()
return try await local.all()
}
func refresh() async throws {
let dtos = try await remote.latest()
try await local.upsert(dtos)
let fresh = try await local.all()
continuation.yield(fresh)
}
nonisolated func observeLatest() -> AsyncStream<[Article]> { stream }
}
5. Errors & Retries
Model failures explicitly in the domain:
public enum DataError: Error, Sendable {
case offline
case notFound
case server(Int)
case decoding
}
Retry transient failures with exponential backoff (use the swift-async-await skill).
6. Testing
Stub the protocols, never the concrete implementations:
struct StubArticleRepository: ArticleRepository {
var articles: [Article] = []
func latest() async throws -> [Article] { articles }
func refresh() async throws {}
func observeLatest() -> AsyncStream<[Article]> { .init { $0.finish() } }
}
Checklist
- Repository protocols live in the domain package.
- DTOs never leak past the data layer.
- Reads serve cache first, then refresh.
- Writes are idempotent (
@Attribute(.unique)or equivalent). - Data sources are actors or otherwise
Sendable. - Errors map to domain cases — no
URLErrorat the view layer.