2

I have some code that they must get fired after the View get completely rendered and drawn, as we know we got onAppear or onChange for a View, they can be useful to know wether the View has been appear or is under change, but they are not useful to be sure that the View has been 100% rendered, for example if we got ForEach, List or Form in the View, you could see the View but because of hierarchy could still ForEach working on View, so that means the View did not completely loaded, it could be get more noticeable if you gave an animation modifier then you could see it as well, So my goal is know how I can be 100% sure my View get rendered with everything inside and it would be no more line code to run for rendering the View?

thanks for reading and helping


Example:

import SwiftUI


struct ContentView: View {

    var body: some View {

        HierarchyView()
            .onAppear() { print("HierarchyView"); print("- - - - - - - - - - -") }

    }

}



struct HierarchyView: View {

    var body: some View {

        ZStack {

            Color
                .blue
                .ignoresSafeArea()
                .onAppear() { print("blue"); print("- - - - - - - - - - -")  }

            VStack {
                
                Text("Top text")
                    .onAppear() { print("Top Text") }
                
                Spacer()
                    .onAppear() { print("Top Spacer") }
                
            }
            .onAppear() { print("Top VStack"); print("- - - - - - - - - - -")  }

            VStack {

                VStack {

                    VStack {

                        ForEach(0..<3, id:\..self) { index in
                            
                            Text(index.description)
                                .onAppear() { print(index.description); if (index == 0) { print("Last of second ForEach!!!") } }
                            
                        }
  
                    }
                    .onAppear() { print("Middle VStack Inside DEEP"); print("- - - - - - - - - - -")  }

                }
                .onAppear() { print("Middle VStack Inside"); print("- - - - - - - - - - -")  }

            }
            .onAppear() { print("Middle VStack Outside"); print("- - - - - - - - - - -")  }
            
            
            
            VStack {
                
                Spacer()
                    .onAppear() { print("Bottom Spacer") }
                
                
                ForEach(0..<3, id:\..self) { index in
                    
                    Text(index.description)
                        .onAppear() { print(index.description); if (index == 0) { print("Last of first ForEach!!!") } }
                    
                }

                
                
                
                Text("Bottom text")
                    .onAppear() { print("Bottom Text") }
                
            }
            .onAppear() { print("Bottom VStack"); print("- - - - - - - - - - -")  }
            
            
        }
        .onAppear() { print("ZStack of HierarchyView"); print("- - - - - - - - - - -") }
        
        
    }
 
}
ios coder
  • 1
  • 4
  • 31
  • 91
  • `.onAppear` of parent is fired after its descendants are initialized and their `body` is computed – New Dev Jan 24 '21 at 21:43
  • I was thinking so, until I observed behaviour of ForEach! the child of parent could render after it's parent in this case child is ForEach, you put in View or VStack or what ever, it will be rendered last! with hierarchy of bottom to top, and last index to first index! And that makes hard to find out the real finished render, on the other hand i do not want put a condition in ForEach to check if this is the index Zero, because it makes the app run heavy, this just about ForEach, there is other possible ways to change what I said like geometry, that is why I want to know, when the View is . . – ios coder Jan 24 '21 at 21:56
  • is completely rendered and it would be no more line code to run. what ever we used in our View, it could be ForEach, geometry or simple Text, I need find out the general way of knowing – ios coder Jan 24 '21 at 22:02
  • I suggest you provide a minimal example of what you mean and where it deviates from your expectations. Or, even, before that - how do you want the animation to work and where it doesn't – New Dev Jan 24 '21 at 22:54
  • It seems you faced the issue before and you know exactly what I am saying, but why you are asking about an example? I do not want limit the answer to an example, as I said the condition of View could result which components of View get first rendered and which part get last! I explained to you that from an example to example would be deferent hierarchy in render process, My question was and is to know the moment of finished product! It means the moment that there is no more line of code to render for View – ios coder Jan 24 '21 at 23:00
  • 1
    I haven't faced your issue before, because I don't fully understand your issue. I told you about when `.onAppear` of parent fires relative to its children. It also depends on what "rendered" mean - generally speaking, a view could "appear", but actually render its content later. There's also lot of under-the-hood things happening with SwiftUI, and a lot of counter-best-practices. Start with a minimal example (or two examples that behave differently) where your expectations deviate from reality to reduce ambiguity, and go from there. – New Dev Jan 24 '21 at 23:08
  • I don't know of a didFinishLoading solution, but if you know the last item to load is going to be the last item in the ForEach, maybe you can put an .onAppear call directly on the last item in the ForEach? – nicksarno Jan 24 '21 at 23:14
  • @nicksarno, thanks nick, actually I am checking last item of ForEach in last item right now! but it expensive! I am looking for cheaper way! do we have didFinishLoading in SwiftUI? – ios coder Jan 24 '21 at 23:18
  • No, I don't think so :( – nicksarno Jan 24 '21 at 23:19
  • @NewDev: I really tried to explain to you what I mean, but I am sorry it was not enough for you! as much as I explain an say that I am looking for general solution not local, you are asking for example, I am looking for general solution, some thing like didFinishLoading – ios coder Jan 24 '21 at 23:20
  • @swiftPunk, any general solution ought to start with solving for a single specific case, then generalizing (if possible) from there. Also, as a general suggestion about stackoverflow - show some code if you want to maximize your chance of getting an answer. Code is much less ambiguous than normal language. So if someone takes their time to answer, they can be more confident that they are solving the real issue. – New Dev Jan 24 '21 at 23:41
  • also, re: "how I can be 100% sure my View get rendered"... how do you imagine it working with something like `LazyVStack`, which doesn't create items until it needs to render on the screen (e.g. when user scrolls) – New Dev Jan 24 '21 at 23:44
  • I get it you are earlier than me here and have more more reputation than me. – ios coder Jan 24 '21 at 23:45
  • @swiftPunk I understand what you say, if you put a `ForEach` in a `VStack`, the `VStack` will *appear* first. But this is not a problem, is it? You can easily use the `onAppear` attached to the root view and *everything* will work as if the view was fully rendered. If you have a concrete example when *it is a real issue*, please add your code. Otherwise it's a purely theoretical question. – pawello2222 Jan 24 '21 at 23:46
  • Ok , I gave you an example but It limits us to this example – ios coder Jan 24 '21 at 23:51
  • @swiftPunk I might not have been precise - can you add an example with a *real issue* where you try to use `onAppear` attached to the root view but you can't because the view is not (in your opinion) rendered? – pawello2222 Jan 25 '21 at 00:02
  • @pawello2222 : that was an example to show you and others that parent View could present it self even before it child get finished rendering! that is the why I am trying to find out the way of general didFinishLoading instead of going putting onAppear on each line to find out the last line of code, – ios coder Jan 25 '21 at 00:08
  • @swiftPunk I was aware of this example, imho it's a theoretical one with no real use. You can just put the code in `onAppear` of the root view and treat the view as fully rendered. – pawello2222 Jan 25 '21 at 00:12
  • @pawello2222 : the all reason that I raised this question it was: that I could not "treat the view as fully rendered", maybe you or others ask why? Why you cannot treat view as fully rendered? Well I would say in app lunch time app trying to build and render Views as soon as possible, when you put a simple animation on your root View, with having a Long range for ForEach you and your app users would see glitchy animation and it brings down all user expectation, even If you use LazyVStack – ios coder Jan 25 '21 at 00:22

