20

XCode Version 13.0 beta (13A5155e) & Targeting iOS 14 or 15

My goal is to create a chat view in SwiftUI. This requires creating a ScrollView with content of varying heights .

After extensive debugging, I've determined that if you have views within the ScrollView that do not have a fixed height, it will stutter when you scroll to the top of the view.

––––

PROJECT: Download this project and try for yourself

struct Message: Identifiable {
  let id = UUID()
  var text: String
}

struct ContentView: View {
  @State var items: [Message] = MockData.randomMessages(count: 100)
  
  var body: some View {
    VStack {
      Button("Shuffle items") {
        items = MockData.randomMessages(count: 100)
      }
      ScrollView {
        LazyVStack(spacing: 10) {
          ForEach(items) { item in
            Text(item.text)
              .background(colors.randomElement()!)
          }
        }
      }
    }
  }
}

My conclusion right now is that LazyVStack only works with child views that have fixed height. This issue alone prevents SwiftUI from being production ready.

Has anyone else tackled this?

RESPONSE FROM APPLE (July 27, 2021):

"On your Mac target this all works but I see there are scrolling issues on iOS. This issue is definitely a bug with SwiftUI on iOS. I recommend that rather than rewrite your app you use a UIViewRepresentable for your UIScrollView (or actually UITable / UICollection View would make the most sense here). If you use a re-usable view such as a table or collection these issues will almost certainly go away. You shouldn't need to rewrite your app but you should add a UIViewRepresentable if this issue is preventing a release."

chrysb
  • 1,351
  • 2
  • 13
  • 20
  • `offset` is an unstable identifier. see [this answer](https://stackoverflow.com/a/57411545/5623035). Let me know if it was related. – Mojtaba Hosseini Jul 23 '21 at 21:32
  • This does not resolve the stuttering issue. You're right though, and I've updated the code to reflect a better approach. – chrysb Jul 24 '21 at 00:26
  • Perhaps you can just implement this screen in UIKit instead? SwiftUI is still very new, it will probably take few more years till it gets close to UIKit in terms of reliability and stability. – Leszek Szary Jul 24 '21 at 17:54
  • Yeah, that's the fallback. This is one of the main views of the app — a chat view. To rewrite this whole thing in UIKit will be quite an undertaking, but I was hoping that SwiftUI could pull it off. This is literally the only thing that's stopping it :( The other alternative is I can use a `List` and lose my smooth transitions, use a VStack and never show more than 30 items at once, or rewrite it in UIKit. I'll probably revisit the `List` approach first. – chrysb Jul 24 '21 at 19:33
  • I have pretty much the same issue and the answer from Apple is disappointing. If you go for `List` as I did, you will probably run into this: https://stackoverflow.com/questions/68383332/leaks-in-navigationview-list-foreach-with-dynamically-generated-views -- so test early for leaks. ;) Still on my list to try is use `List` but with a limited amount of items to avoid the leaking. Or try other alternatives such as `CACollectionView`. Please update on your progress! Thanks for sharing! – Nicolas Jul 28 '21 at 17:59
  • Are there any updates here? I'm running into the same issue and it's quite annoying – Antonio Chan Nov 14 '21 at 07:43
  • The behavior seems fine for me on Xcode 13.3 / iOS 15.4 - if you'd probably show a difference of how you expect it to be... or it has just fixed for now. – Asperi May 02 '22 at 17:23
  • @Asperi just tried it, it's still reproducible on iOS 14 – Claudiu Jul 14 '22 at 08:07

3 Answers3

0

In iOS 15 this bug appears to be fixed.

In iOS 14 I would suggest showing the first n items (enough to fill the height of the screen with buffer) in a VStack and the rest in a LazyVStack. I found in most cases this removes the jitter.

ScrollView {
  VStack(spacing: 10) {
    ForEach(items.prefix(10)) { item in
      Text(item.text)
        .background(colors.randomElement()!)
      }
    }

    LazyVStack(spacing: 10) {
      ForEach(items.dropFirst(10)) { item in
        Text(item.text)
          .background(colors.randomElement()!)
        }
      }
    }
  }
}           
ryanlintott
  • 79
  • 1
  • 6
0

For me the jittering was caused by having an onAppear modifier attached to the view within the ForEach (so it runs on every item in the collection). The jittering occurred even if the hook was running in a background thread and even if it wasn't doing anything at all.

Didn't fix it 100%, but my solution was to only add the onAppear modifier to the elements which truly needed it. In my case, I was using onAppear for continuous scroll (pagination). By only adding onAppear to the nth item in the list, was able to greatly reduce the jittering to a small blip when approaching the end of the list.

Here's a good article on how to apply a modifier conditionally.

Not sure this will help anyone who can't yet bump their min version to iOS 15, but hope it does!

akerra
  • 1,017
  • 8
  • 18
-1

works without any problems on macos 12.beta, xcode 13.beta, target ios 15 and macCatalyst. Tested on ios15 devices and macos 12. I also tried using 10000, and that works well. Maybe your issue happens on older ios and macos. You maybe interested in Swift UI overwhelmed by high-frequency @StateObject updates? where code struggles on ios14 but not ios15.

You could try other ways to see if you can improve the performance, such as:

 ForEach(items.indices, id: \.self) { index in
     Text(items[index]).background(colors.randomElement()!)
 }

or

 ForEach(Array(items.enumerated()), id: \.0) { index, item in
     Text(item).background(colors.randomElement()!)
 }
  • Thanks for taking a look! I targetted iOS 15 and it still happens on both the device and the simulator. I have iPhone 11 Pro Max and a pretty fast iMac, so I don't think performance is the issue here. Did you try shuffling the items and dragging down many times? Happens almost every time for me. – chrysb Jul 21 '21 at 06:30