返回

iPhone 横屏防误触退出?教你恢复双重上滑

IOS

iPhone 横屏应用:如何找回 Home 指示器的“双重上滑”退出?

不少开发者遇到一个情况:用 Xcode 9 或更高版本开发的横屏 App,在 iPhone X 或后续机型上,从屏幕底部上滑一次就直接退回桌面了。这和早期(比如 Xcode 8.3 编译)的 App 表现不一样——那些 App 在横屏时,常常需要先向上滑一次“唤醒” Home 指示器,再滑一次才能真正退出。用户想要的就是后面这种“双重上滑”的效果,并且是在充分利用屏幕空间(没有黑边 letterbox)的前提下实现。

尝试过在 UIViewController 中设置 prefersHomeIndicatorAutoHidden() 返回 true,虽然能让 Home 指示器暂时消失,但手指一碰到屏幕它又冒出来,感觉有点干扰,而且最关键的是,它依然只需要 一次 上滑就能退出 App,并没有解决根本问题。找了一圈似乎没什么直接选项,但既然老 App 能自动实现这种效果,新 App 肯定也有办法做到。

问题根源在哪?

简单说,这其实是 iOS 系统为了适应带 Home 指示器的全面屏设备而做出的设计区分。

  1. 老 App (Xcode 8.3 或更早编译): 这些 App 构建时,还没有针对 iPhone X 的 Safe Area(安全区域)进行适配。当它们在 iPhone X 或更新设备上以横屏模式运行时,iOS 为了兼容性,会自动给它们加上下黑边(Letterboxing)。同时,系统认为这类未适配的横屏应用(特别是游戏类)可能会在屏幕底部有操作区域,为了防止用户在激烈操作中误触 Home 指示器导致退出,就默认开启了一种“边缘保护”机制——也就是需要先滑一下激活,再滑第二下才响应系统手势(如返回主屏)。

  2. 新 App (Xcode 9 或更高版本编译): 从 Xcode 9 开始,开发者被期望(或者说强制)适配 Safe Area。应用会默认占满整个屏幕(状态栏和 Home 指示器区域除外,由 Safe Area 引导布局)。在这种“官方认证”的全屏模式下,系统认为开发者已经妥善处理了底部区域的交互,因此默认取消了那层“边缘保护”,使得一次上滑就能直接退出,以提供更流畅的系统导航体验。

至于 prefersHomeIndicatorAutoHidden,它的作用仅仅是在用户一段时间不操作屏幕时,自动隐藏 Home 指示器这个视觉元素,让内容(比如视频、游戏画面)更沉浸。但它并不改变响应系统手势所需的滑动次数。一旦用户触摸屏幕,指示器就会 reappear,并且单次上滑退出的行为依旧。

所以,关键不在于隐藏指示器,而在于如何告诉系统:“嘿,我这个横屏界面需要启用边缘保护,别一次滑动就退出了!”

解决方案:启用边缘保护

iOS 提供了一个专门的 API 来控制这个行为,它就是 UIViewController 的一个属性:preferredScreenEdgesDeferringSystemGestures

使用 preferredScreenEdgesDeferringSystemGestures

这个属性的作用是告诉系统,应用希望在哪些屏幕边缘推迟(Defer)响应系统级的手势(比如从底部上滑返回主屏幕、从顶部下滑打开通知中心/控制中心)。当用户在被指定的边缘发起手势时,第一次滑动只会触发 App 内的响应(如果 App 有处理该区域的触摸事件的话),并让对应的系统指示器(如 Home 指示器)显现;第二次在相同位置的滑动,才会被识别为系统手势。

原理和作用

它的核心就是让 App “优先认领”来自屏幕边缘的第一次滑动。对于需要防止误触退出的横屏游戏或者绘图应用来说,这非常有用。用户在屏幕底部快速操作时,第一次滑动不会打断应用流程。

代码示例

你需要在希望启用双重上滑的 UIViewController 子类中重写(override)这个计算属性。

Swift:

import UIKit

class YourLandscapeViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // 你的其他设置代码
    }

    // 启用底部边缘的系统手势延迟
    override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
        return .bottom // 通常我们只关心底部的 Home 手势
        // 如果你的应用也需要在顶部边缘防止误触(比如全屏游戏防止拉出通知中心)
        // 可以返回 .all
        // return .all
        // 如果想恢复默认行为(单次滑动退出),返回空集合
        // return []
    }

    // (可选) 配合自动隐藏 Home 指示器以获得更佳沉浸感
    override var prefersHomeIndicatorAutoHidden: Bool {
        return true
    }

    // (可选, iOS 11+) 如果你配合隐藏了指示器,最好也更新状态栏隐藏逻辑
    // 需要在 Info.plist 中设置 "View controller-based status bar appearance" 为 YES
    override var prefersStatusBarHidden: Bool {
        return true
    }

    // (可选, iOS 11+) 定义状态栏隐藏时的动画
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        return .slide
    }
}

Objective-C:

#import "YourLandscapeViewController.h"

@interface YourLandscapeViewController ()

@end

@implementation YourLandscapeViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 你的其他设置代码
}

// 启用底部边缘的系统手势延迟
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
    return UIRectEdgeBottom; // 只关心底部
    // 若要包含所有边缘:
    // return UIRectEdgeAll;
    // 恢复默认:
    // return UIRectEdgeNone; // 或者 UIRectEdge()
}

