Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwifUI onAppear gets called twice

Q1: Why are onAppears called twice?

Q2: Alternatively, where can I make my network call?

I have placed onAppears at a few different place in my code and they are all called twice. Ultimately, I'm trying to make a network call before displaying the next view so if you know of a way to do that without using onAppear, I'm all ears.

I have also tried to place and remove a ForEach inside my Lists and it doesn't change anything.

Xcode 12 Beta 3 -> Target iOs 14 CoreData enabled but not used yet

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")
        }) 
    }
}
like image 517
AvsBest Avatar asked Jul 24 '20 20:07

AvsBest


5 Answers

I had the same exact issue.

What I did was the following:

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

This solves it by making sure only what's inside the the if conditional inside of onLoad() will run once.

Update: Someone on the Apple Developer forums has filed a ticket and Apple is aware of the issue. My solution is a temporary hack until Apple addresses the problem.

like image 60
Justin Cabral Avatar answered Nov 27 '22 07:11

Justin Cabral


I've been using something like this

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

and I use it instead of .onAppear()

 .onFirstAppear{
   self.vm.fetchData()
 }
like image 42
TapulaRasa Avatar answered Nov 27 '22 06:11

TapulaRasa


you can create a bool variable to check if first appear

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
    })
  }
}
like image 35
vdotup Avatar answered Nov 27 '22 07:11

vdotup


You can create the first appear function for this bug

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()
        }
    }
}
like image 30
Calvin Chang Avatar answered Nov 27 '22 08:11

Calvin Chang


For everyone still having this issue and using a NavigationView. Add this line to the root NavigationView() and it should fix the problem.

.navigationViewStyle(StackNavigationViewStyle())

From everything I have tried, this is the only thing that worked.

like image 22
cseh_17 Avatar answered Nov 27 '22 07:11

cseh_17