WWDC Quick Look 💓 By SwiftGGTeam
Discoverable design

Discoverable design

观看原视频

Highlight

Apple 用 Toasty 示例给出可发现设计的五个方法:排列功能优先级、提供视觉暗示、提示手势、按用户行为组织内容,并让用户控制个性化推荐。

核心内容

很多 App 都有一个相同的问题:功能已经做完了,用户却不知道它们存在。

最常见的补救办法是 onboarding(新手引导)。打开 App,先让用户看几屏说明。Toast 应用 Toasty 也遇到这个问题。它既能浏览吐司食谱,也能查看朋友做的吐司,还能拍照生成食谱。

如果把这些功能一条一条塞进引导页,用户很可能直接跳过。即使用户读完,也不一定能在几分钟后记住。

Apple 在这场 session 中给出的方向是:界面要自己说明自己。用户看到屏幕时,应该能在动手前知道能做什么,以及怎么做。

从功能列表回到用户目标

Toasty 的功能很多。浏览食谱、查看我的食谱、拍照生成食谱、搜索、设置、关于页面,都可以说重要。

如果全部放到首页,界面会拥挤。如果全部收进汉堡菜单,用户又看不到入口。

Apple 的做法是先按用户目标排序。浏览食谱和查看我的食谱最常用,放进底部 tab bar(标签栏)。拍照生成食谱重要,但属于“我的食谱”里的动作,所以放在该页面顶部。设置和个人偏好改动不频繁,可以放在 profile(个人资料)图标后面。

这里的判断标准很具体:功能是否属于 App 的核心,用户是否经常使用,入口位置是否符合手指够得到的范围。

用界面暗示替代引导页

可发现设计不要求删掉所有说明文字。

它要求说明出现在用户需要的地方。图标不清楚,就加文字标签。搜索框让人不知道输入什么,就给出“按食材、地点、朋友搜索”的提示。拍照功能需要用户把吐司放到合适位置,就在相机画面里显示轮廓、禁用快门按钮,并在角度合适时给动画和触觉反馈。

这比提前在引导页里讲一遍更直接。用户正在操作,相同的信息才有用。

个性化推荐也要可控制

Toasty 还有机器学习推荐。它会根据用户喜欢的食谱,生成 Tasty Picks(个性化推荐)。

推荐系统的问题是来源不透明。用户看到一组吐司,不知道这是编辑精选,还是机器学习结果,也不知道如何纠正它。

Apple 的做法是把原因写出来,例如“Because you added avocado toast”。用户知道推荐来自自己添加过的牛油果吐司。再提供“Edit suggestion”按钮,让用户选择“Suggest Less Avocado Toast”或“Stop Suggesting Avocado Toast”。

推荐需要透明。用户能看懂来源,也能修改结果,才会继续信任它。

详细内容

这场 session 没有提供代码片段。它是一场设计方法演讲。下面用可执行的 SwiftUI 原型,把 transcript 中的关键方法落到界面结构上。示例只使用系统控件和文案,不引入演讲中没有出现的自定义 API。

1. 按重要性排列功能

03:18)Apple 的第一条原则是 prioritize important features(优先展示重要功能)。核心功能要立刻可见,非核心功能可以多一次导航。

Toasty 没有把所有功能塞进一个屏幕,也没有放进汉堡菜单。它把“推荐”和“我的食谱”放进 tab bar,把“新建食谱”放在我的食谱页面顶部,把个人偏好放进 profile 图标。

import SwiftUI

struct ToastyAppView: View {
    var body: some View {
        TabView {
            ForYouView()
                .tabItem {
                    Label("For You", systemImage: "sparkles")
                }

            RecipesView()
                .tabItem {
                    Label("Recipes", systemImage: "book")
                }
        }
    }
}

struct ForYouView: View {
    var body: some View {
        NavigationView {
            List {
                Section("Tasty Picks") {
                    Text("Restaurant toast near you")
                    Text("Toast shared by friends")
                }
            }
            .navigationTitle("For You")
            .toolbar {
                Button {
                    showProfile()
                } label: {
                    Image(systemName: "person.crop.circle")
                }
            }
        }
    }
}

struct RecipesView: View {
    var body: some View {
        NavigationView {
            List {
                Button {
                    captureToast()
                } label: {
                    Label("New", systemImage: "camera")
                }

                Section("My Recipes") {
                    Text("Avocado toast")
                    Text("Blueberry toast")
                }
            }
            .navigationTitle("Recipes")
            .searchable(text: .constant(""), prompt: "Search ingredients, places, or friends")
        }
    }
}

func showProfile() {}
func captureToast() {}

关键点:

  • TabView 把最重要、最高频的两个页面放到底部标签栏,用户打开 App 就能看到。
  • Label("For You", systemImage: "sparkles") 同时提供文字和图标,避免图标含义不清。
  • toolbar 中的 profile 按钮承载个人偏好,适合不频繁修改的内容。
  • Button 放在 RecipesView 的列表顶部,让“拍照生成食谱”在相关页面内保持醒目。
  • .searchableprompt 给出输入例子,解决搜索框空白时不知道搜什么的问题。

2. 用文字、图标和上下文提供视觉暗示

08:12)第二条原则是 visual cues(视觉暗示)。视觉暗示的作用是告诉用户某个元素是什么、能做什么、何时该做。

Apple 在 Toasty 中提到三个例子。底部标签栏的图标需要文字标签。相机按钮只用相机图标不够,因为相机可能代表拍照、扫描文档或扫描二维码。搜索框需要提示用户可以按食材、地点或朋友搜索。

import SwiftUI

struct DiscoverableSearchView: View {
    @State private var query = ""

    var body: some View {
        NavigationView {
            List {
                Section("Try searching") {
                    Text("Ingredients")
                    Text("Places")
                    Text("Friends")
                }

                Section("Actions") {
                    Button(action: captureToast) {
                        Label("New", systemImage: "camera")
                    }
                }
            }
            .navigationTitle("Toasty")
            .searchable(
                text: $query,
                prompt: "Search ingredients, places, or friends"
            )
        }
    }
}

func captureToast() {}

关键点:

  • @State private var query 保存搜索输入,示例可以直接在 SwiftUI 中运行。
  • Section("Try searching") 在界面里给出可搜索内容类型,帮助用户开始输入。
  • Label("New", systemImage: "camera") 用“New”说明相机按钮的结果是新建食谱。
  • .searchableprompt 把 transcript 中“ingredients, places, or friends”的提示放进系统搜索框。
  • NavigationViewList 使用 iOS 常见结构,降低用户学习成本。

3. 把手势当快捷方式,并用动画提示

12:59)第三条原则是 hint at gestures(提示手势)。手势能让导航更流畅,但手势本身不可见。

Apple 的建议很直接:优先使用平台标准手势;自定义手势要贴近真实世界;手势只做快捷方式,不要替代可见按钮。

Toasty 的食谱详情页支持向下滑动返回列表,同时保留明确的返回按钮。点击返回按钮时,动画方向也向下,暗示用户可以下滑。

import SwiftUI

struct RecipeDetailView: View {
    @Environment(\.dismiss) private var dismiss
    @State private var offsetY: CGFloat = 0

    var body: some View {
        VStack(spacing: 24) {
            Button("Back to recipes") {
                withAnimation(.easeInOut) {
                    offsetY = 700
                }
                dismiss()
            }

            Text("Avocado toast")
                .font(.largeTitle)

            Text("Swipe down as a shortcut to go back.")
                .foregroundStyle(.secondary)
        }
        .offset(y: offsetY)
        .gesture(
            DragGesture()
                .onEnded { value in
                    if value.translation.height > 120 {
                        dismiss()
                    }
                }
        )
    }
}

关键点:

  • @Environment(\.dismiss) 使用系统提供的关闭能力,保留明确的返回路径。
  • Button("Back to recipes") 是主要操作入口,用户不需要先发现手势。
  • withAnimation(.easeInOut) 让点击按钮后的运动方向与下滑手势一致。
  • .offset(y: offsetY) 让视图向下移动,匹配“swipe down to go back”的方向。
  • DragGesture 只在用户下滑超过阈值时触发 dismiss(),手势是快捷方式。

4. 按用户行为组织大量内容

17:37)第四条原则是 organize by behavior(按行为组织)。当内容很多时,分类不能只看数据字段,还要看用户在真实生活中怎样发现内容。

Toasty 起初按食材分类,例如牛油果、蓝莓。但食材组合太多,用户会被分类淹没。后来改成餐厅推荐、朋友分享和 Tasty Picks。前两类来自现实行为:试新餐厅、听朋友推荐。第三类来自个性化推荐。

import SwiftUI

struct ToastCategory: Identifiable {
    let id = UUID()
    let title: String
    let items: [String]
}

