Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: Bug report
about: Something behaves incorrectly
title: ""
labels: bug
assignees: ""
---

**What happened**

**What you expected**

**Pose / size-class configuration**
<!-- e.g. iPhone Duo folded, iPhone Duo unfolded, iPhone portrait, iPad Split View -->

**Environment**
- DuoLayoutKit version:
- Xcode version:
- OS / device:

**Minimal reproduction**
<!-- A small View or snippet that shows the problem -->
19 changes: 19 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an addition or change
title: ""
labels: enhancement
assignees: ""
---

**The problem**
<!-- What layout situation is hard to express with the current API? -->

**Proposed API**
<!-- Sketch the call site you'd like to write -->

**Alternatives considered**

**Notes**
<!-- DuoLayoutKit adapts through size classes, safe areas, and geometry — not
device model or orientation. Proposals should fit that model. -->
10 changes: 10 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Summary

<!-- What does this change and why? -->

## 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
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 & docs
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
- name: Build documentation
run: >
xcodebuild docbuild
-scheme DuoLayoutKit
-destination 'generic/platform=iOS'
-skipPackagePluginValidation
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 7 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
// 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

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"
),
Expand Down
116 changes: 102 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
# 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.

<!-- demo GIF coming soon -->

## 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

### 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`:

Expand All @@ -30,23 +38,103 @@ 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).

## 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

- [ ] Fold-state-aware layout container
- [x] Fold-state-aware layout container (`DuoLayout`, `DuoLayoutMode`)
- [ ] Inner/outer display helpers
- [ ] 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).
Loading
Loading