From dfd2fd7e08aa872088d1406cfb9908525ab28f93 Mon Sep 17 00:00:00 2001 From: RyosukeYunoki Date: Thu, 10 Sep 2026 16:05:40 +0900 Subject: [PATCH 1/3] Add fold-state-aware layout core for v0.1.0 Introduce DuoLayoutMode (derived from the horizontal size class), the DuoLayout container view, and the \.duoLayoutMode environment value with .publishDuoLayoutMode(). Set the deployment target to iOS 17. Add swift-testing unit tests, a GitHub Actions CI workflow, an MIT LICENSE, and real usage examples in the README. --- .github/workflows/ci.yml | 36 ++++++++ LICENSE | 21 +++++ Package.swift | 9 +- README.md | 90 +++++++++++++++++-- Sources/DuoLayoutKit/DuoLayout.swift | 44 +++++++++ Sources/DuoLayoutKit/DuoLayoutKit.swift | 2 - Sources/DuoLayoutKit/DuoLayoutMode.swift | 42 +++++++++ .../EnvironmentValues+DuoLayoutMode.swift | 49 ++++++++++ .../DuoLayoutKitTests/DuoLayoutKitTests.swift | 6 -- .../DuoLayoutModeTests.swift | 33 +++++++ 10 files changed, 313 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 LICENSE create mode 100644 Sources/DuoLayoutKit/DuoLayout.swift delete mode 100644 Sources/DuoLayoutKit/DuoLayoutKit.swift create mode 100644 Sources/DuoLayoutKit/DuoLayoutMode.swift create mode 100644 Sources/DuoLayoutKit/EnvironmentValues+DuoLayoutMode.swift delete mode 100644 Tests/DuoLayoutKitTests/DuoLayoutKitTests.swift create mode 100644 Tests/DuoLayoutKitTests/DuoLayoutModeTests.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..03c9d2b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Build & test (macOS host) + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - name: Show Swift version + run: swift --version + - name: Build + run: swift build -v + - name: Test + run: swift test -v + + ios-build: + name: Build for iOS + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - name: Build for generic iOS device + run: > + xcodebuild build + -scheme DuoLayoutKit + -destination 'generic/platform=iOS' + -skipPackagePluginValidation diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4be44ba --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ryosuke Yunoki + +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. diff --git a/Package.swift b/Package.swift index 07e4c2d..700eeb8 100644 --- a/Package.swift +++ b/Package.swift @@ -5,16 +5,19 @@ import PackageDescription let package = Package( name: "DuoLayoutKit", + platforms: [ + .iOS(.v17), + .macCatalyst(.v17), + // Supported so `swift test` can exercise the pure layout logic on a Mac host. + .macOS(.v14), + ], products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "DuoLayoutKit", targets: ["DuoLayoutKit"] ), ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. .target( name: "DuoLayoutKit" ), diff --git a/README.md b/README.md index 180eda7..6569b99 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,13 @@ states, inner/outer displays, and split layouts. ## Why -iPhone Duo introduces folded and unfolded poses, an inner and an outer -display, and new multitasking layouts. DuoLayoutKit gives you small, -composable SwiftUI helpers so your app adapts to every pose without -scattering size-class checks across your codebase. +iPhone Duo introduces folded and unfolded poses and new multitasking +layouts. Apple's guidance is to adapt with **size classes**, not device or +orientation checks. DuoLayoutKit wraps that guidance in small, composable +SwiftUI helpers so your app expresses layout *intent* — "compact" vs. +"expanded" — instead of scattering `horizontalSizeClass` comparisons across +your codebase. Because it is built on size classes, the same code also does +the right thing on non-foldable iPhones and in iPad Split View. ## Installation @@ -30,19 +33,90 @@ dependencies: [ ## Usage +### `DuoLayout` — branch on the current layout mode + +```swift +import SwiftUI +import DuoLayoutKit + +struct LibraryScreen: View { + var body: some View { + DuoLayout { mode in + switch mode { + case .compact: + NavigationStack { + BookList() + } + case .expanded: + NavigationSplitView { + BookList() + } detail: { + BookDetail() + } + } + } + } +} +``` + +The closure re-runs whenever the mode changes — unfolding iPhone Duo, +rotating, or entering Split View. + +### `\.duoLayoutMode` — read the mode anywhere + +Publish the mode once near the root of your scene, then read it from any +descendant without threading it through initializers: + ```swift -// API under design — coming with v0.1.0 +@main +struct BooksApp: App { + var body: some Scene { + WindowGroup { + RootView() + .publishDuoLayoutMode() + } + } +} + +struct Toolbar: View { + @Environment(\.duoLayoutMode) private var mode + + var body: some View { + HStack { + if mode.isExpanded { + SearchField() + } + AddButton() + } + } +} +``` + +### `DuoLayoutMode` — the model + +`DuoLayoutMode` is derived purely from the horizontal size class: + +| Context | Mode | +| --- | --- | +| iPhone Duo unfolded · iPad full screen · wide split pane | `.expanded` | +| iPhone Duo folded · iPhone portrait · narrow split pane | `.compact` | + +```swift +DuoLayoutMode(horizontalSizeClass: .regular) // .expanded +DuoLayoutMode(horizontalSizeClass: .compact) // .compact ``` ## Requirements -- iOS 27.1+ -- Xcode 27.1+ +- iOS 17+ / Mac Catalyst 17+ - Swift 6+ +Testing the folded and unfolded poses in Simulator additionally requires +Xcode 27.1 and the iPhone Duo simulator (DeviceHub). + ## Roadmap -- [ ] Fold-state-aware layout container +- [x] Fold-state-aware layout container (`DuoLayout`, `DuoLayoutMode`) - [ ] Inner/outer display helpers - [ ] Split layout utilities - [ ] UIKit support diff --git a/Sources/DuoLayoutKit/DuoLayout.swift b/Sources/DuoLayoutKit/DuoLayout.swift new file mode 100644 index 0000000..28eb6bd --- /dev/null +++ b/Sources/DuoLayoutKit/DuoLayout.swift @@ -0,0 +1,44 @@ +import SwiftUI + +/// A container that builds different content for the compact and expanded +/// ``DuoLayoutMode``, switching automatically as the scene resizes — for example when +/// iPhone Duo is unfolded, or when a window enters Split View. +/// +/// ```swift +/// DuoLayout { mode in +/// switch mode { +/// case .compact: +/// NavigationStack { LibraryList() } +/// case .expanded: +/// NavigationSplitView { +/// LibrarySidebar() +/// } detail: { +/// LibraryDetail() +/// } +/// } +/// } +/// ``` +/// +/// The closure receives the current mode, and the same value is published into the +/// environment (`\.duoLayoutMode`) for descendant views, so nested content does not +/// need to thread the mode through manually. +public struct DuoLayout: View { + + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + private let content: (DuoLayoutMode) -> Content + + /// Creates a layout container. + /// + /// - Parameter content: A view builder invoked with the current ``DuoLayoutMode`` + /// whenever it changes. + public init(@ViewBuilder content: @escaping (DuoLayoutMode) -> Content) { + self.content = content + } + + public var body: some View { + let mode = DuoLayoutMode(horizontalSizeClass: horizontalSizeClass) + content(mode) + .environment(\.duoLayoutMode, mode) + } +} diff --git a/Sources/DuoLayoutKit/DuoLayoutKit.swift b/Sources/DuoLayoutKit/DuoLayoutKit.swift deleted file mode 100644 index 08b22b8..0000000 --- a/Sources/DuoLayoutKit/DuoLayoutKit.swift +++ /dev/null @@ -1,2 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book diff --git a/Sources/DuoLayoutKit/DuoLayoutMode.swift b/Sources/DuoLayoutKit/DuoLayoutMode.swift new file mode 100644 index 0000000..825c748 --- /dev/null +++ b/Sources/DuoLayoutKit/DuoLayoutMode.swift @@ -0,0 +1,42 @@ +import SwiftUI + +/// A semantic description of how much horizontal space the current scene offers. +/// +/// `DuoLayoutMode` is derived entirely from the active horizontal size class, which is +/// the signal Apple recommends for adapting layout on iPhone Duo. It deliberately does +/// **not** try to detect the device model, the physical fold angle, or the interface +/// orientation — none of those are reliable inputs for layout decisions. +/// +/// | Context | Horizontal size class | Mode | +/// | --- | --- | --- | +/// | iPhone Duo, unfolded (inner display) | `.regular` | ``expanded`` | +/// | iPhone Duo, folded (outer display) | `.compact` | ``compact`` | +/// | iPhone in portrait | `.compact` | ``compact`` | +/// | iPad full screen, or a wide split-view pane | `.regular` | ``expanded`` | +/// +/// Because the mapping is expressed in terms of size classes, code written against +/// `DuoLayoutMode` also behaves correctly on non-foldable devices and in +/// multitasking configurations. +public enum DuoLayoutMode: Sendable, Hashable, CaseIterable { + + /// A single-column experience suited to a narrow scene, such as the outer display + /// of a folded iPhone Duo or a portrait iPhone. + case compact + + /// A multi-column experience suited to a wide scene, such as the inner display of + /// an unfolded iPhone Duo or a full-screen iPad. + case expanded + + /// Derives the layout mode from a horizontal size class. + /// + /// - Parameter horizontalSizeClass: The value from + /// `@Environment(\.horizontalSizeClass)` (SwiftUI) or + /// `traitCollection.horizontalSizeClass` (UIKit). A `nil` value — which SwiftUI + /// reports before a view is placed in a window — is treated as ``compact``. + public init(horizontalSizeClass: UserInterfaceSizeClass?) { + self = horizontalSizeClass == .regular ? .expanded : .compact + } + + /// Whether this mode represents a wide, multi-column scene. + public var isExpanded: Bool { self == .expanded } +} diff --git a/Sources/DuoLayoutKit/EnvironmentValues+DuoLayoutMode.swift b/Sources/DuoLayoutKit/EnvironmentValues+DuoLayoutMode.swift new file mode 100644 index 0000000..235ff90 --- /dev/null +++ b/Sources/DuoLayoutKit/EnvironmentValues+DuoLayoutMode.swift @@ -0,0 +1,49 @@ +import SwiftUI + +private struct DuoLayoutModeKey: EnvironmentKey { + static let defaultValue = DuoLayoutMode.compact +} + +extension EnvironmentValues { + + /// The current ``DuoLayoutMode`` for the surrounding view hierarchy. + /// + /// The value defaults to ``DuoLayoutMode/compact`` until an ancestor publishes the + /// real mode with ``SwiftUI/View/publishDuoLayoutMode()``. Read it from any + /// descendant view: + /// + /// ```swift + /// @Environment(\.duoLayoutMode) private var layoutMode + /// ``` + public var duoLayoutMode: DuoLayoutMode { + get { self[DuoLayoutModeKey.self] } + set { self[DuoLayoutModeKey.self] = newValue } + } +} + +private struct PublishDuoLayoutMode: ViewModifier { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + func body(content: Content) -> some View { + content.environment(\.duoLayoutMode, DuoLayoutMode(horizontalSizeClass: horizontalSizeClass)) + } +} + +extension View { + + /// Computes the current ``DuoLayoutMode`` from the active horizontal size class and + /// publishes it into the environment for descendant views to read via + /// `\.duoLayoutMode`. + /// + /// Apply it once, high in the hierarchy — typically on the root of a scene: + /// + /// ```swift + /// WindowGroup { + /// RootView() + /// .publishDuoLayoutMode() + /// } + /// ``` + public func publishDuoLayoutMode() -> some View { + modifier(PublishDuoLayoutMode()) + } +} diff --git a/Tests/DuoLayoutKitTests/DuoLayoutKitTests.swift b/Tests/DuoLayoutKitTests/DuoLayoutKitTests.swift deleted file mode 100644 index 022ab91..0000000 --- a/Tests/DuoLayoutKitTests/DuoLayoutKitTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Testing -@testable import DuoLayoutKit - -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. -} diff --git a/Tests/DuoLayoutKitTests/DuoLayoutModeTests.swift b/Tests/DuoLayoutKitTests/DuoLayoutModeTests.swift new file mode 100644 index 0000000..ff5f409 --- /dev/null +++ b/Tests/DuoLayoutKitTests/DuoLayoutModeTests.swift @@ -0,0 +1,33 @@ +import SwiftUI +import Testing +@testable import DuoLayoutKit + +@Suite("DuoLayoutMode derivation") +struct DuoLayoutModeTests { + + @Test("A regular horizontal size class is expanded") + func regularIsExpanded() { + #expect(DuoLayoutMode(horizontalSizeClass: .regular) == .expanded) + } + + @Test("A compact horizontal size class is compact") + func compactIsCompact() { + #expect(DuoLayoutMode(horizontalSizeClass: .compact) == .compact) + } + + @Test("A missing horizontal size class falls back to compact") + func nilIsCompact() { + #expect(DuoLayoutMode(horizontalSizeClass: nil) == .compact) + } + + @Test("isExpanded matches the case") + func isExpandedFlag() { + #expect(DuoLayoutMode.expanded.isExpanded) + #expect(!DuoLayoutMode.compact.isExpanded) + } + + @Test("The environment default is compact") + func environmentDefault() { + #expect(EnvironmentValues().duoLayoutMode == .compact) + } +} From 085b64aa95fa6868d72a49a3c10b12bc8a618514 Mon Sep 17 00:00:00 2001 From: RyosukeYunoki Date: Thu, 10 Sep 2026 16:14:56 +0900 Subject: [PATCH 2/3] Add DocC catalog, changelog, contributing guide, and repo templates Add a DocC documentation catalog, CHANGELOG.md (Keep a Changelog), CONTRIBUTING.md, GitHub PR and issue templates, README badges and a Documentation section, and a docbuild step in CI. Expand the unit tests to 7 cases. --- .github/ISSUE_TEMPLATE/bug_report.md | 22 ++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 19 +++++++ .github/PULL_REQUEST_TEMPLATE.md | 10 ++++ .github/workflows/ci.yml | 8 ++- CHANGELOG.md | 26 ++++++++++ CONTRIBUTING.md | 43 ++++++++++++++++ README.md | 26 +++++++--- .../DuoLayoutKit.docc/DuoLayoutKit.md | 51 +++++++++++++++++++ .../EnvironmentValues+DuoLayoutMode.swift | 3 +- .../DuoLayoutModeTests.swift | 12 +++++ 10 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Sources/DuoLayoutKit/DuoLayoutKit.docc/DuoLayoutKit.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2a665f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: Something behaves incorrectly +title: "" +labels: bug +assignees: "" +--- + +**What happened** + +**What you expected** + +**Pose / size-class configuration** + + +**Environment** +- DuoLayoutKit version: +- Xcode version: +- OS / device: + +**Minimal reproduction** + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..fa1e208 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an addition or change +title: "" +labels: enhancement +assignees: "" +--- + +**The problem** + + +**Proposed API** + + +**Alternatives considered** + +**Notes** + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4ee6984 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## Summary + + + +## Checklist + +- [ ] `swift test` passes +- [ ] Public symbols have `///` doc comments +- [ ] `CHANGELOG.md` updated under `[Unreleased]` +- [ ] No branching on device model, `UIScreen.main`, idiom, or interface orientation diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03c9d2b..7da7eb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: run: swift test -v ios-build: - name: Build for iOS + name: Build for iOS & docs runs-on: macos-15 steps: - uses: actions/checkout@v4 @@ -34,3 +34,9 @@ jobs: -scheme DuoLayoutKit -destination 'generic/platform=iOS' -skipPackagePluginValidation + - name: Build documentation + run: > + xcodebuild docbuild + -scheme DuoLayoutKit + -destination 'generic/platform=iOS' + -skipPackagePluginValidation diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b11c8e3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +All notable changes to DuoLayoutKit are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- `DuoLayoutMode` — a semantic `.compact` / `.expanded` value derived from the + horizontal size class, with an `init(horizontalSizeClass:)` and an `isExpanded` + helper. +- `DuoLayout` — a container view that rebuilds its content for the current + `DuoLayoutMode` and publishes that mode into the environment. +- `\.duoLayoutMode` environment value and the `.publishDuoLayoutMode()` view + modifier for reading the mode from any descendant view. +- DocC catalog, MIT license, and GitHub Actions CI (macOS host tests + iOS build). + +### Notes + +- Verification of the folded / unfolded poses in the iPhone Duo simulator is + pending the public Xcode 27.1 beta. + +[Unreleased]: https://github.com/yunodevs/DuoLayoutKit/commits/main diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8bd2df9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to DuoLayoutKit + +Thanks for your interest in improving DuoLayoutKit. This is a small, focused +library, so the bar for contributions is mostly about keeping it that way. + +## Ground rules + +- **Build on size classes, not device or orientation checks.** DuoLayoutKit + follows Apple's iPhone Duo guidance: adapt to the *available space* through + size classes, safe areas, and view/scene geometry. Pull requests that branch + on `UIDevice`, `UIScreen.main`, idiom, or interface orientation will not be + merged. +- **Public API stays documented.** Every `public` symbol needs a `///` doc + comment. +- **Keep the surface small.** New API should earn its place. Open an issue to + discuss anything beyond a bug fix before writing code. + +## Development + +```sh +swift build +swift test +``` + +`swift test` runs the pure layout logic on the macOS host. Building for iOS: + +```sh +xcodebuild build -scheme DuoLayoutKit -destination 'generic/platform=iOS' +``` + +CI runs both on every pull request. + +## Submitting changes + +1. Fork and create a topic branch (`feat/…` or `fix/…`). +2. Add or update tests for behaviour changes. +3. Update `CHANGELOG.md` under `[Unreleased]`. +4. Open a pull request against `main` with a clear description. + +## Reporting bugs + +Use the issue templates. For layout bugs, please say which pose / size-class +configuration you saw the problem in. diff --git a/README.md b/README.md index 6569b99..a6e6801 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ # DuoLayoutKit -Adaptive layout utilities for iPhone Duo — SwiftUI helpers for foldable -states, inner/outer displays, and split layouts. +[![CI](https://github.com/yunodevs/DuoLayoutKit/actions/workflows/ci.yml/badge.svg)](https://github.com/yunodevs/DuoLayoutKit/actions/workflows/ci.yml) +[![Swift 6](https://img.shields.io/badge/Swift-6-orange.svg)](https://swift.org) +[![Platforms](https://img.shields.io/badge/platforms-iOS%2017%2B%20%7C%20Mac%20Catalyst%2017%2B-blue.svg)](#requirements) +[![SwiftPM](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://swift.org/package-manager) +[![License: MIT](https://img.shields.io/badge/license-MIT-lightgrey.svg)](LICENSE) + +Adaptive layout utilities for iPhone Duo — SwiftUI helpers that turn the +folded and unfolded poses into plain layout intent. @@ -19,9 +25,8 @@ the right thing on non-foldable iPhones and in iPad Split View. ### Swift Package Manager -In Xcode: **File > Add Package Dependencies…** and enter: -https://github.com/yunodevs/DuoLayoutKit - +In Xcode: **File > Add Package Dependencies…** and enter +`https://github.com/yunodevs/DuoLayoutKit`. Or add it to your `Package.swift`: @@ -114,6 +119,12 @@ DuoLayoutMode(horizontalSizeClass: .compact) // .compact Testing the folded and unfolded poses in Simulator additionally requires Xcode 27.1 and the iPhone Duo simulator (DeviceHub). +## Documentation + +Every public symbol ships with doc comments, and the package includes a +[DocC catalog](Sources/DuoLayoutKit/DuoLayoutKit.docc). Build it in Xcode with +**Product > Build Documentation**. + ## Roadmap - [x] Fold-state-aware layout container (`DuoLayout`, `DuoLayoutMode`) @@ -121,6 +132,9 @@ Xcode 27.1 and the iPhone Duo simulator (DeviceHub). - [ ] Split layout utilities - [ ] UIKit support +See [CHANGELOG.md](CHANGELOG.md) for release notes and +[CONTRIBUTING.md](CONTRIBUTING.md) to get involved. + ## License -MIT +DuoLayoutKit is available under the MIT license. See [LICENSE](LICENSE). diff --git a/Sources/DuoLayoutKit/DuoLayoutKit.docc/DuoLayoutKit.md b/Sources/DuoLayoutKit/DuoLayoutKit.docc/DuoLayoutKit.md new file mode 100644 index 0000000..903ce96 --- /dev/null +++ b/Sources/DuoLayoutKit/DuoLayoutKit.docc/DuoLayoutKit.md @@ -0,0 +1,51 @@ +# ``DuoLayoutKit`` + +Adaptive layout utilities for iPhone Duo — SwiftUI helpers that turn the folded +and unfolded poses into plain layout intent. + +## Overview + +iPhone Duo introduces folded and unfolded poses and new multitasking layouts. +Apple's guidance is to adapt with **size classes**, not device or orientation +checks. DuoLayoutKit wraps that guidance in a small set of SwiftUI helpers so +your code expresses *intent* — compact versus expanded — instead of scattering +`horizontalSizeClass` comparisons across the codebase. + +Because everything is derived from the horizontal size class, the same code also +behaves correctly on non-foldable iPhones and in iPad Split View. + +```swift +DuoLayout { mode in + switch mode { + case .compact: + NavigationStack { BookList() } + case .expanded: + NavigationSplitView { + BookList() + } detail: { + BookDetail() + } + } +} +``` + +To read the mode deeper in the hierarchy, publish it once near the root of your +scene and pick it up from the environment: + +```swift +RootView() + .publishDuoLayoutMode() + +// elsewhere +@Environment(\.duoLayoutMode) private var mode +``` + +## Topics + +### Layout mode + +- ``DuoLayoutMode`` + +### Containers + +- ``DuoLayout`` diff --git a/Sources/DuoLayoutKit/EnvironmentValues+DuoLayoutMode.swift b/Sources/DuoLayoutKit/EnvironmentValues+DuoLayoutMode.swift index 235ff90..09c59a3 100644 --- a/Sources/DuoLayoutKit/EnvironmentValues+DuoLayoutMode.swift +++ b/Sources/DuoLayoutKit/EnvironmentValues+DuoLayoutMode.swift @@ -9,8 +9,7 @@ extension EnvironmentValues { /// The current ``DuoLayoutMode`` for the surrounding view hierarchy. /// /// The value defaults to ``DuoLayoutMode/compact`` until an ancestor publishes the - /// real mode with ``SwiftUI/View/publishDuoLayoutMode()``. Read it from any - /// descendant view: + /// real mode with `publishDuoLayoutMode()`. Read it from any descendant view: /// /// ```swift /// @Environment(\.duoLayoutMode) private var layoutMode diff --git a/Tests/DuoLayoutKitTests/DuoLayoutModeTests.swift b/Tests/DuoLayoutKitTests/DuoLayoutModeTests.swift index ff5f409..a33ffe4 100644 --- a/Tests/DuoLayoutKitTests/DuoLayoutModeTests.swift +++ b/Tests/DuoLayoutKitTests/DuoLayoutModeTests.swift @@ -30,4 +30,16 @@ struct DuoLayoutModeTests { func environmentDefault() { #expect(EnvironmentValues().duoLayoutMode == .compact) } + + @Test("Publishing the mode into the environment overrides the default") + func environmentOverride() { + var environment = EnvironmentValues() + environment.duoLayoutMode = .expanded + #expect(environment.duoLayoutMode == .expanded) + } + + @Test("allCases covers both modes exactly once") + func caseIterable() { + #expect(DuoLayoutMode.allCases == [.compact, .expanded]) + } } From d9c90f47ac41d763d864afac6f7c020e65fc2b89 Mon Sep 17 00:00:00 2001 From: RyosukeYunoki Date: Thu, 10 Sep 2026 16:17:30 +0900 Subject: [PATCH 3/3] Lower swift-tools-version to 6.1 for CI compatibility GitHub's macos-15 runners ship Xcode 16.4 (Swift tools 6.1). The package uses no 6.2-only features, so this widens toolchain support without changing behaviour. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 700eeb8..5d51992 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.2 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription