Skip to content

Latest commit

 

History

30 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

👯 Ensemble

Tests Swift 6.3 iOS 17+ Class B

Buy Me a Coffee

Ensemble is a SwiftUI presentation-state library for composing self-contained, reloadable features.

A ViewData value remembers the latest successful data and its current lifecycle phase. A ViewDataContext feeds it from one-shot asynchronous operations or asynchronous sequences, while AsyncContent decides whether a view should show the latest data, retained data, placeholders, a failure, or nothing.

  • Seed presentation state immediately, load one value, or bind a stream of updates.
  • Distinguish not-yet-loaded, loaded-but-empty, and reset observable state.
  • Keep useful data visible while a refresh is loading or fails.
  • Recover after a failure without terminating the stream.
  • Reload cold sources with .resubscribe or signal a hot source with .refresh(action).
  • Animate presentation presence separately from caller-owned successful-content changes.
AsyncContent(viewModel.entries) { entries, _ in
  List(entries) { entry in
    EntryRow(entry: entry)
  }
}

Install

Ensemble requires Swift 6.3 and supports iOS 17+ and macOS 14+. It is available through Swift Package Manager.

dependencies: [
  .package(url: "https://github.com/mtzaquia/ensemble.git", from: "2.0.0"),
]

Add the Ensemble product to the app target, then import Ensemble where it is used.

Five-minute start

Start with a source that returns an asynchronous sequence of Result values. Own one ViewData value and retain its ViewDataContext for as long as the subscription should remain active:

import Ensemble

struct Entry: Identifiable, Sendable {
  let id: Int
  let title: String
}

enum EntryFailure: Error, Sendable {
  case unavailable
}

final class EntriesUseCase {
  func values() -> AsyncStream<Result<[Entry], EntryFailure>> {
    AsyncStream { continuation in
      continuation.yield(.success([
        Entry(id: 1, title: "The latest value"),
      ]))
      continuation.finish()
    }
  }
}

@MainActor
final class EntriesViewModel {
  let entries = ViewData<[Entry]>()

  private let context = ViewDataContext()
  private let useCase: EntriesUseCase

  init(useCase: EntriesUseCase) {
    self.useCase = useCase
  }

  func start() {
    context.bind({ [useCase] in useCase.values() }, to: entries)
  }
}

ViewData is observable, so the surrounding view model does not need observation solely to expose it. Render the value with AsyncContent and start the binding from a stable ancestor:

import SwiftUI

struct EntriesView: View {
  @State private var viewModel: EntriesViewModel

  init(useCase: EntriesUseCase) {
    _viewModel = State(initialValue: EntriesViewModel(useCase: useCase))
  }

  var body: some View {
    ZStack {
      AsyncContent(viewModel.entries) { entries, _ in
        List(entries) { entry in
          Text(entry.title)
        }
      }
    }
    .task {
      viewModel.start()
    }
  }
}

With this AsyncContent initializer, loading and failure render nothing until a successful value exists, then retain that value. The presentation policies guide shows how to add placeholder or failure UI and where presentation and content animation belong.

The surrounding ZStack remains mounted while AsyncContent is empty, making it a reliable place to start the binding. Releasing the context cancels its subscription.

That is the core idea: bind observable presentation data once, then render its latest useful value with AsyncContent.

Documentation

  • Getting started — connect a source, choose its reload behavior, and render the first result.
  • Presentation policies — choose placeholder, retained, hidden, and failure-content behavior per view.
  • Sources and lifecycle — adapt source update types and understand reload, reset, completion, and cancellation.
  • Diagnostics — inspect load, binding, reload, and cancellation activity.

Sample app

Open SampleApp/SampleApp.xcodeproj to explore dedicated failure content, seeded content retained through refresh, independently loading sections, and the placeholder-first stable-ID animation scenario. See SampleApp/README.md for the scenario guide and manual animation checks.

License

Copyright (c) 2026 @mtzaquia

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A lightweight framework for composing self-contained, reloadable SwiftUI features.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages