How to make the date change simultaneously in all views? - swift

Date not change simultaneously in all views.
I want to link two calendars. Standard and custom. But they don't connect.
When I change the date in one, it doesn't change in the other.
I made Published:
import Combine
import Foundation
class CustomCalendar: ObservableObject {
#Published var currentDate = Date()
var currentThreeWeek: [Date] = []
init() {
fetchCurrentThreeWeek()
}
func fetchCurrentThreeWeek() {
let calendar = Calendar.current
var todayDay = DateInterval(start: Date(), duration: 1814400).start
let lastDay = DateInterval(start: Date(), duration: 1814400).end
currentThreeWeek.append(todayDay)
while todayDay < lastDay {
todayDay = calendar.date(byAdding: .day, value: 1, to: todayDay)!
currentThreeWeek.append(todayDay)
}
}
func extractDate(date: Date, format: String) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
dateFormatter.locale = Locale(identifier: "rus")
return dateFormatter.string(from: date)
}
func isToday(date: Date) -> Bool {
let calendar = Calendar.current
return calendar.isDate(currentDate, inSameDayAs: date)
}
}
When I select a date it doesn't change in other views.
import SwiftUI
struct FilterView: View {
#StateObject private var calendar = CustomCalendar()
#Binding var filterViewIsPresented: Bool
let todayDay = DateInterval(start: Date(), duration: 1814400).start
let lastDay = DateInterval(start: Date(), duration: 1814400).end
var body: some View {
VStack {
DatePicker("", selection: $calendar.currentDate, in: todayDay...lastDay, displayedComponents: .date)
.labelsHidden()
.environment(\.locale, Locale.init(identifier: "ru"))
HorizontalCalendarView()
HorizontalCalendarView()
}
}
}
struct FilterView_Previews: PreviewProvider {
static var previews: some View {
FilterView(filterViewIsPresented: .constant(false))
}
}
Custom calendar. On tap Gesture I change currentDate
import SwiftUI
struct HorizontalCalendarView: View {
#StateObject private var calendar = CustomCalendar()
var body: some View {
ScrollViewReader { value in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(calendar.currentThreeWeek, id: \.self) { day in
VStack(spacing: 0) {
Text(calendar.extractDate(date: day, format: "dd"))
.font(.title3)
.fontWeight(.bold)
Text(calendar.extractDate(date: day, format: "EEE"))
RoundedRectangle(cornerRadius: 10, style: .continuous)
.frame(width: calendar.isToday(date: day) ? 40 : 0, height: 5)
.opacity(calendar.isToday(date: day) ? 1 : 0)
.padding(4)
}
.frame(width: 45, height: 45)
.foregroundStyle(calendar.isToday(date: day) ? .primary : .secondary )
.foregroundColor(calendar.isToday(date: day) ? .white : .black)
.padding(8)
.background(
ZStack {
if calendar.isToday(date: day) {
RoundedRectangle(cornerRadius: 10, style: .continuous)
}
}
)
.onTapGesture {
withAnimation(.easeIn(duration: 0.2)) {
calendar.currentDate = day
value.scrollTo(calendar.currentDate, anchor: .leading)
}
}
}
.padding(9)
}
}
Text(calendar.currentDate.formatted())
}
}
}
struct HorizontalCalendarView_Previews: PreviewProvider {
static var previews: some View {
HorizontalCalendarView()
}
}
How can I do this?

