SwiftUI 5.0(iOS 17)TipKit 让用户更懂你的 App

在这里插入图片描述

概览

作为我们秃头开发者来说,写出一款创意炸裂的 App 还不足以吸引用户眼球,更重要的是如何让用户用最短的时间掌握我们 App 的使用技巧。

在这里插入图片描述

从 iOS 17 开始, 推出了全新的 TipKit 框架专注于此事。有了它,我们再也不用自己写 App 用户帮助以及使用指南的逻辑和界面了。

使用 TipKit 非常简单,接下来就让我们一起走进 TipKit 的世界吧!

本文代码全部在 Xcode 15 beta8 上编译,在 iOS 17 beta8 上运行。


博文对应的视频课在此,请小伙伴们恣意观看

SwiftUI 5.0 TipKit 让用户更懂你的 App


1. 什么是 TipKit?

在这里插入图片描述

TipKit 是  在 WWDC 23 上推出的一款新框架,用于在界面显示提示(Tips)来帮助用户快速发掘我们 App 的使用特性。

在这里插入图片描述
在这里插入图片描述

目前该框架仍属于 beta 阶段,意味着它还有很多不确定性。

如果我没有记错, 直到 Xcode beta4 才将 TipKit 提供给开发者,而且现在 官网 TipKit 的示例代码在 Xcode beta8 中已提示语法错误了(我们后面会说明):

在这里插入图片描述


对于  这种习惯性“谜之”行为的更多细节,感兴趣的小伙伴们可以到如下链接中观赏:


2. 创建一个 Tip

按照 SwiftUI 的“习性”,一个 Tip 同时意味着外观和逻辑双重含义。

创建一个提示很简单,只需遵循 Tip 协议即可:

struct FavoriteTip: Tip {
    var title: Text {
        Text("收藏最爱的图片")
            .bold()
    }
    
    var message: Text? {
        Text("将心仪的图片保存到相册中")
            .font(.headline)
            .foregroundStyle(.gray.gradient)
    }
}

Tip 协议还有很多其它可选属性,比如我们还可以为 Tip 界面进一步增加图片修饰:

struct FavoriteTip: Tip {
    var image: Image? {
        Image(systemName: "heart")
    }
}

3. TipKit 显示的两种方式

在 Tip 创建之后如何显示它们呢?有两种方式:嵌入和弹出。

我们可以直接将 Tip 嵌在视图中:

struct ContentView: View {
    let favTip = FavoriteTip()
    
    var body: some View {
        NavigationStack {
            VStack {
                
                TipView(favTip)
            }
            .padding()
            .navigationTitle("TitKit演示")
        }
    }
}

显示效果如下:

在这里插入图片描述

或者我们还可以将 Tip 直接依附于某一个视图,比如图片或按钮:

struct ContentView: View {
    let favTip = FavoriteTip()
    
    var body: some View {
        NavigationStack {
            VStack {...}
            .padding()
            .navigationTitle("TitKit演示")
            .toolbar {
                ToolbarItem {
                    Image(systemName: "heart")
                        .font(.title.weight(.black))
                        .foregroundStyle(.pink.gradient)
                        .popoverTip(favTip, arrowEdge: .top)
                }
            }
        }
    }
}

在这里插入图片描述

我们可以根据不同需求来组合使用这两种显示方式。


更多定制 Tip 外观的方法,请小伙伴们移步下面的博文观赏:


4. TipKit 全局配置

其实,TipKit 框架会在 App (本地目录)中存放一些相关的配置信息。理论上说,它们可能会通过 iCloud 同步到其它设备上去,这意味着在不同设备上相同 App 中的 TipKit 共享同一组配置。

我们可以进一步定制 TipKit 的配置细节,比如 Tip 显示频率、配置数据库保存的本地位置等等:

import SwiftUI
import TipKit

@main
struct TipKitTestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {                    
                    try? Tips.configure([
                        .displayFrequency(.immediate),
                        .datastoreLocation(.applicationDefault)
                    ])
                    
