3

Thanks for taking your time to help others :)

And for instance, I already checked all the solutions of this post, but none worked for us.

Posted also at Apple Developer, I publish here also hoping for better luck. Thanks!

Bug description:

As we are building a chat, we need messages to display in the reverse order of a List/ ScrollView has. That's why we are putting everything upsideDown.

On iOS 15 and iOS 16 (not in 14), there is an strange behaviour (seems a bug) when showing contextMenu on a reversed List/ScrollView.

Depending on the method you use, it will even disappear. Let's dive in.

Some considerations:

The point of reversing the UI is that chat like list, shows at the bottom the last message, and scrolls to the top, not the normal (opposite) behaviour).

  • We did try to reverse the array, scroll to bottom on appear, but leads to so many problems with scrollTo on iOS 14...not so good performance...
  • If we are trying to do reversing the UI is because we've seen it's much efficient but also because there are no extra bugs (apart from the one exposed in this post, obviously)

Code simple demo to show what happens.

import SwiftUI

struct ContentView: View {
    var body: some View {
        ScrollView { // It also happens using List (instead ScrollView + LazyVStack)
            LazyVStack {
                ForEach(1..<101, id: \.self) { msg in
                    messageView(msg)
                        .upsideDown() // Upside down each element, you can try .upsideDownBis()
                }
            }
        }
        .upsideDown() // Upside down entire ScrollView, you can try .upsideDownBis()
    }

    private func messageView(_ msg: Int) -> some View {
        Text("Msg no. \(msg)")
            .padding()
            .background(Color.red.cornerRadius(8))
            .contextMenu {
                    Button(action: {
                        print("A")
                    }) {
                        Text("Button A")
                    }

                    Button(action: {
                        print("B")
                    }) {
                        Text("Button B")
                    }
            }
    }
}

// Just for setting upside down list/ ScrollView
extension View {
    func upsideDown() -> some View {
        self
            .rotationEffect(Angle(radians: .pi))
            .scaleEffect(x: -1, y: 1, anchor: .center)
    }

    // This one just rotates, does not disappear.
    func upsideDownBis() -> some View {
        self
            .rotationEffect(Angle(radians: .pi))
    }
}

GIF resources of this behaviour on iOS 15 and 16:

iOS 15/16 when applying upsideDown() modifier -> Bug:

iOS 15/16, upsideDown() modifier -> Bug

iOS 15/16 when applying upsideDownBis() modifier -> Undesired animation: iOS 15/16, upsideDownBis() modifier -> Undesired animation

What we have checked?

As mentioned before, every solution posted at this post and some others. The ideas apart from those have been:

  1. As scaleEffect is really causing the worst part of the bug, I did try to set x: -1 to x: 1, when performing a contextMenu gesture... But seems faster than longPressGesture "recogniser".
  2. Similar to 1. but on upsideDownBis() trying to avoid the animation, setting it to nil, nor making the modifier not to apply in that case... So, as things stand, I can't recognize a longPressGesture before contextMenu does appear.

Questions

  1. Is there any work around for this? To not have that animation with upsideDownBis() ?
  2. Is there any work around for this? To not have that bug with upsideDown() ?
MglSaRu
  • 115
  • 5
  • Any reason not to reverse the array / list of messages instead of rotating/scaling the view(s)? – DonMag Jan 23 '23 at 18:40
  • Difficults go-to-last button scroll to. But most importantly: It makes ScrollView to load at the top, then scroll to bottom what does not perform so well, as reversing UI. The scrollTo doesn't work great neither, leaving gaps in iOS 14. – MglSaRu Jan 23 '23 at 19:07
  • @MglSaRu That is why I still prefer UIKit over SwiftUI. With UIKit I just reverse the array and no issues at all! – arturdev Jan 23 '23 at 20:05

1 Answers1

2

To create a chat app in SwiftUI, use the following key SwiftUI features:

  • Use a ScrollViewReader to access the desired position on the scroll view.
  • Use a GeometryReader to accurately assess the scroll-area size for the particular device's display size and orientation.
  • Use a Spacer() in a VStack to push the content to the display bottom

The following example, is to provide an efficient SwiftUI-only approach without needing to do the upside-down UI and other suggestions.

The typical chat behavior that is included:

  • the list will scroll up as it grows
  • the oldest text displays on top and newest on bottom
  • the scrolling remains smooth and fast
  • a content menu is included

An example chat app with typical chat scrolling behaviors

Message type has a unique identifier which is needed by the ScrollViewReader:

struct Message: Codable, Hashable, Identifiable {
    var id: UUID
    var text: String
}

The GeometryReader and minHeight are used to force the VStack to fill the available display area. The Spacer() pushes the list to the bottom if the list isn't large enough to fill the display:

struct ChatStyleScrollView<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        GeometryReader { proxy in
            ScrollView(.vertical, showsIndicators: false) {
                VStack {
                    Spacer()
                    content
                }
                .frame(minHeight: proxy.size.height)
            }
        }
    }
}

Your provided messageView() function was modified to use the Message type rather than Int. The ChatStyleScrollView is placed in a ScrollViewReader and uses the Message.id to scroll to the bottom:

struct ContentView: View {
    @State private var messages: [Message] = []
    @State private var text: String = ""
    @State private var targetMessage: Message?
    
    var body: some View {
        VStack(spacing: 0) {
            ScrollViewReader { scrollView in
                ChatStyleScrollView() {
                    ForEach(messages) { msg in
                        messageView(msg)
                    }
                }
                .onChange(of: targetMessage) { msg in
                    if let msg = msg {
                        withAnimation(.default) {
                            scrollView.scrollTo(msg.id)
                        }
                    }
                }
                
                HStack {
                    TextField("Message", text: $text)
                        .frame(height: 44)
                    Button(action: send) { Text("Send") }
                }
                .padding(.horizontal)
            }
        }
    }
    
    private func send() {
        guard !text.isEmpty else { return }
        let msg = Message(id: UUID(), text: text)
        messages.append(msg)
        text = ""
        targetMessage = msg
    }
    
    private func messageView(_ msg: Message) -> some View {
        HStack {
            Text("Msg no. \(msg.text)")
                .padding()
                .background(Color.red.cornerRadius(8))
                .contextMenu {
                    Button(action: {
                        print("A")
                    }) {
                        Text("Button A")
                    }
                    
                    Button(action: {
                        print("B")
                    }) {
                        Text("Button B")
                    }
                }
        }
        .frame(maxWidth: .infinity)
        .id(msg.id)
    }
}

The code is based on Implementing Reverse Scrolling in SwiftUI by Ratnesh Jan.

Marcy
  • 4,611
  • 2
  • 34
  • 52
  • Thanks for your effort! That's quite similar to what we've tried before trying to set everything upside down... But leads into problems like: how do you pull up the view when keyboard appears? With the reversed List it occurs without anything else but not with the normal order. – MglSaRu Jan 25 '23 at 09:50
  • Have you tried using @FocusState to shift the view? Maybe a new question on the keyboard situation is needed. I feel your app will be better using the SwiftUI features more as intended. – Marcy Jan 25 '23 at 16:10
  • Please, check out, the App has to support iOS 14. FocusState is for iOS 15.0+ – MglSaRu Jan 25 '23 at 16:54
  • Then perhaps a version check where iOS 15+ uses @FocusState and iOS 14 uses a solution such as https://www.vbutko.com/articles/how-to-manage-swiftui-focus-state-in-ios14-and-before/. – Marcy Jan 26 '23 at 02:13