SwiftUI: Scrolling Sticking when using LazyVGrid inside a Scrollview: - ios

The below code seems to work fine on an iPhone 13 mini but stutters or sticks when scrolling on an iPhone 13, not every time but often. The code for the Calendar is here. What could be causing this? 🤔
struct MasterCalendarWithDay: View {
#Environment(\.calendar) var calendar
#State private var selectedDate: Date?
#State private var selectedTabIndex = 0
private var month: DateInterval {
//calendar.dateInterval(of: .year, for: Date())!
DateInterval(start: Calendar.current.date(byAdding: .month, value: show6Months ? -6 : -1, to: Date())!, end: Date())
}
var body: some View {
LoadingView(isShowing: $isLoadingRecoveryData, text: show6Months ? "Loading past 6 months of data. This may take a few seconds." : "Loading...") {
NavigationView {
ScrollView {
ScrollViewReader { value in
VStack {
ZStack {
CalendarView(interval: month) { date in
if date <= Date() {
Button(action: { self.selectedDate = date }) {
//Omitted
}.buttonStyle(PlainButtonStyle())
}
} // end of calender
.onAppear {
value.scrollTo(Date().startOfDay())
}
} //end of Zstack
}
}
} //end of scroll view
.coordinateSpace(name: "pullToRefreshInRecoveryCalendar")
.navigate(using: $selectedDate, destination: makeDestination)
}
.navigationViewStyle(.stack) //This asks SwiftUI to only show one view at a time, regardless of what device or orientation is being used, and prevents constraint warnings in console log. From: https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui
.onChange(of: selectedTabIndex, perform: { value in
})
}
}

I think your ScrollView should be nested inside the ScrollViewReader.
var body: some View {
ScrollViewReader { value in
ScrollView {
...
See
https://developer.apple.com/documentation/swiftui/scrollviewreader

Related

Detect when user taps out of or saves on DatePicker

I'm trying to fetch some data on a network call when a user selects a time from DatePicker.
I originally tested using .onChange but this fires every time that changes, and not on the final tap off.
DatePicker("Title", selection: $currentDate, displayedComponents: .hourAndMinute)
.onChange(of: $currentDate) { value in
print(value)
}
I also tried using the didset{} but that also fired on every change too.
#Published var currentDate: Date = Date() {
didSet { print(currentDate) }
}
What I want to do is once the user selects a time, I fire off some functions. I wanted to not do it every cycle in the wheel.
Is there a way to do this in SwiftUI or UIKit importing into SwiftUI?
Please see attached of what I'm looking at:
You could try rolling your own DatePickerView, such as this approach:
struct ContentView: View {
#State private var birthdate = Date()
var body: some View {
DatePickerView(date: $birthdate)
}
}
struct DatePickerView: View {
#Binding var date: Date
#State var hasChanged = false
var body: some View {
ZStack {
Color.white.opacity(0.01).ignoresSafeArea()
.onTapGesture {
if hasChanged {
print("---> do a network call now with: \(date)")
hasChanged = false
}
}
DatePicker("Birth Date", selection: $date, displayedComponents: .hourAndMinute)
.pickerStyle(.wheel)
.onChange(of: date) { val in
hasChanged = true
}
}
}
}

Wrong selection with LazyHGrid SwiftUI

Hi all I have a problem with selecting cells in a LazyHGrid using SwiftUI
In the TimePickerGridView.swift I create a horizontal list with times that the user can select. To manage cell selection I use #State private var selectedItem: Int = 0
Everything seems to work but I have some problems when I save the user selection to create a date which contains the selected times
When the user selects a cell, it immediately updates a date by setting the hours and minutes.
The date being modified is #Binding var date: Date, this date refers to a RiservationViewModel.swift which is located in the RiservationView structure
struct ReservationsView: View {
#StateObject var viewModels = ReservationViewModel()
var body: some View {
VStack {
TimePickerGridView(date: $viewModels.selectedDate)
}
}
}
Now the problem is that when the user selects the time and creates the date the LazyHGrid continuously loses the selection and has to select the same cell more than once to get the correct selection again ...
At this point the variable date is observed because it can change thanks to another view that contains a calendar where the user selects the day.
How can I solve this problem? where is my error?
private extension Date {
var hour: Int { Calendar.current.component(.hour, from: self) }
var minute: Int { Calendar.current.component(.minute, from: self) }
var nextReservationTime: TimePicker {
let nextHalfHour = self.minute < 30 ? self.hour : (self.hour + 1) % 24
let nextHalfMinute = self.minute < 30 ? 30 : 0
return TimePicker(hour: nextHalfHour, minute: nextHalfMinute)
}
}
struct TimePickerGridView: View {
#Binding var date: Date
#State private var selectedItem: Int = 0
#State private var showNoticeView: Bool = false
private var items: [TimePicker] = TimePicker.items
private func setTimeForDate(_ time: (Int, Int)) {
guard let newDate = Calendar.current.date(bySettingHour: time.0, minute: time.1, second: 0, of: date) else { return }
date = newDate
}
private var startIndex: Int {
// se trova un orario tra quelli della lista seleziona l'index appropriato
// altrimenti seleziona sempre l'index 0
items.firstIndex(of: date.nextReservationTime) ?? 0
}
init(date: Binding<Date>) {
// Date = ReservationViewModel.date
self._date = date
}
var body: some View {
VStack(spacing: 20) {
HStack {
Spacer()
TitleView("orario")
VStack {
Divider()
.frame(width: screen.width * 0.2)
}
Button(action: {}) {
Text("RESET")
.font(.subheadline.weight(.semibold))
.foregroundColor(date.isToday() ? .gray : .bronze)
.padding(.vertical, 5)
.padding(.horizontal)
.background(.thinMaterial)
.clipShape(Capsule())
}
Spacer()
}
ZStack {
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { reader in
LazyHGrid(rows: Array(repeating: GridItem(.fixed(60), spacing: 0), count: 2), spacing: 0) {
ForEach(items.indices) { item in
TimePickerGridCellView(date: $date, selectedItem: $selectedItem, picker: items[item], selection: selectedItem == items.indices[item])
.frame(width: (UIScreen.main.bounds.width - horizontalDefaultPadding * 2) / 4)
.onTapGesture {
setTimeForDate((items[item].hour, items[item].minute))
selectedItem = item
}
.onChange(of: date) { _ in
selectedItem = startIndex
}
.onAppear(perform: {
selectedItem = startIndex
})
}
}
.background(Divider())
}
}
.frame(height: showNoticeView ? 70 : 130)
}
}
}
}

Dismiss SwiftUI DatePicker when user taps on already selected date

I want the user to select a date in the past by presenting a SwiftUI DatePicker. The date binding is set to today's date.
Now, I want the user to be able to select today's date. But when the user taps on the already selected date, the DatePicker is not dismissed. It only is dismissed when the user taps on a date that was not already selected. So in my case I want to dismiss the DatePicker when the user taps on 13.
Is there a way to dismiss the DatePicker when the user taps on the preselected date?
Here is how I use the DatePicker:
struct DatePickerView: View {
#Binding var date: Date
var body: some View {
DatePicker(
"Select Date",
selection: $date,
in: ...Date(),
displayedComponents: [.date]
)
.datePickerStyle(.graphical)
.background(Color.white)
.cornerRadius(8)
.padding(.horizontal, 40)
}
}
I had a similar problem and put a .graphical DatePicker in my own popover. The only downside is on iPhone popovers currently show as sheets which is an annoying inconsistency. Now you can control the popover presentation however you like and even have cancel/done actions.
struct DatePickerPopover: View {
#State var showingPicker = false
#State var oldDate = Date()
#Binding var date: Date
let doneAction: () -> ()
var body: some View {
Text(date, format:.dateTime.year())
.foregroundColor(.accentColor)
.onTapGesture {
showingPicker.toggle()
}
.popover(isPresented: $showingPicker, attachmentAnchor: .point(.center)) {
NavigationStack {
DatePicker(selection: $date
, displayedComponents: [.date]){
}
.datePickerStyle(.graphical)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
date = oldDate
showingPicker = false
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
doneAction()
showingPicker = false
}
}
}
}
}
.onAppear {
oldDate = date
}
}
}

