一、前言
做 UI 开发时,多级页面跳转、返回是几乎所有客户端项目都会遇到的刚需。
不管是移动端 APP、游戏 UI、桌面客户端,都会出现这类痛点 :
页面层级多,每个页面单独写返回逻辑,代码冗余、维护麻烦;
部分页面退出前需要二次确认(比如未保存内容、退出当前流程);
页面关闭伴随动画、网络请求等异步操作,普通同步返回逻辑容易错乱;
本文基于栈 (Stack) 先进后出的特性,设计一套通用全局返回架构,可套用在 Unity 大部分 UI 项目中。方案经过实战落地,同时也是面试中非常加分的 UI 架构实战案例。
二、设计思路与应用场景
- 场景举例
拿日常使用的手机 APP 举例:
场景 1:微信 → 通讯录 → 好友资料页 → 聊天页,连续多级页面,点击返回逐层退回;
场景 2:淘宝 APP → 商品列表 → 商品详情 → 下单页,退出下单页需弹窗提示「是否放弃订单」;
场景 3:抖音 APP,页面关闭伴随渐变、位移动画,返回需要等待动画执行完毕再销毁界面。
以上场景,用一套栈结构统一管理,就能彻底告别每个页面手写返回逻辑。 - 架构目标
统一接管所有页面的返回行为,代码复用;
同时支持同步操作、异步动画 / 请求、退出确认弹窗三大能力;
自动管理页面层级,保证返回顺序和打开顺序完全对应;
低侵入,原有 UI 框架无需大幅改造。
三、核心数据结构定义
首先定义数据载体类,用来存储每一个页面对应的返回行为,区分同步、异步、确认校验三类逻辑:
csharp
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
/// <summary>
/// 页面返回行为数据
/// 承载单个页面所有返回相关逻辑
/// </summary>
public class PageBackInfo
{
// 页面标识(日志、调试使用)
public string PageName;
// 同步返回逻辑(关闭页面、刷新数据等)
public Action SyncBackAction;
// 异步返回逻辑(播放动画、网络请求、延时关闭等)
public Func<UniTask> AsyncBackAction;
// 退出确认逻辑(弹窗二次确认,返回true允许退出,false拦截返回)
public Func<UniTask<bool>> CheckConfirmAction;
public PageBackInfo(string pageName, Action syncAction,
Func<UniTask> asyncAction = null, Func<UniTask<bool>> confirmAction = null)
{
PageName = pageName;
SyncBackAction = syncAction;
AsyncBackAction = asyncAction;
CheckConfirmAction = confirmAction;
}
}
全局返回管理器中,使用Stack存储页面层级,栈天然适配「先打开、后返回」的交互逻辑:
csharp
/// <summary>
/// 全局返回管理器(单例,全局唯一)
/// </summary>
public class GlobalBackManager
{
public static GlobalBackManager Instance { get; private set; }
// 页面层级栈
private Stack<PageBackInfo> _pageStack = new Stack<PageBackInfo>();
private void Awake()
{
// 单例防重复创建
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
// 跨场景保留管理器
DontDestroyOnLoad(gameObject);
}
}
四、核心功能实现
- 页面入栈:注册返回行为
每当打开一个新页面,就将该页面的返回信息压入栈。这是使用该架构的唯一接入点。
csharp
/// <summary>
/// 注册页面(页面打开时调用,入栈)
/// </summary>
public void RegisterPage(PageBackInfo pageInfo)
{
if (pageInfo == null) return;
_pageStack.Push(pageInfo);
UnityEngine.Debug.Log($"页面入栈:{pageInfo.PageName} | 当前层级:{_pageStack.Count}");
}
- 核心返回逻辑
点击返回按钮 / 手机返回键时,执行统一返回流程:先校验确认弹窗 → 执行同步逻辑 → 执行异步逻辑 → 页面出栈。
csharp
/// <summary>
/// 全局统一返回入口
/// </summary>
public async void DoBack()
{
// 栈为空,已是根页面,无页面可返回
if (_pageStack.Count == 0)
{
UnityEngine.Debug.Log("已到达首页,无法继续返回");
return;
}
// 取出栈顶页面(当前最顶层页面)
var currentPage = _pageStack.Peek();
// 第一步:执行二次确认校验
if (currentPage.CheckConfirmAction != null)
{
bool canBack = await currentPage.CheckConfirmAction.Invoke();
// 用户选择取消,直接终止返回流程
if (!canBack) return;
}
// 第二步:执行同步返回逻辑
currentPage.SyncBackAction?.Invoke();
// 第三步:执行异步返回逻辑(动画/延时/网络)
if (currentPage.AsyncBackAction != null)
{
await currentPage.AsyncBackAction.Invoke();
}
// 第四步:逻辑执行完毕,页面出栈
_pageStack.Pop();
UnityEngine.Debug.Log($"页面出栈:{currentPage.PageName} | 当前层级:{_pageStack.Count}");
}
- 其他:清空栈(场景切换 / 回到首页)
切换主界面、退出功能模块时,清空栈,避免脏数据残留:
csharp
/// <summary>
/// 清空所有页面层级(回到根页面)
/// </summary>
public void ClearAllPage()
{
_pageStack.Clear();
UnityEngine.Debug.Log("已清空页面返回栈");
}
五、通用业务场景实战演示
下面用大家日常都在用的 APP 交互做演示,覆盖普通页面、确认弹窗、异步动画三大典型场景。
场景 1:多级普通页面(无弹窗、无动画)
模拟流程:首页 → 消息列表页 → 聊天详情页
打开消息列表页,注册返回逻辑:关闭当前页面,回到首页
csharp
// 打开消息列表时调用
var msgPageInfo = new PageBackInfo(
pageName: "消息列表页",
syncAction: () =>
{
// 同步逻辑:关闭消息列表界面
UIManager.CloseUI("MsgListPanel");
}
);
GlobalBackManager.Instance.RegisterPage(msgPageInfo);
打开聊天详情页,注册返回逻辑:关闭聊天页,回到消息列表
csharp
// 打开聊天页时调用
var chatPageInfo = new PageBackInfo(
pageName: "聊天详情页",
syncAction: () =>
{
UIManager.CloseUI("ChatPanel");
}
);
GlobalBackManager.Instance.RegisterPage(chatPageInfo);
返回流程:
点击返回 → 聊天详情页出栈 → 回到消息列表;再次点击返回 → 消息列表出栈 → 回到首页,逻辑完全符合预期。
场景 2:带二次确认的页面(电商下单页)
模拟流程:商城首页 → 商品详情 → 下单结算页
需求:在下单页点击返回,弹出提示「订单尚未提交,确定要退出吗?」,取消则留在当前页,确认则关闭页面。
csharp
// 打开下单结算页时注册
var orderPageInfo = new PageBackInfo(
pageName: "下单结算页",
syncAction: () =>
{
UIManager.CloseUI("OrderPanel");
},
asyncAction: null,
confirmAction: async () =>
{
// 弹出通用确认弹窗,等待用户选择
bool userSelect = await CommonTipPanel.ShowConfirm("订单未提交,是否退出?");
return userSelect;
}
);
GlobalBackManager.Instance.RegisterPage(orderPageInfo);
返回流程:
点击返回 → 触发确认弹窗 → 选「取消」:终止返回;选「确定」:关闭页面、执行出栈。
场景 3:带异步动画的页面(短视频播放页)
模拟流程:推荐列表 → 短视频播放页
需求:页面关闭需要播放 1 秒淡出动画,必须等待动画结束后,再销毁界面。
csharp
// 打开短视频播放页时注册
var videoPageInfo = new PageBackInfo(
pageName: "短视频播放页",
syncAction: null,
asyncAction: async () =>
{
// 执行淡出动画,等待动画播放完成
await VideoPlayerPanel.PlayCloseAnimation();
// 动画结束后关闭界面
UIManager.CloseUI("VideoPlayerPanel");
}
);
GlobalBackManager.Instance.RegisterPage(videoPageInfo);
返回流程:
点击返回 → 执行淡出动画(异步等待)→ 动画结束 → 关闭页面、出栈。
六、方案优缺点 & 踩坑总结
- 优势
通用性极强:一套代码适配所有 UI 页面,游戏、APP、客户端都能用;
解耦性高:页面不用互相依赖,只需要在打开时注册自身返回逻辑;
能力全面:同时支持同步、异步、二次确认,覆盖 90% 以上 UI 返回场景;
层级稳定:依托栈结构,严格保证页面跳转 / 返回顺序,不会出现层级错乱。 - 注意事项(避坑重点,面试须知)
- 必须成对入栈、出栈
页面正常打开要RegisterPage,禁止绕过管理器直接销毁界面,否则栈会残留脏数据,后续返回逻辑异常。 - 场景切换务必清空栈
切换主模块、跳转场景时,调用ClearAllPage(),防止旧页面数据干扰新流程。 - 异步逻辑做好异常捕获
动画、网络请求等异步操作建议增加try-catch,避免异常打断整个返回流程。 - 根页面不注册栈数据
首页 / 主面板作为根节点,不需要入栈,保证点击返回时不会无限循环。
- 必须成对入栈、出栈
七、拓展优化方向(面试加分项)
- 支持一键返回首页
封装方法循环出栈,直接清空所有层级,快速回到根页面; - 适配移动端物理返回键
在 Unity 中监听手机物理返回键,直接调用DoBack(),和原生 APP 体验一致; - 编辑器调试工具
开发 Editor 调试面板,实时查看栈内所有页面名称、层级数量,快速排查 UI 层级问题; - 栈数据缓存优化
针对需要常驻的页面,增加标记,支持「返回不销毁页面」的需求。
八、写在最后
这套基于栈的返回架构,是 UI 开发中非常经典且实用的设计。原理简单、落地成本低,同时能体现数据结构结合业务落地的能力。
日常开发中不用重复编写大量返回逻辑,项目可维护性大幅提升。如果大家在 Unity UI、客户端开发中遇到多级页面管理问题,都可以参考这套思路。
欢迎评论区交流不同项目的 UI 返回实现方案!