Date only updates when app is refreshed SwiftUI - 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

Related

SwiftUI DatePicker not translating titleKey

In the below code the Text contents are translated, but the DatePicker title is not. Both keys exist inside the localization file.
Any idea why this happens?
import SwiftUI
struct TestView: View {
#ObservedObject var viewModel: ViewModel
private let dateRange: ClosedRange<Date> = {
let calendar = Calendar.current
let now = Date()
let past = calendar.date(byAdding: DateComponents(day: 1), to: calendar.date(byAdding: DateComponents(year: -1), to: now)!)!
return past
...
now
}()
var body: some View {
Form {
Text("estimated_monthly_price")
DatePicker("purchase_date", selection: $viewModel.purchaseDate, in: dateRange, displayedComponents: [.date])
}
}
}
this is in Xcode 12.4
If you check DataPicker initialiser its titleKey parameter accepts type as LocalizedStringKey. So try passing it as LocalizedStringKey("purchase_date”) in DataPicker.
public init(_ titleKey: LocalizedStringKey, selection: Binding<Date>, in range: PartialRangeThrough<Date>, displayedComponents: DatePicker<Label>.Components = [.hourAndMinute, .date])

Passing Lat/Long to function in SwiftUI

I am building a SwiftUI app that shows data based on user lat/long. I have based my code off of this sample provided by the framework dev.
With SwiftUI I have my LocationManager set as:
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate{
#Published var userLatitude: Double = 0.0
#Published var userLongitude: Double = 0.0
private let locationManager = CLLocationManager()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.distanceFilter = 100.0
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
userLatitude = location.coordinate.latitude
userLongitude = location.coordinate.longitude
print("Hello I'm here! \(location)")
}
}
Whenever I go back to my ContentView and try to read the Lat/Long it just shows up as 0.0. but if I output them within the body the values show up correctly.
struct ContentView: View {
#State private var times = prayerTimes()
#ObservedObject var locationViewModel = LocationViewModel()
var body: some View {
NavigationView {
PrayerTimeView(times: $times)
.navigationBarTitle(Text("Prayer Times"))
}
}
static func prayerTimes() -> PrayerTimes? {
let cal = Calendar(identifier: Calendar.Identifier.gregorian)
let date = cal.dateComponents([.year, .month, .day], from: Date())
let coordinates = Coordinates(latitude: locationViewMode.userLatitude, longitude: locationViewMode.userLongitude)
var params = CalculationMethod.moonsightingCommittee.params
params.madhab = .hanafi
return PrayerTimes(coordinates: coordinates, date: date, calculationParameters: params)
}
}
prayerTimes() only call once when you init the view. Why don't you make times as a #Published of your ViewModel. When location changes, just update that value.
PrayerTimeView(times: $viewmodel.times)
.navigationBarTitle(Text("Prayer Times"))

Why is #ObservableObject View Model Reset When Timer.TimerPublisher Fires?

