I am implementing a horizontal ScrollView
in SwiftUI to permit customers to scroll via an inventory of days (dayPickerView
). In Xcode’s Canvas, every thing works as anticipated, and scrolling is strictly horizontal. Nevertheless, when working the app on an actual gadget, the scrollable space may be moved barely in all instructions (up, down, and even diagonally).
This sudden habits generally even triggers the pull-to-refresh gesture, making the UI really feel glitchy.
Right here’s a GIF displaying 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(shade: 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", function: .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 {
Process { @MainActor in
await viewModel.fetchData(forceRefresh: false)
}
}
// Подписываемся на уведомление о добавлении/обновлении/удалении записи
NotificationCenter.default.addObserver(
forName: .newSleepRecordAdded,
object: nil,
queue: .predominant
) { _ in
self.refreshData()
}
}
.onDisappear {
// Отписываемся при исчезновении представления
NotificationCenter.default.removeObserver(self, title: .newSleepRecordAdded, object: nil)
}
.refreshable {
// Сбрасываем кэш для режима, который сейчас не отображается
if viewModel.viewMode == .day by day {
viewModel.weeklyData = [] // Сбрасываем недельные данные
} else {
viewModel.dailyData = [] // Сбрасываем дневные данные
}
await viewModel.fetchData(forceRefresh: true)
await MainActor.run {
lastUpdateTime = Date().timeIntervalSince1970
}
}
}
// Customized header part
non-public 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 day by day mode
if viewModel.viewMode == .day by day {
dayPickerView
.padding(.prime, 20)
.padding(.backside, 20)
}
}
.background(Shade("Background"))
}
// Day picker part with horizontal scrolling
non-public var dayPickerView: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
// Present at this time's day first, adopted by the 6 earlier days
// Kind an array with damaging offsets (0 - at this time, -1 - yesterday, and so forth.)
ForEach(0..<7, id: .self) { index in
let offset = -index // Convert the index to a damaging 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)
}
}