MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SwiftUI 3D Touch与Peek and Pop

2023-07-033.4k 阅读

SwiftUI 3D Touch 与 Peek and Pop 概述

在移动应用开发领域,为用户提供丰富且便捷的交互体验至关重要。3D Touch 作为苹果设备上独具特色的交互技术,允许用户通过按压屏幕的不同力度来触发不同的操作。而 Peek and Pop 则是基于 3D Touch 衍生出的一种交互模式,它能让用户在不打开完整视图的情况下预览内容,并通过进一步按压展开该内容。SwiftUI 作为苹果大力推广的新一代构建用户界面的框架,也为开发者提供了集成 3D Touch 与 Peek and Pop 功能的方式。

3D Touch 基础原理

3D Touch 的核心在于压力传感器,它能够感知用户按压屏幕的力度。苹果设备将压力分为多个级别,开发者可以针对不同的压力级别设置不同的响应动作。例如,在主屏幕上,轻度按压可能会触发应用的快捷操作(Quick Actions),而更用力的按压则可能会执行一些更为复杂的操作。

Peek and Pop 交互流程

Peek and Pop 交互包含两个主要阶段:Peek(预览)和 Pop(展开)。当用户以适度的力度按压屏幕上的某个元素(比如一个列表项)时,会触发 Peek 操作,此时会弹出一个该元素内容的预览视图。如果用户继续加大按压力度,就会触发 Pop 操作,将预览视图展开为完整的详细视图。这种交互方式既节省了屏幕空间,又能让用户快速获取所需信息,大大提升了用户体验。

在 SwiftUI 中实现 3D Touch 快捷操作

创建项目并配置

首先,创建一个新的 SwiftUI 项目。在项目的 Info.plist 文件中,需要添加 3D Touch 快捷操作相关的配置。打开 Info.plist,右键点击空白处,选择“添加行”,然后搜索“UIApplicationShortcutItems”,点击展开该项。在这里可以添加多个快捷操作,每个快捷操作由 UIApplicationShortcutItem 组成,它包含以下几个重要属性:

  • UIApplicationShortcutItemType:操作的唯一标识符,用于在代码中识别该快捷操作。
  • UIApplicationShortcutItemTitle:快捷操作在主屏幕上显示的标题。
  • UIApplicationShortcutItemSubtitle:可选的副标题,用于提供更多关于操作的信息。
  • UIApplicationShortcutItemIconType:操作的图标类型,例如 UIApplicationShortcutIconTypeCompose 表示撰写图标。

代码实现

SceneDelegate.swift 文件中,处理 3D Touch 快捷操作的逻辑。在 scene(_:willConnectTo:options:) 方法中,添加以下代码:

if let shortcutItem = options.shortcutItem {
    handleShortcutItem(shortcutItem)
}

然后定义 handleShortcutItem 方法来处理不同的快捷操作:

func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
    switch shortcutItem.type {
    case "com.example.app.shortcut1":
        // 处理第一个快捷操作
        print("执行快捷操作 1")
    case "com.example.app.shortcut2":
        // 处理第二个快捷操作
        print("执行快捷操作 2")
    default:
        break
    }
}

在上述代码中,根据快捷操作的类型 type 来执行不同的逻辑。例如,如果 typecom.example.app.shortcut1,则打印“执行快捷操作 1”。通过这种方式,就可以在 SwiftUI 项目中实现 3D Touch 快捷操作的基本功能。

在 SwiftUI 中集成 Peek and Pop

检测设备是否支持 3D Touch

在实现 Peek and Pop 之前,需要先检测设备是否支持 3D Touch。可以通过以下代码进行检测:

func is3DTouchAvailable() -> Bool {
    if #available(iOS 9.0, *) {
        return UIScreen.main.traitCollection.forceTouchCapability == .available
    }
    return false
}

上述代码使用 UIScreen.main.traitCollection.forceTouchCapability 属性来判断设备是否支持 3D Touch。如果 forceTouchCapability 的值为 .available,则表示设备支持 3D Touch。