                    // Xcode 15 beta4 中过时的语法,目前已不能使用:
                    /*
                    try? await Tips.configure {
                        DisplayFrequency(.immediate)
                        DatastoreLocation(.applicationDefault)
                    }*/
                }
        }
    }
}

如上代码所示,我们需要所有 Tip 立即显示,并且让系统决定配置数据存储的位置(我们也可以自己设置存储路径)。

在代码中,我们注释了之前官方示例中出错的代码片段,这些代码在 Xcode 15 beta8 中已不能使用。

5. TipKit 显示规则

除了全局 Tip 显示限制以外,我还可以设置单个 Tip 之间的显示规则。

比如,假设有两个提示,我们希望 FavoriteTip 在 StartTip 提示关闭后再显示,我们可以在 FavoriteTip 中用特定的规则(Rule)来表示这一约束:

struct FavoriteTip: Tip {
    // 其它代码从略
    
    var rules: [Rule] {
        #Rule(Self.$startTipHasDisplayed) { $0 == true}
    }
    
    @Parameter
    static var startTipHasDisplayed: Bool = false
    
}

现在,我们需要在 StartTip 提示关闭时将 FavoriteTip.startTipHasDisplayed 置为 true 才能触发 FavoriteTip 的显示:

struct ContentView: View {
    
    let startTip = StartTip()
    let favTip = FavoriteTip()
    
    var body: some View {
        NavigationStack {
            
            VStack {
                
                Image("1")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .onTapGesture {
                        // 关闭 startTip 提示
                        startTip.invalidate(reason: .actionPerformed)
                        // 触发 FavoriteTip 提升的显示
                        FavoriteTip.startTipHasDisplayed = true
                    }
                
                TipView(startTip)
            }
            .padding()
            .navigationTitle("TitKit演示")
            .toolbar {
                ToolbarItem {
                    Image(systemName: "heart")
                        .font(.title.weight(.black))
                        .foregroundStyle(.pink.gradient)
                        .popoverTip(favTip, arrowEdge: .top)
                }
            }
        }
    }
}

现在,只有等 StartTip 关闭后,FavoriteTip 提示才能显示出来:
在这里插入图片描述

6. 为 TipKit 增加更多互动性

有时,我们希望提示为用户提供更丰富的交互功能,比如在 Tip 中提供按钮跳转到更详细的使用教程界面。

TipKit 为此也提供了很好的支持,我们可以为 Tip 添加 Action 来驱动交互行为:

struct FavoriteTip: Tip {
    // 其它代码从略
    
    var actions: [Action] {
        [
            Tip.Action(id: "learn-more", title: "了解更多"),
            Tip.Action(id: "forget", title: "下次再说")
        ]
    }
}

在 Tip Action 被触发时,我们可以执行自定义行为:

struct ContentView: View {
    
    let startTip = StartTip()
    let favTip = FavoriteTip()
    
    var body: some View {
        NavigationStack {
            
            VStack {
                
                Image("1")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .onTapGesture {
                        startTip.invalidate(reason: .actionPerformed)
                        
                        FavoriteTip.startTipHasDisplayed = true
                    }
                
                TipView(startTip)
            }
            .padding()
            .navigationTitle("TitKit演示")
            .toolbar {
                ToolbarItem {
                    Image(systemName: "heart")
                        .font(.title.weight(.black))
                        .foregroundStyle(.pink.gradient)
                        .popoverTip(favTip, arrowEdge: .top){ action in
                            switch action.index {
                            case 0:
                                favTip.invalidate(reason: .tipClosed)

                                // 跳转到详细使用教程
                            case 1:
                                favTip.invalidate(reason: .tipClosed)
                                
                                // 直接退出提示
                            default:
                                break
                            }
                        }
                }
            }
        }
    }
}

现在,用户可以选择“了解更多”来进一步学习 App 的使用“秘技”了:

在这里插入图片描述

7. 如何测试 TipKit?

