2

Very simple code! When i scroll down the list, show onAppear(same as task) from 0 to 24, last few items(25-29) does't show. why?

I did some logic depending on this, and succeed with old version swiftui. but now, fail in latest version.

I think maybe for item reusing, but all these things should be hide for developer, We want something that is logical and intuitive.

why? any suggestion?

struct ListTest: View {
    @State private var nums: [Int] = Array(0..<30)
    
    var body: some View {
        VStack {
            List(nums, id: \.self) { i in
                Label("\(i)", systemImage: "\(i).circle")
                    .task {
                        print("task \(i)")
                    }
                    .onAppear {
                        print("onAppear \(i)")
                    }
                    .onDisappear {
                        print("onDisappear \(i)")
                    }
            }
        }
    }
}

logs:

onAppear 11
onAppear 10
onAppear 9
onAppear 8
onAppear 7
onAppear 6
onAppear 5
onAppear 4
onAppear 3
onAppear 2
onAppear 1
onAppear 0
task 11
task 10
task 9
task 8
task 7
task 6
task 5
task 4
task 3
task 2
task 1
task 0
onAppear 12
task 12
onDisappear 0
onAppear 13
task 13
onDisappear 1
onAppear 14
task 14
onDisappear 2
onAppear 15
task 15
onDisappear 3
onAppear 16
task 16
onDisappear 4
onAppear 17
task 17
onDisappear 5
onAppear 18
task 18
onDisappear 6
onAppear 19
task 19
onDisappear 7
onAppear 20
task 20
onDisappear 8
onAppear 21
task 21
onDisappear 9
onAppear 22
task 22
onDisappear 10
onAppear 23
task 23
onDisappear 11
onAppear 24
task 24
onDisappear 12
onDisappear 13
onDisappear 14
onDisappear 15
onDisappear 16
onDisappear 17
onDisappear 18
onDisappear 19
onDisappear 20
foolbear
  • 726
  • 1
  • 7
  • 19

3 Answers3

0

Update: I'm no longer experiencing this issue in Xcode 14.3 and the iOS 16.4 simulator. It may have been fixed earlier though. Log:

onAppear 17
onAppear 16
onAppear 15
onAppear 14
onAppear 13
onAppear 12
onAppear 11
onAppear 10
onAppear 9
onAppear 8
onAppear 7
onAppear 6
onAppear 5
onAppear 4
onAppear 3
onAppear 2
onAppear 1
onAppear 0
task 17
task 16
task 15
task 14
task 13
task 12
task 11
task 10
task 9
task 8
task 7
task 6
task 5
task 4
task 3
task 2
task 1
task 0
onAppear 18
task 18
onAppear 19
task 19
onAppear 20
task 20
onDisappear 0
onAppear 21
task 21
onDisappear 1
onDisappear 2
onAppear 22
task 22
onAppear 23
task 23
onDisappear 3
onAppear 24
task 24
onDisappear 4
onAppear 25
task 25
onDisappear 5
onAppear 26
task 26
onDisappear 6
onAppear 27
task 27
onDisappear 7
onAppear 28
task 28
onDisappear 8
onAppear 29
task 29
onDisappear 9
onDisappear 10

Original answer: List uses UICollectionView (UITableView pre-iOS 16) under the hood, which UIKit developers know will reuse the cell UIViews. .onAppear is connected with UIViews, so all the cells only appear once because as you scroll the cells just move around the screen.

Apple could fix this by connecting .onAppear up to the collection view delegate's willStartDisplayingCell.

malhal
  • 26,330
  • 7
  • 115
  • 133
0

The post I linked got me thinking that label (since it's not changing) may be cached. How to prevent it from caching? well, what if we provided a container of some sort that has to be rebuilt, and hence it's contents have to be rebuilt, and hence the onAppear will be called.

I thought it would require the container to change somehow on each redraw (e.g. with id), but surprisingly it appears that even just having a container is already enough to cause onAppear to work:

struct ListTest: View {
    @State private var nums: [Int] = Array(0..<30)
    
    var body: some View {
        VStack {
            List(nums, id: \.self) { i in
                ZStack { // <-- this is the container
                    Label("\(i)", systemImage: "\(i).circle")
                        .task {
                            print("task \(i)")
                        }
                        .onAppear {
                            print("onAppear \(i)")
                        }
                        .onDisappear {
                            print("onDisappear \(i)")
                        }
                }
            }
        }
    }
}

Running the above on Playground shows:

...
onAppear 24
task 24
onDisappear 12
onDisappear 13
onAppear 25
task 25
onAppear 26
task 26
onDisappear 14
onAppear 27
task 27
onDisappear 15
onAppear 28
task 28
onDisappear 16
onAppear 29
task 29
onDisappear 17
onDisappear 18
onDisappear 19

So I guess this is a workaround for now. Note that I chose ZStack, but you could use HStack or VStack.

Having said that, I would rather switch from List to ScrollView { LazyVStack {. Yes, you will need to do a few things to make it look good with the spacing and divider, but it's giving you a very predictable appear/disappear behavior:

struct ListTest: View {
    @State private var nums: [Int] = Array(0..<30)
    
    var body: some View {
        VStack {
            ScrollView {
                LazyVStack {
                    ForEach($nums, id: \.self) { i in
                        Label("\(i.wrappedValue)", systemImage: "\(i.wrappedValue).circle")
                            .padding(16) // <-- added padding
                            .task {
                                print("task \(i.wrappedValue)")
                            }
                            .onAppear {
                                print("onAppear \(i.wrappedValue)")
                            }
                            .onDisappear {
                                print("onDisappear \(i.wrappedValue)")
                            }
                        Divider() // <-- added divider
}}}}}}
timbre timbre
  • 12,648
  • 10
  • 46
  • 77
  • yes, making list item rebuilt can make item onappear act normal. but add a container, why? it works but way? I have no ideal. – foolbear Mar 15 '23 at 03:05
0

Making list item rebuilt can make item onappear act normal. here is my solution:

    @State private var nums: [Int] = Array(0..<30)
    
    var body: some View {
        VStack {
            List(nums, id: \.self) { i in
                ItemView(num: num)
            }
        }
    }
}

struct ItemView: View {
    @State private var show: Bool = true
    let num: Int
    
    init(num: Int) {
        self.num = num
        print("init \(num)")
    }
    
    var body: some View {
        let _ = print("refresh \(num)")
        Group {
            if show { // <--- HERE
                Label("\(num)", systemImage: "\(num).circle")
            } else {
                Text("")
            }
        }
            .onAppear {
                show = true
                print("onAppear \(num)")
            }
            .onDisappear {
                show = false
                print(("onDisappear \(num)"))
            }
    }
}

foolbear
  • 726
  • 1
  • 7
  • 19