创建 Peek and Pop 交互

在 SwiftUI 中实现 Peek and Pop 需要借助 UIViewControllerRepresentable 协议。首先,创建一个 PeekAndPopViewController,它继承自 UIViewController,并实现 Peek and Pop 的相关逻辑。

import UIKit

class PeekAndPopViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // 配置视图
    }

    override func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
        // 返回预览视图对应的视图控制器
        let previewVC = UIViewController()
        previewVC.view.backgroundColor = .lightGray
        return previewVC
    }

    override func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
        // 执行 Pop 操作,将预览视图展开为完整视图
        show(viewControllerToCommit, sender: self)
    }
}

在上述代码中,previewingContext(_:viewControllerForLocation:) 方法用于返回预览视图对应的视图控制器,这里简单创建了一个背景色为浅灰色的 UIViewController 作为预览视图。previewingContext(_:commit:) 方法则用于执行 Pop 操作,将预览视图展开为完整视图。

然后,创建一个 PeekAndPopView,它遵循 UIViewControllerRepresentable 协议,用于在 SwiftUI 中展示 PeekAndPopViewController

import SwiftUI

struct PeekAndPopView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> PeekAndPopViewController {
        let viewController = PeekAndPopViewController()
        if #available(iOS 9.0, *) {
            if let previewingContext = viewController.view?.registerForPreviewing(with: viewController, sourceView: viewController.view) {
                viewController.addChild(previewingContext)
            }
        }
        return viewController
    }

    func updateUIViewController(_ uiViewController: PeekAndPopViewController, context: Context) {
        // 更新视图控制器
    }
}

makeUIViewController(context:) 方法中,创建了 PeekAndPopViewController 实例,并为其视图注册了预览功能。最后,在 SwiftUI 视图中使用 PeekAndPopView

struct ContentView: View {
    var body: some View {
        PeekAndPopView()
    }
}

通过以上步骤,就可以在 SwiftUI 项目中实现基本的 Peek and Pop 交互。

优化 Peek and Pop 体验

自定义预览视图

在前面的示例中,预览视图只是一个简单的灰色背景视图。在实际应用中,可以根据需求自定义预览视图,展示更丰富的信息。例如,如果是针对文章列表项的 Peek and Pop,预览视图可以显示文章的标题、摘要和图片等内容。

class ArticlePreviewViewController: UIViewController {
    let article: Article

    init(article: Article) {
        self.article = article
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        let titleLabel = UILabel()
        titleLabel.text = article.title
        titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(titleLabel)

        let summaryLabel = UILabel()
        summaryLabel.text = article.summary
        summaryLabel.numberOfLines = 3
        summaryLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(summaryLabel)

        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
            titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),

            summaryLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
            summaryLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            summaryLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
        ])
    }
}

在上述代码中,ArticlePreviewViewController 用于展示文章的预览内容,包括标题和摘要。然后在 PeekAndPopViewControllerpreviewingContext(_:viewControllerForLocation:) 方法中返回 ArticlePreviewViewController 实例:

override func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
    let article = Article(title: "示例文章标题", summary: "这是一篇示例文章的摘要")
    let previewVC = ArticlePreviewViewController(article: article)
    return previewVC
}

通过这种方式,就可以为 Peek and Pop 提供更具针对性和丰富的预览视图。

调整 Peek and Pop 的触发力度

默认情况下,Peek and Pop 的触发力度是由系统设置的。但在某些情况下,开发者可能希望根据应用的特点调整触发力度,以提供更好的用户体验。可以通过 UIViewControllerPreviewingsetPreviewingContext(_:for:) 方法来设置触发力度。例如,以下代码将触发 Peek 操作的力度降低:

if #available(iOS 9.0, *) {
    if let previewingContext = viewController.view?.registerForPreviewing(with: viewController, sourceView: viewController.view) {
        let previewingParameters = UIPreviewingParameters()
        previewingParameters.livePreview = true
        previewingParameters.allowedPreviewKeys = []
        previewingParameters.minimumScaleFactor = 0.8
        previewingContext.setPreviewingContext(previewingParameters, for: viewController.view!)
        viewController.addChild(previewingContext)
    }
}

在上述代码中,通过 UIPreviewingParametersminimumScaleFactor 属性来调整触发 Peek 操作的力度。较小的 minimumScaleFactor 值意味着更低的触发力度。

处理 Peek and Pop 中的交互事件

预览视图中的交互

在预览视图中,可能需要响应用户的交互操作,例如点击按钮、滑动视图等。以在预览视图中添加一个关闭按钮为例,当用户点击关闭按钮时,取消 Peek 操作。

class ArticlePreviewViewController: UIViewController {
    let article: Article

    init(article: Article) {
        self.article = article
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        let closeButton = UIButton(type: .system)
        closeButton.setTitle("关闭", for: .normal)
        closeButton.addTarget(self, action: #selector(dismissPreview), for: .touchUpInside)
        closeButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(closeButton)

        NSLayoutConstraint.activate([
            closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
            closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
        ])

        let titleLabel = UILabel()
        titleLabel.text = article.title
        titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(titleLabel)

        let summaryLabel = UILabel()
        summaryLabel.text = article.summary
        summaryLabel.numberOfLines = 3
        summaryLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(summaryLabel)

        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: closeButton.bottomAnchor, constant: 16),
            titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),

            summaryLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
            summaryLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            summaryLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
        ])
    }

    @objc func dismissPreview() {
        dismiss(animated: true, completion: nil)
    }
}

在上述代码中,创建了一个关闭按钮,并为其添加了点击事件处理方法 dismissPreview。当用户点击关闭按钮时,调用 dismiss(animated:completion:) 方法取消预览视图。

Pop 后的视图交互

在 Pop 操作后,展开的完整视图也需要处理各种交互事件。例如,如果完整视图是一篇文章详情页,可能需要处理滚动、分享等操作。假设展开的文章详情视图是一个 ArticleDetailViewController,在该视图中处理分享操作:

class ArticleDetailViewController: UIViewController {
    let article: Article

    init(article: Article) {
        self.article = article
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        let shareButton = UIButton(type: .system)
        shareButton.setTitle("分享", for: .normal)
        shareButton.addTarget(self, action: #selector(shareArticle), for: .touchUpInside)
        shareButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(shareButton)

        NSLayoutConstraint.activate([
            shareButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
            shareButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
        ])

        // 显示文章详细内容的其他代码
    }

    @objc func shareArticle() {
        let activityViewController = UIActivityViewController(activityItems: [article.title, article.content], applicationActivities: nil)
        present(activityViewController, animated: true, completion: nil)
    }
}

在上述代码中,创建了一个分享按钮,并为其添加了点击事件处理方法 shareArticle。当用户点击分享按钮时,创建一个 UIActivityViewController 来实现文章的分享功能。

与其他 SwiftUI 功能结合使用

与 NavigationView 结合

在 SwiftUI 中,NavigationView 是构建导航结构的常用组件。可以将 Peek and Pop 与 NavigationView 结合使用,提升应用的导航体验。例如,在一个列表视图中,每个列表项支持 Peek and Pop,当用户执行 Pop 操作时,将展开的详细视图推送到 NavigationView 的导航栈中。

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                ForEach(0..<5) { index in
                    NavigationLink(destination: ArticleDetailView(article: articles[index])) {
                        ArticleListItemView(article: articles[index])
                    }
                   .buttonStyle(PlainButtonStyle())
                   .simultaneousGesture(
                        PeekAndPopGesture(article: articles[index])
                    )
                }
            }
           .navigationTitle("文章列表")
        }
    }
}

struct PeekAndPopGesture: Gesture {
    let article: Article

    func makeBody(configuration: Configuration) -> PeekAndPopGestureBody {
        PeekAndPopGestureBody(article: article)
    }
}

