Building the inverted scroll of a messaging app


You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿


Advertisement

Stream's platform empowers developers with the flexibility and scalability they need to build rich conversations and engaging communities

👉 Learn how we can help 👈


Sponsors like Stream really help me grow my content creation, so if you have time please make sure to check out their survey: it’s a direct support to my content creation ☺️


In this article I want to show you a very simple but super powerful trick if you ever need to implement the UI of a chat feature, with an inverted scrolling.

When you want to implement a chat feature, the difficulty is that you need to have an inverted scrolling.

Unlike most scroll views, where the content starts at the top and then users have to scroll down to dig deeper into the content, for a chat feature it works the other way around.

The most recent message will be displayed at the bottom of the screen, and users will need to scroll up if they want to go back into the history of the conversation:

So if you want to implement such a scroll view, your initial approach could be to iterate over the data in reverse order, and then add some code to initially position the scroll view on the most recent message:

This seems to work…

…but let’s see what happens when the user scrolls up into the history of the conversation.

We’re going to need to load more messages, so I’m going to add some code to simulate that:

And now let’s see what happens when we actually start scrolling up:

You can see that as soon as the new data has been added to the array, the scroll immediately jumps to another place in the conversation.

If you don’t see it, look closely how it jumps from 19 to 39.

Of course this is not what we want, and so now we need to write even more code to prevent that from happening.

But I’m sure that you’re starting to see that this situation where we constantly need to add additional code to counterbalance unwanted side effects isn’t ideal and will definitely not scale well.

So let’s get rid of all this extra code and let me show you the trick to implement an inverted scroll view super easily!

First, I’m going to introduce a custom view modifier called FlippedUpsideDown:

As you can see this modifier is super simple: it just flips the content of the view vertically.

Now I’m going to apply this modifier to my scroll view:

And because the scroll view is now upside down, you can see that we are actually getting the inverted scrolling for free!

There’s just one issue: all of the chat messages are also upside down…

So let’s fix this!

As it turns out, it’s surprisingly easy to fix: we just need to also flip upside down each individual ChatMessage that’s contained inside the scroll view.

And that’s it, thanks to this little trick we have now flipped the scroll view vertically while its content still retains its original orientation.

If we try to interact with the scroll view we can see that the scroll indeed starts at the bottom, like we expect, and then when we scroll up we can also see that older messages are nicely loaded and inserted into the scroll view, without any issues.

So remember: if you ever need to implement a scroll view with an inverted scroll, the trick to do it is very simple: flip the scroll view vertically and then flip again each of its child views, and that’s it 👌

I hope you’ve enjoyed this article!

Here’s the code if you want to experiment with it:

import SwiftUI

struct ChatMessage: View {
    
    let text: String
    
    var body: some View {
        HStack {
            Text(text)
                .foregroundStyle(.white)
                .padding()
                .background(.blue)
                .clipShape(
                    RoundedRectangle(cornerRadius: 16)
                )
                .overlay(alignment: .bottomLeading) {
                    Image(systemName: "arrowtriangle.down.fill")
                        .font(.title)
                        .rotationEffect(.degrees(45))
                        .offset(x: -10, y: 10)
                        .foregroundStyle(.blue)
                }
            Spacer()
        }
        .padding(.horizontal)
    }
}

struct BadChatView: View {
    
    @State var data = (0...20).map { $0 }
    
    var body: some View {
        ScrollViewReader { scrollView in
            List(data.reversed(), id: \.self) { int in
                ChatMessage(text: "\(int)")
                    .listRowSeparator(.hidden)
                    .id(int)
                    .onAppear {
                        if int == data.last {
                            loadMoreData()
                        }
                    }
            }
            .listStyle(.plain)
            .onAppear {
                scrollView.scrollTo(data.first!)
            }
        }
    }
    
    func loadMoreData() {
        guard data.count < 40 else { return }
        
        let additionalData = (21...40).map { $0 }
        
        data.append(contentsOf: additionalData)
    }
}

struct GoodChatView: View {
    
    @State var data = (0...20).map { $0 }
    
    var body: some View {
        List(data, id: \.self) { int in
            ChatMessage(text: "\(int)")
                .flippedUpsideDown()
                .listRowSeparator(.hidden)
                .onAppear {
                    if int == data.last {
                        loadMoreData()
                    }
                }
        }
        .listStyle(.plain)
        .flippedUpsideDown()
    }
    
    func loadMoreData() {
        guard data.count < 40 else { return }
        
        let additionalData = (21...40).map { $0 }
        
        data.append(contentsOf: additionalData)
    }
}

struct FlippedUpsideDown: ViewModifier {
    func body(content: Content) -> some View {
        content
            .rotationEffect(.radians(Double.pi))
            .scaleEffect(x: -1, y: 1, anchor: .center)
    }
}

extension View {
    func flippedUpsideDown() -> some View {
        modifier(FlippedUpsideDown())
    }
}
Previous
Previous

AI features in Xcode 16: is it good?

Next
Next

Apple’s official method to design an iOS app 📱