2 Answers2

2

There's no general way, AFAIK, to tell when all possible descendant views have "appeared" (i.e. would have fired their onAppear).

Depending on how you think about it, "appeared" is different than "rendered". Generally speaking, each view decides when it renders its children. For example, LazyVStack will only create its elements when they need to be rendered. A custom view conforming to UIViewControllerRepresentable could decide to do whatever it wants.

With that in mind (assuming render == appear), an approach you could take is to "track" those views that you care about having "appeared", and fire a callback when they all did.

You could create a view modifier to keep track of each view "render" status, and use PreferenceKey to collect all this data:

struct RenderedPreferenceKey: PreferenceKey {
    static var defaultValue: Int = 0
    static func reduce(value: inout Int, nextValue: () -> Int) {
        value = value + nextValue() // sum all those remain to-be-rendered
    }
}

struct MarkRender: ViewModifier {
    @State private var toBeRendered = 1
    func body(content: Content) -> some View {
        content
            .preference(key: RenderedPreferenceKey.self, value: toBeRendered)
            .onAppear { toBeRendered = 0 }
    }
}

Then, create convenience methods on View to simplify its usage:

extension View {
    func trackRendering() -> some View {
        self.modifier(MarkRender())
    }

    func onRendered(_ perform: @escaping () -> Void) -> some View {
        self.onPreferenceChange(RenderedPreferenceKey.self) { toBeRendered in
           // invoke the callback only when all tracked have been set to 0,
           // which happens when all of their .onAppear are called
           if toBeRendered == 0 { perform() }
        }
    }
}

Usage would be:

VStack {
    ForEach(0..<3, id:\..self) { index in
        Text("\(index)").trackRendering()
    }
}
.onRendered {
    print("Rendered")
}
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • @swiftPunk, can you elaborate on the issue? (I fixed a minor copy-paste typo) – New Dev Jan 25 '21 at 14:36
  • 1
    Great solution. I adapted it to LazyVGrid to get it scrolled to a specific item once the grid completes rendering its content. It is useful when LazyVGrid re-builds its items multiple times. In that case LazyVGrid's .onAppear modifier cannot be used more than once. Please see https://stackoverflow.com/a/70517757/1249407. – Alexander Poleschuk Dec 29 '21 at 10:09
0

It may not relate entirely to OP's question, but I'm leaving this here in case it helps someone else. I was facing an issue where onAppear() would get called without the view actually rendering. The view not rendering was the correct and desired behavior, but the fact that onAppear() got triggered was not.

I found out this post while investigating my own issue since I was looking for a way to perform an action after my view first rendered. I emphasize the after because that's where the problem lied for me. You see, Apple's documentation states that onAppear() adds an action to perform before this view appears. But in my case, it did trigger before the apparition of the view, except that the view never actually appeared after it triggered, which again was the expected behavior in my case.

My solution turned out to be inspired by New Dev's accepted answer but without the use of a PreferenceKey. I tried to keep it as simple as possible, and the result works great for me:

extension View {
    /// Adds an action to perform after this view appears for the first time.
    /// 
    /// - Parameter action: The action to perform.
    /// - Returns: A view that triggers `action` after it appears.
    func afterFirstRender(perform action: @escaping () -> Void) -> some View {
        self.modifier(FirstRenderTracker(action: action))
    }
}

// ViewModifier supporting the `afterFirstRender()` extension
struct FirstRenderTracker: ViewModifier {
    @State private var isRendered = false
    let action: () -> Void
    
    func body(content: Content) -> some View {
        content
            .onAppear {
                isRendered = true
            }
            .onDisappear {
                isRendered = false
            }
            .onChange(of: isRendered) { isRendered in
                if isRendered {
                    action()
                }
            }
    }
}

And it would be used this way:

VStack {
    ...
}
.afterFirstRender {
    print("First render")
}

Using this extension, I no longer have the problem where the code in onAppear() would trigger for no reason when the view wouldn't actually render.

Now I'm still not really sure why this works or why onAppear() was triggered at all in my case, but it does the job for my specific case.

Paul-Etienne
  • 796
  • 9
  • 23