struct PeekAndPopGestureBody: Gesture.Body {
    let article: Article

    var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get { AnimatablePair(0, 0) }
        set { }
    }

    func _changed(in state: inout State) {
        // 处理 Peek 操作
    }

    func _ended(in state: inout State) {
        // 处理 Pop 操作
    }
}

在上述代码中,通过 simultaneousGesture 将 Peek and Pop 手势添加到 NavigationLink 上。当用户执行 Pop 操作时,ArticleDetailView 会被推送到 NavigationView 的导航栈中,用户可以通过导航栏返回列表视图。

与 TabView 结合

TabView 用于在应用中创建底部标签栏。可以将 3D Touch 快捷操作与 TabView 结合,让用户通过快捷操作快速切换到特定的标签页。例如,在 SceneDelegate.swift 中处理快捷操作时,根据快捷操作类型切换 TabView 的选中标签:

func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
    switch shortcutItem.type {
    case "com.example.app.shortcut1":
        // 切换到第一个标签页
        tabViewSelection.wrappedValue = 0
    case "com.example.app.shortcut2":
        // 切换到第二个标签页
        tabViewSelection.wrappedValue = 1
    default:
        break
    }
}

在上述代码中,tabViewSelection 是一个 @State 变量,用于控制 TabView 的选中标签。通过这种方式,用户可以通过 3D Touch 快捷操作快速切换到所需的标签页,提高应用的使用效率。

兼容性与注意事项

低版本兼容性

虽然 3D Touch 和 Peek and Pop 是 iOS 9.0 引入的功能,但并非所有 iOS 设备都支持。在开发过程中,要始终考虑低版本设备和不支持 3D Touch 的设备。对于不支持 3D Touch 的设备,可以通过其他交互方式(如长按)来提供类似的功能。例如,在前面的 Peek and Pop 实现中,可以同时添加长按手势,当用户长按列表项时,显示类似预览视图的内容。

struct ArticleListItemView: View {
    let article: Article

    var body: some View {
        VStack {
            Text(article.title)
            Text(article.summary)
        }
       .contentShape(Rectangle())
       .onLongPressGesture {
            // 显示类似 Peek 视图的内容
            let articlePreviewVC = ArticlePreviewViewController(article: article)
            let hostingController = UIHostingController(rootView: EmptyView())
            hostingController.present(articlePreviewVC, animated: true, completion: nil)
        }
    }
}

在上述代码中,为 ArticleListItemView 添加了长按手势,当用户长按列表项时,显示 ArticlePreviewViewController,提供类似 Peek 的功能。

性能优化

在实现 3D Touch 和 Peek and Pop 功能时,要注意性能优化。例如,在创建预览视图和展开视图时,避免加载过多的资源或执行复杂的计算,以免造成卡顿。对于图片等资源,可以采用异步加载的方式,确保在视图显示时能够快速加载完成。同时,合理使用缓存机制,避免重复加载相同的内容。例如,如果预览视图中显示的图片在展开视图中也会显示,可以在预览视图加载图片时将其缓存,在展开视图中直接从缓存中获取,提高加载速度。

测试与用户反馈

在开发完成后,要进行充分的测试,确保 3D Touch 和 Peek and Pop 功能在不同设备、不同系统版本上都能正常工作。同时,收集用户反馈,根据用户的使用习惯和意见对功能进行优化。例如,如果用户反馈 Peek and Pop 的触发力度不合适,可以根据多数用户的反馈调整触发力度。通过不断的测试和优化,为用户提供更加流畅、便捷的交互体验。

通过以上详细的介绍和代码示例,开发者可以在 SwiftUI 项目中全面地实现 3D Touch 与 Peek and Pop 功能,并结合其他 SwiftUI 特性,为用户打造出具有丰富交互体验的移动应用。在实际开发中,要根据应用的需求和特点,灵活运用这些技术,不断优化用户体验,使应用在竞争激烈的市场中脱颖而出。