DatePicker displaying minutes and seconds SwiftUI - datepicker

Having trouble finding a DatePicker in SwiftUI that displays "mm:ss". displayComponents only has .date and .HourAndMinute.
How do I change the DatePicker to display the minutes and seconds without AM / PM?
struct ContentView: View {
#State private var altEventTime = Date()
public var timeFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "mm:ss"
return formatter
}
var body: some View {
Form {
DatePicker(selection: self.$altEventTime, displayedComponents: .hourAndMinute) {
Text("Finish time: \(altEventTime, formatter: timeFormatter)")
}
}
}
}

Related

SwiftUI show List of Dates from Core Data

I try to show the time from in Core Data saved Dates in a List, but it isn't showing anything in the rows of the List. But it creates one row for every entry in Core Data.
struct TopBoxView: View {
#Environment(\.managedObjectContext) var newFlaschen
#FetchRequest(sortDescriptors: []) var flasche: FetchedResults<Flasche>
let dateFormatter = DateFormatter()
var body: some View {
GroupBox() {
VStack {
Text("Letzte Flaschen")
.foregroundColor(.white)
.font(.system(size: 28, weight: .medium, design: .default))
.padding(.top, 25)
List(flasche) { flaschen in //here it should create the List
Text("\(flaschen.zeit ?? "nix")")
.listRowBackground(Color("listRowColor"))
}
.scrollContentBackground(.hidden)
.padding(.horizontal, 13.0)
.multilineTextAlignment(.center)
_VSpacer()
.frame(height: 10)
}
}
.groupBoxStyle(GroupBoxStandardStyle(width: 290))
.padding(.top, 10)
.scrollIndicators(.hidden)
}
}
At the Moment the dates are already saved as Strings in CoreData:
struct BottomBoxView: View {
let dateFormatter = DateFormatter()
#State var menge = ""
#State var time = Date.now
#State var anmerkungen = ""
#Environment(\.managedObjectContext) var newFlasche
#FetchRequest(sortDescriptors: []) var flasche: FetchedResults<Flasche>
func submit() {
let neueFlasche = Flasche(context: newFlasche)
let formattedTime = dateFormatter.string(from: time)
neueFlasche.id = UUID()
neueFlasche.menge = menge
neueFlasche.zeit = formattedTime
neueFlasche.anmerkungen = anmerkungen
try? newFlasche.save()
}
the function is called by a button
I should add, that when i try to show another String, that is created as a string, not as a Date, it shows it without problems.
I tried .formatted and i tried to save it as a string
I don't know what i did different to some of my previous tries, but now I have the Time saved as a Date and with this code I show it:
List(flasche) { flasche in
Text((flasche.zeit?.formatted() ?? "none"))
.listRowBackground(Color("listRowColor"))
}
Thank you, for your Help!

How to make the date change simultaneously in all views?

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

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 Comparison

I need to compare 2 dates and if it is the same date, do something... (dd/MM to dd/MM only). I generate today's date, then set my own date and when compared, the same dates do not match. (When displaying each date (as Text), the result is the same. E.g. 10-04 and 10-04.)
struct eddEntryView : View { // ContentView
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM"
return formatter
}()
#State private var today = Date()
let specificDate: Date = {
var components = DateComponents()
components.day = 14
components.month = 04
let newDate = Calendar.current.date(from: components) ?? Date()
return newDate
}()
var entry: Provider.Entry
var body: some View {
HStack{
VStack(alignment: .leading){
if Calendar.current.isDateInToday(specificDate) // today is 14-04, specific date is set for 14-04, but it doesn't work
{
Text("THE SAME: \(specificDate, formatter: dateFormatter)")
.font(.title)
.foregroundColor(Color(.label))
.bold()
Spacer()
} else {
Text("Not the same day... \(specificDate, formatter: dateFormatter)")
.font(.title)
.foregroundColor(Color(.label))
.bold()
Spacer()
}
}
}
There is a function isDateInToday(Date) on Calendar. You could use it like this:
if Calendar.current.isDateInToday(dueDate) {
//...
}
There is also a function that compares whether to date fall on the same day:
Calendar.current.isDate(today, inSameDayAs: dueDate)
Solution:
let today = Date() // Actual date
func formatDate(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM"
return formatter.string(from: date)
}
and then:
if ("\(formatDate(date: today))") .elementsEqual("14-04") // specific date {
Text("the same: \(formatDate(date: today))")
// ... do something for the same day
Spacer()
} else {
// ...
}

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.