Craft:ネイティブファーストのドキュメント体験
CraftがApple Design Award 2021を受賞した理由:ネイティブパフォーマンス、ネストされたページ、ブロックアーキテクチャ、ワンクリックウェブ公開。Swift実装パターン付き。
Craft: ネイティブファーストのドキュメント体験
「思考のためのツールは、障害ではなく、思考の延長のように感じられるべきだと私たちは信じています。」
Craftは、ネイティブアプリがウェブアプリでは実現できない体験を提供できることを証明しています。深いプラットフォーム統合により構築され、デジタルノートテイキングを紙のように自然に感じさせるレスポンシブ性と洗練さを実現しています。
重要なポイント
- ネイティブはElectronに勝る - 50ms未満の応答時間には、ウェブラッパーではなくプラットフォーム固有のUIが必要
- ページの中にページ - あらゆるブロックがページになり、コンテンツから構造が自然に生まれる
- 複数のレイアウト、同じデータ - リスト、カード、ギャラリー、ボードが各コンテキストに適したビューを提供
- ワンクリックウェブ公開 - 共有ページは受信者のアカウントやアプリのダウンロードなしで機能
- 同一ではなくプラットフォーム適応 - Mac CraftはMacらしく、iOS CraftはiOSらしく
Craftが重要な理由
CraftはApple Design Award 2021を受賞し、クロスプラットフォームの利便性のためにネイティブパフォーマンスを妥協することを拒否することで、App Storeで一貫して高い評価を得ています。
主な実績: - iOS、macOS、Windows向けの真のネイティブアプリ(Electronではない) - すべてのインタラクションで50ms未満のレスポンス時間 - オフラインファーストでシームレスな同期 - **ブロックベースのアーキテクチャ** - ウェブアプリ特有のカクつきなし - アカウント不要で使える美しい共有ページ
コアデザイン哲学
ネイティブファースト、Webラッパーではない
Craftの決定的な選択:各プラットフォーム向けにネイティブアプリを構築し、ビジネスロジックは共有しつつUIはプラットフォーム固有のものを使用する。
ELECTRON/WEBアプローチ CRAFTのネイティブアプローチ
───────────────────────────────────────────────────────────────────
単一コードベース(JavaScript) プラットフォーム固有のUI(Swiftなど)
Webレンダリングエンジン ネイティブレンダリング
クロスプラットフォームの「一貫性」 プラットフォームに適した動作
入力レイテンシ約200ms 入力レイテンシ約16ms
汎用キーボードショートカット プラットフォームネイティブのショートカット
Web標準のテキスト選択 ネイティブテキストエンジン
重要な洞察:ユーザーはどこでも同じ見た目のアプリを求めているわけではありません—自分のプラットフォームでしっくりくるアプリを求めているのです。
Mac Catalyst経由のmacOS(正しい実装方法)
CraftはMac Catalystを使用していますが、真にMacネイティブに感じられるよう大幅にカスタマイズしています:
// Craft's Catalyst customization approach
#if targetEnvironment(macCatalyst)
extension SceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession) {
guard let windowScene = scene as? UIWindowScene else { return }
// ネイティブMacツールバーを有効化
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = createNativeToolbar()
}
// Mac固有のウィンドウサイズ設定
windowScene.sizeRestrictions?.minimumSize = CGSize(width: 800, height: 600)
// ウィンドウタブを有効化
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.forEach { $0.titlebar?.toolbar?.showsBaselineSeparator = false }
}
private func createNativeToolbar() -> NSToolbar {
let toolbar = NSToolbar(identifier: "CraftToolbar")
toolbar.displayMode = .iconOnly
toolbar.delegate = self
return toolbar
}
}
#endif
パターンライブラリ
1. ブロックベースのコンテンツアーキテクチャ
Craftのすべての要素はブロックです。コンテンツを持つすべてのブロックは潜在的にページとなり、認知的負荷なく無限のネストを可能にします。
ブロックタイプ:
TEXT BLOCKS
├── Paragraph (default)
├── Heading 1, 2, 3
├── Quote
├── Callout (info, warning, success)
└── Code (with syntax highlighting)
メディアブロック
├── 画像(キャプション付き)
├── 動画
├── ファイル添付
├── 描画(Apple Pencil)
└── 音声録音
構造ブロック
├── トグル(折りたたみ可能)
├── ページ(ネストされたドキュメント)
├── 区切り線
└── テーブル
実装コンセプト:
protocol CraftBlock: Identifiable {
var id: UUID { get }
var content: BlockContent { get set }
var style: BlockStyle { get set }
var children: [any CraftBlock] { get set }
var metadata: BlockMetadata { get }
}
enum BlockContent {
case text(AttributedString)
case image(ImageData)
case page(PageReference)
case toggle(isExpanded: Bool)
case code(language: String, content: String)
case table(TableData)
}
struct BlockMetadata {
let created: Date
let modified: Date
let createdBy: UserReference?
}
// すべてのブロックは他のブロックを含むことができます
class PageBlock: CraftBlock {
var id = UUID()
var content: BlockContent
var style: BlockStyle
var children: [any CraftBlock] = []
var metadata: BlockMetadata
// ページとは、フルスクリーンで開けるブロックに過ぎない
var canOpenAsPage: Bool { true }
}
重要な洞察: すべてのブロックがページになり得るとき、情報アーキテクチャは事前に決められた構造からではなく、コンテンツから自然に生まれます。
2. ネストされたページ(ページの中のページ)
Craftの特徴的な機能:どんなブロックもページになることができ、ページは無限にネストできます。
DOCUMENT STRUCTURE
───────────────────────────────────────────────────────────────────
📄 Project Alpha
├── 📝 Introduction paragraph
├── 📄 Research Notes ← これはページ(ネストされたドキュメント)
│ ├── 📝 User interviews
│ ├── 📄 Interview: Sarah ← 別のネストされたページ
│ │ └── 📝 Key insights
│ └── 📝 Competitive analysis
├── 📝 Timeline overview
└── 📄 Meeting Notes ← 別のページ
└── 📝 Action items
Navigation: パンくずリストがパスを表示
Project Alpha > Research Notes > Interview: Sarah
SwiftUI実装パターン:
struct PageView: View {
@Bindable var page: PageDocument
@State private var selectedBlock: CraftBlock?
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(page.blocks) { block in
BlockView(block: block, onTap: { tapped in
if tapped.canOpenAsPage {
navigateToPage(tapped)
} else {
selectedBlock = tapped
}
})
}
}
}
.navigationTitle(page.title)
.toolbar {
// パンくずリストナビゲーション
ToolbarItem(placement: .principal) {
BreadcrumbView(path: page.ancestorPath)
}
}
}
}
struct BreadcrumbView: View {
let path: [PageReference]
var body: some View {
HStack(spacing: 4) {
ForEach(Array(path.enumerated()), id: \.element.id) { index, page in
if index > 0 {
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.secondary)
}
Button(page.title) {
navigateTo(page)
}
.buttonStyle(.plain)
.foregroundStyle(index == path.count - 1 ? .primary : .secondary)
}
}
}
}
3. カードレイアウトシステム
Craftはページに5つのビジュアルスタイルを提供し、ドキュメントを一目で把握できるようにします。
カードスタイル:
STYLE 1: LIST (Default)
┌────────────────────────────────────────┐
│ 📄 Page Title │
│ Preview text appears here... │
└────────────────────────────────────────┘
スタイル2: カード(中)
┌──────────────────┐
│ ┌──────────────┐ │
│ │ [Image] │ │
│ └──────────────┘ │
│ ページタイトル │
│ プレビューテキスト... │
└──────────────────┘
スタイル3:カード(大)
┌────────────────────────────────────────┐
│ ┌────────────────────────────────────┐ │
│ │ │ │
│ │ [カバー画像] │ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ ページタイトル │
│ より長いプレビューテキストを表示可能...│
└────────────────────────────────────────┘
スタイル4: ギャラリー(グリッド)
┌────────┐ ┌────────┐ ┌────────┐
│ [画像] │ │ [画像] │ │ [画像] │
│ タイトル │ │ タイトル │ │ タイトル │
└────────┘ └────────┘ └────────┘
スタイル5: ボード(カンバン形式)
│ 未着手 │ 進行中 │ 完了 │
├──────────┼──────────┼──────────┤
│ タスク1 │ タスク3 │ タスク5 │
│ タスク2 │ タスク4 │ │
実装:
enum PageLayoutStyle: String, CaseIterable {
case list = "list"
case cardMedium = "card_medium"
case cardLarge = "card_large"
case gallery = "gallery"
case board = "board"
}
struct PageGridView: View {
let pages: [PageReference]
let style: PageLayoutStyle
var body: some View {
switch style {
case .list:
LazyVStack(spacing: 8) {
ForEach(pages) { page in
ListRowView(page: page)
}
}
case .cardMedium, .cardLarge:
let columns = style == .cardMedium ? 3 : 2
LazyVGrid(columns: Array(repeating: .init(.flexible()), count: columns)) {
ForEach(pages) { page in
CardView(page: page, size: style == .cardLarge ? .large : .medium)
}
}
case .gallery:
LazyVGrid(columns: Array(repeating: .init(.flexible()), count: 4)) {
ForEach(pages) { page in
GalleryThumbnail(page: page)
}
}
case .board:
BoardView(pages: pages)
}
}
}
4. ページ背景とテーマ設定
各ページは背景やアクセントカラーを通じて、独自のビジュアルアイデンティティを持つことができます。
struct PageAppearance {
// 背景オプション
enum Background {
case solid(Color)
case gradient(Gradient)
case image(ImageReference)
case paper(PaperTexture)
}
// 書き心地のための用紙テクスチャ
enum PaperTexture: String {
case none
case lined
case grid
case dotted
}
var background: Background = .solid(.white)
var accentColor: Color = .blue
var iconEmoji: String?
var coverImage: ImageReference?
}
// ページに適用
struct PageContainer: View {
let page: PageDocument
var body: some View {
ZStack {
// 背景レイヤー
backgroundView(for: page.appearance.background)
// コンテンツレイヤー
PageContentView(page: page)
}
}
@ViewBuilder
private func backgroundView(for background: PageAppearance.Background) -> some View {
switch background {
case .solid(let color):
color.ignoresSafeArea()
case .gradient(let gradient):
LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom)
.ignoresSafeArea()
case .image(let ref):
AsyncImage(url: ref.url) { image in
image.resizable().scaledToFill()
} placeholder: {
Color.gray.opacity(0.1)
}
.ignoresSafeArea()
.overlay(.ultraThinMaterial)
case .paper(let texture):
PaperTextureView(texture: texture)
}
}
}
5. 共有ページ(ウェブパブリッシング)
Craftはドキュメントから美しくレスポンシブなウェブページを生成します。閲覧にアカウントは不要です。
CRAFTのドキュメント ウェブ上の共有ページ
───────────────────────────────────────────────────────────────────
📄 プロジェクト提案書 https://www.craft.do/s/abc123
├── 📝 エグゼクティブサマリー →
├── 📄 予算詳細 クリーンでレスポンシブなレイアウト
├── 📝 タイムライン タイポグラフィを維持
└── 📄 チーム紹介 画像を最適化
ダークモード対応
Craftアカウント不要
主な特徴: - ワンクリックで公開 - カスタムドメイン対応 - パスワード保護オプション - ビューの分析機能 - SEOフレンドリーなレンダリング - すべてのデバイスでレスポンシブ対応
ビジュアルデザインシステム
カラーパレット
extension Color {
// Craftの特徴的なパレット
static let craftPurple = Color(hex: "#6366F1") // プライマリアクセント
static let craftBackground = Color(hex: "#FAFAFA") // ライトモード
static let craftSurface = Color(hex: "#FFFFFF")
// セマンティックカラー
static let craftSuccess = Color(hex: "#10B981")
static let craftWarning = Color(hex: "#F59E0B")
static let craftError = Color(hex: "#EF4444")
static let craftInfo = Color(hex: "#3B82F6")
// テキスト階層
static let craftTextPrimary = Color(hex: "#111827")
static let craftTextSecondary = Color(hex: "#6B7280")
static let craftTextMuted = Color(hex: "#9CA3AF")
}
タイポグラフィ
struct CraftTypography {
// ドキュメントタイポグラフィ
static let title = Font.system(size: 32, weight: .bold, design: .default)
static let heading1 = Font.system(size: 28, weight: .semibold)
static let heading2 = Font.system(size: 22, weight: .semibold)
static let heading3 = Font.system(size: 18, weight: .medium)
static let body = Font.system(size: 16, weight: .regular)
static let caption = Font.system(size: 14, weight: .regular)
// コードブロック
static let code = Font.system(size: 14, weight: .regular, design: .monospaced)
// 行の高さ(読みやすさのためにゆとりを持たせる)
static let bodyLineSpacing: CGFloat = 8
static let headingLineSpacing: CGFloat = 4
}
スペーシングシステム
struct CraftSpacing {
// ブロック間隔
static let blockGap: CGFloat = 4 // ブロック間
static let sectionGap: CGFloat = 24 // セクション間
static let pageMargin: CGFloat = 40 // ページ端(デスクトップ)
static let mobileMargin: CGFloat = 16 // ページ端(モバイル)
// コンテンツ幅
static let maxContentWidth: CGFloat = 720 // 最適な読みやすさ
static let wideContentWidth: CGFloat = 960 // テーブル、ギャラリー
}
アニメーションの設計思想
スムーズなページ遷移
// ページ間のナビゲーションではマッチドジオメトリを使用
struct NavigationTransition: View {
@Namespace private var namespace
@State private var selectedPage: PageReference?
var body: some View {
ZStack {
// ページリスト
PageListView(
onSelect: { page in
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
selectedPage = page
}
},
namespace: namespace
)
// 選択されたページはカード位置から展開される
if let page = selectedPage {
PageDetailView(page: page)
.matchedGeometryEffect(id: page.id, in: namespace)
.transition(.scale.combined(with: .opacity))
}
}
}
}
ブロックアニメーション
// ブロックは並べ替え時にアニメーションする
struct AnimatedBlockList: View {
@Binding var blocks: [CraftBlock]
var body: some View {
LazyVStack(spacing: CraftSpacing.blockGap) {
ForEach(blocks) { block in
BlockView(block: block)
.transition(.asymmetric(
insertion: .move(edge: .top).combined(with: .opacity),
removal: .move(edge: .bottom).combined(with: .opacity)
))
}
}
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: blocks)
}
}
私たちの仕事への教訓
1. ネイティブパフォーマンスは投資する価値がある
ユーザーは16msと200msのレイテンシの違いを感じ取ります。ネイティブアプリは体感で勝ります。
2. ページの中のページが自然な整理を可能にする
構造はコンテンツから自然に生まれるようにしましょう。ユーザーに最初から階層を決めさせないでください。
3. 同じコンテンツに複数のビジュアルレイアウト
カード、リスト、ギャラリー—同じデータでも、異なる文脈には異なる表示方法を。
4. 摩擦なく共有する
ワンクリックでのWeb公開により、「これをどうやって共有すればいいの?」という問題が完全に解消されます。
5. プラットフォームに適切であること、同一であることではない
MacのCraftはMacアプリらしく感じられます。iOSのCraftはiOSアプリらしく感じられます。それが目指すべき姿です。
よくある質問
CraftはNotionとどう違いますか?
CraftはネイティブアプリでAppleプラットフォーム専用に構築されていますが、NotionはWebベースです。そのためCraftは50ms未満の応答時間を実現し、完全にオフラインで動作し、Apple Pencil、ショートカット、システム全体の検索などのiOS/macOS機能と深く統合されています。Notionはより多くのデータベース機能を提供しますが、Craftは執筆体験を優先しています。
Craftはオフラインで使えますか?
はい。Craftはすべてのドキュメントをローカルに保存し、接続時にiCloud経由で同期します。インターネットなしでもドキュメントの作成、編集、整理が可能です。 再接続すると変更は自動的に同期されます。
Craftページを共有するとどうなりますか?
Craftはcraft.doのURLでレスポンシブなウェブページを生成します。受信者はアカウントを作成したりアプリをインストールしたりすることなく、任意のブラウザで閲覧できます。ページはドキュメントのタイポグラフィ、画像、ネストされたページ構造を保持します。
CraftはMarkdownをサポートしていますか?
Craftは独自のブロック形式を使用しますが、Markdownへのエクスポートが可能です。コンテンツをMarkdownとしてコピーしたり、ドキュメント全体をエクスポートしたりできます。編集中は一部のMarkdownショートカット(見出しの#など)が機能しますが、Craftはプレーンテキストのマークアップよりもビジュアル編集を重視しています。
Craftでネストされたページはどのように機能しますか?
任意のテキストブロックは、ページアイコンを押すか/pageと入力することでページに変換できます。ネストされたページは親ドキュメント内にインラインで表示され、フルスクリーンで開くことができます。パンくずナビゲーションが階層内の現在位置を示します。これにより、最初からフォルダ構造を強制することなく、自然な整理が可能になります。