SwiftUI onAppear 被调用两次

Posted

技术标签:

【中文标题】SwiftUI onAppear 被调用两次【英文标题】:SwifUI onAppear gets called twice 【发布时间】:2020-11-14 17:57:21 【问题描述】:

Q1:为什么 onAppears 会被调用两次?

Q2:或者,我可以在哪里拨打网络电话?

我在代码中的几个不同位置放置了 onAppears,它们都被调用了两次。最终,我试图在显示下一个视图之前进行网络调用,所以如果你知道不使用 onAppear 的方法,我会全力以赴。

我还尝试在我的列表中放置和删除 ForEach,但它并没有改变任何东西。

Xcode 12 Beta 3 -> 目标 ios 14 CoreData 已启用但尚未使用

struct ChannelListView: View 

@EnvironmentObject var channelStore: ChannelStore
@State private var searchText = ""
@ObservedObject private var networking = Networking()

var body: some View 
    NavigationView 
        VStack 
            SearchBar(text: $searchText)
                .padding(.top, 20)
             
            List() 

                ForEach(channelStore.allChannels)  channel in
                    
                    NavigationLink(destination: VideoListView(channel: channel)
                                    .onAppear(perform: 
                        print("PREVIOUS VIEW ON APPEAR")
                    )) 
                        ChannelRowView(channel: channel)
                    
                
                .listStyle(GroupedListStyle())
            
            .navigationTitle("Channels")
            
        
    


struct VideoListView: View 

@EnvironmentObject var videoStore: VideoStore
@EnvironmentObject var channelStore: ChannelStore
@ObservedObject private var networking = Networking()

var channel: Channel

var body: some View 
    
    List(videoStore.allVideos)  video in
        VideoRowView(video: video)
    
        .onAppear(perform: 
            print("LIST ON APPEAR")
        )
        .navigationTitle("Videos")
        .navigationBarItems(trailing: Button(action: 
            networking.getTopVideos(channelID: channel.channelId)  (videos) in
                var videoIdArray = [String]()
                videoStore.allVideos = videos
                
                for video in videoStore.allVideos 
                    videoIdArray.append(video.videoID)
                
                
                for (index, var video) in videoStore.allVideos.enumerated() 
                    networking.getViewCount(videoID: videoIdArray[index])  (viewCount) in
                        video.viewCount = viewCount
                        videoStore.allVideos[index] = video
                        
                        networking.setVideoThumbnail(video: video)  (image) in
                            video.thumbnailImage = image
                            videoStore.allVideos[index] = video
                        
                    
                
            
        ) 
            Text("Button")
        )
        .onAppear(perform: 
            print("BOTTOM ON APPEAR")
        ) 
    

【问题讨论】:

【参考方案1】:

我也有同样的问题。

我做了以下事情:

struct ContentView: View 

    @State var didAppear = false
    @State var appearCount = 0

    var body: some View  
       Text("Appeared Count: \(appearrCount)"
           .onAppear(perform: onLoad)
    

    func onLoad() 
        if !didAppear 
            appearCount += 1
            //This is where I loaded my coreData information into normal arrays
        
        didAppear = true
    

这通过确保只有 onLoad() 的 if 条件内部的内容才会运行一次来​​解决这个问题。

更新:Apple 开发者论坛上的某个人提出了投诉,Apple 已意识到该问题。在 Apple 解决问题之前,我的解决方案是临时破解。

【讨论】:

是的,我刚刚验证了这在 15b1 中已修复。呃,我希望他们能向后移植这样的东西。 当视图被重新实例化时这不起作用,这就是我的情况【参考方案2】:

你可以创建一个布尔变量来检查是否首先出现

struct VideoListView: View 
  @State var firstAppear: Bool = true

  var body: some View 
    List 
      Text("")
    
    .onAppear(perform: 
      if !self.firstAppear  return 
      print("BOTTOM ON APPEAR")
      self.firstAppear = false
    )
  

【讨论】:

【参考方案3】:

您可以为此错误创建第一个出现函数

extension View 

    /// Fix the SwiftUI bug for onAppear twice in subviews
    /// - Parameters:
    ///   - perform: perform the action when appear
    func onFirstAppear(perform: @escaping () -> Void) -> some View 
        let kAppearAction = "appear_action"
        let queue = OperationQueue.main
        let delayOperation = BlockOperation 
            Thread.sleep(forTimeInterval: 0.001)
        
        let appearOperation = BlockOperation 
            perform()
        
        appearOperation.name = kAppearAction
        appearOperation.addDependency(delayOperation)
        return onAppear 
            if !delayOperation.isFinished, !delayOperation.isExecuting 
                queue.addOperation(delayOperation)
            
            if !appearOperation.isFinished, !appearOperation.isExecuting 
                queue.addOperation(appearOperation)
            
        
        .onDisappear 
            queue.operations
                .first  $0.name == kAppearAction ?
                .cancel()
        
    

【讨论】:

这使我的模拟器崩溃,queue.addOperation(delayOperation) 行出现此错误:*** -[NSOperationQueue addOperation:]: operation is already enqueued on a queue @Jeehut 我更新了答案 :) 但我认为 Apple 正在解决这个问题,自上次 XCode 更新以来,我收到的重复 onAppear 调用减少了【参考方案4】:

我一直在使用类似的东西

   import SwiftUI

    struct OnFirstAppearModifier: ViewModifier 
      let perform:() -> Void
      @State private var firstTime: Bool = true
   
   func body(content: Content) -> some View 
       content
           .onAppear
               if firstTime
                   firstTime = false
                   self.perform()
               
           
      
  


   extension View 
      func onFirstAppear( perform: @escaping () -> Void ) -> some View 
        return self.modifier(OnFirstAppearModifier(perform: perform))
      
   

我用它代替 .onAppear()

 .onFirstAppear
   self.vm.fetchData()
 

【讨论】:

【参考方案5】:

对于仍然遇到此问题并使用 NavigationView 的所有人。将此行添加到根 NavigationView() 应该可以解决问题。

.navigationViewStyle(StackNavigationViewStyle())

从我尝试过的所有方法来看,这是唯一有效的方法。

【讨论】:

这对我不起作用。【参考方案6】:

我们不必在.onAppear(perform) 上这样做 这可以在 View 的 init 上完成

【讨论】:

我将此解决方案与 ListView 和 DetailView 一起使用。在详细视图的初始化中,我正在调用我的演示者以从核心数据中获取数据。到目前为止,我只看到了 init 调用了一次,仍然看到了 onAppear 调用了 2 次。我喜欢这种方法,因为我不必在每个视图中添加任何 didAppear 布尔标志。 这个解决方案很糟糕,但似乎对我来说也最有效,因为我确实需要在每次我的视图再次出现时执行代码,而不仅仅是第一次。否则,每次加载数据时都会重复调用 onAppear。【参考方案7】:

让我们假设您现在正在设计 SwiftUI,并且您的 PM 也是物理学家和哲学家。有一天他告诉你我们应该统一 UIView 和 UIViewController,就像量子力学和相对论一样。 OK,你和你的领导志同道合,投票支持“Simplicity is Tao”,并创建一个名为“View”的原子。现在你说:“视野就是一切,视野就是一切”。这听起来很棒,而且似乎可行。好吧,你提交代码并告诉 PM……。

onAppearonDisAppear 存在于每个视图中,但您真正需要的是页面生命周期回调。如果你像viewDidAppear 一样使用onAppear,那么你会遇到两个问题:

    受父视图影响,子视图会多次重建,导致onAppear被多次调用。 SwiftUI 是封闭源代码,但您应该知道:view = f(view)。所以,onAppear 将运行以返回一个新视图,这就是为什么 onAppear 会被调用两次。

我想告诉你onAppear是对的!你必须改变你的想法。不要在onAppearonDisAppear 中运行生命周期代码!您应该在“行为区域”中运行该代码。例如,在导航到新页面的按钮中。

【讨论】:

你是对的绅士!谢谢我在init()而不是onAppear中制定了我的逻辑。谢谢!【参考方案8】:

如果其他人在我的船上,我现在是这样解决的:

struct ChannelListView: View 

@State private var searchText = ""
@State private var isNavLinkActive: Bool = false
@EnvironmentObject var channelStore: ChannelStore
@ObservedObject private var networking = Networking()

var body: some View 
    NavigationView 
        VStack 
            SearchBar(text: $searchText)
                .padding(.top, 20)
            List(channelStore.allChannels)  channel in
                ZStack 
                    NavigationLink(destination: VideoListView(channel: channel)) 
                        ChannelRowView(channel: channel)
                    
                    HStack 
                        Spacer()
                        Button 
                            isNavLinkActive = true
                            
                            // Place action/network call here
                            
                         label: 
                            Image(systemName: "arrow.right")
                        
                        .foregroundColor(.gray)
                    
                
                .listStyle(GroupedListStyle())
            
            .navigationTitle("Channels")
            
        
    

【讨论】:

【参考方案9】:

我有这个应用程序:

@main
struct StoriesApp: App 
    
    var body: some Scene 
        WindowGroup 
            TabView 
                NavigationView 
                    StoriesView()
                
            
        
    
    

这是我的 StoriesView:

// ISSUE

struct StoriesView: View 
    
    @State var items: [Int] = []
    
    var body: some View 
        List 
            ForEach(items, id: \.self)  id in
                StoryCellView(id: id)
            
        
        .onAppear(perform: onAppear)
    
    
    private func onAppear() 
        ///////////////////////////////////
        // Gets called 2 times on app start <--------
        ///////////////////////////////////
    
    

我通过测量 onAppear() 调用之间的 diff time 解决了这个问题。根据我的观察, onAppear() 的两次调用发生在 0.02 到 0.45 秒之间:

// SOLUTION

struct StoriesView: View 
    
    @State var items: [Int] = []
    
    @State private var didAppearTimeInterval: TimeInterval = 0
    
    var body: some View 
        List 
            ForEach(items, id: \.self)  id in
                StoryCellView(id: id)
            
        
        .onAppear(perform: onAppear)
    
    
    private func onAppear() 
        if Date().timeIntervalSince1970 - didAppearTimeInterval > 0.5 
            ///////////////////////////////////////
            // Gets called only once in 0.5 seconds <-----------
            ///////////////////////////////////////
        
        didAppearTimeInterval = Date().timeIntervalSince1970
    
    

【讨论】:

【参考方案10】:

就我而言,我发现层次结构中的一些视图,.onAppear()(和.onDisappear())只被调用一次,正如预期的那样。我用它来发布我在需要对这些事件采取行动的视图中收听的通知。这是一个严重的黑客攻击,我已经验证该错误已在 iOS 15b1 中修复,但 Apple 确实需要向后移植该修复程序。

【讨论】:

以上是关于SwiftUI onAppear 被调用两次的主要内容,如果未能解决你的问题,请参考以下文章

iOS14.2 SwiftUI PageTabView 会多次调用 ChildView onAppear 方法

SwiftUI onAppear:动画持续时间被忽略

视图模型中的 Firestore 方法未在 SwiftUI onAppear 方法中调用

SwiftUI解决List子项无法正确触发onAppear和onDisappear事件的问题

SwiftUI解决List子项无法正确触发onAppear和onDisappear事件的问题

如何正确处理更新视图中的选取器(SwiftUI)