SwiftUI Список автоматически прокручивается вверх при загрузке данных

Вопрос или проблема

Я сталкиваюсь с двумя проблемами с List в SwiftUI, и я прикрепил GIF и изображение для справки:

Автоматическая прокрутка вверх при обновлении данных:
Каждый раз, когда я обновляю свои данные (например, перезагружаю или изменяю список), List автоматически прокручивается вверх. Я предпочел бы, чтобы он оставался на текущей позиции прокрутки. Как я могу предотвратить это поведение и сохранить стабильное положение прокрутки?
Действия с проводами затрагивают все элементы:
Я реализовал действия с проводами, используя .swipeActions в своем List. Однако, когда я провожу по любому элементу, действия с проводами, похоже, применяются ко всем строкам в списке, а не только к той, с которой я взаимодействовал. Как я могу ограничить действия с проводами только конкретным элементом, по которому я провожу?
Дополнительная информация:
Я использую List, обернутый в NavigationView.
Данные обновляются динамически, так как они получаются из удаленного API.
Для действий с проводами я использую модификатор .swipeActions.

Загрузка представления


 private var historySection: some View {
        VStack(spacing: 0) {
            if viewModel.isLoading || viewModel.isLoadingMore {
                ForEach(0..<3, id: \.self) { _ in
                    SkeletonHistorySectionView()
                }
            } else {
                ForEach(groupedItems, id: \.key) { date, items in
                    VStack(spacing: 0) {
                        Text(viewModel.dateFormatter.string(from: date))
                            .font(.footnote)
                            .foregroundColor(.gray)
                            .padding(.leading)
                            .frame(maxWidth: .infinity)
                            .padding(.top)

                        ForEach(items) { item in
                            HistoryRow(item: item, dateFormatter: viewModel.dateFormatter, timeFormatter: viewModel.timeFormatter, categoryDictionary: categoryDictionary, accountDictionary: accountDictionary, viewModel: viewModel)
                                .onTapGesture {
                                    selectedItem = item
                                    showingDetailSheet.toggle()
                                }
                                .swipeActions {
                                    Button {
                                       //ТЕСТ
                                    } label: {
                                        Label("ТЕСТ.", systemImage: "pencil")
                                    }
                                    .tint(.accentColor)
                                }
                        }
                        .padding(.bottom, 8)
                    }
                }
            }
        }
    }

struct HistoryRow: View {
    let item: HistoryItem
    let dateFormatter: DateFormatter
    let timeFormatter: DateFormatter
    let categoryDictionary: [Int: Category]
    let accountDictionary: [Int: Account]
    
    @ObservedObject var viewModel: HistoryViewModel
    
    var body: some View {
        HStack {
            Image(systemName: item.iconName)
                .resizable()
                .scaledToFit()
                .frame(width: 35, height: 35)
                .foregroundColor(.accentColor)

            VStack(alignment: .leading) {
                Text(item.title)
                    .font(.headline)
                Text(mainAccountName(for: item.accountId ?? 0))
                    .font(.subheadline)
                    .foregroundColor(.gray)
            }
            Spacer()
            Text(formattedAmount)
                .font(.headline)
                .foregroundColor(item.type == "expense" ? .red : .green)
        }
        .contentShape(Rectangle())
        .padding(.top)
        .padding(.bottom)
    }
    
    func mainAccountName(for accountId: Int) -> String {
        guard let account = accountDictionary[accountId] else { return "Не указан" }
        return account.name
    }
    
    private var formattedAmount: String {
        let amount = item.amount
        return NumberFormatterHelper.formatNumberWithSign(item.type == "expense" ? -amount : amount, currency: viewModel.selectedCurrency)
    }
}



