精工:原生優先文檔卓越
Craft為何贏得2021年Apple Design Award:原生效能、巢狀頁面、區塊架構和一鍵網頁發布。包含Swift實作模式。
Craft:原生優先的文件卓越體驗
「我們相信,思考工具應該像是思維的延伸,而非障礙。」
Craft 證明了原生應用程式能夠提供網頁應用程式無法匹敵的體驗。透過深度的平台整合,它實現了讓數位筆記感覺如同紙筆般自然的響應速度與精緻度。
重點摘要
- 原生勝過 Electron - 低於 50ms 的響應時間需要平台專屬的 UI,而非網頁包裝器
- 頁面中的頁面 - 任何區塊都能成為頁面,讓結構從內容中自然湧現
- 多種佈局,同一份資料 - 列表、卡片、相簿和看板為使用者提供適合各種情境的檢視方式
- 一鍵網頁發布 - 分享頁面無需接收者擁有帳號或下載應用程式
- 平台適性,而非一致性 - Mac 版 Craft 有 Mac 的感覺;iOS 版 Craft 有 iOS 的感覺
為何 Craft 重要
Craft 榮獲 2021 年 Apple Design Award,並持續獲得 App Store 的肯定,正是因為它拒絕為了跨平台的便利性而犧牲原生效能。
主要成就: - 在 iOS、macOS、Windows 上的真正原生應用程式(非 Electron) - 所有互動的響應時間低於 50ms - 離線優先並具備無縫同步 - 基於區塊的架構,卻沒有網頁應用程式的卡頓 - 美觀的分享頁面,無需帳號即可使用
核心設計理念
原生優先,而非網頁包裝
Craft 的關鍵抉擇:為每個平台打造原生應用程式,使用共享的商業邏輯但採用平台專屬的 UI。
ELECTRON/WEB APPROACH CRAFT'S NATIVE APPROACH
───────────────────────────────────────────────────────────────────
Single codebase (JavaScript) Platform-specific UI (Swift, etc.)
Web rendering engine Native rendering
Cross-platform "consistency" Platform-appropriate behavior
~200ms input latency ~16ms input latency
Generic keyboard shortcuts Platform-native shortcuts
Web-standard text selection Native text engine
關鍵洞見:使用者想要的不是在各處看起來都一樣的應用程式——他們想要的是在自己的平台上感覺對勁的應用程式。
macOS via Catalyst(正確的做法)
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 }
// Enable native Mac toolbar
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = createNativeToolbar()
}
// Mac-specific window sizing
windowScene.sizeRestrictions?.minimumSize = CGSize(width: 800, height: 600)
// Enable window tabs
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)
MEDIA BLOCKS
├── Image (with caption)
├── Video
├── File attachment
├── Drawing (Apple Pencil)
└── Audio recording
STRUCTURAL BLOCKS
├── Toggle (collapsible)
├── Page (nested document)
├── Divider
└── Table
實作概念:
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?
}
// Every block can contain other blocks
class PageBlock: CraftBlock {
var id = UUID()
var content: BlockContent
var style: BlockStyle
var children: [any CraftBlock] = []
var metadata: BlockMetadata
// A page is just a block that can be opened full-screen
var canOpenAsPage: Bool { true }
}
關鍵洞見:當每個區塊都能成為頁面時,資訊架構便從內容中自然湧現,而非受限於預設的結構。
2. 巢狀頁面(頁面中的頁面)
Craft 的招牌功能:任何區塊都能成為頁面,而頁面可以無限巢狀。
DOCUMENT STRUCTURE
───────────────────────────────────────────────────────────────────
📄 Project Alpha
├── 📝 Introduction paragraph
├── 📄 Research Notes ← This is a page (nested document)
│ ├── 📝 User interviews
│ ├── 📄 Interview: Sarah ← Another nested page
│ │ └── 📝 Key insights
│ └── 📝 Competitive analysis
├── 📝 Timeline overview
└── 📄 Meeting Notes ← Another page
└── 📝 Action items
Navigation: Breadcrumb trail shows path
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 {
// Breadcrumb navigation
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 種頁面視覺樣式,讓文件一目瞭然。
卡片樣式:
樣式 1:LIST(預設)
┌────────────────────────────────────────┐
│ 📄 頁面標題 │
│ 預覽文字顯示於此... │
└────────────────────────────────────────┘
樣式 2:CARD(中型)
┌──────────────────┐
│ ┌──────────────┐ │
│ │ [圖片] │ │
│ └──────────────┘ │
│ 頁面標題 │
│ 預覽文字... │
└──────────────────┘
樣式 3:CARD(大型)
┌────────────────────────────────────────┐
│ ┌────────────────────────────────────┐ │
│ │ │ │
│ │ [封面圖片] │ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ 頁面標題 │
│ 較長的預覽文字,空間更充裕... │
└────────────────────────────────────────┘
樣式 4:GALLERY(網格)
┌────────┐ ┌────────┐ ┌────────┐
│ [圖片] │ │ [圖片] │ │ [圖片] │
│ 標題 │ │ 標題 │ │ 標題 │
└────────┘ └────────┘ └────────┘
樣式 5:BOARD(看板式)
│ 待辦 │ 進行中 │ 已完成 │
├──────────┼──────────┼──────────┤
│ 任務 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 {
// Background options
enum Background {
case solid(Color)
case gradient(Gradient)
case image(ImageReference)
case paper(PaperTexture)
}
// Paper textures for writing feel
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?
}
// Applied to page
struct PageContainer: View {
let page: PageDocument
var body: some View {
ZStack {
// Background layer
backgroundView(for: page.appearance.background)
// Content layer
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 可以從文件生成精美的響應式網頁。無需帳號即可瀏覽。
DOCUMENT IN CRAFT SHARE PAGE ON WEB
───────────────────────────────────────────────────────────────────
📄 Project Proposal https://www.craft.do/s/abc123
├── 📝 Executive Summary →
├── 📄 Budget Details Clean, responsive layout
├── 📝 Timeline Typography preserved
└── 📄 Team Bios Images optimized
Dark mode supported
No Craft account needed
主要功能: - 一鍵發布 - 可使用自訂網域 - 密碼保護選項 - 瀏覽數據分析 - SEO 友善的渲染 - 所有裝置上皆響應式呈現
視覺設計系統
色彩配置
extension Color {
// Craft's signature palette
static let craftPurple = Color(hex: "#6366F1") // Primary accent
static let craftBackground = Color(hex: "#FAFAFA") // Light mode
static let craftSurface = Color(hex: "#FFFFFF")
// Semantic colors
static let craftSuccess = Color(hex: "#10B981")
static let craftWarning = Color(hex: "#F59E0B")
static let craftError = Color(hex: "#EF4444")
static let craftInfo = Color(hex: "#3B82F6")
// Text hierarchy
static let craftTextPrimary = Color(hex: "#111827")
static let craftTextSecondary = Color(hex: "#6B7280")
static let craftTextMuted = Color(hex: "#9CA3AF")
}
字型排版
struct CraftTypography {
// Document typography
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)
// Code blocks
static let code = Font.system(size: 14, weight: .regular, design: .monospaced)
// Line heights (generous for readability)
static let bodyLineSpacing: CGFloat = 8
static let headingLineSpacing: CGFloat = 4
}
間距系統
struct CraftSpacing {
// Block spacing
static let blockGap: CGFloat = 4 // Between blocks
static let sectionGap: CGFloat = 24 // Between sections
static let pageMargin: CGFloat = 40 // Page edges (desktop)
static let mobileMargin: CGFloat = 16 // Page edges (mobile)
// Content width
static let maxContentWidth: CGFloat = 720 // Optimal reading
static let wideContentWidth: CGFloat = 960 // Tables, galleries
}
動畫設計理念
流暢的頁面轉場
// Navigation between pages uses matched geometry
struct NavigationTransition: View {
@Namespace private var namespace
@State private var selectedPage: PageReference?
var body: some View {
ZStack {
// Page list
PageListView(
onSelect: { page in
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
selectedPage = page
}
},
namespace: namespace
)
// Selected page expands from its card position
if let page = selectedPage {
PageDetailView(page: page)
.matchedGeometryEffect(id: page.id, in: namespace)
.transition(.scale.combined(with: .opacity))
}
}
}
}
區塊動畫
// Blocks animate when reordering
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. 無摩擦分享
一鍵網頁發布徹底消除了「我要怎麼分享這個?」的問題。
5. 適應平台,而非平台一致
Mac 上的 Craft 有 Mac 應用的感覺。iOS 上的 Craft 有 iOS 應用的感覺。這才是目標。
常見問題
Craft 與 Notion 有什麼不同?
Craft 是原生應用(專為 Apple 平台打造),而 Notion 是基於網頁的。這意味著 Craft 能達到低於 50ms 的回應時間,完全離線運作,並深度整合 iOS/macOS 功能,如 Apple Pencil、捷徑和系統級搜尋。Notion 提供更多資料庫功能;Craft 則優先考慮書寫體驗。
我可以離線使用 Craft 嗎?
可以。Craft 在本地儲存所有文件,並在連線時透過 iCloud 同步。您可以在沒有網路的情況下建立、編輯和整理文件。重新連線時變更會自動同步。
分享 Craft 頁面時會發生什麼?
Craft 會在 craft.do 網址生成響應式網頁。收件人可以在任何瀏覽器中查看,無需建立帳戶或安裝應用程式。頁面會保留您文件的字體排印、圖片和巢狀頁面結構。
Craft 支援 Markdown 嗎?
Craft 使用自己的區塊格式,但可以匯出為 Markdown。您可以將內容複製為 Markdown 或匯出整份文件。編輯時某些 Markdown 快捷鍵可用(如 # 表示標題),但 Craft 強調視覺化編輯而非純文字標記。
Craft 中的巢狀頁面如何運作?
任何文字區塊都可以按下頁面圖示或輸入 /page 來轉換為頁面。巢狀頁面會內嵌顯示在父文件中,並可全螢幕開啟。麵包屑導航顯示您在層級中的位置。這創造了自然的組織方式,無需預先強制建立資料夾結構。