ObservableObject with DatePicker in SwiftUI - swiftui

Can you help me with the following source code:
import SwiftUI
class DateHandler {
/* create a variable
Class to manage start date and end date
*/
var date1 : Date {
didSet{
print("StartDate: \(date1)")
}
}
var date2 : Date {
didSet{
print("EndDate: \(date2)")
}
}
var startDate:Date {
get{
return date1
}
set(newValue){
date1 = toLocalTime(date: newValue)
}
}
var endDate:Date {
get{
return date2
}
set(newValue){
date2 = addOneDay(date: toLocalTime(date: newValue))
}
}
init(){
self.date1 = Date()
self.date2 = Date()
}
//Customize time zone
func toLocalTime(date : Date) -> 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
let newDate = calendar.date(bySettingHour: 00, minute: 00, second: 00, of: date)
let dateLocalTimezone = Date(timeInterval: seconds, since: newDate!)
return dateLocalTimezone
}
func addOneDay(date : Date) -> Date {
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: date)
return tomorrow!
}
}
//Class to use DateHandler() as a ObservableObject
class DateHandlerHelper : ObservableObject{
#Published var dateHandler = DateHandler()
}
struct ContentView: View {
#ObservedObject var dateHandlerHelper = DateHandlerHelper()
#State var startDateHelper = Date()
#State var endDateHelper = Date()
var body: some View {
DatePicker("Date1", selection: $startDateHelper)
.onAppear(perform: {
print("onAppear")
dateHandlerHelper.dateHandler.startDate = startDateHelper
print(dateHandlerHelper.dateHandler.startDate)
})
.onChange(of: startDateHelper) { newValue in
print("onChange")
dateHandlerHelper.dateHandler.startDate = newValue
if newValue > endDateHelper{
print("Correction Date 1")
startDateHelper = endDateHelper
dateHandlerHelper.dateHandler.startDate = endDateHelper
}
print(dateHandlerHelper.dateHandler.startDate)
}
DatePicker("Date2", selection: $endDateHelper)
.onAppear(perform: {
dateHandlerHelper.dateHandler.endDate = endDateHelper
print(dateHandlerHelper.dateHandler.endDate)
})
.onChange(of: endDateHelper) { newValue in
print("onChange")
dateHandlerHelper.dateHandler.endDate = newValue
print(dateHandlerHelper.dateHandler.endDate)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have a view with two DatePickers to select a start date and an end date. For the start date and end date there is the class DateHandler.
I want to handle the DateHandler class as an ObservableObject. Because of the getter/setter function used, I can only address the DateHandler class indirectly through the DateHandlerHelper class. Is there a better/simpler way here?
If I address the variables dateHandlerHelper.dateHandler.startDate and dateHandlerHelper.dateHandler.endDate directly via the DatePickers, I can change the values, but the DatePickers themselves are not updated. For this reason I have introduced the variables startDateHelper and endDateHelper in the view. Do you have any idea how I can do this differently to save me the variables?

I don't see why you need to do those date calculations every time instead I changed so that they are done once when the DateHelper class is initialized.
class DateHandler: ObservableObject {
#Published var startDate: Date
#Published var endDate: Date
init(){
startDate = Self.toLocalTime(date: .now)
//I assume endDate also should be adjusted for local time?
let date = Self.addOneDay(date: .now)
endDate = Self.toLocalTime(date: date)
}
private static func toLocalTime(date : Date) -> Date {
// removed for brevity
}
private static func addOneDay(date : Date) -> Date {
// removed for brevity
}
}
And then the view would be much simpler
struct ContentView: View {
#ObservedObject var dateHandler = DateHandler()
var body: some View {
VStack {
DatePicker("Date1", selection: $dateHandler.startDate)
DatePicker("Date2", selection: $dateHandler.endDate)
}
}
}

This seems to be the right way:
class DateModel : ObservableObject{
var date1 : Date{
didSet{
print(date1)
}
}
var date2 : Date{
didSet{
print(date2)
}
}
var startDate:Date {
get{
return date1
}
set(newValue){
date1 = toLocalTime(date: newValue)
startDateString = date1.toString()
print(startDateString)
}
}
var endDate:Date {
get{
return date2
}
set(newValue){
date2 = toLocalTime(date: newValue)
}
}
init(){
self.date1 = Date()
self.date2 = Date()
}
//Customize time zone
func toLocalTime(date : Date) -> Date {
//Selection of the current calendar
let calendar = Calendar.current
//Time zone selection
let timezone = TimeZone.current
//Determine number of seconds between time zone and GMT
let seconds = TimeInterval(timezone.secondsFromGMT(for: date))
//Adjusting the input value
let newDate = calendar.date(bySettingHour: 00, minute: 00, second: 00, of: date)
let dateLocalTimezone = Date(timeInterval: seconds, since: newDate!)
return dateLocalTimezone
}
}
struct ContentView: View {
#ObservedObject var dateModel = DateModel()
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
var body: some View {
Form{
Section{
DatePicker("start date", selection: $dateModel.startDate, displayedComponents: [.date]).onChange(of: dateModel.startDate) { newValue in
print(newValue.toString())
}
DatePicker("final date", selection: $dateModel.endDate, displayedComponents: [.date])
}
}
}
}

Related

Date only updates when app is refreshed SwiftUI

I want to print the date, however, it states the sam until I refresh the app. this is my code
struct Dates: View {
func todayDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-dd'T'HH:mm:ssZ"
let today = Date() /// Date() is the current date
let todayAsString = dateFormatter.string(from: today)
return todayAsString
}
var body: some View {
Text(todayDate())
Would anyone know why this is happening?
Display Live Time in SwiftUI
You need to create a Timer with the time interval you want to update the Date label. In this case we will update the label every 1 second. First, the ViewModel (the Timer cannot be declared inside the SwiftUI View because it is an struct):
class ViewModel: ObservableObject {
#Published var currentTime = ""
var timer = Timer()
init() {
let repeatEveryXSeconds: TimeInterval = 1
timer = Timer.scheduledTimer(withTimeInterval: repeatEveryXSeconds, repeats: true, block: { [weak self] timer in
self?.currentTime = DateFormatter.myFormatter.string(from: Date())
})
}
}
extension DateFormatter {
static let myFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm:ss"
return formatter
}()
}
And the view itself:
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
Text("Date is: \(viewModel.currentTime)")
.frame(width: 200)
.padding()
}
}
You can use also CalendarUserNotifications triggers: https://developer.apple.com/documentation/usernotifications/scheduling_a_notification_locally_from_your_app

How to choose certain days that can't be chosen by the user in DatePicker?

Let's say I have a simple DatePicker View:
struct ContentView: View {
#State private var date = Date()
var body: some View {
DatePicker("Date", selection: $date)
.datePickerStyle(GraphicalDatePickerStyle())
}
}
How can I make it so the user is not allowed to tap on either Sundays or Holidays?
Okay, so this was no easy task. After a lot of investigation, I ended up using WenchaoD's FSCalendar.
It took a lot of work, and I added other stuff to the calendar other that what I originally wanted, but this is the end result:
struct MyCalendar: UIViewRepresentable {
typealias UIViewType = FSCalendar
#Binding var selectedDate: Date
#Binding var listOfAvailableTimes: [String]
#Binding var appointmentType: String
#Binding var currentPage: Date
#ObservedObject var eventsManager = EventsManager()
var calendar = FSCalendar()
var today: Date {
return Date()
}
func makeCoordinator() -> MyCalendar.Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> FSCalendar {
eventsManager.fetchHolidays(year: 2021)
calendar.delegate = context.coordinator
calendar.dataSource = context.coordinator
calendar.scrollDirection = .horizontal
calendar.scope = .month
calendar.locale = Locale(identifier: "es")
calendar.appearance.titleFont = UIFont.systemFont(ofSize: 17)
calendar.appearance.headerTitleFont = UIFont.boldSystemFont(ofSize: 18)
calendar.appearance.weekdayFont = UIFont.boldSystemFont(ofSize: 16)
calendar.appearance.todayColor = .none
calendar.appearance.titleTodayColor = .black
calendar.appearance.weekdayTextColor = .systemGray
calendar.appearance.headerTitleColor = .black
calendar.placeholderType = .none
calendar.appearance.headerMinimumDissolvedAlpha = 0
return calendar
}
func updateUIView(_ uiView: FSCalendar, context: Context) {
uiView.setCurrentPage(currentPage, animated: true)
calendar.reloadData()
}
class Coordinator: NSObject, FSCalendarDelegateAppearance, FSCalendarDataSource, FSCalendarDelegate {
fileprivate let gregorian: Calendar = Calendar(identifier: .gregorian)
var parent: MyCalendar
init(_ calendar: MyCalendar) {
self.parent = calendar
}
var formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
return formatter
}()
//STARTING HERE IS WHERE YOU CAN SEE THE CODE TO INDICATE
//WHICH DAYS SHOULD NOT BE SELECTED. I GOT THE LIST OF HOLIDAYS
//FROM AN API. LET ME KNOW IF YOU NEED HELP WITH THIS OTHER PART.
func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, titleDefaultColorFor date: Date) -> UIColor? {
// IF DATE IS A HOLIDAY, IT SHOULD APPEAR RED
let listOfHolidays = self.parent.eventsManager.holidays
for holiday in listOfHolidays {
guard let excludedDate = formatter.date(from: holiday) else { return nil }
if date.compare(excludedDate) == .orderedSame {
return .red
}
}
// IF DATE IS A SUNDAY, IT SHOULD APPEAR RED
if [1].contains((self.gregorian.component(.weekday, from: date))) {
return .red
}
return nil
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
var stringSelectedDate: String {
let year = Calendar.current.component(.year, from: date)
let month = Calendar.current.component(.month, from: date)
let day = Calendar.current.component(.day, from: date)
let stringDate = String(format: "%04d%02d%02d", year, month, day)
return stringDate
}
self.parent.eventsManager.searchAppointment(day: stringSelectedDate, appointmentType: self.parent.appointmentType) { (appointment) in
self.parent.listOfAvailableTimes = appointment?.times ?? [""]
}
self.parent.selectedDate = date
print("Date Selected = \(formatter.string(from: date))")
}
func minimumDate(for calendar: FSCalendar) -> Date {
return Date()
}
func maximumDate(for calendar: FSCalendar) -> Date {
return Date().addingTimeInterval((60 * 60 * 24) * 365)
}
func calendar(_ calendar: FSCalendar, shouldSelect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool {
let listOfHolidays = self.parent.eventsManager.holidays
for holiday in listOfHolidays {
// IF DATE IS A HOLIDAY, IT SHOULD NOT BE CLICKABLE
guard let excludedDate = formatter.date(from: holiday) else { return true }
if date.compare(excludedDate) == .orderedSame {
return false
}
}
// IF DATE IS A SUNDAY, IT SHOULD NOT BE CLICKABLE
if [1].contains((self.gregorian.component(.weekday, from: date))) {
return false
}
return true
}
}
}
So in ContentView, instead of using DatePicker, I used MyCalendar

