I am implementing a horizontal ScrollView
in SwiftUI to permit customers to scroll by a listing of days (dayPickerView
). In Xcode’s Canvas, all the pieces works as anticipated, and scrolling is strictly horizontal. Nonetheless, when operating the app on an actual system, the scrollable space could be moved barely in all instructions (up, down, and even diagonally).
This surprising conduct typically even triggers the pull-to-refresh gesture, making the UI really feel glitchy.
Right here’s a GIF exhibiting the difficulty:
Gif
What I attempted:
-
Wrapping
ScrollView
inGeometryReader
to detect offsets. -
Including
.simultaneousGesture(DragGesture())
to restrict motion. -
Utilizing
.contentShape(Rectangle())
to limit interactions.
None of those options labored.
My code:
var physique: some View {
ZStack {
Shade("Background")
.ignoresSafeArea()
VStack(spacing: 0) {
customHeader
// Content material in white card with rounded corners
ZStack {
RoundedRectangle(cornerRadius: 40)
.fill(Shade.white)
.shadow(colour: Shade.black.opacity(0.1), radius: 5, x: 0, y: 0)
ScrollView {
VStack(spacing: 20) {
viewModePicker
if viewModel.isLoading {
loadingView
} else if isDataEmpty {
emptyStateView
} else {
statisticsView
chartView
if hasDataToShow {
recordsListView
}
}
// Add backside padding for higher scrolling expertise
Spacer()
.body(peak: 20)
}
.padding(.backside)
}
.padding(.horizontal, 2) // Small horizontal padding for scroll view
}
.padding(.horizontal, 0)
.padding(.prime, 10)
.padding(.backside, 5)
.edgesIgnoringSafeArea(.backside)
}
}
.navigationBarHidden(true)
.sheet(isPresented: $showingAddRecord) {
NavigationView {
AddSleepRecordView(childId: childId)
}
}
.onChange(of: showingAddRecord) { oldValue, newValue in
if !newValue { // Если форма была закрыта
refreshData()
}
}
.alert("Помилка", isPresented: $showingAlert) {
Button("OK", position: .cancel) {
viewModel.errorMessage = nil
}
} message: {
if let error = viewModel.errorMessage {
Textual content(error)
}
}
.onChange(of: viewModel.errorMessage) { _, newValue in
showingAlert = newValue != nil
}
.onAppear {
let currentTime = Date().timeIntervalSince1970
let shouldRefresh = currentTime - lastUpdateTime > 300 // 5 минут
if shouldRefresh {
refreshData()
} else {
Job { @MainActor in
await viewModel.fetchData(forceRefresh: false)
}
}
// Подписываемся на уведомление о добавлении/обновлении/удалении записи
NotificationCenter.default.addObserver(
forName: .newSleepRecordAdded,
object: nil,
queue: .principal
) { _ in
self.refreshData()
}
}
.onDisappear {
// Отписываемся при исчезновении представления
NotificationCenter.default.removeObserver(self, identify: .newSleepRecordAdded, object: nil)
}
.refreshable {
// Сбрасываем кэш для режима, который сейчас не отображается
if viewModel.viewMode == .each day {
viewModel.weeklyData = [] // Сбрасываем недельные данные
} else {
viewModel.dailyData = [] // Сбрасываем дневные данные
}
await viewModel.fetchData(forceRefresh: true)
await MainActor.run {
lastUpdateTime = Date().timeIntervalSince1970
}
}
}
// Customized header part
personal var customHeader: some View {
VStack(spacing: 0) {
HStack(spacing: 16) {
// Again button
IconButtonCircle(systemName: "chevron.left", model: .main, measurement: .medium, buttonSize: 40) {
presentationMode.wrappedValue.dismiss()
}
Spacer()
// Add report button
TextIconButtonLeft("Додати запис", systemName: "plus.circle.fill", model: .main, measurement: .small) {
showingAddRecord = true
}
.body(maxWidth: 200)
}
.padding(.horizontal)
.padding(.vertical, 12)
// Solely present day selector in each day mode
if viewModel.viewMode == .each day {
dayPickerView
.padding(.prime, 20)
.padding(.backside, 20)
}
}
.background(Shade("Background"))
}
// Day picker part with horizontal scrolling
personal var dayPickerView: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
// Present at this time's day first, adopted by the 6 earlier days
// Type an array with unfavourable offsets (0 - at this time, -1 - yesterday, and so on.)
ForEach(0..<7, id: .self) { index in
let offset = -index // Convert the index to a unfavourable offset
let date = Calendar.present.date(byAdding: .day, worth: offset, to: Date()) ?? Date()
dayButton(for: date)
}
}
.padding(.prime, 10)
.padding(.backside, 10)
.padding(.horizontal, 24)
}
}