Wrong selection with LazyHGrid SwiftUI - ios

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)
}
}
}
}

Related

Im trying to make a day picker for a calendar type app, but selecting the day doesn't work for some reason. (I have comments on where the code breaks)

Im trying to make a day picker for a calendar type app, but selecting the day doesn't work for some reason. (I have comments on where the code breaks).
The issue is that the variable selectedDate doesn't update.
Basically the code has a loop from 0 to 100 days, and I just multiply current date by the iterator to get 100 future dates. I need the code to change selectedDate to whatever date I pick from the list
I have these two variables to keep track:
#State var currentDate = Date()
#State var selectedDate = Date()
(I think the problem comes from my use of the ForEach loop but I'm not sure)
ForEach(0..<100) { day in
if (selectedDate == (currentDate + TimeInterval((86400 * day)))) {
Button {
selectedDate = (currentDate + TimeInterval((86400 * day)))
// error here
} label: {
ZStack {
VStack {
Text("\((currentDate + TimeInterval((86400 * day))).formatted(.dateTime.day()))")
.foregroundColor(.white)
Text("\((currentDate + TimeInterval((86400 * day))).formatted(.dateTime.weekday(.short)))")
.foregroundColor(.white)
}
}
}
} else {
Button {
selectedDate = (currentDate + TimeInterval((86400 * day)))
// and here
} label: {
ZStack {
VStack {
Text("\((selectedDate + TimeInterval((86400 * day))).formatted(.dateTime.day()))")
.foregroundColor(.white)
Text("\((selectedDate + TimeInterval((86400 * day))).formatted(.dateTime.weekday(.short)))")
.foregroundColor(.white)
}
}
}
}
}
you could try this approach, keeping your logic but using currentDate.addingTimeInterval(TimeInterval((86400 * day))):
struct ContentView: View {
#State var currentDate = Date()
#State var selectedDate = Date()
var body: some View {
VStack {
Text("selected day: \(selectedDate.formatted(.dateTime.day())), \(selectedDate.formatted(.dateTime.weekday(.wide)))")
ScrollView (.horizontal){
HStack {
ForEach(0..<100) { day in
let theDate = currentDate.addingTimeInterval(TimeInterval((86400 * day)))
if (selectedDate == theDate) {
Button {
selectedDate = theDate
} label: {
VStack {
Text("\(theDate.formatted(.dateTime.day()))")
.foregroundColor(.green)
Text("\(theDate.formatted(.dateTime.weekday(.short)))")
.foregroundColor(.green)
}
}
} else {
Button {
selectedDate = theDate
} label: {
VStack {
Text("\(theDate.formatted(.dateTime.day()))")
.foregroundColor(.red)
Text("\(theDate.formatted(.dateTime.weekday(.short)))")
.foregroundColor(.blue)
}
}
}
}
}
}
}
}
}

SwiftUI - Inconsistent data for two diferent subviews inside a ForEach

I'm building an app which makes graphs and calculates some basic stuff. On the main screen, I have a ForEach loop inside a List that shows the saved charts. When entering the NavigationLink inside the List, the destination view does not correspond with the label shown (video below).
I had to use a custom ForEach extension to deal with the bindings https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/ (Apple announced that on iOS 15 ForEach will accept bindings, but I'm developing for iOS 14).
The code of the List:
List{
ForEach($chartData.calibrations) { index, data in
VStack{
NavigationLink(
destination: DetailedChartView(index: index, currentData: data, isDetailShown: $detailVisible),
isActive: $detailVisible,
label: {
SavedListItem(index: index, savedData: self.chartData.calibrations[index])
})
.navigationBarHidden(true)
}
}.onDelete(perform: removeRows)
}.id(UUID())
.listStyle(PlainListStyle())
The code of the label (SavedListItem)
struct SavedListItem: View {
var index: Int
var data: ChartDataObject
var formattedSlope = ""
var formattedOrigin = ""
var formattedCoef = ""
init(index: Int, savedData: ChartDataObject) {
self.data = savedData
self.index = index
self.formattedSlope = String(format: "%.2f", data.slope)
self.formattedOrigin = String(format: "%.2f", abs(data.origin))
self.formattedCoef = String(format: "%.3f", data.regressionCoef)
}
var body: some View {
HStack {
Text("\(index + 1)").bold()
VStack(alignment: .leading,spacing: 10) {
Text("\(data.name)").font(.title3)
Text("\(formatDate(data.date))")
}.padding()
Spacer()
if data.origin > 0 {
VStack {
VStack {
Text("y = \(self.formattedSlope)x + \(formattedOrigin)")
Spacer().frame(height: 10)
Text("R2 = \(formattedCoef)")
}
}
}
else if data.origin == 0 {
VStack {
VStack {
Text("y = \(formattedSlope)x")
Spacer().frame(height: 10)
Text("R2 = \(formattedCoef)")
}
}
}
else if data.origin < 0 {
VStack {
VStack {
Text("y = \(formattedSlope)x - \(formattedOrigin)")
Spacer().frame(height: 10)
Text("R2 = \(formattedCoef)")
}
}
}
else {
Text("There was an error")
}
}
.padding()
}
private func formatDate(_ date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
let dateString = dateFormatter.string(from: date)
return dateString
}
}
And the code of DetailedChartView:
struct DetailedChartView: View {
#EnvironmentObject var calibrationsController: SavedCalibrationsController
var index: Int
#Binding var currentData: ChartDataObject
var copyData: ChartDataObject {
return currentData
}
#State var isEditing: Bool = false
#Binding var isDetailShown: Bool
var body: some View {
NavigationView{
VStack(spacing: 10) {
Text("y = \(currentData.origin) + \(currentData.slope)x")
Text("Slope: \(currentData.slope)")
Text("Origin: \(currentData.origin)")
Text("R2: \(currentData.regressionCoef)")
Divider()
RegressionChart(data: currentData).frame(height: 500)
}.navigationBarHidden(true)
}
.navigationBarTitle(currentData.name)
.toolbar(content: {
Button(action: {
self.isEditing = true
}, label: {
Text("Edit")
})
})
.sheet(isPresented: $isEditing, onDismiss: {
currentData = copyData
calibrationsController.saveData()
isEditing = false
isDetailShown = false
}, content: {
EditView(calibrationsController: calibrationsController, editVisible: $isEditing, isDetailShown: $isDetailShown, index: index, data: $currentData).environmentObject(calibrationsController)
})
}
}
When there is only one item the List behaves as expected.
This issue is driving me nuts: if I use a List (without the ForEach), everything works as expected but I cant change the ForEach because I lose the .onDelete() functionality, and deleting the items inside the detailed view (which has an edit button), gives me an index out of range error (another story...).
Sorry for the long post!
EDIT: Minimal reproductible example
https://drive.google.com/file/d/1WN_tGR_kVNVNqEOW054d6kMBlq8HGkF5/view?usp=sharing
remove
isActive: $detailVisible,
in MainList NavigationLink.

Swift: Finding and plotting the hours in between two Dates

I am trying to create a UI like the below. I have a startDate and endDate (e.g. 1:55am and 11:35am). How can I proportionally (using Geometry reader) find each hour in between two dates and plot them as is done here? I am thinking at some point I need to use seconds or timeIntervalSince perhaps, but can't quite get my head around how best to go about it?
my code so far which gets the hours correctly, but I need to space them out proportionally according to their times:
What my code looks like:
struct AsleepTimeView: View {
#EnvironmentObject var dataStore: DataStore
static let sleepTimeFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}()
var body: some View {
Color.clear.overlay(
GeometryReader { geometry in
if let mostRecentSleepDay = dataStore.pastSevenSleepDays?.last, let firstSleepSpan = mostRecentSleepDay.sleepSpans.first, let lastSleepSpan = mostRecentSleepDay.sleepSpans.last {
VStack(alignment: .center) {
HStack {
Text("\(firstSleepSpan.startDate, formatter: Self.sleepTimeFormat) ")
.font(.footnote)
Spacer()
Text("\(lastSleepSpan.endDate, formatter: Self.sleepTimeFormat)")
.font(.footnote)
}
HStack(spacing: 3) {
ForEach(dataStore.sleepOrAwakeSpans) { sleepOrAwakeSpan in
RoundedRectangle(cornerRadius: 5)
.frame(width: getWidthForRoundedRectangle(proxy: geometry, spacing: 3, seconds: sleepOrAwakeSpan.seconds, sleepOrAwakeSpans: athlyticDataStore.sleepOrAwakeSpans), height: 10)
.foregroundColor(sleepOrAwakeSpan.asleep == false ? TrackerConstants.scaleLevel5Color : TrackerConstants.scaleLevel8Color)
}
}
HStack {
ForEach(getHoursBetweenTwoDates(startDate: firstSleepSpan.startDate, endDate: lastSleepSpan.endDate).map { Calendar.current.component(.hour, from: $0) }, id: \.self) { hour in
HStack {
Text("\(hour)")
.font(.footnote)
Spacer()
}
}
}
HStack {
HStack {
Circle()
.foregroundColor(TrackerConstants.scaleLevel8Color)
.frame(width: 10, height: 10)
Text("Asleep")
.font(.footnote)
}
HStack {
Circle()
.foregroundColor(TrackerConstants.scaleLevel5Color)
.frame(width: 10, height: 10)
Text("Awake")
.font(.footnote)
}
Spacer()
}
}
}
}) // end of overlay
}
//helper
private func getWidthForRoundedRectangle(proxy: GeometryProxy, spacing: Int, seconds: TimeInterval, sleepOrAwakeSpans: [SleepOrAwakeSpan]) -> CGFloat {
let totalSpace = (sleepOrAwakeSpans.count - 1) * spacing
let totalSleepTime = sleepOrAwakeSpans.map { $0.endTime.timeIntervalSince($0.startTime) }.reduce(0, +)
guard totalSleepTime > 0 else { return 0}
let width = (proxy.size.width - CGFloat(totalSpace)) * CGFloat(seconds / totalSleepTime)
return width
}
func datesRange(from: Date, to: Date, component: Calendar.Component) -> [Date] {
// in case of the "from" date is more than "to" date,
// it should returns an empty array:
if from > to { return [Date]() }
var tempDate = from
var array = [tempDate]
while tempDate < to {
tempDate = Calendar.current.date(byAdding: component, value: 1, to: tempDate)!
array.append(tempDate)
}
return array
}
func getHoursBetweenTwoDates(startDate: Date, endDate: Date) -> [Date] {
var finalArrayOfHours = [Date]()
guard endDate > startDate else { return finalArrayOfHours }
let arrayOfHours = datesRange(from: startDate, to: endDate, component: .hour)
for date in arrayOfHours {
let hour = date.nearestHour()
finalArrayOfHours.append(hour)
}
return finalArrayOfHours
}
}
extension Date {
func nearestHour() -> Date {
return Date(timeIntervalSinceReferenceDate:
(timeIntervalSinceReferenceDate / 3600.0).rounded(.toNearestOrEven) * 3600.0)
}
}
There are a whole suite of methods in the Calendar class to help with what you're trying to do.
Off the top of my head, Id say you'd do something like the following:
In a loop, use date(byAdding:to:wrappingComponents:) to add an hour at a time to your startDate. If the result is ≤ endDate, use component(_:from:) to get the hour of the newly calculated date.

Content offset in ScrollView SwiftUI

I built a simple horizontal calendar with SwiftUI and it works fine. But I want that it automatically scroll to the current date at app launch. How can I do this? I assume I should use GeometryReader to get a position of frame with current date and set an offset based on it but ScrollView doesn't have a content offset modifier. I know that in iOS 14 we now have ScrollViewReader but what about iOS 13?
struct MainCalendar: View {
#Environment(\.calendar) var calendar
#State private var month = Date()
#State private var selectedDate: Date = Date()
var body: some View {
VStack(spacing: 30) {
MonthAndYearView(date: $month)
WeekDaysView(date: month)
}
.padding(.vertical)
}
}
struct MonthAndYearView: View {
#Environment(\.calendar) var calendar
private let formatter = DateFormatter.monthAndYear
#Binding var date: Date
var body: some View {
HStack(spacing: 20) {
Button(action: {
self.date = calendar.date(byAdding: .month, value: -1, to: date)!
}, label: {
Image(systemName: "chevron.left")
})
Text(formatter.string(from: date))
.font(.system(size: 18, weight: .semibold))
Button(action: {
self.date = calendar.date(byAdding: .month, value: 1, to: date)!
}, label: {
Image(systemName: "chevron.right")
})
}
}
}
struct WeekDaysView: View {
#Environment(\.calendar) var calenar
let date: Date
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(days, id: \.self) { day in
VStack(spacing: 20) {
Text(daySymbol(date: day))
.font(.system(size: 14, weight: .regular))
if calenar.isDateInToday(day) {
Text("\(dayNumber(date: day))")
.foregroundColor(Color.white)
.background(Circle().foregroundColor(.blue)
.frame(width: 40, height: 40))
} else {
Text("\(dayNumber(date: day))")
}
}
}
}
.padding([.horizontal])
.padding(.bottom, 15)
}
}
private func dayNumber(date: Date) -> String {
let formatter = DateFormatter.dayNumber
let dayNumber = formatter.string(from: date)
return dayNumber
}
private var days: [Date] {
guard let interval = calenar.dateInterval(of: .month, for: date) else { return [] }
return calenar.generateDates(inside: interval, matching: DateComponents(hour: 0, minute: 0, second: 0))
}
private func daySymbol(date: Date) -> String {
let dayFormatter = DateFormatter.weekDay
let weekDay = dayFormatter.string(from: date)
return weekDay
}
}
This library https://github.com/Amzd/ScrollViewProxy is solved my problem.
struct WeekDaysView: View {
let date: Date
#Environment(\.calendar) var calendar
#State private var scrollTarget: Int? = nil
var body: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(Array(zip(days.indices, days)), id: \.0) { index, day in
VStack(spacing: 20) {
Text(daySymbol(date: day))
.font(.system(size: 14, weight: .regular))
.scrollId(index)
if calendar.isDateInToday(day) {
Text("\(dayNumber(date: day))")
.foregroundColor(Color.white)
.background(Circle().foregroundColor(.blue)
.frame(width: 40, height: 40))
.onAppear {
scrollTarget = index
}
} else {
Text("\(dayNumber(date: day))")
}
}
}
}
.padding([.horizontal])
.padding(.bottom, 15)
}
.onAppear {
DispatchQueue.main.async {
withAnimation {
proxy.scrollTo(scrollTarget, alignment: .center, animated: true)
}
}
}
}
}