// (可选) 配合自动隐藏 Home 指示器
- (BOOL)prefersHomeIndicatorAutoHidden {
    return YES;
}

// (可选, iOS 11+) 控制状态栏隐藏
- (BOOL)prefersStatusBarHidden {
    return YES;
}

// (可选, iOS 11+) 状态栏隐藏动画
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation {
    return UIStatusBarAnimationSlide;
}

@end

详细步骤

  1. 定位视图控制器: 找到你的 App 中那个需要实现双重上滑的横屏界面的 UIViewController 类文件。
  2. 添加重写方法: 在该类的实现中(.swift 文件或 .m 文件),添加上面示例中的 preferredScreenEdgesDeferringSystemGestures 属性重写代码。
  3. 选择边缘: 根据需要决定返回值是 .bottom(仅底部)、.all(所有边缘,包括顶部可能触发通知/控制中心的手势)还是其他组合。对于只解决 Home 指示器误触问题,.bottom 通常足够。
  4. 编译运行: 重新编译你的 App 并在 iPhone X 或更新机型的模拟器/真机上运行,切换到该横屏界面,尝试从底部上滑。你会发现第一次上滑没有反应(或者只是让 Home 指示器更明显),需要第二次上滑才能退出 App。

进阶使用技巧

  • 动态控制: 这个属性是可读的,意味着你可以根据 App 的内部状态动态改变其返回值。比如,只在游戏进行中启用边缘延迟,在暂停菜单或静态界面时恢复默认行为(返回 [].none)。这需要调用 setNeedsUpdateOfScreenEdgesDeferringSystemGestures() 来通知系统需要重新查询这个属性值。

    // 假设有一个变量 `isGameActive` 控制游戏状态
    var isGameActive: Bool = false {
        didSet {
            // 当游戏状态改变时,请求更新边缘手势延迟设置
            setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
        }
    }
    
    override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
        return isGameActive ? .bottom : [] // 游戏激活时延迟底部手势,否则不延迟
    }
    
  • UIScrollView 的交互: 如果你的界面底部有一个 UIScrollView(或其子类,如 UITableView, UICollectionView),并且内容滚动到了最底部,那么第一次从底部边缘开始的上滑通常会被 ScrollView 的 “bounce” 效果(弹性效果)消耗掉,不会触发应用逻辑,也不会被视为激活系统手势的第一步。这种情况下,用户可能需要更明确地滑第二次才能退出。这通常是符合预期的,但需要了解这个交互细节。

  • 谨慎使用 .all 虽然 .all 可以同时保护顶部和底部边缘,但它也会让用户调出通知中心和控制中心变得困难(都需要两次滑动)。只在确实需要防止顶部误触的场景(比如顶部也有密集操作区域的全屏游戏)才使用 .all。否则,仅使用 .bottom 对用户干扰最小。

关于 prefersHomeIndicatorAutoHidden 的补充

现在我们回头看 prefersHomeIndicatorAutoHidden。它和 preferredScreenEdgesDeferringSystemGestures 是两个独立但可以协同工作的特性。

  • prefersHomeIndicatorAutoHidden = true:让 Home 指示器在不活动时淡出,追求视觉上的“无干扰”。
  • preferredScreenEdgesDeferringSystemGestures = .bottom (或 .all):让底部(或所有)边缘的系统手势需要两次滑动才触发,追求操作上的“防误触”。

对于需要高度沉浸感的横屏应用(如视频播放器全屏、游戏),两者结合使用效果最佳:

  1. preferredScreenEdgesDeferringSystemGestures 防止意外退出。
  2. prefersHomeIndicatorAutoHidden 在用户不触摸屏幕时隐藏那个小白条,让视野更干净。

就像上面代码示例里展示的那样,你可以同时重写这两个属性。

override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
    return .bottom // 需要双击退出
}

override var prefersHomeIndicatorAutoHidden: Bool {
    return true // 并且在空闲时隐藏指示器
}

平衡用户体验与功能

虽然我们找到了技术上的解决方案,但请务必考虑用户体验。

  • 仅在必要时使用: “双重上滑”增加了退出 App 的步骤。对于非游戏、非全屏绘图等不需要在屏幕边缘进行精细或快速操作的应用,强制用户多滑一次可能会带来挫败感。确保你的应用场景确实存在误触风险,且这种风险的干扰大于增加退出步骤带来的不便。
  • 告知用户(如果非显而易见): 如果你的应用不是典型的游戏,但在特定模式下启用了边缘延迟,可以考虑在首次进入该模式时,用一个简单的提示(比如一个短暂的 coach mark)告知用户需要两次上滑才能退出。
  • 充分测试: 在多种设备和 iOS 版本上测试启用边缘延迟后的交互。确保它在你期望的场景下工作,并且没有引入其他意外的交互问题。

通过理解 iOS 对新旧 App 的处理差异,并正确使用 preferredScreenEdgesDeferringSystemGestures 这个 API,你就能在现代 Xcode 项目中,为你的横屏应用找回那层必要的“边缘保护”,有效防止 Home 指示器的误触退出问题,同时还能充分利用全面屏的显示区域。