SwifUI onAppear gets called twice

17.6k views Asked by At

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

There are 10 answers

3
Wasim On

We don't have to do it on .onAppear(perform) This can be done on init of View

2
Calvin Chang On

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()
        }
    }
}
0
MarK On

I've got this app:

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

And here is my 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 <--------
        ///////////////////////////////////
    }
    
}

I've resolved the issue by measuring the diff time between onAppear() calls. According to my observations double calls of onAppear() happen between 0.02 and 0.45 seconds:

// 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
    }
    
}
0
TapulaRasa On

I've been using something like this

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()
 }
0
vdotup On

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
    })
  }
}
0
AvsBest On

In case someone else is in my boat, here is how I solved it for now:

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")
            }
        }
    }
}
2
Justin Cabral On

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.

1
cseh_17 On

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.

0
Rick On

In my case, I found that a few views up the hierarchy, .onAppear() (and .onDisappear()) was only being called once, as expected. I used that to post notifications that I listen to down in the views that need to take action on those events. It’s a gross hack, and I’ve verified that the bug is fixed in iOS 15b1, but Apple really needs to backport the fix.

3
Tom Yang On

Let us assume you are now designing a SwiftUI and your PM is also a physicist and philosopher. One day he tells you we should to unify UIView and UIViewController, like Quantum Mechanics and the Theory of Relativity. OK, you are like-minded with your leader, voting for "Simplicity is Tao", and create an atom named "View". Now you say: "View is everything, view is all". That sounds awesome and seems feasible. Well, you commit the code and tell the PM….

onAppear and onDisAppear exists in every view, but what you really need is a Page lifecycle callback. If you use onAppear like viewDidAppear, then you get two problems:

  1. Being influenced by the parent, the child view will rebuild more than one time, causing onAppear to be called many times.
  2. SwiftUI is closed source, but you should know this: view = f(view). So, onAppear will run to return a new View, which is why onAppear is called twice.

I want to tell you onAppear is right! You MUST CHANGE YOUR IDEAS. Don’t run lifecycle code in onAppear and onDisAppear! You should run that code in the "Behavior area". For example, in a button navigating to a new page.