SwiftUI中计时器引发页面跳转后闪退问题求助
问题:引导页Timer导致页面跳转后自动退回
问题场景与现象
- 引导页(
OnboardingView)通过Timer.TimerPublisher实现轮播项自动切换,用绑定isActive的NavigationLink跳转到登录页(LoginView) - 从登录页进入注册页、从个人资料页进入我的账户页时,目标页面短暂显示后会自动退回上一页
- 移除Timer或其监听逻辑后,页面跳转完全正常;试用原生
Timer和Timer.TimerPublisher均出现该问题,怀疑与menuIndex变量有关
相关代码
struct OnboardingView: View { //MARK: - PROPERTIES @State var menuIndex = 0 @State var openLogin = false let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect() //MARK: - BODY var body: some View { NavigationLink(destination: LoginView(), isActive: $openLogin) {} VStack (spacing: 22) { ProfileButton(openProfile: $openLogin) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.horizontal, 22) OnboardingMenuItem(text: onboardingMenu[menuIndex].title, image: onboardingMenu[menuIndex].image) .frame(height: 80) .animation(.easeInOut, value: menuIndex) .onReceive(timer) { input in if menuIndex < onboardingMenu.count - 1 { menuIndex = menuIndex + 1 } else { menuIndex = 0 } } ... } } }
问题原因
- 导航结构不规范:
NavigationLink直接放在body根层级,未嵌套在NavigationStack(或iOS15及以下的NavigationView)中,违反SwiftUI导航的基本规则 - Timer触发视图重绘导致导航状态重置:Timer每4秒修改
menuIndex,触发OnboardingView的body重绘。此时根层级的NavigationLink会被重新创建,绑定的isActive状态虽为@State,但重绘过程中会意外重置导航栈状态,把已push的页面弹回
解决方案
方案1:规范导航结构+启停Timer
struct OnboardingView: View { //MARK: - PROPERTIES @State var menuIndex = 0 @State var openLogin = false // 改为@State持有TimerPublisher,方便控制启停 @State private var timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect() //MARK: - BODY var body: some View { // 新增NavigationStack(iOS16+,旧版用NavigationView) NavigationStack { VStack (spacing: 22) { ProfileButton(openProfile: $openLogin) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.horizontal, 22) OnboardingMenuItem(text: onboardingMenu[menuIndex].title, image: onboardingMenu[menuIndex].image) .frame(height: 80) .animation(.easeInOut, value: menuIndex) .onReceive(timer) { input in menuIndex = menuIndex < onboardingMenu.count - 1 ? menuIndex + 1 : 0 } // 将NavigationLink移到NavigationStack内部 NavigationLink(destination: LoginView(), isActive: $openLogin) { EmptyView() } ... } // 监听跳转状态,启停Timer .onChange(of: openLogin) { isOpen in if isOpen { // 跳转后暂停Timer,避免触发视图重绘 timer.upstream.connect().cancel() } else { // 返回引导页时重启Timer(可选,根据需求调整) timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect() } } } } }
方案2:将Timer逻辑移到ViewModel(更推荐)
把Timer和menuIndex的管理抽离到ViewModel,避免视图重绘影响导航状态:
import Combine class OnboardingViewModel: ObservableObject { @Published var menuIndex = 0 private var timer: AnyCancellable? private let menuCount: Int init(menuCount: Int) { self.menuCount = menuCount startTimer() } // 启动Timer func startTimer() { timer = Timer.publish(every: 4, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in guard let self = self else { return } self.menuIndex = self.menuIndex < self.menuCount - 1 ? self.menuIndex + 1 : 0 } } // 停止Timer func stopTimer() { timer?.cancel() } } // 在View中使用ViewModel struct OnboardingView: View { @StateObject private var vm = OnboardingViewModel(menuCount: onboardingMenu.count) @State var openLogin = false var body: some View { NavigationStack { VStack (spacing: 22) { ProfileButton(openProfile: $openLogin) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.horizontal, 22) OnboardingMenuItem(text: onboardingMenu[vm.menuIndex].title, image: onboardingMenu[vm.menuIndex].image) .frame(height: 80) .animation(.easeInOut, value: vm.menuIndex) NavigationLink(destination: LoginView(), isActive: $openLogin) { EmptyView() } ... } .onChange(of: openLogin) { isOpen in isOpen ? vm.stopTimer() : vm.startTimer() } } } }
内容的提问来源于stack exchange,提问作者Elias Al Zaghrini
相关产品推荐
相关产品推荐

