You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
                    }
                }
            ...
        }
    }
}

问题原因

  1. 导航结构不规范:NavigationLink直接放在body根层级,未嵌套在NavigationStack(或iOS15及以下的NavigationView)中,违反SwiftUI导航的基本规则
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 05:06:25