func loadMoreHistoryData(reset: Bool = false, reportMonth: String, reportCategoryId: Int = 0) async {
        if isLoadingMore { return }

        isLoadingMore = true
        defer { isLoadingMore = false }

        if reset {
            currentPage = 1
            historyItems = []
            isAllDataLoaded = false
        }

        do {
            let (newItems, totalIncome, totalExpenses) = try await dataService.getRecentTransactions(page: currentPage, size: pageSize, reportMonth: reportMonth, reportCategoryId: reportCategoryId)
            
            // Сохраняем статистику
            self.totalIncome = totalIncome
            self.totalExpenses = totalExpenses
            
            DispatchQueue.main.async {
                self.historyItems.append(contentsOf: newItems)
                self.currentPage += 1
                if newItems.count < self.pageSize {
                    self.isAllDataLoaded = true
                }
            }
        } catch {
            DispatchQueue.main.async {
                self.errorMessage = error.localizedDescription
            }
        }
    }


 func loadMoreHistoryForCategory(reportMonth: String, selectedCategoryId: Int) async {
        guard !isLoadingMore, !isAllDataLoaded else { return }

        isLoadingMore = true
        defer { isLoadingMore = false }

        do {
            let (newItems, totalIncome, totalExpenses) = try await dataService.getHistoryForCategory(page: currentPage, size: pageSize, reportMonth: reportMonth, reportCategoryId: selectedCategoryId)
            
            // Сохраняем статистику
            self.totalIncome = totalIncome
            self.totalExpenses = totalExpenses
            
            if newItems.count < pageSize {
                isAllDataLoaded = true
            }

            if !newItems.isEmpty {
                historyItems.append(contentsOf: newItems)
                currentPage += 1
            }
        } catch {
            print("Ошибка подгрузки истории для категории \(selectedCategoryId) за месяц \(reportMonth): \(error.localizedDescription)")
        }
    }

 private var historySection: some View {
        VStack(spacing: 0) {
            if viewModel.isLoading || viewModel.isLoadingMore {
                ForEach(0..<3, id: \.self) { _ in
                    SkeletonHistorySectionView()
                }
            } else {
                ForEach(groupedItems, id: \.key) { date, items in
                    VStack(spacing: 0) {
                        Text(viewModel.dateFormatter.string(from: date))
                            .font(.footnote)
                            .foregroundColor(.gray)
                            .padding(.leading)
                            .frame(maxWidth: .infinity)
                            .padding(.top)

                        ForEach(items) { item in
                            HistoryRow(item: item, dateFormatter: viewModel.dateFormatter, timeFormatter: viewModel.timeFormatter, categoryDictionary: categoryDictionary, accountDictionary: accountDictionary, viewModel: viewModel)
                                .onTapGesture {
                                    selectedItem = item
                                    showingDetailSheet.toggle()
                                }
                                .swipeActions {
                                    Button {
                                       //ТЕСТ
                                    } label: {
                                        Label("ТЕСТ.", systemImage: "pencil")
                                    }
                                    .tint(.accentColor)
                                }
                        }
                        .padding(.bottom, 8)
                    }
                }
            }
        }
    }

struct HistoryRow: View {
    let item: HistoryItem
    let dateFormatter: DateFormatter
    let timeFormatter: DateFormatter
    let categoryDictionary: [Int: Category]
    let accountDictionary: [Int: Account]
    
    @ObservedObject var viewModel: HistoryViewModel
    
    var body: some View {
        HStack {
            Image(systemName: item.iconName)
                .resizable()
                .scaledToFit()
                .frame(width: 35, height: 35)
                .foregroundColor(.accentColor)

            VStack(alignment: .leading) {
                Text(item.title)
                    .font(.headline)
                Text(mainAccountName(for: item.accountId ?? 0))
                    .font(.subheadline)
                    .foregroundColor(.gray)
            }
            Spacer()
            Text(formattedAmount)
                .font(.headline)
                .foregroundColor(item.type == "expense" ? .red : .green)
        }
        .contentShape(Rectangle())
        .padding(.top)
        .padding(.bottom)
    }
    
    func mainAccountName(for accountId: Int) -> String {
        guard let account = accountDictionary[accountId] else { return "Не указан" }
        return account.name
    }
    
    private var formattedAmount: String {
        let amount = item.amount
        return NumberFormatterHelper.formatNumberWithSign(item.type == "expense" ? -amount : amount, currency: viewModel.selectedCurrency)
    }
}



