xcodelistviewswiftuiscrollscrollviewreader

How do I cause my List to scroll to show the last line in the backing Array of String using SwiftUI under macOS


I've tried several solutions that I've found but none work for me, mostly lengthy with compile errors that I didn't understand how to fix. I collaborated with an AI today and after the third iteration edited this compact solution that compiled - but the List still does not scroll automatically. I'm very, very new at Swift.

The backing Array of String is updated by a published and observableObject:

class Logger: ObservableObject {
    
    @Published var messageArray = ["Start by selecting the project's data folder."]
    
    func addMessage(_ msg: String) {
        messageArray.append("\(timeNow()) *  \(msg)")
    }

I create an instance in my main (and only) ContentView

@ObservedObject var logger = Logger()

The code below is in a VStack and updates but does not scroll when the messageArray is changed by some other code. It is for macOS not IOS

    List(logger.messageArray, id: \.self) {
        message in
        Text(message)
    }
    .padding()
    .background(ScrollViewReader { proxy in
        Color.clear.onAppear {
            proxy.scrollTo(logger.messageArray.last)
        }
        .onReceive(logger.$messageArray) { _ in
            withAnimation {
                proxy.scrollTo(logger.messageArray.last)
            }
        }
    })

I hope you can help me with this as I've spent way too much time on it.


Solution

  • You could try this approach, to scroll to the last line of the list. It uses the important id and .onChange() and has the ScrollViewReader wrapping the List. Works for me.

    class Logger: ObservableObject {
        
        @Published var messageArray = ["message"]
        
        func addMessage(_ msg: String) {
            messageArray.append("\(UUID().uuidString.prefix(5)) *  \(msg)") // <-- for testing
        }
        
    }
    
    struct ContentView: View {
        @StateObject var logger = Logger() // <-- here
        
        var body: some View {
            ScrollViewReader { proxy in
                List (logger.messageArray, id: \.self) { message in
                    Text(message).id(message) // <-- here, need id
                }
                .onChange(of: logger.messageArray) { _ in    // <-- here
                    withAnimation {
                        proxy.scrollTo(logger.messageArray.last)
                    }
                }
                .padding()
            }
            Button("Add message") {
                logger.addMessage("test message")  // <-- for testing
            }
        }
        
    }