I have a type that vends a Timer.TimerPublisher, which you can see below:
import Combine
import Foundation
struct TimerClient {
// MARK: Properties
var timerValueChange: () -> AnyPublisher<Date, Never>
// MARK: Initialization
init(
timerValueChange: #escaping () -> AnyPublisher<Date, Never>
) {
self.timerValueChange = timerValueChange
}
}
extension TimerClient {
// MARK: Properties
static var live: Self {
Self {
return Timer
.TimerPublisher(
interval: 1,
runLoop: .main,
mode: .common
)
.autoconnect()
.share()
.eraseToAnyPublisher()
}
}
}
I have a View that is used to save new events. It is injected with a view model that uses TimerClient so that I can disable the Save button if users attempt to save an event with a past date:
import Combine
final class CountdownEventEntryViewModel: ObservableObject {
// MARK: Properties
private let nowSubject = CurrentValueSubject<Date, Never>(Date())
private let calendar: Calendar
private let timerClient: TimerClient
private var cancellables = Set<AnyCancellable>()
#Published var eventTitle = ""
#Published var isAllDay = false
#Published var eventDate = Date()
#Published var eventTime = Date()
#Published var shouldDisableSaveButton = true
// MARK: Initialization
init(
calendar: Calendar = .autoupdatingCurrent,
timerClient: TimerClient
) {
self.calendar = calendar
self.timerClient = timerClient
observeTimer()
observeCurrentDateChanges()
}
// MARK: UI Configuration
private func disableSaveButton() -> Bool {
if eventTitle.trimmingCharacters(in: .whitespaces).isEmpty {
return true
}
if isAllDay {
let startOfToday = calendar.startOfDay(for: nowSubject.value)
let startOfSelectedDate = calendar.startOfDay(for: eventDate)
return startOfSelectedDate <= startOfToday
} else {
return normalizedSelectedDate() <= nowSubject.value
}
}
private func observeTimer() {
timerClient.timerValueChange().sink(receiveValue: { [weak self] newDate in
self?.nowSubject.send(newDate)
})
.store(in: &cancellables)
}
private func observeCurrentDateChanges() {
nowSubject.sink(receiveValue: { [weak self] _ in
self?.shouldDisableSaveButton = self?.disableSaveButton() ?? false
})
.store(in: &cancellables)
}
}
The view model works well, and it updates the Save button if an attempt is made to select a past date:
import SwiftUI
struct CountdownEventEntryView: View {
// MARK: Properties
#ObservedObject var viewModel: CountdownEventEntryViewModel
var body: some View {
Form {
Section {
TextField(
viewModel.eventTitlePlaceholderKey,
text: $viewModel.eventTitle
)
}
Section {
Toggle(
isOn: $viewModel.isAllDay.animation(),
label: {
Text(viewModel.isAllDayLabelTextKey)
}
)
DatePicker(
viewModel.eventDatePickerLabelTextKey,
selection: $viewModel.eventDate,
displayedComponents: [.date]
)
if !viewModel.isAllDay {
DatePicker(
viewModel.eventTimePickerLabelTextKey,
selection: $viewModel.eventTime,
displayedComponents: [.hourAndMinute]
)
}
}
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text(viewModel.saveButtonTitleKey)
}
.disabled(viewModel.shouldDisableSaveButton)
}
}
}
}
After the entry view is dismissed, the List that displays saved events is updated according to the following view model:
import Combine
import CoreData
final class CountdownEventsViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
// MARK: Properties
private let calendar: Calendar
private let timerClient: TimerClient
private var cancellables = Set<AnyCancellable>()
#Published var now = Date()
#Published var countdownEvents = [CountdownEvent]()
// MARK: Initialization
init(
calendar: Calendar = .autoupdatingCurrent,
timerClient: TimerClient
) {
self.calendar = calendar
self.timerClient = timerClient
super.init()
observeTimer()
fetchCountdownEvents()
}
// MARK: Timer Observation
private func observeTimer() {
timerClient.timerValueChange().sink(receiveValue: { [weak self] newDate in
self?.now = newDate
})
.store(in: &cancellables)
}
// MARK: Event Display
func formattedTitle(for event: CountdownEvent) -> String {
event.title ?? untitledLabelKey
}
func formattedDate(for event: CountdownEvent) -> String {
guard let date = event.date else {
return dateUnknownLabelKey
}
if event.isAllDay {
return DateFormatter.dateOnlyFormatter.string(from: date)
}
return DateFormatter.dateAndTimeFormatter.string(from: date)
}
func formattedTimeRemaining(from date: Date, to event: CountdownEvent) -> String {
guard let eventDate = event.date else {
return dateUnknownLabelKey
}
let allowedComponents: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let dateComponents = calendar.dateComponents(allowedComponents, from: date, to: eventDate)
guard let formatted = DateComponentsFormatter.eventTimeRemainingFormatter.string(from: dateComponents) else {
return dateUnknownLabelKey
}
return formatted
}
}
The view model is used in a View that displays all saved items in a list:
import SwiftUI
struct CountdownEventsView: View {
// MARK: Properties
#ObservedObject private var viewModel: CountdownEventsViewModel
#State private var showEventEntry = false
#State private var now = Date()
var body: some View {
List {
Section {
ForEach(viewModel.countdownEvents) { event in
VStack(alignment: .leading) {
Text(viewModel.formattedTitle(for: event))
Text(viewModel.formattedDate(for: event))
Text(viewModel.formattedTimeRemaining(from: now, to: event))
}
}
}
}
.toolbar {
ToolbarItem(placement: .navigation) {
HStack(spacing: 30) {
Button(action: showSettings) {
Label(viewModel.settingsButtonTitleKey, systemImage: viewModel.settingsButtonImageName)
}
Button(action: {
showEventEntry = true
}, label: {
Label(
title: { Text(viewModel.addEventButtonTitleKey) },
icon: { Image(systemName: viewModel.addEventButtonImageName) }
)
})
}
}
.sheet(
isPresented: $showEventEntry,
content: {
NavigationView {
CountdownEventEntryView(
viewModel:
CountdownEventEntryViewModel(
timerClient: .live
)
)
}
}
)
.onReceive(viewModel.$now, perform: { now in
self.now = now
})
}
// MARK: Initialization
init(viewModel: CountdownEventsViewModel) {
self.viewModel = viewModel
}
}
This works as expected, and the Text elements are updated with the expected values. Additionally, I am able to scroll the List and see the Text values update thanks to adding the Timer to the main runloop and common mode. However, when I navigate to the event-entry view, the view model seems to get reset when the timer fires.
Below, you can see that the entered text is reset with every fire of the timer:
It seems that my view model is somehow being recreated, but both view models are #ObservableObjects, so I am not sure why I'm seeing the values reset after the timer fires. The DatePicker, Toggle, TextField, and Button values are all reset to their defaults when the timer fires.
What am I missing that is causing the text to clear when the timer fires?
If it helps, the project is located here. Be sure to use the save-countdown-events branch. Additionally, I am modeling my client types after Point-Free's teachings about dependency injection (code), if that helps provide more context.

SwiftUI Timer.publish causing whole screen to refresh

GIF of Entire Screen Refreshing
I am currently learning combine and MVVM. My problem is when I try to use a timer.publish, eventually I'm going to create a stop button, it causes the entire screen to refresh instead of the Text I have .onReceive.
I was hoping someone could provide me some insight on how I'm using publishers and observers incorrectly.
View:
import SwiftUI
import Combine
struct ApptCardView: View {
#ObservedObject var apptCardVM: ApptCardViewModel
#State var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text("\(currentDate)")
.onReceive(timer) { input in
self.currentDate = input
}
Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
ForEach(0..<apptCardVM.typeChoice.count) {
Text(self.apptCardVM.typeChoice[$0])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
View Model:
import Foundation
import Combine
class ApptCardViewModel: ObservableObject, Identifiable {
#Published var appt: ApptEvent
#Published var typeChoice = ["Quick", "Long", "FullService"]
#Published var typeIndex: Int = 0
private var cancellables = Set<AnyCancellable>()
init(appt: ApptEvent) {
self.appt = appt
}
}
If you want to refresh only a part of body, then separate that part into dedicated subview, eg:
struct ApptCardView: View {
#ObservedObject var apptCardVM: ApptCardViewModel
var body: some View {
VStack {
CurrentDateView() // << here !!
Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
ForEach(0..<apptCardVM.typeChoice.count) {
Text(self.apptCardVM.typeChoice[$0])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
struct CurrentDateView: View {
#State private var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(currentDate)")
.onReceive(timer) { input in
self.currentDate = input // << refresh only own body !!
}
}
}

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