struct BehaviorBasedContentView: View {
    let categories = [
        ToastCategory(
            title: "From restaurants",
            items: ["Cafe avocado toast", "Market tomato toast"]
        ),
        ToastCategory(
            title: "Shared by friends",
            items: ["Mylène's blueberry toast", "Jiabao's banana toast"]
        ),
        ToastCategory(
            title: "Tasty Picks",
            items: ["Suggested sesame toast", "Suggested honey toast"]
        )
    ]

    var body: some View {
        List(categories) { category in
            Section(category.title) {
                ForEach(category.items, id: \.self) { item in
                    Text(item)
                }
            }
        }
        .navigationTitle("For You")
    }
}

关键点:

  • ToastCategory 表示一个可见分类,分类名来自用户行为场景。
  • From restaurants 对应 transcript 中用户通过餐厅发现新吐司。
  • Shared by friends 对应朋友推荐的发现路径。
  • Tasty Picks 对应机器学习推荐生成的个性化内容。
  • Section(category.title) 用标题把内容分组,让用户一眼看到分类边界。

5. 让用户理解并控制推荐系统

22:35)第五条原则是 convey a sense of control(传达控制感)。用户要知道推荐来自哪里,也要能修改不想要的推荐。

Apple 区分了 explicit feedback(显式反馈)和 implicit feedback(隐式反馈)。显式反馈是用户主动告诉系统想多看或少看什么。隐式反馈来自用户行为,例如查看食谱、分享吐司或添加食谱。

Toasty 最终用更明确的文案说明后果:“Suggest toast like this”“Because you added avocado toast”“Edit suggestion”。

import SwiftUI

struct RecommendationControlView: View {
    @State private var showingEditOptions = false

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Tasty Picks")
                .font(.title)

            HStack {
                Text("Because you added avocado toast")
                    .foregroundStyle(.secondary)

                Button("Edit suggestion") {
                    showingEditOptions = true
                }
            }

            Text("Banana avocado toast")
                .font(.headline)

            HStack {
                Button("Suggest toast like this") {
                    suggestMore()
                }

                Button("Less") {
                    suggestLess()
                }
            }
        }
        .padding()
        .confirmationDialog("Edit suggestion", isPresented: $showingEditOptions) {
            Button("Suggest Less Avocado Toast") {
                suggestLessAvocadoToast()
            }

            Button("Stop Suggesting Avocado Toast", role: .destructive) {
                stopSuggestingAvocadoToast()
            }
        }
    }
}

func suggestMore() {}
func suggestLess() {}
func suggestLessAvocadoToast() {}
func stopSuggestingAvocadoToast() {}

关键点:

  • Text("Because you added avocado toast") 披露推荐的输入来源,解释为什么看到这些内容。
  • Button("Edit suggestion") 直接写出用户能编辑推荐,不藏在只有图标的更多菜单里。
  • Button("Suggest toast like this") 用动词说明点击后的后果是影响推荐。
  • Button("Less") 对应减少相似内容的显式反馈。
  • confirmationDialog 展示两个具体选项,用户能选择少推荐或停止推荐牛油果吐司。

核心启发

  1. 做什么:给笔记 App 增加“按最近行为推荐模板”的首页区块。 为什么值得做:session 提到个性化内容适合大量内容场景,但必须披露来源。 怎么开始:用最近创建的笔记类型生成推荐,并在区块标题下写“因为你最近创建了会议笔记”,旁边放“编辑建议”按钮。

  2. 做什么:给图片编辑 App 的手势操作增加可见替代入口。 为什么值得做:Apple 建议手势作为快捷方式,不能替代主要操作。 怎么开始:保留双指缩放和滑动切换滤镜,同时在工具栏提供“缩放”“上一个”“下一个”按钮,并让动画方向与手势一致。

  3. 做什么:给搜索页增加可输入内容示例。 为什么值得做:搜索框容易出现空白页问题,用户不知道该搜什么。 怎么开始:在 .searchable 的 prompt 中写出可搜索维度,例如“搜索项目、成员或标签”,并在空状态展示三个示例查询。

  4. 做什么:给内容型 App 重新设计分类,不按数据库字段分组。 为什么值得做:session 中 Toasty 从食材分类改为餐厅、朋友和推荐,分类贴近发现行为。 怎么开始:访谈用户如何找到内容,把“来源”“场景”“关系”作为一级分组,再用 Section 或卡片标题强化边界。

  5. 做什么:给推荐流加入“为什么看到”和“减少此类内容”。 为什么值得做:用户能理解隐式反馈来源,也能纠正推荐结果。 怎么开始:在推荐卡片下显示原因,例如“因为你收藏了 X”,再提供“减少类似内容”“停止推荐 X”两个操作。

关联 Session

评论

GitHub Issues · utterances