func loadMoreHistoryData(reset: Bool = false, reportMonth: String, reportCategoryId: Int = 0) async {
        if isLoadingMore { return }

        isLoadingMore = true
        defer { isLoadingMore = false }

        if reset {
            currentPage = 1
            historyItems = []
            isAllDataLoaded = false
        }

        do {
            let (newItems, totalIncome, totalExpenses) = try await dataService.getRecentTransactions(page: currentPage, size: pageSize, reportMonth: reportMonth, reportCategoryId: reportCategoryId)
            
            // Сохраняем статистику
            self.totalIncome = totalIncome
            self.totalExpenses = totalExpenses
            
            DispatchQueue.main.async {
                self.historyItems.append(contentsOf: newItems)
                self.currentPage += 1
                if newItems.count < self.pageSize {
                    self.isAllDataLoaded = true
                }
            }
        } catch {
            DispatchQueue.main.async {
                self.errorMessage = error.localizedDescription
            }
        }
    }


 func loadMoreHistoryForCategory(reportMonth: String, selectedCategoryId: Int) async {
        guard !isLoadingMore, !isAllDataLoaded else { return }

        isLoadingMore = true
        defer { isLoadingMore = false }

        do {
            let (newItems, totalIncome, totalExpenses) = try await dataService.getHistoryForCategory(page: currentPage, size: pageSize, reportMonth: reportMonth, reportCategoryId: selectedCategoryId)
            
            // Сохраняем статистику
            self.totalIncome = totalIncome
            self.totalExpenses = totalExpenses
            
            if newItems.count < pageSize {
                isAllDataLoaded = true
            }

            if !newItems.isEmpty {
                historyItems.append(contentsOf: newItems)
                currentPage += 1
            }
        } catch {
            print("Ошибка подгрузки истории для категории \(selectedCategoryId) за месяц \(reportMonth): \(error.localizedDescription)")
        }
    }

Ответ или решение

Проблема автопрокрутки списка в SwiftUI при обновлении данных и ограничения действий при свайпе

Ваша ситуация, связанная с использованием компонента List в SwiftUI, поднимает два основных вопроса: автоматическая прокрутка списка к началу после обновления данных и неправильное поведение кнопок на свайпах, которые применяются ко всем элементам списка. Рассмотрим каждый из этих вопросов более детально.

1. Автопрокрутка списка к началу при обновлении данных

Когда вы обновляете данные вашего списка, он возвращается к прокрутке в верхнюю часть. Это может произойти из-за того, как SwiftUI перерисовывает представление списка при изменении состояния данных.

Решение: Использование State и Identifiable

Чтобы сохранить положение прокрутки, необходимо убедиться, что идентификаторы элементов в списке уникальны и не изменяются, когда обновляются данные. В вашем случае это может быть достигнуто с помощью корректного использования id: в ForEach, а также использования @State для управления списком.

Вот пример корректной реализации:

ForEach(viewModel.historyItems, id: \.id) { item in
    HistoryRow(item: item, ...)
}

Также можно использовать ScrollViewReader для сохранения текущей позиции прокрутки и его восстановления. Вот как это выглядит:

ScrollViewReader { scrollView in
    List {
        ForEach(viewModel.historyItems, id: \.id) { item in
            HistoryRow(item: item ...)
        }
    }
    .onAppear {
        // Здесь вы можете управлять сохранением позиции
    }
}

Вы можете запоминать позицию прокрутки при загрузке данных, а затем использовать ее для возврата в нужную точку после обновления состояния.

2. Ограничение действии при свайпе только на текущий элемент

Когда вы применяете модификатор .swipeActions, важно убедиться, что каждое действие относится только к текущему элементу списка, а не ко всем одновременно.

Решение: Передача параметров в swipeActions

Как правило, каждый элемент списка должен иметь свой собственный контекст. Убедитесь, что ваши действия в swipeActions привязаны к конкретному объекту. Например:

ForEach(viewModel.historyItems, id: \.id) { item in
    HistoryRow(item: item ...)
        .swipeActions(edge: .trailing) {
            Button {
                // Действие только для текущего `item`
            } label: {
                Label("Действие", systemImage: "pencil")
            }
            .tint(.accentColor)
        }
}

Следовательно, если каждое действие связано с конкретным объектом, SwiftUI будет корректно обрабатывать свайпы и применять действия только к тем элементам, к которым вы обращаетесь.

Заключение

Обработка обновления данных и правильная реализация действий на свайп являются важными аспектами для создания эффективного пользовательского интерфейса в SwiftUI. Использование уникальных идентификаторов, а также применение ScrollViewReader позволит избежать проблем с прокруткой, а правильное связывание действий с конкретными элементами списка улучшит взаимодействие с пользователем.

Надеюсь, это поможет вам решить ваши проблемы в приложении. Удачи в разработке!

Оцените материал
Добавить комментарий

Капча загружается...