agentsclimarketplace

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.

Install
npx -y skills add almasumdev/awesome-ios-agent-skills --skill ios-data-layer

Assembled 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 URLError at the view layer.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.