I am new to the swiftUI. Right now, I am making an app that takes down your task progress. In the app, I need to refill the list with goals of user have every day (I guess 12 AM), where and how do I check the time in swift? I know that we could use app delegate in storyboard, but for SwiftUI, after applying CoreData Manager, the app delegate has gone and we have app.swift instead, where should I do the checking now? Thank you!
Building off of Leo Dabus' suggestion to watch for NSCalendarDayChanged notifications here's some code showing how that can be done in SwiftUI.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text(viewModel.displayDate)
// List of goals
}
}
}
class ContentViewModel: ObservableObject {
#Published var currentDate: Date = Date()
var displayDate: String {
Self.simpleDateFormatter.string(from: currentDate)
}
private static let simpleDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM d"
return formatter
}()
init() {
NotificationCenter.default.addObserver(self, selector: #selector(dayDidChange), name: .NSCalendarDayChanged, object: nil)
}
#objc
func dayDidChange() {
currentDate = Date()
}
}
You can use - (void)applicationSignificantTimeChange:(UIApplication *)application; in AppDelegate to monitor such changes.
You can also register for a notification in AppDelegate UIApplication.significantTimeChangeNotification
iOS will call both the registered notification method as well above delegate method.
NotificationCenter.default.addObserver(self, selector: #selector(timeChanged), name: UIApplication.significantTimeChangeNotification , object: nil)
#objc func timeChanged() {
print("App Time Changed")
}
In case you want to hook up with your SwiftUI directly, you can register your Swift view with your publisher.
Publisher will listen for notification name UIApplication.significantTimeChangeNotification.
Either of the ways can be used based on your requirement.
struct ContentView: View {
#State var dayDetails: String = "Hello World"
var body: some View {
Text(dayDetails)
.padding().onReceive(NotificationCenter.default.publisher(for: UIApplication.significantTimeChangeNotification), perform: { _ in
dayDetails = "Day has changed"
})
}
}
You can use the NSCalendarDayChanged notification to execute some code when the day changes.
struct ContentView: View {
#State var text: String = "Hello World"
var body: some View {
Text(text)
.onReceive(NotificationCenter.default.publisher(for: Notification.Name.NSCalendarDayChanged)) { _ in
text = "Day has changed"
})
}
}
Related
I don't know how to read a property that is in a struct from a class that is an Observable Object.
Context:
I'm trying to build an app which consists of 2 views:
a custom calendar;
a popup with a header 'Daily Joke', date formatted as 'MM-dd-yyyy' and a joke text that is fetched from Firebase using id. When the user clicks on a date in the calendar, the popup appears and shows the joke for a selected date.
The problem is that the 'currentDate' property (holds the value of the selected date) that I reference in the ObservableObject of the 'getJoke' class won't update when the user selects a different date. It always fetches the joke on today's date and not on the one the user has selected.
Here is the code of:
the custom calendar (selected date is held in the property 'currentDate')
import SwiftUI
import grpc
struct CustomDatePicker: View {
#State var currentDate: Date
#State var dailyJokePopUp = false
//some code here
// When the user selects the date, currentDate property changes to the selected date
.onTapGesture {
currentDate = value.date
}
// Getting selected day for displaying in dailyJokePopUp
func getCurrentDay()->String{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
let date = dateFormatter.string(from: currentDate)
return date
}
the class which is an #ObservableObject (I use it to add a listener to the Firebase to fetch the joke text by its id. Here I need to read 'currentDate' which is originally declared and changed in CustomDatePicker. I need to do it to check if 'currentDate' matches the id in Firebase (that way the joke text is fetched from Firebase on the selected date)).
class getJoke : ObservableObject {
#Published var data = [JokeX]()
#Published var noData = false
#Published var currentDate = Date()
//some code here including adding SnapShotListener
let callCDP = CustomDatePicker(currentDate: currentDate).getCurrentDay()
if id == callCDP {
self.data.append(joke_text_imported)}
}
}
}
}
the popup (I call the result of the #ObservableObject to get the display the text fetched from Firebase)
import SwiftUI
struct dailyJokePopUp: View {
#Binding var show: Bool
#ObservedObject var Jokes = getJoke()
var currentDate: Date = Date()
//some code here
ForEach(self.Jokes.data){i in
Text(i.joke_text)
}
//some code here
}
I can suspect something is wrong with how I declare properties. I've tried various wrappers (#Binding, #StateObject), but I got confused and it didn't work. Hope someone can be kind enough to help me solve the problem.
ViewModel
class getJoke: ObservableObject {
#Published var currentDate = Date()
}
View that can change passing data
struct CustomDatePicker: View {
#Binding var currentDate: Date
var body: some View{
VStack {
DatePicker(selection: $currentDate, displayedComponents: .date){
Text("Select your date")
}
.datePickerStyle(.compact)
}
}
}
And put everything together
struct ContentView: View {
#StateObject var vm = getJoke()
var body: some View {
VStack(spacing: 40) {
CustomDatePicker(currentDate: $vm.currentDate)
Button {
print(vm.currentDate)
} label: {
Text("Show selected date")
}
}
}
}
I have a SettingsManager singleton for my entire app that holds a bunch of user settings. And I've got several ViewModels that reference and can edit the SettingsManager.
The app basically looks like this...
import PlaygroundSupport
import Combine
import SwiftUI
class SettingsManager: ObservableObject {
static let shared = SettingsManager()
#AppStorage("COUNT") var count = 10
}
class ViewModel: ObservableObject {
#Published var settings = SettingsManager.shared
func plus1() {
settings.count += 1
objectWillChange.send()
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(action: viewModel.plus1) {
Text("\(viewModel.settings.count)")
}
}
}
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController
Frustratingly, it works about 85% of the time. But 15% of the time, the values don't update until navigating away from the view and then back.
How can I get #AppStorage to play nice with my View Model / MVVM framework?!
Came across this question researching this exact issue. I came down on the side of letting SwiftUI do the heavy lifting for me. For example:
// Use this in any view model you need to update the value
extension UserDefaults {
static func setAwesomeValue(with value: Int) {
UserDefaults.standard.set(value, forKey: "awesomeValue")
}
static func getAwesomeValue() -> Int {
return UserDefaults.standard.bool(forKey: "awesomeValue")
}
}
// In any view you need this value
struct CouldBeAnyView: some View {
#AppStorage("awesomeValue") var awesomeValue = 0
}
AppStorage is just a wrapper for UserDefaults. Whenever the view model updates the value of "awesomeValue", AppStorage will automatically pick it up. The important thing is to pass the same key when declaring #AppStorage. Probably shouldn't use a string literal but a constant would be easier to keep track of?
This SettingsManager in a cancellables set solution adapted from the Open Source ACHN App:
import PlaygroundSupport
import Combine
import SwiftUI
class SettingsManager: ObservableObject {
static let shared = SettingsManager()
#AppStorage("COUNT") var count = 10 {
willSet { objectWillChange.send() }
}
}
class ViewModel: ObservableObject {
#Published var settings = SettingsManager.shared
var cancellables = Set<AnyCancellable>()
init() {
settings.objectWillChange
.sink { [weak self] _ in
self?.objectWillChange.send()
}
.store(in: &cancellables)
}
func plus1() {
settings.count += 1
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(action: viewModel.plus1) {
Text(" \(viewModel.settings.count) ")
}
}
}
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController
Seems to be slightly less glitchy, but still isn't 100% rock-solid consistent :(
Leaving this here to hopefully inspire someone with my attempt
I think is very clear from this dummy example: if you remove the ForEach code row, magically, the propagation will flow and clock will tick, otherwise it will freeze once the detail view is presented.
class ModelView: ObservableObject {
#Published var clock = Date()
init() {
Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.print()
.assign(to: &$clock)
}
}
struct ContentView: View {
#StateObject var viewModel = ModelView()
var body: some View {
NavigationView {
List {
NavigationLink("Clock", destination: Text(viewModel.clock, formatter: formatter))
ForEach(0..<1) { _ in } // <- Remove this row and the clock will work
}
}
}
var formatter: DateFormatter {
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter
}
}
I'd say it is rather some kind of coincidence that it works in first case, because destination is (or can be) copied on navigation (that's probably happens in second case).
The correct approach would be to have separated standalone view for details with own observer. Parent view should not update child view property if there is no binding.
Tested and worked with Xcode 12.4 / iOS 14.4
struct ContentView: View {
#StateObject var viewModel = ModelView()
var body: some View {
NavigationView {
List {
NavigationLink("Clock", destination: DetailsView(viewModel: viewModel))
ForEach(0..<1) { _ in } // <- Remove this row and the clock will work
}
}
}
}
struct DetailsView: View {
#ObservedObject var viewModel: ModelView
var body: some View {
Text(viewModel.clock, formatter: formatter)
}
var formatter: DateFormatter {
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter
}
}
I have a calendar view like so:
struct CalendarView: View {
#Environment(\.calendar) var calendar
#State var daysForMonthView: [DateInterval]
...
}
where i need to initialize the daysForMonthView array by using the #Environment(\.calendar). How can I do that?
Trying to do:
init() {
_daysForMonthView = State(initialValue: calendar.daysForMonthViewContaining(date: Date()))
}
produces a Variable 'self.daysForMonthView' used before being initialized -error.
You need to assign all the properties before you can access the #Environment. For this reason you can't use calendar to initialise daysForMonthView.
A possible solution is to use onAppear:
struct CalendarView: View {
#Environment(\.calendar) private var calendar
#State private var daysForMonthView: [DateInterval] = [] // assign empty
var body: some View {
//...
.onAppear {
daysForMonthView = calendar...
}
}
}
If for some reason you need calendar to be available in the init, you can pass it as a parameter in init:
struct ContentView: View {
#Environment(\.calendar) private var calendar
var body: some View {
CalendarView(calendar: calendar)
}
}
struct CalendarView: View {
private let calendar: Calendar
#State private var daysForMonthView: [DateInterval]
init(calendar: Calendar) {
self.calendar = calendar
self._daysForMonthView = .init(initialValue: calendar...)
}
//...
}
Note: the downside of this approach is that a change to the calendar will reinitialise the whole CalendarView.
I've always used delegation in UIKit and WatchKit to communicate between objects as far as passing around data from e.g. a WorkoutManager ViewModel that receives delegate callbacks from HealthKit during an HKworkout for calories, heart rates, to an InterfaceController.
I'm now trying to use Combine and SwiftUI to pass around the same data and am a little lost. I'm using a WorkoutManager class as an environment object that I initialize in my ContentView:
class WorkoutManager: NSObject, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate, ObservableObject {
#Published var totalEnergyBurned: Double = 0
//How to subscribe to the changes?
//Omitted HealthKit code that queries and pushes data into totalEnergyBurned here
}
struct ContentView: View {
let healthStore = HKHealthStore()
#StateObject var workoutManager = WorkoutManager()
var sessionTypes = [SessionType.Game, SessionType.Practice, SessionType.Pickup]
var body: some View {
List {
ForEach(sessionTypes) { sessionType in
NavigationLink(destination: LiveWorkoutView(sessionType: sessionType)) {
SessionTypeRow(name: sessionType.stringValue)
}
}
}
.navigationTitle("Let's Go!")
.onAppear {
let authorizationStatus = healthStore.authorizationStatus(for: HKSampleType.workoutType())
switch authorizationStatus {
case .sharingAuthorized:
print("sharing authorized")
case .notDetermined:
print("not determined")
HealthKitAuthManager.authorizeHealthKit()
case .sharingDenied:
print("sharing denied")
HealthKitAuthManager.authorizeHealthKit()
default:
print("default in healthStore.authorizationStatus in ContentView")
HealthKitAuthManager.authorizeHealthKit()
}
}
}
}
My goal is to Publish the changes to all of the children of ContentView but I'm not sure how to subscribe to the changes?
import SwiftUI
struct LiveWorkoutView: View {
#State var sessionType: SessionType
#StateObject var workoutManager = WorkoutManager()
var body: some View {
VStack {
Text("\(workoutManager.totalEnergyBurned)")
Button(action: {
workoutManager.stopWorkout()
}) {
Text("End Workout")
}
}
.onAppear {
workoutManager.startWorkout()
workoutManager.sessionType = sessionType
}
.navigationTitle(sessionType.stringValue)
}
}
//How to subscribe to the changes?
You don't. #StateObject injects subscriber in view, so just use workoutManager. totalEnergyBurned property somewhere (where needed) in view body and view will be refreshed automatically once this property changed (eg. you assign new value to it from HealthKit callback.