I've been looking for a way in SwiftUI to get today's view only with hour interval's like iOS's Calendar app, if SwiftUI have default built in view for something like that then that's great, if not and there is a GitHub library to do the work then I'm okay with using that as well, I found online only a way to show a Year / Month view in SwiftUI:
import SwiftUI
fileprivate extension DateFormatter {
static var month: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM"
return formatter
}
static var monthAndYear: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
return formatter
}
}
fileprivate extension Calendar {
func generateDates(
inside interval: DateInterval,
matching components: DateComponents
) -> [Date] {
var dates: [Date] = []
dates.append(interval.start)
enumerateDates(
startingAfter: interval.start,
matching: components,
matchingPolicy: .nextTime
) { date, _, stop in
if let date = date {
if date < interval.end {
dates.append(date)
} else {
stop = true
}
}
}
return dates
}
}
struct WeekView<DateView>: View where DateView: View {
#Environment(\.calendar) var calendar
let week: Date
let content: (Date) -> DateView
init(week: Date, #ViewBuilder content: #escaping (Date) -> DateView) {
self.week = week
self.content = content
}
private var days: [Date] {
guard
let weekInterval = calendar.dateInterval(of: .weekOfYear, for: week)
else { return [] }
return calendar.generateDates(
inside: weekInterval,
matching: DateComponents(hour: 0, minute: 0, second: 0)
)
}
var body: some View {
HStack {
ForEach(days, id: \.self) { date in
HStack {
if self.calendar.isDate(self.week, equalTo: date, toGranularity: .month) {
self.content(date)
} else {
self.content(date).hidden()
}
}
}
}
}
}
struct MonthView<DateView>: View where DateView: View {
#Environment(\.calendar) var calendar
let month: Date
let showHeader: Bool
let content: (Date) -> DateView
init(
month: Date,
showHeader: Bool = true,
#ViewBuilder content: #escaping (Date) -> DateView
) {
self.month = month
self.content = content
self.showHeader = showHeader
}
private var weeks: [Date] {
guard
let monthInterval = calendar.dateInterval(of: .month, for: month)
else { return [] }
return calendar.generateDates(
inside: monthInterval,
matching: DateComponents(hour: 0, minute: 0, second: 0, weekday: calendar.firstWeekday)
)
}
private var header: some View {
let component = calendar.component(.month, from: month)
let formatter = component == 1 ? DateFormatter.monthAndYear : .month
return Text(formatter.string(from: month))
.font(.title)
.padding()
}
var body: some View {
VStack {
if showHeader {
header
}
ForEach(weeks, id: \.self) { week in
WeekView(week: week, content: self.content)
}
}
}
}
struct CalendarView<DateView>: View where DateView: View {
#Environment(\.calendar) var calendar
let interval: DateInterval
let content: (Date) -> DateView
init(interval: DateInterval, #ViewBuilder content: #escaping (Date) -> DateView) {
self.interval = interval
self.content = content
}
private var months: [Date] {
calendar.generateDates(
inside: interval,
matching: DateComponents(day: 1, hour: 0, minute: 0, second: 0)
)
}
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(months, id: \.self) { month in
MonthView(month: month, content: self.content)
}
}
}
}
}
struct RootView: View {
#Environment(\.calendar) var calendar
private var year: DateInterval {
calendar.dateInterval(of: .year, for: Date())!
}
var body: some View {
CalendarView(interval: year) { date in
Text("30")
.hidden()
.padding(8)
.background(Color.blue)
.clipShape(Circle())
.padding(.vertical, 4)
.overlay(
Text(String(self.calendar.component(.day, from: date)))
)
}
}
}
and I'm trying to get only today's view like this one (without the days of the month at the top):
UPDATE:
My progress so far to recreate the timeline view.
import SwiftUI
struct TimelineView: View {
let hours: [String] = ["12 AM","1 AM", "2 AM", "3 AM", "4 AM", "5 AM", "6 AM", "7 AM", "8 AM", "9 AM", "10 AM", "11 AM", "12 PM", "1 PM", "2 PM", "3 PM", "5 PM", "6 PM", "7 PM", "8 PM", "9 PM", "10 PM", "11 PM"]
var body: some View {
ScrollView {
HStack {
VStack(spacing: 24) {
ForEach(hours, id: \.self) { hour in
HStack {
Text(hour)
.font(Font.custom("Avenir", size: 9))
.frame(width: 28, height: 20, alignment: .center)
VStack {
Divider()
}
}
}
}
Spacer()
}
}
.padding(.top, 150)
.padding(.bottom, 32)
.padding(.horizontal, 16)
.background(backgroundPrimary)
.ignoresSafeArea()
}
}
I need help re structuring the array of time that I made our strings because I'm pretty sure its not the right way, plus I would like to make it Date type as I have events coming with time stamp and would need to compare the array of time I have with the event's time to see which block it will fill 🤔
There is no built-in way that will provide you functionality you needed but i can give you a hint so that you can create this on your own using Spreadsheet -> fill your timeline in spreadsheet and then add a horizontal view that have width match to its parent's offset and height of 2 or 3, then you can add timer of 60 second that will update position of this line inside parent offset using some calculations.
Related
I have made a qr code generator with date and I want to add another picker for hours, and I have tried to conver Int to String, there isn't any problem, when I tried to convert multiple Int to string and it doesn't work and also i want to change the Int format to something like this 12 02:11, first is integer space and time. how could I do that?
My Code:
struct GenerateQRCode: View {
#Binding var time: Date
#Binding var hours: Int
let hour = ["3","6","9","12"]
let filter = CIFilter.qrCodeGenerator()
let cont = CIContext()
var dateFormatter: DateFormatter {
let df = DateFormatter()
df.dateFormat = "HH:mm"
return df
}
var body: some View {
NavigationView{
Image(uiImage: imageGenerate(times:time, hours: hours))
.interpolation(.none)
.resizable()
.frame(width: 150, height: 150, alignment: .center)
}.navigationBarBackButtonHidden(true)
}
func imageGenerate(hours: Int, times: Date)-> UIImage { //<--here how to add integer parameter?
let str = dateFormatter.string(from: start)
let ts = String(hours)
let com = ts + str
let data = com.data(using: .utf8)
filter.setValue(data, forKey: "inputMessage") //
if let qr = filter.outputImage {
if let qrImage = cont.createCGImage(qr, from: qr.extent){
return UIImage(cgImage: qrImage)
}
}
return UIImage(systemName: "xmark") ?? UIImage()
}
}
Preview:
import Foundation
import SwiftUI
import CoreImage.CIFilterBuiltins
struct DatePicker: View {
#State var Time = Date()
#State var sHours = Int()
#State var navigated = false
let hour = ["3", "6", "9", "12"]
var body: some View {
NavigationView{
VStack{
Section{
Text("Please Select Time")
DatePicker("", selection: $startTime, displayedComponents: [.hourAndMinute])
.datePickerStyle(.wheel)
}
Section{
Text("Please Select Minutes")
Picker(selection: $sMinutes, label: Text("Please Select Minutes"))
{
ForEach(0 ..< minutes.count) {
index in Text(self.minutes[index]).tag(index)
}
}
}
Section
{
NavigationLink(destination: GenerateQRCode(start: $Time, minutes: $sHours), isActive: self.$navigated)
{
Text("Complete")
}
}.padding(100)
}.navigationBarTitle("Visitor")
}
}
}
struct DatePicker_Previews: PreviewProvider {
static var previews: some View {
DatePicker()
}
}
Output: "012:30"
what i expected output, should be like this: "3 12:30" first should be hours and then time. i don't know why it only shows 0 if my picker turns to 3. How can i solve it out?
I want to call "getTestCounts" before displaying "Text(testCounts.counts[index].number)".
However, if I use onAppear, I end up with an infinite loop.
I think that since we are creating unique IDs with UUID and spinning them around in ForEach, we end up with an infinite loop.
Infinite loop when using onAppear in SwiftUI
I tried to solve this problem using init() with reference to
"Cannot use instance member 'calendar' within property initializer; property initializers run before 'self' is available"
The following error occurs.
How can I solve this problem? Thanks for suggestions.
Here is the UI I want to create
I want to display the API response value under the date.
Here is the source code where the infinite loop occurs.
struct CalendarView: View {
#Binding var year: String
#Binding var month: String
#Binding var day: String
#EnvironmentObject var testCounts: TestCounts
var body: some View {
let calendar = Calendar(identifier: .gregorian)
let selectedDate = calendar.date(from: DateComponents(year: Int(year), month: Int(month), day: Int(day)))
let calendarDates = generateDates(selectedDate!)
LazyVGrid(columns: Array(repeating: GridItem(.fixed(60.0), spacing: 0), count: 7), spacing: 0) {
ForEach(calendarDates) { date in
Button(action: {}, label: {
VStack(spacing: 0) {
if let date = date.date, let day = Calendar.current.day(for: date) {
Text("\(day)").fontWeight(.semibold).frame(width: 45, alignment: .leading).foregroundColor(Color("dayTextBrown"))
ForEach(0 ..< testCounts.counts.count, id: \.self) { index in
if testCounts.counts[index].date == DateTime.dateToStr(date) {
Text(testCounts.counts[index].number)
Text(testCounts.counts[index].hcount)
}
}
} else {
Text("").frame(width: 45, alignment: .leading)
}
}.frame(width: 60, height: 60).onAppear {
getTestCounts(date.date ?? Date(), "all")
}
}).background(.white).border(Color("drabBrown"), width: 1)
}
}
}
func getTestCounts(_ date: Date, _ timeType: String) {
let since = Calendar.current.startOfMonth(for: date)
let stringSince = DateTime.dateToStr(since!)
let until = Calendar.current.endOfMonth(for: date)
let stringUntil = DateTime.dateToStr(until!)
TestCountsApi(LoginInformation.shared.token, shopId: LoginInformation.shared.defaultShopId, since: stringSince, until: stringUntil, timeType: timeType).request { json, error, result in
switch result {
case .success, .successWithMessage:
TestCounts.shared.setTestCounts(json!)
case .apiError:
errorMessage = json!.message!
case .communicationError:
errorMessage = error!.localizedDescription
case .otherError:
errorMessage = "otherError"
}
}
}
}
struct CalendarDates: Identifiable {
var id = UUID()
var date: Date?
}
func generateDates(_ date: Date) -> [CalendarDates] {
var days = [CalendarDates]()
let startOfMonth = Calendar.current.startOfMonth(for: date)
let daysInMonth = Calendar.current.daysInMonth(for: date)
guard let daysInMonth = daysInMonth, let startOfMonth = startOfMonth else {
return []
}
for day in 0 ..< daysInMonth {
days.append(CalendarDates(date: Calendar.current.date(byAdding: .day, value: day, to: startOfMonth)))
}
}
guard let firstDay = days.first, let lastDay = days.last,
let firstDate = firstDay.date, let lastDate = lastDay.date,
let firstDateWeekday = Calendar.current.weekday(for: firstDate),
let lastDateWeekday = Calendar.current.weekday(for: lastDate)
else { return [] }
let firstWeekEmptyDays = firstDateWeekday - 1
let lastWeekEmptyDays = 7 - lastDateWeekday
for _ in 0 ..< firstWeekEmptyDays {
days.insert(CalendarDates(date: nil), at: 0)
}
for _ in 0 ..< lastWeekEmptyDays {
days.append(CalendarDates(date: nil))
}
return days
}
class TestCounts: ObservableObject {
struct TestCount {
var date: String
var number: Int
var hcount: Int
}
static let shared = TestCounts()
#Published var counts: [TestCount] = []
func setTestCounts(_ json: TestCountsJson) {
counts = []
if let countsJsons = json.counts {
for countJson in countsJsons {
counts.append(TestCount(
date: countJson.date ?? "",
number: countJson.number ?? 0,
hcount: countJson.hcount ?? 0
))
}
}
}
}
Here is the source code that tries to use init().
struct CalendarView: View {
#Binding var year: String
#Binding var month: String
#Binding var day: String
#EnvironmentObject var testCounts: TestCounts
let calendar = Calendar(identifier: .gregorian)
let selectedDate = calendar.date(from: DateComponents(year: Int(year), month: Int(month), day: Int(day)))
let calendarDates = generateDates(selectedDate!)
var body: some View {
LazyVGrid(columns: Array(repeating: GridItem(.fixed(60.0), spacing: 0), count: 7), spacing: 0) {
ForEach(calendarDates) { date in
Button(action: {}, label: {
VStack(spacing: 0) {
if let date = date.date, let day = Calendar.current.day(for: date) {
Text("\(day)").fontWeight(.semibold).frame(width: 45, alignment: .leading).foregroundColor(Color("dayTextBrown"))
ForEach(0 ..< testCounts.counts.count, id: \.self) { index in
if testCounts.counts[index].date == DateTime.dateToStr(date) {
Text(testCounts.counts[index].number)
Text(testCounts.counts[index].hcount)
}
}
} else {
Text("").frame(width: 45, alignment: .leading)
}
}.frame(width: 60, height: 60)
}).background(.white).border(Color("drabBrown"), width: 1)
}
}
}
}
class TestViewModel {
var date: Date
var timeType: String
var errorMessage = ""
init (date: Date, timeType: String) {
self.date = date
self.timeType = timeType
getTestCounts(date, timeType)
}
func getTestCounts(_ date: Date, _ timeType: String) {
let since = Calendar.current.startOfMonth(for: date)
let stringSince = DateTime.dateToStr(since!)
let until = Calendar.current.endOfMonth(for: date)
let stringUntil = DateTime.dateToStr(until!)
TestCountsApi(LoginInformation.shared.token, shopId: LoginInformation.shared.defaultShopId, since: stringSince, until: stringUntil, timeType: timeType).request { json, error, result in
switch result {
case .success, .successWithMessage:
print("success")
TestCounts.shared.setTestCounts(json!)
case .apiError:
self.errorMessage = json!.message!
print("errorMessage")
case .communicationError:
self.errorMessage = error!.localizedDescription
print("errorMessage")
case .otherError:
self.errorMessage = "otherError"
print("errorMessage")
}
}
}
}
I need help with dates. How do you initialize the DatePicker with a stored date? Say, for example, that the user entered an entry with a date. He then determines the date is wrong and would like to change the date. But currently the DatePicker in this code will always default to today's date instead of the stored date.
The state parameter startDate can't be initialized with a stored date above the body. It appears that I need to set startDate in the body.
import SwiftUI
import Combine
struct GetDate: View {
#ObservedObject var userData : UserData = UserData()
#State private var startDate = Date()
var body: some View {
//let startDate = userData.startDate
VStack (alignment: .leading) {
Form {
Text("startdate 1 = \(startDate)")
// enter start date
Section(header: Text("Enter Start Date")) {
HStack {
Image(systemName: "calendar.badge.clock")
.resizable()
.foregroundColor(Color(.systemRed))
.frame(width: 35, height: 35)
Spacer()
DatePicker("", selection: $startDate, in: ...Date(), displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
}
}
Button ( action: {
userData.saveDates(startDate: startDate)
}) {
HStack {
Spacer()
Text("Save Dates?")
Spacer()
}
}
}
.font(.body)
.navigationBarTitle("Get Date", displayMode: .inline)
}
}
}
class UserData: ObservableObject {
#Published var startDate: Date = Date() {
didSet {
UserDefaults.standard.set(startDate, forKey: "startDate") // save
}
}
init() {
// save / retrieve trip dates
if let sdate = UserDefaults.standard.object(forKey: "startDate") as? Date {
startDate = sdate
print("startDate 2 = \(startDate)")
} else {
UserDefaults.standard.set(Date(), forKey: "startDate")
}
}
func saveDates(startDate: Date) -> () {
self.startDate = startDate
UserDefaults.standard.set(self.startDate, forKey: "startDate")
print("startDate 3 = \(self.startDate)")
}
}
Use #AppStorage property wrapper:
struct GetDate: View {
#AppStorage("startDate") var startDate = Date()
#State private var selectedDate = Date()
var body: some View {
VStack (alignment: .leading) {
Form {
Text("startdate 1 = \(startDate)")
Section(header: Text("Enter Start Date")) {
HStack {
Image(systemName: "calendar.badge.clock")
.resizable()
.foregroundColor(Color(.systemRed))
.frame(width: 35, height: 35)
Spacer()
DatePicker("", selection: $selectedDate, in: ...Date(), displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
}
}
Button ( action: {
startDate = selectedDate // save
}) {
HStack {
Spacer()
Text("Save Dates?")
Spacer()
}
}
}
.font(.body)
.navigationBarTitle("Get Date", displayMode: .inline)
}
.onAppear {
selectedDate = startDate
}
}
}
// Support #AppStorage for `Date` type.
extension Date: RawRepresentable {
private static let formatter = ISO8601DateFormatter()
public var rawValue: String {
Date.formatter.string(from: self)
}
public init?(rawValue: String) {
self = Date.formatter.date(from: rawValue) ?? Date()
}
}
Just reset startDate to userData.startDate in .onAppear().
.onAppear {
startDate = userData.startDate
}
I have a basic SwiftUI date picker that shows a calendar widget when tapped:
DatePicker(
"Date",
selection: $date,
in: ...Date(),
displayedComponents: [.date]
)
When you select a date (8th October in the example above), the calendar remains on screen and in order to collapse it, you need to tap outside of it.
Is it possible to automatically collapse it when a date is selected?
I ended up with a rather hacky solution that seems to do the job:
Add a #State variable that holds the calendar ID:
#State private var calendarId: Int = 0
Chain the DatePicker call with .id, .onChange and .onTapGesture actions:
DatePicker(
"Date", selection: $date, in: ...Date(), displayedComponents: [.date]
)
.id(calendarId)
.onChange(of: date, perform: { _ in
calendarId += 1
})
.onTapGesture {
calendarId += 1
}
#chris.kobrzak provided a good direction, and I ended up solving this with:
struct ContentView: View {
#State var calendarId: UUID = UUID()
#State var someday: Date = Date()
var body: some View {
VStack {
DatePicker("Day", selection: $someday, displayedComponents: [.date])
.labelsHidden()
.id(calendarId)
.onChange(of: whatday) { _ in
calendarId = UUID()
}
AnotherView(someday)
}
}
}
This is just an updated answer following #Chris Kobrzak as above.
I am using XCode 14.1 and iOS 15+ and 16+ (iPad and iPhone) and it seems to work without error today in Nov 2022.
I have seen some folk using the same .id() method complain that it doesn’t work.
I haven’t tested this but note that I am using the CompactDatePickerStyle(), maybe it doesn’t work the same on other styles.
The reason this hack works is the .id() is for the ‘view’ (DatePicker being a view). When you change the id of a view you basically reset it (in this case closing the DatePicker).
There is a good explanation about .id() here: https://swiftui-lab.com/swiftui-id/
Why this isn’t built into the control seems rather a joke but hey…
Note I have ripped the following out of a real App. I've edited it in a dumb text editor to post on here so there may be some silly syntax errors and odd remnants of the original code.
import SwiftUI
struct FooView: View {
#Published var dateOfBirth: Date = Date()
#State private var datePickerId: Int = 0
private var dateOfBirthRange: ClosedRange<Date> {
let dateFrom = Calendar.current.date(byAdding: .year, value: -160, to: Date())!
let dateTo: Date = Date()
return dateFrom...dateTo
}
var body: some View {
Form {
ZStack(alignment: .leading) {
Text("Date of Birth")
.offset(y: -36)
.foregroundColor(Color.accentColor)
.scaleEffect(0.9, anchor: .leading)
DatePicker(
"",
selection: $dateOfBirth,
in: dateOfBirthRange,
displayedComponents: .date
)
.datePickerStyle(CompactDatePickerStyle())
.labelsHidden()
.id(datePickerId)
.onChange(of: dateOfBirth) { _ in
datePickerId += 1
}
}
.padding(.top, 24)
.animation(.default, value: "")
}
}
}
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 but that's ok.
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
}
}
}
I need to be able to add or minus 1 month from the current date.
So far I have this code:
import SwiftUI
struct DateView: View {
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("yyyy MMMM")
return formatter
}()
var date = Date()
var body: some View {
HStack {
Button(action: {
print("Button Pushed")
}) {
Image(systemName: "chevron.left")
.padding()
}
Spacer()
Text("\(date, formatter: Self.dateFormat)")
Spacer()
Button(action: {
print("Button Pushed")
}) {
Image(systemName: "chevron.right")
.padding()
}
}
.padding()
.background(Color.yellow)
}
}
struct DateView_Previews: PreviewProvider {
static var previews: some View {
DateView()
}
}
I would like to change the date displayed to be +1 month or -1 month depending on which chevron I will tap.
I am new to swift and swiftui and don't know what action I should use. I think it's related to DateComponents, but what should I do about it now? I am stuck. Please help me.
To better visualise what I have and want to do, here is an image of my current result:
Swift 5
Function to add or subtract month from current date.
func addOrSubtractMonth(month: Int) -> Date {
Calendar.current.date(byAdding: .month, value: month, to: Date())!
}
Now calling the function
// Subtracting
var monthSubtractedDate = addOrSubtractMonth(-7)
// Adding
var monthAddedDate = addOrSubtractMonth(7)
To Add date pass prospective value
To Subtract pass negative value
You can use the Calendar to add or subtract months/days/hours etc to your Date. Apple's documentation on the Calendar can be found here.
Below is a working example, showing how to increase/decrease the month by 1.
struct ContentView: View {
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("yyyy MMMM")
return formatter
}()
#State var date = Date()
var body: some View {
HStack {
Button(action: {
print("Button Pushed")
self.changeDateBy(-1)
}) {
Image(systemName: "chevron.left")
.padding()
}
Spacer()
Text("\(date, formatter: Self.dateFormat)")
Spacer()
Button(action: {
print("Button Pushed")
self.changeDateBy(1)
}) {
Image(systemName: "chevron.right")
.padding()
}
}
.padding()
.background(Color.yellow)
}
func changeDateBy(_ months: Int) {
if let date = Calendar.current.date(byAdding: .month, value: months, to: date) {
self.date = date
}
}
}