데스크탑 앱
container CLI(Apple Silicon 위에서 경량 리눅스 컨테이너를 돌리는 macOS 네이티브 런타임)를 위한 SwiftUI 네이티브 컨트롤 서피스다.
컨테이너·이미지·볼륨·네트워크·레지스트리를 GUI로 관리하면서도, 실행 전 항상 실제 container 명령어를 미리 보여줘 "명령줄을 숨기지 않는다."
Swift 6.2 + SwiftUI + SwiftTerm(터미널) + Sparkle(자동업데이트) + Yams(Compose YAML 파싱)로 만든 순수 Swift Package Manager 프로젝트, Xcode 프로젝트 파일조차 없다.
container CLI엔 없는 앱 관리형 재시작 정책·헬스체크까지 직접 구현했다.
(저장소: tdeverx/contained-app · Swift · v1.0.0 · PolyForm Noncommercial)
이 저장소가 대체 무엇인가.
Contained는 컨테이너 런타임 자체를 만든 게 아니다. Apple이 2026년에 공개한 container CLI(github.com/apple/container — Apple Silicon 전용 경량 VM 기반 리눅스 컨테이너 런타임)를 명령줄 그대로 감싸는 SwiftUI 앱이다. 컨테이너 목록을 카드로 보여주고, 로그를 실시간 스트리밍하고, 터미널 탭에서 container exec로 직접 쉘에 들어가고, 이미지를 빌드·푸시하는 모든 작업을 GUI 클릭 몇 번으로 처리한다.
Docker Desktop을 써봤다면: Docker Desktop과 똑같은 일상 워크플로우(컨테이너 카드, 이미지 관리, 로그 뷰어) + "지금 실행될 정확한 CLI 명령어 미리보기" + 앱이 직접 구현한 재시작 정책이라고 이해하면 된다. Docker 데몬 대신 Apple의 VM 기반 런타임이 뒤에서 돌아간다는 점이 근본적으로 다르다.
container 명령어로 조작한다. Contained는 이 CLI의 GUI 프론트엔드다.Apple Container 생태계 초기 진입 + Docker Desktop 무거움 대비 포지션.
Apple의 container CLI는 아직 생태계 초기 단계다. CLI만 존재하고 공식 GUI가 없는 상태에서, Contained는 가장 이른 시점에 나온 SwiftUI 네이티브 GUI 중 하나로 자리를 잡았다. Docker Desktop은 Electron 기반(정확히는 자체 프레임워크지만 무겁다는 평가가 널리 퍼져있다)이라 메모리 사용량이 크고, macOS 네이티브 느낌이 부족하다는 불만이 꾸준히 제기돼왔다. Contained는 100% SwiftUI로 macOS 디자인 언어(Glass 효과, 툴바, 메뉴바 확장 등)를 그대로 따르면서, 어도비 앱들처럼 백그라운드에서 무거운 헬퍼 프로세스를 상시 구동하지 않는다.
| 항목 | Docker Desktop | Contained |
|---|---|---|
| 기반 런타임 | 자체 리눅스 VM + dockerd | Apple container CLI (Apple Silicon 전용 경량 VM) |
| UI 기술 | 독자 프레임워크(무겁다는 평 다수) | 100% SwiftUI (AppKit은 터미널 뷰 한 곳만) |
| CLI 투명성 | GUI 뒤에 명령어 숨김 | 실행 전 항상 "Reveal CLI"로 실제 명령어 노출 |
| 재시작 정책 | Docker 엔진 네이티브 지원 | CLI엔 없음 → 앱이 폴링으로 직접 구현(RestartWatchdog) |
| 라이선스/과금 | 기업 유료 정책 있음 | PolyForm Noncommercial (개인 무료, 상업 금지) |
| 플랫폼 | Win/Mac/Linux | macOS 26+ Apple Silicon 전용 |
Docker Desktop은 GUI가 CLI를 완전히 감춘다. Contained는 반대로 "GUI로 조작하되, 실제 실행될 명령어를 항상 보여준다"는 철학을 택했다. 또한 container CLI 자체엔 Docker의 --restart always 같은 정책이 없는데, Contained가 앱 레벨에서 상태를 폴링하며 크래시한 컨테이너를 지수 백오프로 재시작하는 워치독을 직접 구현했다 — CLI의 기능 공백을 GUI 레이어에서 메운 좋은 사례다.
터미널에서 container run --detach --publish 8080:80 nginx를 매번 손으로 치는 대신, Contained에서 "Run" 폼에 포트와 볼륨을 입력하고 "Reveal CLI" 버튼을 누르면 정확히 어떤 명령어가 실행될지 먼저 보여준다. 확인하고 실행하면 카드가 그리드에 나타나고, 크래시하면 라벨에 재시작 정책이 걸려있는 경우 앱이 알아서 재시작을 시도한다.
메뉴바 아이콘에서 컨테이너 상태를 바로 확인할 수 있고, ⌘K 커맨드 팔레트로 이미지 검색부터 로그 확인까지 키보드만으로 처리할 수 있다.
SwiftUI/AppKit, Apple container CLI, Process 래핑, 빌드 툴체인 각각 상세.
| 라이브러리 | 역할 | 비고 |
|---|---|---|
SwiftTerm 1.2.0+ | VT100/xterm 터미널 에뮬레이터 + PTY 호스트 | AppKit 기반. 앱에서 유일하게 AppKit에 직접 닿는 지점(NSViewRepresentable로 브릿지) |
Yams 5.0.0+ | 순수 Swift YAML 파서 | compose.yaml 임포트 기능에 사용 |
Sparkle 2.6.0+ | macOS 사실상 표준 자동 업데이트 프레임워크 | Stable/Beta/Nightly 3채널 appcast.xml 배포 |
| 계층 | 기술 | 설명 |
|---|---|---|
| UI 프레임워크 | SwiftUI (macOS 26 타겟) | 뷰 76개 파일, 앱 전체 사실상 100% SwiftUI |
| 상태 관리 | @Observable 매크로 | 구형 ObservableObject 대신 Swift 최신 관찰 패턴 사용 |
| 영속화 | SwiftData | 실행 히스토리·활동 로그 저장(Swift Charts로 시각화) |
| 동시성 | Swift Concurrency (async/await, @MainActor) | withCheckedThrowingContinuation으로 Process 콜백을 async로 브릿지 |
| 빌드 시스템 | Swift Package Manager (SPM) | Xcode 프로젝트 파일(.xcodeproj) 자체가 없음 — swift build로 빌드 |
| 백엔드(런타임) | Apple container CLI | Contained는 이 CLI를 서브프로세스로 호출만 함 — 직접 컨테이너 런타임을 구현하지 않음 |
| 도구 | 역할 |
|---|---|
| Swift 6.2 / SPM | 주 언어 및 빌드 시스템. Package.swift로 타겟 2개(실행형 + 코어 라이브러리) 정의 |
scripts/bundle.sh | SPM 빌드 산출물을 .app 번들 형태로 패키징 |
scripts/make-dmg.sh | 배포용 .dmg 이미지 생성 (채널별 배경 이미지 포함) |
scripts/appcast.sh | Sparkle용 appcast.xml 생성/갱신 |
| GitHub Actions | .github/workflows/{nightly,beta,stable,pr}.yml — 채널별 자동 빌드·서명·배포 |
Contained.entitlements | 앱 권한 설정 — 샌드박스 미적용(표준 경로 파일시스템 직접 탐색 필요하기 때문) |
.xcodeproj/.xcworkspace 없이 Package.swift 매니페스트만으로 앱을 정의·빌드하는 방식. 커맨드라인에서 swift build, swift test로 완결되며, CI에서 Xcode GUI 없이도 빌드 가능하다는 장점이 있다. Contained는 실행형 타겟(Contained)과 순수 로직 라이브러리 타겟(ContainedCore)을 분리해 테스트 용이성을 높였다.SwiftUI 뷰 ↔ CLI 래퍼(Process) ↔ container 런타임 흐름 + 핵심 설계 패턴.
Package.swift가 타겟을 두 개로 분리한다. ContainedCore는 SwiftUI 의존성이 전혀 없는 순수 로직(모델, CLI argv 빌더, JSON 디코더)이라 단위 테스트가 쉽다. Contained는 SwiftUI 앱 셸(뷰, @Observable 스토어, 디자인 시스템)이다. 실제로 Tests/ContainedCoreTests가 Tests/ContainedAppTests와 분리돼 있어, 로직 테스트는 UI 없이 빠르게 돌릴 수 있다.
ContainerCommands(순수 argv 문자열 배열 생성, side-effect 없음) → CommandRunner(실제 Process 실행) → ContainerClient(타입 디코딩 파사드)로 나뉜다. 이 분리 덕분에 "어떤 명령어가 만들어지는가"를 CLI 실행 없이 테스트할 수 있고, "Reveal CLI" 기능(실행 전 명령어 미리보기)도 ContainerCommands만 호출해서 구현 가능하다.
// ContainerCommands.swift — 순수 argv 빌더
public enum ContainerCommands {
static let jsonFormat = ["--format", "json"]
public static func list(all: Bool) -> [String] {
var args = ["list"]
if all { args.append("--all") }
return args + jsonFormat
}
// 비밀번호는 argv가 아닌 stdin으로 — 프로세스 목록에 노출 안 됨
public static func registryLogin(server: String, username: String) -> [String] {
["registry", "login", "--username", username,
"--password-stdin", server]
}
}
CommandRunner는 Process와 Pipe로 CLI를 실행하되, stdout/stderr를 동시에 읽어 한쪽 파이프 버퍼가 가득 차서 교착(deadlock)되는 걸 막는다. 로그 tail이나 이미지 pull 진행률처럼 실시간 스트림이 필요한 경우엔 AsyncThrowingStream으로 감싸고, 소비 측 Task가 취소되면 onTermination에서 자식 프로세스를 terminate()해 좀비 프로세스를 방지한다.
// CommandRunner.swift 핵심 구조 (요약)
public final class CommandRunner: CommandRunning {
public func run(_ arguments: [String], stdin: Data?) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
let process = Process()
process.executableURL = self.executableURL
process.arguments = arguments
let outPipe = Pipe(); let errPipe = Pipe()
process.standardOutput = outPipe
process.standardError = errPipe
try? process.run()
// stdout/stderr 동시 읽기 → 교착 방지
process.waitUntilExit()
continuation.resume(returning: /* 읽은 데이터 */ Data())
}
}
}
// 스트리밍: 취소 시 자식 프로세스 자동 종료
public func stream(_ arguments: [String]) -> AsyncThrowingStream<String, Error> {
AsyncThrowingStream { continuation in
continuation.onTermination = { _ in process.terminate() }
}
}
}
container CLI엔 Docker의 --restart always 같은 네이티브 재시작 정책이 없다. Contained는 매 새로고침(refresh) 주기마다 컨테이너 상태를 이전 상태와 diff해서, running → stopped로 전환됐고 contained.restart 라벨이 붙은 컨테이너를 감지하면 start 명령을 재실행한다. 재시도는 최대 5회, 지수 백오프로 제한해 크래시 루프를 방지한다.
// RestartWatchdog.swift — 핵심 아이디어
/// container CLI엔 네이티브 --restart가 없으므로, 매 refresh마다
/// 상태를 diff해서 running→stopped 전환 + contained.restart 라벨이
/// always/on-failure인 컨테이너에 한해 start를 재발행한다.
@MainActor
final class RestartWatchdog {
private let maxRetries = 5
private var lastState: [String: RuntimeStatus] = [:]
private var attempts: [String: Int] = [:]
private var nextEligible: [String: Date] = [:]
}
앱 전체에서 유일하게 AppKit에 직접 닿는 부분이 터미널 탭이다. SwiftTerm의 LocalProcessTerminalView(AppKit 뷰)를 NSViewRepresentable로 감싸 SwiftUI 트리에 끼워넣고, container exec --interactive --tty <id> <shell>을 그대로 실행한다.
// TerminalTab.swift — NSViewRepresentable로 AppKit 뷰 브릿지
func makeNSView(context: Context) -> LocalProcessTerminalView {
let view = LocalProcessTerminalView(frame: .zero)
var env = ProcessInfo.processInfo.environment
env["TERM"] = "xterm-256color"
view.startProcess(
executable: executableURL.path,
args: ["exec", "--interactive", "--tty", containerID, shell],
environment: env.map { "\(\$0.key)=\(\$0.value)" })
return view
}
// 뷰가 사라지면 exec 자식 프로세스도 함께 종료
static func dismantleNSView(_ nsView: LocalProcessTerminalView, coordinator: Coordinator) {
nsView.terminate()
}
샌드박스가 적용되지 않은 앱이므로, CLILocator가 표준 설치 경로(/usr/local/bin/container, /opt/homebrew/bin/container)를 직접 파일시스템 검사로 찾는다. 부트스트랩 시퀀스에서 container --version을 실행해 1.0. 접두사 버전만 지원하도록 검증한다.
// CLILocator.swift
public enum CLILocator {
public static let defaultCandidates = [
"/usr/local/bin/container",
"/opt/homebrew/bin/container",
]
public static func isSupported(_ version: String?) -> Bool {
guard let version else { return false }
return version.hasPrefix("1.0.")
}
}
Swift 소스 / 뷰 / 모델 구성 — 약 170개 파일.
contained.tint, contained.icon, contained.nickname, contained.restart 같은 자체 키를 심어서 개인화·재시작 정책 상태를 저장하는 방식. 일반 UI 커스터마이징(색상·아이콘)은 로컬 전용으로 유지하려 하지만, 재시작 정책처럼 "기능적으로 동작해야 하는" 라벨은 실제로 컨테이너 자체에 왕복 저장돼야 하므로 예외적으로 CLI 라벨에 반영된다.이 저장소에서 무엇을 배울 수 있는가.
Contained는 "GUI가 CLI를 대체하는 게 아니라 보조한다"는 철학을 코드로 구현한 좋은 사례다. ContainerCommands(순수 argv 빌더) → CommandRunner(실행) → ContainerClient(디코딩)의 3계층 분리를 그대로 따라 하면, 어떤 CLI 도구든 안전하게 GUI로 감쌀 수 있다.
배울 것: 순수 함수로 명령어 인자 배열 만들기(테스트 용이), "실행 전 미리보기" 기능을 위한 계층 분리, CLI 출력(JSON) 디코딩과 UI 모델 분리.
CommandRunner.swift는 Foundation의 Process와 Pipe를 다루는 실전 패턴을 보여준다. stdout/stderr를 동시에 읽어야 교착을 막을 수 있다는 것, stdin으로 비밀번호를 흘려보내 argv 노출을 피하는 것, 스트리밍 중 Task 취소 시 자식 프로세스를 확실히 종료(terminate())하는 것까지 — 프로덕션 수준의 서브프로세스 관리 코드다.
배울 것: Process.standardOutput/standardError 파이프 연결, withCheckedThrowingContinuation으로 콜백 API를 async/await로 브릿지, AsyncThrowingStream과 onTermination으로 리소스 정리 보장.
ObservableObject + @Published 구식 패턴 대신 Swift 최신 @Observable 매크로를 전역적으로 사용한다. AppModel, ContainersStore 등 모든 스토어가 @MainActor @Observable final class로 선언되고, .environment(app)으로 SwiftUI 뷰 트리에 주입된다.
배울 것: @Observable과 ObservableObject의 차이(세밀한 프로퍼티 단위 추적, 보일러플레이트 감소), @MainActor 격리로 UI 업데이트 스레드 안전성 보장, environment 기반 의존성 주입 패턴.
순수 SwiftUI 앱이라도 완전한 터미널 에뮬레이션처럼 SwiftUI가 못 하는 기능은 성숙한 AppKit 라이브러리(SwiftTerm)를 브릿지해서 해결한다. makeNSView/updateNSView/dismantleNSView 생명주기를 정확히 구현하면 SwiftUI와 AppKit을 매끄럽게 공존시킬 수 있다.
배울 것: NSViewRepresentable 프로토콜 구현, Coordinator 패턴으로 델리게이트 연결, 뷰 해제 시 리소스(자식 프로세스) 정리 타이밍.
Xcode 프로젝트 없이 SPM만으로 배포 가능한 .app 번들을 만드는 전체 파이프라인을 갖추고 있다. scripts/bundle.sh가 swift build 산출물을 번들링하고, scripts/make-dmg.sh가 채널별(stable/beta/nightly) 배경 이미지가 다른 DMG를 생성하며, Sparkle이 appcast.xml로 자동 업데이트를 제공한다.
배울 것: SPM 실행형 타겟을 macOS .app 번들로 패키징하는 절차, Sparkle 프레임워크로 자동 업데이트 구현, GitHub Actions로 3채널(Stable/Beta/Nightly) 빌드 파이프라인 구성, 코드 서명 없이/있이의 배포 차이.
ContainedCore(순수 로직)와 Contained(SwiftUI 셸)의 타겟 분리는 "테스트하기 쉬운 코드"를 설계하는 원칙을 보여준다. Tests/ContainedCoreTests의 Fixtures에는 실제 container list --format json 응답을 흉내낸 JSON 픽스처가 있어, CLI 실행 없이도 디코딩 로직을 검증할 수 있다.
배울 것: 비즈니스 로직과 UI 프레임워크 의존성 분리, JSON 픽스처 기반 디코더 테스트, 멀티 타겟 SPM 패키지 설계.
실행 환경 · 설치 조건.
| 항목 | 요구사항 |
|---|---|
| OS | macOS 26 이상 (Tahoe 이후) |
| CPU | Apple Silicon 전용 (Intel Mac 미지원) |
| 필수 사전 설치 | Apple container CLI 1.0.0이 PATH에 있어야 함 (표준 경로: /usr/local/bin 또는 /opt/homebrew/bin) |
| 개발 환경 | Xcode 26 / Swift 6.2+ (로컬 빌드 시) |
| 앱 상태 | Pre-1.0, 활발히 다듬는 중 (스크린샷 캡션 원문: "Pre-1.0 and actively polishing") |
| 라이선스 | PolyForm Noncommercial License 1.0.0 — 비상업적 사용만 허용, 포크 시 이름/아이콘 변경 필요 |
| 배포 채널 | Stable / Beta / Nightly 3채널, 기본값은 Nightly |
Contained는 GUI일 뿐, 컨테이너 런타임 자체를 포함하지 않는다. Apple의 container CLI(github.com/apple/container)를 먼저 설치하지 않으면 앱 실행 시 "CLI Missing" 부트스트랩 화면만 보게 된다. 또한 CLI 버전이 1.0.x가 아니면 "unsupported" 상태로 안내된다 — 코드에서 hasPrefix("1.0.")로 엄격히 체크한다.
App Sandbox가 적용되지 않은 앱이라, CLILocator가 /usr/local/bin/container, /opt/homebrew/bin/container 같은 표준 설치 경로를 직접 검사해 바이너리를 찾는다. Homebrew로 설치했든 공식 인스톨러로 설치했든 대부분 자동 인식된다.
난이도별 4개.
Apple Silicon Mac(macOS 26+)에서 Apple의 container CLI를 먼저 설치하고, Contained 앱을 빌드/실행해 alpine 이미지를 pull한 뒤 컨테이너를 실행해본다.
# 1) Apple container CLI 설치 (공식 저장소 릴리스 참고)
container --version # 1.0.x 확인
# 2) Contained 클론 & 빌드
git clone https://github.com/tdeverx/contained-app
cd contained-app
swift build
./scripts/bundle.sh debug
open Contained.app
확인 포인트: 앱 실행 시 부트스트랩 화면이 "ready" 상태로 넘어가는지, 컨테이너 카드가 그리드에 나타나는지 확인.
Run 폼에서 포트 매핑·볼륨·환경변수를 채운 뒤 "Reveal CLI" 버튼으로 미리보기 명령어를 확인하고, 터미널에서 똑같은 명령어를 직접 쳐서 결과가 동일한지 비교한다.
# 예상되는 미리보기 명령어 형태
container run --detach --name my-nginx \
--publish 8080:80 \
--volume ~/data:/usr/share/nginx/html \
--env FOO=bar \
nginx
확인 포인트: RunSpec.arguments()가 만든 argv와 실제 터미널 명령어가 정확히 일치하는지, GUI 폼 필드와 CLI 플래그의 1:1 매핑 관계를 이해한다.
Contained의 CommandRunner/ContainerCommands 계층 분리를 참고해, 다른 CLI 도구(예: git, docker, 자신이 만든 스크립트)를 감싸는 미니 SwiftUI 앱을 만들어본다.
// 순수 argv 빌더 (테스트 가능)
enum GitCommands {
static func status() -> [String] { ["status", "--short"] }
static func log(count: Int) -> [String] { ["log", "-\(count)", "--oneline"] }
}
// 실제 실행은 CommandRunner가 담당 (Contained 코드 참고)
확인 포인트: stdout/stderr를 동시에 읽지 않으면 어떤 상황에서 교착이 발생하는지 직접 재현해본다(큰 출력을 만드는 명령어로 테스트).
RestartWatchdog.swift를 참고해, 특정 라벨이 붙은 컨테이너가 크래시하면 최대 N회까지 지수 백오프로 재시작하는 로직을 직접 구현해본다. Contained 소스의 Tests/ContainedAppTests에서 관련 테스트가 있는지 찾아 패턴을 검증한다.
@MainActor
final class MyWatchdog {
private let maxRetries = 5
private var attempts: [String: Int] = [:]
private var nextEligible: [String: Date] = [:]
func checkAndRestart(previous: [String: RuntimeStatus],
current: [String: RuntimeStatus]) async {
// running → stopped 전환 감지 후 지수 백오프로 재시작
}
}
확인 포인트: 백오프 시간이 지수적으로 늘어나는지, maxRetries 초과 시 재시작을 포기하고 상태를 사용자에게 알리는지 확인한다.
Swift/SwiftUI · 컨테이너 · macOS 시스템 프로그래밍 이해를 위한 주차별 학습 경로.
목표: Swift 문법(옵셔널, 구조체 vs 클래스, 프로토콜), SwiftUI로 간단한 화면 만들기.
자료: Apple 공식 "The Swift Programming Language", "Introducing SwiftUI" 튜토리얼(developer.apple.com).
실습: 카드 그리드 UI를 SwiftUI LazyVGrid로 만들어보기. Contained의 ContainersGridView 구조 참고.
목표: @Observable 매크로의 동작 원리, environment 기반 의존성 주입 이해.
자료: WWDC "Discover Observation in SwiftUI" 세션, Apple Observation 프레임워크 문서.
실습: AppModel과 유사한 전역 상태 스토어를 만들어 여러 뷰에서 공유해보기.
목표: Process, Pipe, FileHandle로 외부 CLI를 실행하고 출력을 캡처하는 법.
자료: Apple Foundation Process 클래스 문서, "Swift에서 셸 명령어 실행하기" 관련 블로그 글들.
실습: CommandRunner.swift를 그대로 따라 작성해보고, 교착 상황을 의도적으로 재현 후 해결한다.
목표: 컨테이너와 VM의 차이, Apple의 경량 VM 기반 컨테이너 런타임이 Docker와 어떻게 다른지 이해.
자료: github.com/apple/container README 및 아키텍처 문서, Docker 공식 문서의 컨테이너 vs VM 설명.
실습: container CLI만으로 (GUI 없이) 이미지 pull → run → exec → logs 전체 흐름을 손으로 실행해보고, Contained가 이를 어떻게 GUI로 매핑했는지 소스와 대조한다.
목표: SwiftUI에서 AppKit 뷰를 통합하는 방법, 생명주기 관리.
자료: Apple NSViewRepresentable 문서, SwiftTerm 저장소(migueldeicaza/SwiftTerm) README.
실습: TerminalTab.swift를 참고해 간단한 텍스트 뷰를 AppKit NSTextView로 브릿지해보기.
목표: SPM 프로젝트를 .app/.dmg로 패키징하고 Sparkle로 자동 업데이트를 붙이는 전체 흐름.
자료: Sparkle 공식 문서(sparkle-project.org), scripts/bundle.sh·scripts/make-dmg.sh·scripts/appcast.sh 소스 정독.
실습: 자신의 SPM 실행형 프로젝트를 번들링해 .app으로 만들고, GitHub Actions로 릴리스 자동화 워크플로우를 구성해본다.
이 저장소를 이해하는 데 필요한 핵심 용어.
container CLI
run, list, logs, exec 등)를 갖는다.Process로 외부 실행파일을 띄우고, Pipe로 그 표준입출력을 연결해 데이터를 주고받는다. Contained의 CommandRunner가 이 API로 Apple container CLI를 호출한다.@Observable 스토어(AppModel, ContainersStore 등)가 사실상 ViewModel 역할을 하고, ContainedCore의 Model들이 Model 계층, SwiftUI 뷰들이 View 계층에 해당한다.ObservableObject+@Published를 대체). 클래스에 붙이면 프로퍼티 단위로 세밀하게 변경을 추적해, 실제로 사용된 프로퍼티가 바뀔 때만 관련 뷰를 다시 그린다. Contained의 모든 스토어가 @MainActor @Observable final class로 선언된다.container는 Virtualization.framework 기반 경량 VM으로 격리를 구현한다. Contained는 런타임을 만들지 않고 Apple 런타임의 CLI를 호출만 한다.Package.swift 매니페스트 하나로 타겟·의존성·리소스를 정의한다. Contained는 Xcode 프로젝트 파일 없이 SPM만으로 앱 전체를 빌드한다.NSView 기반) 뷰를 끼워넣기 위한 프로토콜. makeNSView/updateNSView/dismantleNSView 세 메서드를 구현하면 된다. Contained는 이걸로 SwiftTerm의 터미널 뷰를 SwiftUI 안에 통합한다.for await 구문으로 소비한다. Contained는 로그 tail이나 이미지 pull 진행률처럼 계속 흘러나오는 CLI 출력을 이 타입으로 감싸 UI에 스트리밍한다.appcast.xml이라는 RSS 유사 피드를 읽어 새 버전 유무를 확인하고 다운로드·설치를 자동화한다. Contained는 Stable/Beta/Nightly 3개 채널을 각각 다른 appcast 피드로 운영한다.더 깊이 파고들기 위한 공식 자료.