Craft:原生优先的文档卓越
Craft为何赢得2021年Apple Design Award:原生性能、嵌套页面、块架构和一键网页发布。包含Swift实现模式。
Craft:原生优先的文档卓越体验
"我们相信,思维工具应该像思想的延伸,而非障碍。"
Craft 证明了原生应用能够提供网页应用无法企及的体验。凭借深度的平台集成,它实现了让数字笔记如纸笔般自然流畅的响应速度与精致质感。
核心要点
- 原生胜过 Electron - 低于 50 毫秒的响应时间需要平台原生 UI,而非网页封装
- 页中有页 - 任何块都可以成为页面,让结构从内容中自然涌现
- 多种布局,同一数据 - 列表、卡片、画廊和看板为用户在不同场景提供最合适的视图
- 一键网页发布 - 分享页面无需接收者注册账号或下载应用
- 适配平台,而非千篇一律 - Mac 版 Craft 有 Mac 的风格;iOS 版 Craft 有 iOS 的风格
为何 Craft 如此重要
Craft 荣获 2021 年 Apple 设计大奖,并持续获得 App Store 的认可,正是因为它拒绝为跨平台便利性而牺牲原生性能。
主要成就: - 真正的原生应用,支持 iOS、macOS、Windows(非 Electron) - 所有交互的响应时间均低于 50 毫秒 - 离线优先,无缝同步 - 基于块的架构,没有网页应用的卡顿感 - 美观的分享页面,无需账户即可使用
核心设计理念
原生优先,而非网页封装
Craft 的关键抉择:为每个平台构建原生应用,使用共享的业务逻辑但采用平台特定的 UI。
ELECTRON/WEB 方案 CRAFT 的原生方案
───────────────────────────────────────────────────────────────────
单一代码库 (JavaScript) 平台特定 UI (Swift 等)
Web 渲染引擎 原生渲染
跨平台"一致性" 平台适配行为
~200ms 输入延迟 ~16ms 输入延迟
通用键盘快捷键 平台原生快捷键
Web 标准文本选择 原生文本引擎
关键洞察:用户不想要在每个平台上看起来都一样的应用——他们想要在自己的平台上感觉恰到好处的应用。
通过 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 中的每个元素都是一个块。每个包含内容的块都可能成为一个页面,实现无限嵌套而不增加认知负担。
块类型:
文本块
├── 段落(默认)
├── 标题 1、2、3
├── 引用
├── 标注(信息、警告、成功)
└── 代码(带语法高亮)
媒体块
├── 图片(带标题)
├── 视频
├── 文件附件
├── 绘图(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 的标志性特性:任何块都可以成为页面,且页面可以无限嵌套。
文档结构
───────────────────────────────────────────────────────────────────
📄 Project Alpha
├── 📝 介绍段落
├── 📄 研究笔记 ← 这是一个页面(嵌套文档)
│ ├── 📝 用户访谈
│ ├── 📄 访谈:Sarah ← 另一个嵌套页面
│ │ └── 📝 关键洞察
│ └── 📝 竞争分析
├── 📝 时间线概览
└── 📄 会议记录 ← 另一个页面
└── 📝 行动事项
导航:面包屑路径显示当前位置
Project Alpha > 研究笔记 > 访谈: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 种页面视觉样式,让文档一目了然。
卡片样式:
样式 1:列表(默认)
┌────────────────────────────────────────┐
│ 📄 页面标题 │
│ 预览文本显示在此处... │
└────────────────────────────────────────┘
样式 2:卡片(中)
┌──────────────────┐
│ ┌──────────────┐ │
│ │ [图片] │ │
│ └──────────────┘ │
│ 页面标题 │
│ 预览文本... │
└──────────────────┘
样式 3:卡片(大)
┌────────────────────────────────────────┐
│ ┌────────────────────────────────────┐ │
│ │ │ │
│ │ [封面图片] │ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ 页面标题 │
│ 更长的预览文本,拥有更多空间... │
└────────────────────────────────────────┘
样式 4:画廊(网格)
┌────────┐ ┌────────┐ ┌────────┐
│ [图片] │ │ [图片] │ │ [图片] │
│ 标题 │ │ 标题 │ │ 标题 │
└────────┘ └────────┘ └────────┘
样式 5:看板(Kanban 风格)
│ 待办 │ 进行中 │ 已完成 │
├──────────┼──────────┼──────────┤
│ 任务 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. 无摩擦分享
一键网页发布彻底解决了"我该如何分享这个?"的问题。
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 URL 生成一个响应式网页。收件人无需创建账户或安装应用,即可在任何浏览器中查看。页面会保留您文档的排版、图片和嵌套页面结构。
Craft 支持 Markdown 吗?
Craft 使用自己的块格式,但支持导出为 Markdown。您可以将内容复制为 Markdown 或导出整个文档。编辑时某些 Markdown 快捷方式可用(如 # 表示标题),但 Craft 更强调可视化编辑而非纯文本标记。
Craft 中的嵌套页面如何工作?
按下页面图标或输入 /page,任何文本块都可以变成页面。嵌套页面会内嵌显示在父文档中,也可以全屏打开。面包屑导航会显示您在层级结构中的位置。这种方式无需预先建立文件夹结构,即可实现自然的内容组织。