SwifUI onAppear gets called twice
Asked Answered
I

10

36

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")
        }) 
    }
}
Intensify answered 24/7, 2020 at 20:14 Comment(0)
L
18

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

Lance answered 23/11, 2021 at 11:14 Comment(4)
You are right gentleman !!! thanks I made my logic in init() rather than onAppear. Thanks!Nakada
That is exactly what i thought, on SwiftUI views have to be rebuilt all the time, so in short the view = f(view), what i didnt think about was certain things should go to the init (specially hard to think on it when init is created for free on structs). For that i thank you milord!Giffin
This is an amazing and inspiring answer. By letting go of the unpredictable and capricious nature of onAppear{}, I was able to put my code into button handlers. Then I knew exactly when the event should unfold, instead of relying on the mysterious optimizations that make Views appear and disappear. Thanks again for saving my day.Pocketbook
How do you then handle Views which use an EnvironmentObject? I can‘t use init{} there properlyTurgescent
L
17

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()
 }
Longo answered 3/6, 2021 at 6:29 Comment(0)
F
16

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.

Ferminafermion answered 28/7, 2020 at 15:0 Comment(2)
Yup, I just verified that this is fixed in 15b1. Ugh, I wish they'd backport stuff like this.Convalesce
This does not work when the view is re-instantatiated, which is what happens in my caseCrystalcrystalline
H
8

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
    })
  }
}
Huai answered 25/7, 2020 at 0:44 Comment(0)
A
7

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.

Antifederalist answered 24/8, 2021 at 13:6 Comment(2)
This don't work for me.Buzz
Only solution that worked for meSpradlin
B
5

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()
        }
    }
}
Broadbill answered 16/12, 2020 at 15:19 Comment(2)
This makes the simulator crash for me with this error on the queue.addOperation(delayOperation) line: *** -[NSOperationQueue addOperation:]: operation is already enqueued on a queueFurnishing
@Furnishing I updated the answer :) But I think Apple is fixing this issue, since last XCode update, I got fewer duplicate onAppear callsBroadbill
G
2

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

Genealogy answered 28/11, 2020 at 21:29 Comment(3)
I used this solution with a ListView and DetailView. On the init of the detail view, I'm calling my presenter to get data from core data. So far, I've only seen the init called one time and still see the onAppear called 2 times. I like this approach as I don't have to add any didAppear boolean flags in each view.Unintentional
This solution is gross, but seems to work best for me, too, as I do need to execute code each time my view appears again, not just the first time. And otherwise, onAppear is called repeatedly, each time I load my data.Convalesce
Show me the codeBellebelleek
I
1

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")
            }
        }
    }
}
Intensify answered 25/7, 2020 at 16:3 Comment(0)
C
1

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.

Convalesce answered 6/7, 2021 at 1:3 Comment(0)
C
0

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
    }
    
}
Chickadee answered 14/5, 2021 at 12:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.