TipKit 全局配置存储在本地带有持久化特性,为了便于开发者即时测试, 提供了一些方法来快速显示或隐藏全部或指定 Tip:

在这里插入图片描述

一般的,要想在 Xcode 预览中正确测试 TipKit 的行为,我们需要在每次视图刷新时重置 TipKit 数据库,否则 Tip 不会正常显示:

#Preview {
    ContentView()
        .task {
            // 在每次视图刷新时将 TipKit 数据库重置为初始状态
            try? Tips.resetDatastore()
            
            try? Tips.configure([
                .displayFrequency(.immediate),
                .datastoreLocation(.applicationDefault)
            ])
        }
}

总结

在本篇博文中,我们介绍了 SwiftUI 5.0(iOS 17)中新引进的开发框架 TipKit,使用它我们可以非常方便和快速的向用户介绍我们 App 中的各种特性和使用指南,小伙伴们还不快操练起来!

感谢观赏,再会!😎

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
好的,这是一个手势解锁的Demo,你可以参考一下: ``` import UIKit class GestureLockViewController: UIViewController { // MARK: - Properties var buttons = [UIButton]() var selectedButtons = [UIButton]() var lines = [CAShapeLayer]() var touchPoint: CGPoint? var isTouching = false // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white let margin: CGFloat = 40 let distance: CGFloat = 80 let buttonWidth: CGFloat = 60 let buttonHeight: CGFloat = 60 let viewWidth = view.bounds.width let viewHeight = view.bounds.height for i in 0..<9 { let row = CGFloat(i / 3) let col = CGFloat(i % 3) let x = margin + col * (buttonWidth + distance) let y = margin + row * (buttonHeight + distance) let button = UIButton(frame: CGRect(x: x, y: y, width: buttonWidth, height: buttonHeight)) button.layer.cornerRadius = buttonWidth / 2 button.layer.borderWidth = 2 button.layer.borderColor = UIColor.lightGray.cgColor button.tag = i view.addSubview(button) buttons.append(button) } } // MARK: - Gesture Methods override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } let point = touch.location(in: view) for button in buttons { if button.frame.contains(point) && !selectedButtons.contains(button) { touchPoint = button.center selectedButtons.append(button) isTouching = true break } } } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard isTouching, let touch = touches.first else { return } let point = touch.location(in: view) touchPoint = point for button in buttons { if button.frame.contains(point) && !selectedButtons.contains(button) { button.isSelected = true selectedButtons.append(button) let line = CAShapeLayer() line.strokeColor = UIColor.gray.cgColor line.fillColor = UIColor.clear.cgColor line.lineWidth = 3 view.layer.addSublayer(line) lines.append(line) break } } drawLines() } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { isTouching = false touchPoint = nil for button in buttons { button.isSelected = false } validatePassword() clearSelectedButtons() clearLines() } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { isTouching = false touchPoint = nil for button in buttons { button.isSelected = false } clearSelectedButtons() clearLines() } // MARK: - Private Methods private func drawLines() { guard let point = touchPoint else { return } let linePath = UIBezierPath() linePath.move(to: point) for button in selectedButtons { linePath.addLine(to: button.center) } if let lastButton = selectedButtons.last, isTouching { linePath.addLine(to: lastButton.convert(lastButton.center, to: view)) } lines.last?.path = linePath.cgPath } private func clearSelectedButtons() { for button in selectedButtons { button.isSelected = false } selectedButtons.removeAll() } private func clearLines() { for line in lines { line.removeFromSuperlayer() } lines.removeAll() } private func validatePassword() { let password = selectedButtons.map { "\($0.tag)" }.joined() print("Gesture password: \(password)") } } ``` 这个Demo实现了一个3x3的手势解锁界面,使用了`UIButton`和`CAShapeLayer`来实现。当用户滑动手指时,会根据手指位置,自动连接之前选中的按钮,形成一条线。当用户抬起手指时,会根据选中的按钮的顺序,输出一个密码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大熊猫侯佩

赏点钱让我买杯可乐好吗 ;)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值