How to make a timer in SwiftUI keep firing when changing tab with tabview

I have a timer that fires every half second and that leads to the calling of a function that outputs a set of strings that are used to display a countdown to a specific date. It works when I create a new event and then switch over to the tab that contains the information for the countdown, but when I switch back to the add event tab and then back it stops counting down.
The timer is made using this:
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
It runs later using this
ForEach(eventNames.indices, id: \.self) { index in
VStack{
Text("Your event " + "\(self.eventNames[index])" + " is in " + "\(self.string[index])")
.onReceive(self.timer) { input in
self.differenceDate(numbers: index)
}
}
}
And finally, it calls this function
func differenceDate(numbers: Int) {
self.formatter.unitsStyle = .full
self.formatter.allowedUnits = [.day, .hour, .minute, .second]
//self.formatter.maximumUnitCount = 2
self.now = Date();
if self.now > self.eventDates[numbers] {
self.eventNames[numbers] = "";
}
else {
self.string[numbers] = self.formatter.string(from: self.now, to: self.eventDates[numbers]) ?? ""
}
}
This is the full code
import SwiftUI
struct ContentView: View {
#State private var selection = 0
#State private var eventDates = [Date]()
#State private var eventNames = [String]()
#State private var currentName = "";
#State private var counter = 0;
#State private var placeholderText = "Event Name";
#State private var selectedDate = Date();
var numbers = 0;
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
#State var now = Date();
#State var string = [String]();
var formatter = DateComponentsFormatter();
func differenceDate(numbers: Int) {
self.formatter.unitsStyle = .full
self.formatter.allowedUnits = [.day, .hour, .minute, .second]
//self.formatter.maximumUnitCount = 2
self.now = Date();
if self.now > self.eventDates[numbers] {
self.eventNames[numbers] = "";
}
else {
self.string[numbers] = self.formatter.string(from: self.now, to: self.eventDates[numbers]) ?? ""
}
}
var body: some View {
TabView(selection: $selection){
//Page 1
VStack{
Text("Add New Event")
.underline()
.font(.title)
.padding(15)
// .onReceive(self.timer) { input in
// self.differenceDate(numbers: index)
// //}
// }
// .minimumScaleFactor(0.1)
TextField("\(placeholderText)", text: $currentName)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 1)
.padding(5)
)
Text("When is your event?")
DatePicker("Please enter a date", selection: $selectedDate, displayedComponents: .date)
.labelsHidden()
.scaledToFill()
Button(action: {
if self.currentName != "" {
self.eventNames.append(self.currentName)
self.eventDates.append(self.selectedDate)
self.string.append("")
self.currentName = "";
}
})
{
Text("Add Event")
.font(.headline)
.foregroundColor(.black)
}
.padding(25)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 3)
.padding(5)
)
}
//Tab 1
.tabItem {
VStack {
Image(systemName: "calendar")
Text("Add Event")
}
}
.tag(1)
//Page 2
VStack{
Text("Your Events").underline()
.font(.title)
.padding(15)
ForEach(eventNames.indices, id: \.self) { index in
VStack{
Text("Your event " + "\(self.eventNames[index])" + " is in " + "\(self.string[index])")
.onReceive(self.timer) { input in
self.differenceDate(numbers: index)
}
}
}
}
//Tab 2
.font(.title)
.tabItem {
VStack {
Image(systemName: "flame.fill")
Text("Countdowns")
}
}
.tag(0)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I was wondering if there was a workaround or how to keep the timer firing while the tab changes or pause it when the tab changes and then start it again when the tab is swapped back over.
It needs to attach the .onReceive to the TabView and it will be received on all tabs, like
TabView {
...
// << all tab items here
...
}
.onReceive(self.timer) { _ in
self.differenceDate()
}
and iterate indexes inside of handler
func differenceDate() {
for numbers in eventNames.indices {
// current body here
}
}

Resources