You have two options:
Pass calendar in HorizontalCalendarView constructor and use #ObservedObject property wrapper instead of #StateObject:
struct HorizontalCalendarView: View {
#ObservedObject private var calendar: CustomCalendar
init(_ calendar: CustomCalendar) {
self.calendar = calendar
}
...
and just pass it in FilterView
HorizontalCalendarView(calendar)
Another option is to use #EnvironmentObject (it's preferred method for deeply nested views in your example option 1 is better):
struct HorizontalCalendarView: View {
#EnvironmentObject private var calendar: CustomCalendar = CustomCalendar()
...
then you have to pass calendar with environmentObject modifier:
HorizontalCalendarView().environmentObject(calendar)
Note: in order to #EnvironmentObject works as expected it is not necessary to use environmentObject modifier on actual view, you can use this modifier in any of parent views. That makes it perfect for deep nested views

Related

Streak builder app using Swift and SwiftUI

I am looking to make a Streak builder app using Swift and SwiftUI. However, I am finding it difficult to create the logic for the counter using the Date(). Any suggestions will be highly appreciated.
Mainly I wanna replicate the Streak thing from the Duolingo app.
// I have this extension to follow up on the Date from the time user clicks it.
extension Date {
// for tomorow's Date
static var tomorrow: Date { return Date().dayAfter }
static var today: Date {return Date()}
var dayAfter: Date {
return Calendar.current.date(byAdding: .day, value: 1, to: Date())!
}
}
// More or less this was supposed to be my view.
struct StreakTrial: View {
#State var counter = 0
#State var TapDate: Date = Date.today
var body: some View {
NavigationView {
VStack{
Button {
// if TapDate != Date.today {
// counter += 1
// let TapDate = Date.tomorrow
// }
// else if TapDate == Date.tomorrow {
// counter = counter
// }
} label: {
Image(systemName: "flame")
.resizable()
.frame(width: 40, height: 50)
.padding()
.scaledToFit()
.background(Color.gray)
.foregroundColor(Color.orange)
.cornerRadius(12)
Text("\(counter)").foregroundColor(.gray)
}
Text("\(Date())")
.padding()
Text("\(Date().dayAfter)")
.padding()
}
}
}
}
**SO I TRIED SOME TUTORIALS OF #NICK SARNO WHICH GOES LIKE SWIFTFUL THINKING ON YOUTUBE AND GOT IT DONE**
*This code is compatible with Xcode 14.0*
import SwiftUI
extension Date {
// for tomorow's Date
static var tomorrow: Date { return Date().dayAfter }
static var today: Date {return Date()}
var dayAfter: Date {
return Calendar.current.date(byAdding: .day, value: 1, to: Date())!
// just add .minute after byAdding: , to create a streak minute counter and check the logic.
}
static func getTodayDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E d MMM yyyy"
//to continue with the minute streak builder just add "E d MMM yyyy h:mm a" above, it will allow date formatting with minutes and follow the changes in dayAfter
return dateFormatter.string(from: Date.today)
}
static func getTomDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E d MMM yyyy"
return dateFormatter.string(from: Date.tomorrow)
}
}
struct StreakApp: View {
#AppStorage("counter") var counter = 0
#AppStorage("tapDate") var TapDate: String?
#AppStorage("Tappable") var ButtonTapped = false
var body: some View {
NavigationView {
VStack{
VStack {
Text("\(counter)").foregroundColor(.gray)
Text("Restore your streak on ")
Text(TapDate ?? "No Date")
Image(systemName: "flame")
.resizable()
.frame(width: 40, height: 50)
.padding()
.scaledToFit()
.background(ButtonTapped ? Color.red : Color.gray)
.foregroundColor(ButtonTapped ? Color.orange : Color.black)
.cornerRadius(12)
}
Button {
if TapDate == nil {
//Check if user has already tapped
self.ButtonTapped = true
counter += 1
self.TapDate = ("\(Date.getTomDate())")
}
else if ("\(Date.getTodayDate())") == TapDate {
//Check for the consecutive Day of Streak
self.TapDate = ("\(Date.getTomDate())")
counter += 1
//Let's light the flame back again.
self.ButtonTapped = true
}
} label: {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.foregroundColor(.black)
.frame(width: 120, height: 40)
.overlay {
Text("Add Streak")
.foregroundColor(.white)
}
}
.padding()
//This button is only for testing purpose.
Button {
self.TapDate = nil
self.ButtonTapped = false
self.counter = 0
} label: {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.foregroundColor(.black)
.frame(width: 160, height: 40)
.overlay {
Text("Reset Streak")
.foregroundColor(.white)
}
}
}
//Ensuer the flame dies out if we run into any other day except today or tommorow.
.onAppear {
if ("\(Date.getTodayDate())") == TapDate ||
("\(Date.getTomDate())") == TapDate {
self.ButtonTapped = true
}
//Breaking the Streak
else {
self.TapDate = nil
self.ButtonTapped = false
self.counter = 0
}
}
}
}
}
struct StreakApp_Previews: PreviewProvider {
static var previews: some View {
StreakApp()
}
}