SwiftUI #EnvironmentObject: Not Found?

I have a struct that I want to turn into an EnvironmentObject so I can pass it to a child struct, but when I do, it crashes by saying MissingEnvironmentObjectError: Missing EnvironmentObject. My struct looks like:
class event: ObservableObject {
#Published var Name: String
#Published var CalendarID: Int
var timeStart: Date
var timeEnd: Date
var checklist = [checklistObject]()
init(_ eventName: String, _ calID: Int, _ timeStart: Date, _ timeEnd: Date) {
Name = eventName
CalendarID = calID
self.timeStart = timeStart
self.timeEnd = timeEnd
logger.log("Successfully created new event")
}
func newChecklistItem(Content: String){
checklist.append(checklistObject(Content, false))
}
func getChecklistSize() -> Int {
return checklist.count
}
}
and my program looks like:
import SwiftUI
struct checklistDisplayRow: View {
#EnvironmentObject var Event: event
var itemID: Int
init(itemID: Int){
self.itemID = itemID
Event.newChecklistItem(Content: "Stuff")
}
var body: some View {
HStack{
Toggle("", isOn: $Event.checklist[itemID].complete)
Text("hi")
}
}
}
struct checklistDisplayRow_Previews: PreviewProvider {
static var previews: some View {
checklistDisplayRow(itemID: 0)
.environmentObject(event("Title", 1, Date(timeIntervalSince1970: 1576800000), Date(timeIntervalSince1970: 1576800060)))
}
}
Thanks in advance for any help.
At the moment, you are setting the environment object for the preview, but you also need to set it in the hierarchy above where you are using checklistDisplayRow.
For example, if you were using a few checklistDisplayRows in a VStack (for the items in an event's checklist), you could do something like this:
VStack {
ForEach(checklist, id: \.id) { item in
checklistDisplayRow(itemID: item.id)
}
}.environmentObject(
event("Title", 1, Date(timeIntervalSince1970: 1576800000), Date(timeIntervalSince1970: 1576800060)
)
I hope that's helpful!

SwiftUI Picker desn't bind with ObservedObject

I'm trying to fill up a Picker with data fetched asynchronously from external API.
This is my model:
struct AppModel: Identifiable {
var id = UUID()
var appId: String
var appBundleId : String
var appName: String
var appSKU: String
}
The class that fetches data and publish is:
class AppViewModel: ObservableObject {
private var appStoreProvider: AppProvider? = AppProvider()
#Published private(set) var listOfApps: [AppModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
self.loading = true
appStoreProvider?.dataProviderAppList { [weak self] (appList: [AppModel]) in
guard let self = self else {return}
DispatchQueue.main.async() {
self.listOfApps = appList
self.loading = false
}
}
}
init() {
fetchAppList()
}
}
The View is:
struct AppView: View {
#ObservedObject var appViewModel: AppViewModel = AppViewModel()
#State private var selectedApp = 0
var body: some View {
ActivityIndicatorView(isShowing: self.appViewModel.loading) {
VStack{
// The Picker doesn't bind with appViewModel
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName).tag(app.appName)
}
}
// The List correctly binds with appViewModel
List {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName.capitalized)
}
}
}
}
}
}
While the List view binds with the observed object appViewModel, the Picker doesn't behave in the same way. I can't realize why. Any help ?
I filed bug report, FB7670992. Apple responded yesterday, suggesting that I confirm this behavior in iOS 14, beta 1. It appears to now have been resolved.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Picker("", selection: $viewModel.wheelPickerValue) {
ForEach(viewModel.objects) { object in
Text(object.string)
}
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
}
Where
struct Object: Identifiable {
let id = UUID().uuidString
let string: String
}
class ViewModel: ObservableObject {
private var counter = 0
#Published private(set) var objects: [Object] = []
#Published var segmentedPickerValue: String = ""
#Published var wheelPickerValue: String = ""
fileprivate func nextSetOfValues() {
let newCounter = counter + 3
objects = (counter..<newCounter).map { value in Object(string: "\(value)") }
let id = objects.first?.id ?? ""
segmentedPickerValue = id
wheelPickerValue = id
counter = newCounter
}
init() {
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
self.nextSetOfValues()
}
timer.fire()
}
}
Results in:
I can't put this into your code because it is incomplete but here is a sample.
Pickers aren't meant to be dynamic. They have to be completely reloaded.
class DynamicPickerViewModel: ObservableObject {
#Published private(set) var listOfApps: [YourModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
loading = true
DispatchQueue.main.async() {
self.listOfApps.append(YourModel.addSample())
self.loading = false
}
}
init() {
fetchAppList()
}
}
struct DynamicPicker: View {
#ObservedObject var vm = DynamicPickerViewModel()
#State private var selectedApp = ""
var body: some View {
VStack{
//Use your loading var to reload the picker when it is done
if !vm.loading{
//Picker is not meant to be dynamic, it needs to be completly reloaded
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.vm.listOfApps){ app in
Text(app.name!).tag(app.name!)
}
}
}//else - needs a view while the list is being loaded/loading = true
List {
ForEach(self.vm.listOfApps){ app in
Text(app.name!.capitalized)
}
}
Button(action: {
self.vm.fetchAppList()
}, label: {Text("fetch")})
}
}
}
struct DynamicPicker_Previews: PreviewProvider {
static var previews: some View {
DynamicPicker()
}
}