How to automatically collapse DatePicker in a form when other field is being edited?

I got a simple SwiftUI view:
import SwiftUI
struct AddItemView: View {
#State private var title = ""
#State private var date = Date()
var body: some View {
Form {
Section {
TextField("Title", text: $title)
DatePicker(
selection: $date,
in: Date()...,
displayedComponents: .date,
label: { Text("Date") }
)
}
}
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
I am trying to achieve the following:
If DatePicker is expanded (user tapped date picker, picker showing wheel to select date) and then starts typing text in TextField, DatePicker should automatically switch to initial, minimized mode (just showing label and selected date). Please take a look at screenshot. This is a behaviour in a stock Calendar.app, for example, when creating events.
Any help appreciated, thank you.
Here is possible approach. The idea is to reset DatePicker component for each of events result in editing.
Tested with Xcode 11.4 / iOS 13.4
struct AddItemView: View {
#State private var title = ""
#State private var date = Date()
#State private var pickerReset = UUID()
var body: some View {
Form {
Section {
TitleTextField()
DatePicker(
selection: $date,
in: Date()...,
displayedComponents: .date,
label: { Text("Date") }
).id(self.pickerReset)
}
}
}
private func TitleTextField() -> some View {
let boundText = Binding<String>(
get: { self.title },
set: { self.title = $0; self.pickerReset = UUID() }
)
return TextField("Title", text: boundText, onEditingChanged: { editing in
if editing {
self.pickerReset = UUID()
}
})
}
}

Change selected date format from DatePicker SwiftUI

Is there a way to format the date?(from the position indicated by the arrow in the picture) I know it is formatted based on the locale but is there a way to format it myself?
struct ContentView: View {
#State private var selectedDate = Date()
var body: some View {
Form {
DatePicker(selection: $selectedDate, in: ...Date(), displayedComponents: .date) {
Text("From*")
}
}
}
}
The only way I could figure to accomplish this is to create my own custom DatePicker view and use onAppear on the TextField to update an #State selectedDateText: String variable for displaying in the TextField. This feels like a hack and I’m almost embarrassed to post it but it works. I’m new at Swift and iOS programming in general so I’m sure someone will come along with a better answer so I’ll offer this for what it’s worth. My custom view is something like this:
struct CustomDatePicker: View {
#Binding var date: Date
#State private var showPicker: Bool = false
#State private var selectedDateText: String = "Date"
private var selectedDate: Binding<Date> {
Binding<Date>(get: { self.date}, set : {
self.date = $0
self.setDateString()
})
} // This private var I found… somewhere. I wish I could remember where
// To take the selected date and store it as a string for the text field
private func setDateString() {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM dd, yyyy"
self.selectedDateText = formatter.string(from: self.date)
}
var body: some View {
VStack {
HStack {
Text("Date:")
.frame(alignment: .leading)
TextField("", text: $selectedDateText)
.onAppear() {
self.setDateString()
}
.disabled(true)
.onTapGesture {
self.showPicker.toggle()
}
.multilineTextAlignment(.trailing)
}
if showPicker {
DatePicker("", selection: selectedDate,
displayedComponents: .date)
.datePickerStyle(WheelDatePickerStyle())
.labelsHidden()
}
}
}
}
EDIT: I figured out where I got the private var code. It was from this post: How to detect a value change of a Datepicker using SwiftUI and Combine?

Resources