How can I use TimelineView in Swift 5 to refresh a text every second?

I'm pretty new at swift coding and I'm trying to do a very simple project: just a clock that shows the time. I'm using TimelineView to refresh the time every second, but it's not working. This is my code:
import SwiftUI
struct ContentView: View {
#State var hour: Int = Calendar.current.component(.hour, from: Date())
#State var minute: Int = Calendar.current.component(.minute, from: Date())
#State var second: Int = Calendar.current.component(.second, from: Date())
var body: some View {
ZStack {
VStack{
Spacer()
HStack {
TimelineView(.periodic(from: .now, by: 1)) { timeline in
Text(String(hour))
Text(String(minute))
Text(String(second))
}
}
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewInterfaceOrientation(.portrait)
}
}
}
Since my hour, minute and second variables are #State and I'm using the TimelineView, they should refresh every second, shouldn't they?
I'm very confused and I would appreciate some help. Thank you very much.
You have to observe changes in the timeline.
Here I used onChange and update the value of min, sec, and hour.
struct TimerView: View {
var date: Date
#State var hour: Int = Calendar.current.component(.hour, from: Date())
#State var minute: Int = Calendar.current.component(.minute, from: Date())
#State var second: Int = Calendar.current.component(.second, from: Date())
var body: some View {
VStack {
Text(String(hour))
Text(String(minute))
Text(String(second))
}
.onChange(of: date) { _ in
second += 1
if second > 59 {
minute += 1
second = 0
if minute > 59 {
hour += 1
minute = 0
if hour > 23 {
hour = 0
}
}
}
}
}
}
struct ContentView: View { var body: some View {
ZStack {
VStack{
Spacer()
HStack {
TimelineView(.periodic(from: .now, by: 1)) { timeline in
TimerView(date: timeline.date)
}
}
Spacer()
}
}
}
}
As the documentation says (https://developer.apple.com/documentation/swiftui/timelineview):
A timeline view acts as a container with no appearance of its own. Instead, it redraws the content it contains at scheduled points in time
The content it contains is defined in the closure you provide:
TimelineView(...) { timeline in
// content which gets redrawn
}
Inside this closure you have access to a TimelineView.Context (https://developer.apple.com/documentation/swiftui/timelineview/context). With the help of this context, you can access the date which triggered the update / redraw like so:
TimelineView(.periodic(from: .now, by: 1)) { timeline in
Text("\(timeline.date)")
}
This will produce the following output:
To improve formatting, you could use a DateFormatter (https://developer.apple.com/documentation/foundation/dateformatter):
struct ContentView: View {
private let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .medium
return dateFormatter
}()
var body: some View {
TimelineView(.periodic(from: .now, by: 1)) { timeline in
Text("\(dateFormatter.string(from: timeline.date))")
}
}
}
Just make your hour-minute-second as computed property not #State
struct ContentView: View {
var hour: Int {
Calendar.current.component(.hour, from: Date())
}
var minute: Int {
Calendar.current.component(.minute, from: Date())
}
var second: Int {
Calendar.current.component(.second, from: Date())
}
var body: some View {
TimelineView(.periodic(from: .now, by: 1.0)) { timeline in
HStack {
Text(String(hour))
Text(String(minute))
Text(String(second))
}
}
}
}

SwiftUI - Date handling

maybe you can help me with following question:
I am programming a UI with two date pickers (startDatePicker and endDatePicker).
The startDatePicker should be updated itself to the date of endDatePicker in UI when the date of startEndPicker is smaller than the date of endDatePicker.
Do you habe any idea how I can realise that?
content.swift
import SwiftUI
struct ContentView: View {
#State var startDate = Date()
#State var endDate = Date()
#ObservedObject var dateModel = Period.shared
var body: some View {
VStack{
startDatePicker
endDatePicker
}
.onAppear {
// print("VStack")
// dateModel.startDate = startDate
// dateModel.endDate = endDate
}
}
var startDatePicker: some View{
DatePicker("Start", selection: $startDate, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.frame(width: 250, height: 50, alignment: .center)
.onAppear(perform: {
print("StartDate.onAppear")
dateModel.startDate = dateModel.toLocalTime(date: startDate, type: true)
print(dateModel.startDate)
})
.onChange(of: startDate, perform: { startDate in
print("StartDate.onChange")
dateModel.startDate = dateModel.toLocalTime(date: startDate, type: true)
print(dateModel.startDate)
})
}
var endDatePicker: some View{
DatePicker("End", selection: $endDate, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.frame(width: 250, height: 50, alignment: .center)
.onAppear(perform: {
print("EndDate.onAppear")
dateModel.endDate = dateModel.toLocalTime(date: endDate, type: false)
print(dateModel.endDate)
})
.onChange(of: endDate, perform: { endDate in
print("EndDate.onChange")
dateModel.endDate = dateModel.toLocalTime(date: endDate, type: false)
if dateModel.endDate < dateModel.startDate{
print("Error")
dateModel.startDate = dateModel.endDate
}
print(dateModel.endDate)
})
}
}
Datahandler.swift
class Period : ObservableObject{
static let shared = Period()
#Published var startDate: Date = Date()
#Published var endDate: Date = Date()
func toLocalTime(date : Date, type: Bool) -> Date {
var startDate : Date?
var endDate : Date?
var dateLocalTimezone : Date?
//Auswahl der aktuellen Kalender
let calendar = Calendar.current
//Auswahl der Zeitzone
let timezone = TimeZone.current
//Bestimmen Anzahl Sekunden zwischen Zeitzone und GMT
let seconds = TimeInterval(timezone.secondsFromGMT(for: date))
//Anpassen des eingelesenen Werts
if type == true {
startDate = calendar.date(bySettingHour: 00, minute: 00, second: 00, of: date)
dateLocalTimezone = Date(timeInterval: seconds, since: startDate!)
}else{
endDate = calendar.date(bySettingHour: 23, minute: 59, second: 00, of: date)
dateLocalTimezone = Date(timeInterval: seconds, since: endDate!)
}
return dateLocalTimezone!
}
}
Is there a better way for the code? The idea is to separate the part for dates from the UI.
You're almost there. You can move logic from onAppeat to init(), and from .onChange to didSet, like this:
struct ContentView: View {
#ObservedObject var dateModel = Period.shared
var body: some View {
VStack{
startDatePicker
endDatePicker
}
}
var startDatePicker: some View{
DatePicker("Start", selection: $dateModel.startDate, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.frame(width: 250, height: 50, alignment: .center)
}
var endDatePicker: some View{
DatePicker("End", selection: $dateModel.endDate, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.frame(width: 250, height: 50, alignment: .center)
}
}
class Period : ObservableObject{
static let shared = Period()
#Published var startDate: Date {
didSet {
let localTime = Self.toLocalTime(date: startDate, type: true)
if startDate != localTime {
startDate = localTime
}
}
}
#Published var endDate: Date {
didSet {
let localTime = Self.toLocalTime(date: endDate, type: false)
if endDate != localTime {
endDate = localTime
}
if endDate < startDate{
print("Error")
startDate = endDate
}
print(endDate)
}
}
init() {
startDate = Self.toLocalTime(date: Date(), type: true)
endDate = Self.toLocalTime(date: Date(), type: false)
}
static private func toLocalTime(date : Date, type: Bool) -> Date {
var startDate : Date?
var endDate : Date?
var dateLocalTimezone : Date?
//Auswahl der aktuellen Kalender
let calendar = Calendar.current
//Auswahl der Zeitzone
let timezone = TimeZone.current
//Bestimmen Anzahl Sekunden zwischen Zeitzone und GMT
let seconds = TimeInterval(timezone.secondsFromGMT(for: date))
//Anpassen des eingelesenen Werts
if type == true {
startDate = calendar.date(bySettingHour: 00, minute: 00, second: 00, of: date)
dateLocalTimezone = Date(timeInterval: seconds, since: startDate!)
}else{
endDate = calendar.date(bySettingHour: 23, minute: 59, second: 00, of: date)
dateLocalTimezone = Date(timeInterval: seconds, since: endDate!)
}
return dateLocalTimezone!
}
}
Your toLocalTime returns wrong value for end date, like I pass 2021-08-18 23:59:00 +0000 and the result is 2021-08-19 23:59:00 +0000 which is the next day. This proceeds to recursion of didSet, but I'll leave fixing this logic to your
It's not a good fit for SO to ask for "a better way", since every solution has its pros and cons and thus there are a lot of opinions. But the latter I guess, is pretty much consolidated (which is in itself highly opinionated ;) )
I would like to add some refactoring suggestions:
You can make the DatePickerView a reusable component:
struct DatePickerView: View {
let title: String
let date: Date
let setDateAction: (Date) -> Void
var body: some View {
let binding: Binding<Date> = .init {
return self.date
} set: { newValue in
self.setDateAction(newValue)
}
DatePicker(title, selection: binding, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
}
}
Then, it can be used in the ContentView as follows:
struct ContentView: View {
let state: TwoDatePickers.ViewModel.ViewState
let setStartDate: (Date) -> ()
let setEndDate: (Date) -> ()
var body: some View {
VStack{
Text("Start: \(state.startDate)")
.padding()
Text("End: \(state.endDate)")
.padding()
DatePickerView(
title: "Start",
date: state.startDate,
setDateAction: self.setStartDate)
.padding()
DatePickerView(
title: "End",
date: state.endDate,
setDateAction: self.setEndDate)
.padding()
}
}
}
You might notice that ContentView has no coupling to any certain kind of view model or model, and no logic what so ever. That's by intention to make it reusable as well and let the logic be done elsewhere.
Now, you can implement your view model plus logic as a separated component as well. I am cheating here a bit, since it internally uses a reusable "store" component ;)
extension TwoDatePickers {
static let store = Oak.Store(state: .init(),
update: update,
scheduler: DispatchQueue.main)
final class ViewModel: ObservableObject {
typealias ViewState = TwoDatePickers.State
init() {
cancellableState = store.sink { state in
self.viewState = self.view(state)
}
}
#Published
private(set) var viewState: ViewState = .init()
private var cancellableState: AnyCancellable!
private func view(_ state: State) -> ViewState { state }
func setStartDate(_ date: Date) {
store.input.send(.setStartDate(date))
}
func setEndDate(_ date: Date) {
store.input.send(.setEndDate(date))
}
}
}
Note, that the ViewModel is just a thin wrapper which connects the store and provides a view function which returns a ViewState from the store's state. The ViewState is the thing that completely and unambiguously defines what the view should render, while the store's State is there to perform the logic and may contain additional data. That is, ViewState is a function of State.
The "store" is implemented much like in Redux or Elm, which can be done in a few lines of code.
As mentioned, this is also a reusable component. It is event driven, unidirectional and uses internally a Finite State Machine to change state and generate outputs.
So, you have to implement an update function, which is basically the heart of your logic:
extension TwoDatePickers {
struct State {
var startDate: Date = Date()
var endDate: Date = Date()
}
enum Event {
case setStartDate(Date)
case setEndDate(Date)
}
typealias Command = Void
static func update(state: State, setter: (State) -> Void, event: Event) -> Void {
switch (state, event) {
case (_, .setStartDate(let date)):
let adjustedEndDate = Date(
timeIntervalSinceReferenceDate: max(
date.timeIntervalSinceReferenceDate,
state.endDate.timeIntervalSinceReferenceDate))
setter(State(startDate: date, endDate: adjustedEndDate))
case (_, .setEndDate(let date)):
let adjustedStartDate = Date(
timeIntervalSinceReferenceDate: min(
date.timeIntervalSinceReferenceDate,
state.startDate.timeIntervalSinceReferenceDate))
setter(State(startDate: adjustedStartDate, endDate: date))
default:
print("not handled: \(state), \(event)")
break
}
}
}
To wire all the things together, we may use a "root view". Here you can see how eventually the view model gets connected to the views:
struct TwoDatePickersSceneView: View {
#StateObject var viewModel = TwoDatePickers.ViewModel()
var body: some View {
TwoDatePickers.ContentView(
state: viewModel.viewState,
setStartDate: viewModel.setStartDate,
setEndDate: viewModel.setEndDate
)
}
}
What's left is the reusable implementation of the store. I post it here as a bonus:
https://gist.github.com/couchdeveloper/4100f1ec8470980c5c49adc119240de1
Final note:
This is an example how we can decompose a typical SwiftUI feature and separate it into several reusable components. You don't have to do this extra effort when the feature is small and tidy. However, this solution, especially using a Finite State Machine to solve UI problems scales much better for more complex problems.

SwiftUI: how to update the range of a ForEach loop based on the value of a Picker

So I'm trying to have a ForEach loop update the amount of times a View is looped based on what Month (value in a picker) is selected. In my case, they will be looping based on the number of days in the month of the selected month for the given year. I already have a function that gives me the number of days in each month, however, when I plug that into the ForEach Loop, it only gets run based on the first month selected and stays iterating the number of days of that month for the rest. Here is my code for the ForEach Loop:
ForEach(0..<getRange(year: yearIndex, month: monthIndex + indexCheck)) { i in
NavigationLink(destination: ContentView(day: yearData[yearIndex].months[monthIndex].dayInfo[i])) {
DayRow(day: yearData[yearIndex].months[monthIndex].dayInfo[i])
}
}
and here is the getRange() Function:
func getRange(year: Int, month: Int) -> Int {
return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year + 2020, month: month + 1))!)!.count
}
The yearIndex variable is linked to the picker value of three years, (2020, 2021, 2022). Here is the code for it:
Picker("Years", selection: $yearIndex) {
ForEach(0 ..< year.count) { i in
Text(String(self.year[i])).tag(i)
}
}
.pickerStyle(SegmentedPickerStyle())
The monthIndex variable is linked to the picker with the months in the year (Jan-Dec). Here is the code for it:
Picker("Month", selection: $monthIndex) {
ForEach(0 ..< monthArray.count) { i in
Text(self.monthArray[i]).tag(i)
}
}
.padding(.bottom, 2)
I'm not sure what I'm doing wrong, and I'm not sure how to do this, so any help would be greatly appreciated! I'm still quite new to Swift/SwiftUI, so any advice to better code this would also be appreciated!
EDIT: Here is a minimal reproducible example as requested:
struct ContentView: View {
#State var year = [2020, 2021, 2022]
//monthSymbols gets an array of all the months
#State var monthArray = DateFormatter().monthSymbols!
#State var yearIndex = 0
#State var monthIndex = 0
#State var indexCheck = 0
#State var indexTest = 0
var body: some View {
NavigationView {
List {
Section {
VStack {
Picker("Years", selection: $yearIndex) {
ForEach(0 ..< year.count) { i in
Text(String(self.year[i])).tag(i)
}
}
.pickerStyle(SegmentedPickerStyle())
Divider()
Picker("Month", selection: $monthIndex) {
ForEach(0 ..< monthArray.count) { i in
Text(self.monthArray[i]).tag(i)
}
}
.padding(.bottom, 2)
}
}
Section(header: Text("What I love about you")) {
ForEach(0..<getRange(year: yearIndex, month: monthIndex + indexCheck)) { i in
NavigationLink(destination: DetailsView()) {
Text("Row \(i)")
}
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle(Text("\(monthArray[monthIndex + indexCheck]) \(String(year[yearIndex]))"))
}
}
func getRange(year: Int, month: Int) -> Int {
return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year + 2020, month: month + 1))!)!.count
}
}
struct YearView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I cleaned a little bit your code so it's more readable.
Here is the ContentView:
struct ContentView: View {
let yearArray = [2020, 2021, 2022]
let monthArray = DateFormatter().monthSymbols!
// you don't need to operate on indices, you can use real values
#State var selectedYear = 2020
#State var selectedMonth = 1
// improved readability
var combinedYearMonth: String {
"\(monthArray[selectedMonth - 1]) \(selectedYear)"
}
var body: some View {
NavigationView {
List {
pickerSection
daySelectionSection
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle(combinedYearMonth)
}
}
}
The part responsible for displaying list sections:
// sections extracted to a private extension (you can still have everything in one `ContentView` struct if you want)
private extension ContentView {
var pickerSection: some View {
Section {
yearPicker
monthPicker
}
}
var daySelectionSection: some View {
Section(header: Text("What I love about you")) {
ForEach(dayRange, id: \.self) { day in
NavigationLink(destination: DetailsView()) {
Text("Day \(day)")
}
}
}
}
// create a range of days in the `selectedMonth` for the `selectedYear`
var dayRange: Range<Int> {
let dateComponents = DateComponents(year: selectedYear, month: selectedMonth)
let calendar = Calendar.current
let date = calendar.date(from: dateComponents)!
return calendar.range(of: .day, in: .month, for: date)!
}
}
And the part with pickers:
private extension ContentView {
var yearPicker: some View {
Picker("Years", selection: $selectedYear) {
ForEach(yearArray, id: \.self) { year in
Text(String(year)) // <- no need for `.tag()` if the `id` parameter in `ForEach` is specified
}
}
.pickerStyle(SegmentedPickerStyle())
}
var monthPicker: some View {
Picker("Month", selection: $selectedMonth) {
ForEach(1...12, id: \.self) { month in
Text(self.monthArray[month - 1])
}
}
.padding(.bottom, 2)
}
}

DatePicker on Mac not saving date until return key is pressed

I'm adapting my iPad app to Mac with Mac Catalyst and am having a problem with the datePicker (it has a datePickerMode of time). On iPad the datePicker is a wheel and whenever the user scrolls on the date picker the dateChanged action is fired. But on Mac the date picker is not a scroller and is instead a type of text input. I can type and change all the time values on Mac, but the dateChanged action won't be fired until I press the return key.
I would like to get the dateChange action fired whenever a user is entering in a time. How can I do this? I tried adding different targets to the datePicker but nothing work.
I actually prefer to have the date scroller on the Mac so if anyone knows how to do this instead I would greatly appreciate it (I looked all over the internet for this and found nothing)!
Here's my code:
class DateVC: UIViewController {
#IBOutlet weak var datePicker: UIDatePicker!
override func viewDidLoad() {
super.viewDidLoad()
//Just show the time
datePicker.datePickerMode = .time
}
//Action connected to datePicker. This is not called until I press enter on Mac
#IBAction func datePickerChanged(_ sender: Any) {
//do actions
}
}
I have filed a bug report with Apple about 1 week ago.
For now I a doing the following to force the datepicker to use the wheel format. This fires the onchangedlistener as the wheels are spun.
if #available(macCatalyst 13.4, *) {
datePickerView.preferredDatePickerStyle = .wheels
}
Because your function linked with #IBAction which is to be called upon action, like 'button press'
you should follow different approach.
let datePicker = UIDatePicker()
datePicker.datePickerMode = .date
dateTextField.inputView = datePicker
datePicker.addTarget(self, action: #selector(datePickerChanged(picker:)), for: .valueChanged)
and here is your function:
#objc func datePickerChanged(picker: UIDatePicker) {
//do your action here
}
Here is a SwiftUI solution that displays a date and time scroller picker on iPad, iPhone and Mac Catalyst. Works without pressing the return key. You can easily display just the HoursMinutesPicker if desired.
import SwiftUI
struct ContentView: View {
#State var date = Date()
var body: some View {
NavigationView {
NavigationLink(destination: DateHMPicker(date: self.$date)) {
VStack {
Text("Show time")
Text("\(self.date)")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DateHMPicker: View {
var titleKey: LocalizedStringKey = ""
#Binding var date: Date
var body: some View {
HStack {
Spacer()
DatePicker(titleKey, selection: self.$date, displayedComponents: .date).datePickerStyle(WheelDatePickerStyle())
HoursMinutesPicker(date: self.$date)
Spacer()
}
}
}
struct HoursMinutesPicker: View {
#Binding var date: Date
#State var hours: Int = 0
#State var minutes: Int = 0
var body: some View {
HStack {
Spacer()
Picker("", selection: Binding<Int>(
get: { self.hours},
set : {
self.hours = $0
self.update()
})) {
ForEach(0..<24, id: \.self) { i in
Text("\(i) hours").tag(i)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 90).clipped()
Picker("", selection: Binding<Int>(
get: { self.minutes},
set : {
self.minutes = $0
self.update()
})) {
ForEach(0..<60, id: \.self) { i in
Text("\(i) min").tag(i)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 90).clipped()
Spacer()
}.onAppear(perform: loadData)
}
func loadData() {
self.hours = Calendar.current.component(.hour, from: date)
self.minutes = Calendar.current.component(.minute, from: date)
}
func update() {
if let newDate = Calendar.current.date(bySettingHour: self.hours, minute: self.minutes, second: 0, of: date) {
date = newDate
}
}
}
How about using DatePicker for the date part, and the following textfields for the time input part.
import SwiftUI
import Combine
struct ContentView: View {
#State var date = Date()
var body: some View {
NavigationView {
NavigationLink(destination: DateHMPicker(date: self.$date)) {
VStack {
Text("Show time")
Text("\(self.date)")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DateHMPicker: View {
#State var labelText = ""
#Binding var date: Date
var body: some View {
HStack {
Spacer()
DatePicker(labelText, selection: self.$date, displayedComponents: .date)
Spacer()
HoursMinutesPicker(date: self.$date).frame(width: 90)
}.fixedSize()
}
}
struct TextFieldTime: View {
let range: ClosedRange<Int>
#Binding var value: Int
var handler: () -> Void
#State private var isGood = false
#State private var textValue = ""
#State private var digits = 2
var body: some View {
TextField("", text: $textValue)
.font(Font.body.monospacedDigit())
.onReceive(Just(textValue)) { txt in
// must be numbers
var newTxt = txt.filter {"0123456789".contains($0)}
if newTxt == txt {
// restrict the digits
if newTxt.count > self.digits {
newTxt = String(newTxt.dropLast())
}
// check the number
self.isGood = false
if let number = NumberFormatter().number(from: newTxt) {
if self.range.contains(number.intValue) {
self.textValue = newTxt
self.value = number.intValue
self.isGood = true
} else {
self.textValue = self.textValue.count == 1
? String(self.range.lowerBound) : String(self.textValue.dropLast())
}
}
if self.value >= 0 && self.isGood {
self.handler()
}
} else {
self.textValue = newTxt.isEmpty ? String(self.range.lowerBound) : newTxt
}
}.onAppear(perform: {
self.textValue = String(self.value)
self.digits = String(self.range.upperBound).count
})
.fixedSize()
}
}
struct HoursMinutesPicker: View {
#Binding var date: Date
#State var separator = ":"
#State var hours: Int = 0
#State var minutes: Int = 0
var body: some View {
HStack (spacing: 1) {
TextFieldTime(range: 0...23, value: self.$hours, handler: self.update)
Text(separator)
TextFieldTime(range: 0...59, value: self.$minutes, handler: self.update)
}.onAppear(perform: loadData).padding(5)
}
func loadData() {
self.hours = Calendar.current.component(.hour, from: date)
self.minutes = Calendar.current.component(.minute, from: date)
}
func update() {
let baseDate = Calendar.current.dateComponents([.year, .month, .day], from: date)
var dc = DateComponents()
dc.year = baseDate.year
dc.month = baseDate.month
dc.day = baseDate.day
dc.hour = self.hours
dc.minute = self.minutes
if let newDate = Calendar.current.date(from: dc), date != newDate {
date = newDate
}
}
}
I didn't find a solution to this using the built in UIDatePicker. I ended up moving to using JBCalendarDatePicker which accomplished the same look/feel.
If you prefer the wheel format of the DatePicker in Mac Catalyst, then change your Date Picker style to Wheels. It will display correctly on the Mac.
I converted my iPhone/iPad app to run on Mac Catalyst. I'm using Xcode 11.4.1 on MacOS Catalina 10.15.5. I was having a problem displaying the DatePicker as a wheel on the Mac version of the app. On the Mac emulator, it displayed as a text field, but the calendar would display when clicking on one of the date fields. I felt it would be a better user experience to stay with the wheel display.
A very simple workaround is to select the DatePicker in Storyboard. Display the Attributes Inspector. Change your Style from "Automatic" to "Wheels", and the display will go back to displaying as a wheel on the Mac Catalyst version.