SwiftUI ViewModel update

I have a View with many Buttons and if the User tap on a Button the Viewmodel need to update the current Button with the increased value.
class ProductVM: ObservableObject {
#Published var product : Product
init(product: Product) {
self.product = product
}
public func increaseAmount() {
var myInt = Int(self.product.amount) ?? 0
myInt += 1
self.product.amount = String(myInt)
print(myInt)
print("...")
}
}
the problem is the myInt is every time just 1 and the value can't be updated.
HOW can i update the value and save it in the current Model so that the View know its increased ??!!
struct singleButtonView: View {
#ObservedObject var productVM : ProductVM
func updatePos(){
self.productVM.increaseAmount()
}
}
and i call it with
singleButtonView(productVM: ProductVM(product: product))
Nested ObservableObjects need to be updated manually. Here is an example how this could look like:
class Product: ObservableObject, Identifiable, Codable {
let id: Int
let name: String
let prize: Double
#Published var amount: Int = 0
enum CodingKeys: String, CodingKey {
case id
case name
case prize
case amount
}
init() {
self.id = 0
self.name = "name"
self.prize = 0
}
init(id: Int, name: String, prize: Double) {
self.id = id
self.name = name
self.prize = prize
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(Int.self, forKey: .id)
name = try values.decode(String.self, forKey: .name)
prize = try values.decode(Double.self, forKey: .prize)
amount = try values.decode(Int.self, forKey: .amount)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(prize, forKey: .prize)
try container.encode(amount, forKey: .amount)
}
}
class ProductVM: ObservableObject {
#Published var product: Product
var cancelable: AnyCancellable? = nil
init(product: Product) {
self.product = product
self.cancelable = product.objectWillChange.sink(receiveValue: {
self.objectWillChange.send()
})
}
public func increaseAmount() {
self.product.amount += 1
}
}
struct ContentView: View {
#ObservedObject var productVM = ProductVM(product: Product())
var body: some View {
VStack {
Button(action: {
self.productVM.increaseAmount()
}) {
Text("Add")
}
Text("\(self.productVM.product.amount)")
}
}
}
I hope this helps!
Credits