init commit - bring project over from Mood
This commit is contained in:
@@ -1,90 +0,0 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Shared
|
||||
//
|
||||
// Created by Trey Tartt on 1/10/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
|
||||
animation: .default)
|
||||
private var items: FetchedResults<Item>
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(items) { item in
|
||||
NavigationLink {
|
||||
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
|
||||
} label: {
|
||||
Text(item.timestamp!, formatter: itemFormatter)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteItems)
|
||||
}
|
||||
.toolbar {
|
||||
#if os(iOS)
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
}
|
||||
#endif
|
||||
ToolbarItem {
|
||||
Button(action: addItem) {
|
||||
Label("Add Item", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("Select an item")
|
||||
}
|
||||
}
|
||||
|
||||
private func addItem() {
|
||||
withAnimation {
|
||||
let newItem = Item(context: viewContext)
|
||||
newItem.timestamp = Date()
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteItems(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
offsets.map { items[$0] }.forEach(viewContext.delete)
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let itemFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .medium
|
||||
return formatter
|
||||
}()
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="true" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21C52" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="MoodEntry" representedClassName="MoodEntry" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="moodValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
||||
<element name="MoodEntry" positionX="-63" positionY="-18" width="128" height="74"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -2,19 +2,67 @@
|
||||
// FeelsApp.swift
|
||||
// Shared
|
||||
//
|
||||
// Created by Trey Tartt on 1/10/22.
|
||||
// Created by Trey Tartt on 1/5/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import BackgroundTasks
|
||||
|
||||
@main
|
||||
struct FeelsApp: App {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
let persistenceController = PersistenceController.shared
|
||||
|
||||
|
||||
init() {
|
||||
// persistenceController.fillInMissingDates()
|
||||
|
||||
BGTaskScheduler.shared.cancelAllTaskRequests()
|
||||
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in
|
||||
BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
}.onChange(of: scenePhase) { phase in
|
||||
if phase == .background {
|
||||
BGTask.scheduleBackgroundProcessing()
|
||||
print("background")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BGTask {
|
||||
static let updateDBMissingID = "com.88oak.dbUpdateMissing"
|
||||
|
||||
class func runFillInMissingDatesTask(task: BGProcessingTask) {
|
||||
BGTask.scheduleBackgroundProcessing()
|
||||
|
||||
task.expirationHandler = {
|
||||
task.setTaskCompleted(success: false)
|
||||
}
|
||||
|
||||
PersistenceController.shared.fillInMissingDates()
|
||||
task.setTaskCompleted(success: true)
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
class func scheduleBackgroundProcessing() {
|
||||
let request = BGProcessingTaskRequest(identifier: BGTask.updateDBMissingID)
|
||||
request.requiresNetworkConnectivity = false
|
||||
request.requiresExternalPower = false
|
||||
|
||||
var runDate = Calendar.current.date(byAdding: .day, value: 1, to: Date())
|
||||
runDate = Calendar.current.date(bySettingHour: 0, minute: 1, second: 0, of: runDate!)
|
||||
request.earliestBeginDate = runDate
|
||||
|
||||
do {
|
||||
try BGTaskScheduler.shared.submit(request)
|
||||
} catch {
|
||||
print("Could not schedule image fetch: (error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
143
Shared/LocalNotification.swift
Normal file
143
Shared/LocalNotification.swift
Normal file
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// LocalNotification.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/8/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
class LocalNotification {
|
||||
public enum ActionType: String {
|
||||
case horrible = "HORRIBLE_ACTION"
|
||||
case bad = "BAD_ACTION"
|
||||
case average = "AVERAGE_ACTION"
|
||||
case good = "GOOD_ACTION"
|
||||
case great = "GREAT_ACTION"
|
||||
}
|
||||
static let categoryName = "MOOD_UPDATE"
|
||||
|
||||
public class func testIfEnabled(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
|
||||
if success {
|
||||
completion(.success(true))
|
||||
} else if let error = error {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class func scheduleReminder(atTime time: Date) {
|
||||
let _ = LocalNotification.createNotificationCategory()
|
||||
|
||||
let notificationContent = UNMutableNotificationContent()
|
||||
notificationContent.title = "How was your day?"
|
||||
notificationContent.badge = NSNumber(value: 1)
|
||||
notificationContent.sound = .default
|
||||
notificationContent.categoryIdentifier = LocalNotification.categoryName
|
||||
|
||||
let calendar = Calendar.current
|
||||
let time = calendar.dateComponents([.hour,.minute], from: time)
|
||||
|
||||
var datComp = DateComponents()
|
||||
datComp.hour = time.hour
|
||||
datComp.minute = time.minute
|
||||
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: datComp, repeats: true)
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request) { (error : Error?) in
|
||||
if let theError = error {
|
||||
print(theError.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class func createNotificationCategory() -> UNNotificationCategory {
|
||||
let moodCategory =
|
||||
UNNotificationCategory(identifier: LocalNotification.categoryName,
|
||||
actions: [horribleAction, badAction, averageAction, goodAction, greatAction],
|
||||
intentIdentifiers: [],
|
||||
hiddenPreviewsBodyPlaceholder: "",
|
||||
options: .customDismissAction)
|
||||
// Register the notification type.
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.setNotificationCategories([moodCategory])
|
||||
|
||||
return moodCategory
|
||||
}
|
||||
|
||||
private class var horribleAction: UNNotificationAction {
|
||||
let acceptAction = UNNotificationAction(identifier: ActionType.horrible.rawValue,
|
||||
title: "Horrible",
|
||||
options: [])
|
||||
return acceptAction
|
||||
}
|
||||
|
||||
private class var badAction: UNNotificationAction {
|
||||
let acceptAction = UNNotificationAction(identifier: ActionType.bad.rawValue,
|
||||
title: "Bad",
|
||||
options: [])
|
||||
return acceptAction
|
||||
}
|
||||
|
||||
private class var averageAction: UNNotificationAction {
|
||||
let acceptAction = UNNotificationAction(identifier: ActionType.average.rawValue,
|
||||
title: "Average",
|
||||
options: [])
|
||||
return acceptAction
|
||||
}
|
||||
|
||||
private class var goodAction: UNNotificationAction {
|
||||
let acceptAction = UNNotificationAction(identifier: ActionType.good.rawValue,
|
||||
title: "Good",
|
||||
options: [])
|
||||
return acceptAction
|
||||
}
|
||||
|
||||
private class var greatAction: UNNotificationAction {
|
||||
let acceptAction = UNNotificationAction(identifier: ActionType.great.rawValue,
|
||||
title: "Great",
|
||||
options: [])
|
||||
return acceptAction
|
||||
}
|
||||
|
||||
public class func removeNotificaiton() {
|
||||
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationDelegate: NSObject, ObservableObject, UNUserNotificationCenterDelegate {
|
||||
@Published var notificationCounter = 0
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
}
|
||||
|
||||
func requestAuthorization() {
|
||||
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler([.badge, .banner, .sound])
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
if let action = LocalNotification.ActionType(rawValue: response.actionIdentifier) {
|
||||
switch action {
|
||||
case .horrible:
|
||||
PersistenceController.shared.add(mood: .horrible, forDate: Date())
|
||||
case .bad:
|
||||
PersistenceController.shared.add(mood: .bad, forDate: Date())
|
||||
case .average:
|
||||
PersistenceController.shared.add(mood: .average, forDate: Date())
|
||||
case .good:
|
||||
PersistenceController.shared.add(mood: .good, forDate: Date())
|
||||
case .great:
|
||||
PersistenceController.shared.add(mood: .great, forDate: Date())
|
||||
}
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
63
Shared/Models/Mood.swift
Normal file
63
Shared/Models/Mood.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Mood.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/5/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum Mood: Int {
|
||||
case horrible
|
||||
case bad
|
||||
case average
|
||||
case good
|
||||
case great
|
||||
case missing
|
||||
|
||||
var strValue: String {
|
||||
switch self {
|
||||
case .horrible:
|
||||
return "Horrible"
|
||||
case .bad:
|
||||
return "Bad"
|
||||
case .average:
|
||||
return "Average"
|
||||
case .good:
|
||||
return "Good"
|
||||
case .great:
|
||||
return "Great"
|
||||
case .missing:
|
||||
return "Missing"
|
||||
}
|
||||
}
|
||||
|
||||
static var allValues: [Mood] {
|
||||
return [Mood.horrible, Mood.bad, Mood.average, Mood.good, Mood.great]
|
||||
}
|
||||
|
||||
var icon: Text {
|
||||
switch self {
|
||||
|
||||
case .horrible:
|
||||
return Text("😫")
|
||||
case .bad:
|
||||
return Text("🙁")
|
||||
case .average:
|
||||
return Text("😐")
|
||||
case .good:
|
||||
return Text("🙂")
|
||||
case .great:
|
||||
return Text("😆")
|
||||
case .missing:
|
||||
return Text("🚫")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mood: Identifiable {
|
||||
var id: Int {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
19
Shared/Models/MoodEntryExtension.swift
Normal file
19
Shared/Models/MoodEntryExtension.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// MoodEntryExtension.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/5/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
extension MoodEntry {
|
||||
var moodString: String {
|
||||
return Mood.init(rawValue: Int(self.moodValue))?.strValue ?? "NA"
|
||||
}
|
||||
|
||||
var mood: Mood {
|
||||
return Mood.init(rawValue: Int(self.moodValue))!
|
||||
}
|
||||
}
|
||||
8
Shared/Mooood.xcdatamodeld/.xccurrentversion
Normal file
8
Shared/Mooood.xcdatamodeld/.xccurrentversion
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Shared.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
11
Shared/Mooood.xcdatamodeld/Shared.xcdatamodel/contents
Normal file
11
Shared/Mooood.xcdatamodeld/Shared.xcdatamodel/contents
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21C52" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="MoodEntry" representedClassName="MoodEntry" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="moodValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="MoodEntry" positionX="-63" positionY="-18" width="128" height="74"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -2,20 +2,71 @@
|
||||
// Persistence.swift
|
||||
// Shared
|
||||
//
|
||||
// Created by Trey Tartt on 1/10/22.
|
||||
// Created by Trey Tartt on 1/5/22.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
struct PersistenceController {
|
||||
static let shared = PersistenceController()
|
||||
static let shared = PersistenceController.persistenceController
|
||||
|
||||
private static var persistenceController: PersistenceController {
|
||||
#if targetEnvironment(simulator)
|
||||
return PersistenceController(inMemory: false)
|
||||
#else
|
||||
return PersistenceController(inMemory: false)
|
||||
#endif
|
||||
|
||||
static var preview: PersistenceController = {
|
||||
let result = PersistenceController(inMemory: true)
|
||||
let viewContext = result.container.viewContext
|
||||
for _ in 0..<10 {
|
||||
let newItem = Item(context: viewContext)
|
||||
}
|
||||
|
||||
public var viewContext: NSManagedObjectContext {
|
||||
return PersistenceController.shared.container.viewContext
|
||||
}
|
||||
|
||||
public func add(mood: Mood, forDate date: Date) {
|
||||
let newItem = MoodEntry(context: viewContext)
|
||||
newItem.timestamp = Date()
|
||||
newItem.moodValue = Int16(mood.rawValue)
|
||||
newItem.date = date
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
|
||||
public func moodEntries(forStartDate date: Date, count: Int) -> [MoodEntry] {
|
||||
let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
|
||||
|
||||
fetchRequest.fetchLimit = count
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
|
||||
|
||||
var calendar = Calendar.current
|
||||
calendar.timeZone = NSTimeZone.local
|
||||
|
||||
let dateFrom = calendar.startOfDay(for: Date())
|
||||
|
||||
// Set predicate as date being today's date
|
||||
let fromPredicate = NSPredicate(format: "date <= %@", dateFrom as NSDate)
|
||||
let datePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate])
|
||||
fetchRequest.predicate = datePredicate
|
||||
let entries = try! viewContext.fetch(fetchRequest)
|
||||
|
||||
if entries.count >= count {
|
||||
return Array(entries)
|
||||
} else {
|
||||
return entries
|
||||
}
|
||||
}
|
||||
|
||||
func populateTestData() {
|
||||
for idx in 1..<25 {
|
||||
let newItem = MoodEntry(context: viewContext)
|
||||
newItem.timestamp = Date()
|
||||
newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue)
|
||||
newItem.date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
|
||||
}
|
||||
do {
|
||||
try viewContext.save()
|
||||
@@ -25,11 +76,69 @@ struct PersistenceController {
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
return result
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func populateMemory() {
|
||||
for idx in 1..<25 {
|
||||
let newItem = MoodEntry(context: viewContext)
|
||||
newItem.timestamp = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
|
||||
newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue)
|
||||
newItem.date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
|
||||
}
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
|
||||
func fillInMissingDates() {
|
||||
let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
|
||||
let entries = try! viewContext.fetch(fetchRequest)
|
||||
|
||||
if let earliestDate = entries.last?.date {
|
||||
let diffInDays = Calendar.current.dateComponents([.day], from: earliestDate, to: Date()).day
|
||||
|
||||
for idx in 1..<diffInDays! {
|
||||
let searchDay = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
|
||||
if entries.filter({ Calendar.current.isDate($0.date!, inSameDayAs:searchDay!) }).isEmpty {
|
||||
self.add(mood: .missing, forDate: searchDay!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clearDB() {
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "MoodEntry")
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
|
||||
do {
|
||||
try viewContext.executeAndMergeChanges(using: deleteRequest)
|
||||
try viewContext.save()
|
||||
} catch let error as NSError {
|
||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||
}
|
||||
}
|
||||
|
||||
public func randomEntries(count: Int) -> [MoodEntry] {
|
||||
var entries = [MoodEntry]()
|
||||
|
||||
for idx in 0..<count {
|
||||
let newItem = MoodEntry(context: viewContext)
|
||||
newItem.timestamp = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
|
||||
newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue)
|
||||
newItem.date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
|
||||
entries.append(newItem)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
let container: NSPersistentCloudKitContainer
|
||||
|
||||
|
||||
init(inMemory: Bool = false) {
|
||||
container = NSPersistentCloudKitContainer(name: "Feels")
|
||||
if inMemory {
|
||||
@@ -40,17 +149,31 @@ struct PersistenceController {
|
||||
if let error = error as NSError? {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
|
||||
|
||||
/*
|
||||
Typical reasons for an error here include:
|
||||
* The parent directory does not exist, cannot be created, or disallows writing.
|
||||
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
||||
* The device is out of space.
|
||||
* The store could not be migrated to the current model version.
|
||||
Check the error message to determine what the actual problem was.
|
||||
*/
|
||||
Typical reasons for an error here include:
|
||||
* The parent directory does not exist, cannot be created, or disallows writing.
|
||||
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
||||
* The device is out of space.
|
||||
* The store could not be migrated to the current model version.
|
||||
Check the error message to determine what the actual problem was.
|
||||
*/
|
||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension NSManagedObjectContext {
|
||||
|
||||
/// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
|
||||
///
|
||||
/// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
|
||||
/// - Throws: An error if anything went wrong executing the batch deletion.
|
||||
public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
|
||||
batchDeleteRequest.resultType = .resultTypeObjectIDs
|
||||
let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
|
||||
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
|
||||
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
|
||||
}
|
||||
}
|
||||
|
||||
20
Shared/Random.swift
Normal file
20
Shared/Random.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Random.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/9/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Random {
|
||||
static var widgetUpdateTime: Date {
|
||||
let components = DateComponents(hour: 0, minute: 30, second: 0)
|
||||
var updateTime = Date()
|
||||
if let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()),
|
||||
let tomorrowMorning = Calendar.current.date(byAdding: components, to: tomorrow) {
|
||||
updateTime = tomorrowMorning
|
||||
}
|
||||
return updateTime
|
||||
}
|
||||
}
|
||||
79
Shared/views/AddMoodHeaderView.swift
Normal file
79
Shared/views/AddMoodHeaderView.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// AddMoodHeaderView.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/5/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct AddMoodHeaderView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(UIColor.secondarySystemBackground)
|
||||
|
||||
VStack {
|
||||
Text("How are you feeling today?")
|
||||
.font(.title)
|
||||
.foregroundColor(Color(UIColor.label))
|
||||
.padding()
|
||||
HStack{
|
||||
ForEach(Mood.allValues) { mood in
|
||||
VStack {
|
||||
Button(action: {
|
||||
addItem(withMoodValue: mood.rawValue)
|
||||
}, label: {
|
||||
mood.icon
|
||||
.font(.system(size: 50))
|
||||
})
|
||||
|
||||
//Text(mood.strValue)
|
||||
}.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
|
||||
.frame(minHeight: 85, maxHeight: 140)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func addItem(withMoodValue moodValue: Int) {
|
||||
withAnimation {
|
||||
let newItem = MoodEntry(context: viewContext)
|
||||
newItem.timestamp = Date()
|
||||
newItem.moodValue = Int16(moodValue)
|
||||
newItem.date = Date()
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddMoodHeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
AddMoodHeaderView().environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
|
||||
|
||||
AddMoodHeaderView().preferredColorScheme(.dark).environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
|
||||
|
||||
AddMoodHeaderView().environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
|
||||
|
||||
AddMoodHeaderView().preferredColorScheme(.dark).environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
152
Shared/views/ContentView.swift
Normal file
152
Shared/views/ContentView.swift
Normal file
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Shared
|
||||
//
|
||||
// Created by Trey Tartt on 1/5/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import Charts
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
@State private var showingSheet = false
|
||||
@State private var showTodayInput = true
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \MoodEntry.date, ascending: false)],
|
||||
animation: .spring())
|
||||
private var items: FetchedResults<MoodEntry>
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
mainView
|
||||
.tabItem {
|
||||
Label("Main", systemImage: "list.dash")
|
||||
}
|
||||
|
||||
GraphView()
|
||||
.tabItem {
|
||||
Label("Graph", systemImage: "chart.line.uptrend.xyaxis")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var settingsButtonView: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
showingSheet.toggle()
|
||||
}, label: {
|
||||
Image(systemName: "gear")
|
||||
.foregroundColor(Color(UIColor.systemGray))
|
||||
.font(.system(size: 20))
|
||||
}).sheet(isPresented: $showingSheet) {
|
||||
SettingsView()
|
||||
}.padding(.trailing)
|
||||
}
|
||||
}
|
||||
|
||||
private var listView: some View {
|
||||
List {
|
||||
ForEach(items) { item in
|
||||
HStack {
|
||||
item.mood.icon
|
||||
.font(.system(size: 50))
|
||||
VStack {
|
||||
Text("\(item.moodString)")
|
||||
.font(.title)
|
||||
.foregroundColor(Color(UIColor.systemGray))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text(item.date!, style: .date)
|
||||
.font(.body)
|
||||
.foregroundColor(Color(UIColor.label))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(.leading)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteItems)
|
||||
}
|
||||
}
|
||||
|
||||
private var mainView: some View {
|
||||
VStack{
|
||||
settingsButtonView
|
||||
if shouldShowTodayInput() {
|
||||
AddMoodHeaderView()
|
||||
.frame(minHeight: 85, maxHeight: 180)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
} else {
|
||||
HeaderStatsView(entries: [
|
||||
//x - position of a bar, y - height of a bar
|
||||
BarChartDataEntry(x: 1, y: 1),
|
||||
BarChartDataEntry(x: 2, y: 5),
|
||||
BarChartDataEntry(x: 3, y: 2),
|
||||
BarChartDataEntry(x: 4, y: 4),
|
||||
BarChartDataEntry(x: 5, y: 1)
|
||||
])
|
||||
.frame(minHeight: 85, maxHeight: 180)
|
||||
}
|
||||
listView
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteItems(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
offsets.map { items[$0] }.forEach(viewContext.delete)
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldShowTodayInput() -> Bool {
|
||||
let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
|
||||
|
||||
var calendar = Calendar.current
|
||||
calendar.timeZone = NSTimeZone.local
|
||||
|
||||
// Get today's beginning & end
|
||||
let dateFrom = calendar.startOfDay(for: Date()) // eg. 2016-10-10 00:00:00
|
||||
let dateTo = calendar.date(byAdding: .day, value: 1, to: dateFrom)!
|
||||
// Note: Times are printed in UTC. Depending on where you live it won't print 00:00:00 but it will work with UTC times which can be converted to local time
|
||||
|
||||
// Set predicate as date being today's date
|
||||
let fromPredicate = NSPredicate(format: "%@ <= %K", dateFrom as NSDate, #keyPath(MoodEntry.date))
|
||||
let toPredicate = NSPredicate(format: "%K < %@", #keyPath(MoodEntry.date), dateTo as NSDate)
|
||||
let datePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate, toPredicate])
|
||||
fetchRequest.predicate = datePredicate
|
||||
let entries = try! self.viewContext.count(for: fetchRequest)
|
||||
|
||||
return entries == 0
|
||||
}
|
||||
}
|
||||
|
||||
private let itemFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .medium
|
||||
return formatter
|
||||
}()
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView().environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
|
||||
.onAppear(perform: {
|
||||
PersistenceController.shared.populateMemory()
|
||||
})
|
||||
|
||||
ContentView()
|
||||
.preferredColorScheme(.dark)
|
||||
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
|
||||
}
|
||||
}
|
||||
16
Shared/views/GraphView.swift
Normal file
16
Shared/views/GraphView.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// GraphView.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/8/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct GraphView: View {
|
||||
var body: some View {
|
||||
Text("this is a graph")
|
||||
}
|
||||
}
|
||||
82
Shared/views/HeaderStatsView.swift
Normal file
82
Shared/views/HeaderStatsView.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// HeaderStatsView.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/8/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Charts
|
||||
|
||||
struct HeaderStatsView : UIViewRepresentable {
|
||||
//Bar chart accepts data as array of BarChartDataEntry objects
|
||||
var entries : [BarChartDataEntry]
|
||||
// this func is required to conform to UIViewRepresentable protocol
|
||||
func makeUIView(context: Context) -> BarChartView {
|
||||
//crate new chart
|
||||
let chart = BarChartView()
|
||||
chart.drawGridBackgroundEnabled = false
|
||||
chart.drawValueAboveBarEnabled = false
|
||||
|
||||
chart.xAxis.drawAxisLineEnabled = false
|
||||
chart.xAxis.labelTextColor = .clear
|
||||
|
||||
chart.rightAxis.drawAxisLineEnabled = false
|
||||
chart.rightAxis.labelTextColor = .clear
|
||||
|
||||
chart.leftAxis.drawAxisLineEnabled = false
|
||||
chart.leftAxis.labelTextColor = .clear
|
||||
|
||||
chart.xAxis.drawGridLinesEnabled = false
|
||||
chart.leftAxis.drawGridLinesEnabled = false
|
||||
chart.leftAxis.axisLineColor = .clear
|
||||
chart.rightAxis.axisLineColor = .clear
|
||||
|
||||
chart.legend.textColor = .clear
|
||||
chart.legend.enabled = false
|
||||
|
||||
chart.drawBordersEnabled = false
|
||||
chart.drawMarkers = false
|
||||
// chart.yAxis.drawGridLinesEnabled = false
|
||||
chart.rightAxis.drawGridLinesEnabled = false
|
||||
chart.borderColor = .clear
|
||||
//it is convenient to form chart data in a separate func
|
||||
chart.data = addData()
|
||||
return chart
|
||||
}
|
||||
|
||||
// this func is required to conform to UIViewRepresentable protocol
|
||||
func updateUIView(_ uiView: BarChartView, context: Context) {
|
||||
//when data changes chartd.data update is required
|
||||
uiView.data = addData()
|
||||
}
|
||||
|
||||
func addData() -> BarChartData{
|
||||
let data = BarChartData()
|
||||
//BarChartDataSet is an object that contains information about your data, styling and more
|
||||
let dataSet = BarChartDataSet(entries: entries)
|
||||
// change bars color to green
|
||||
dataSet.colors = [NSUIColor.green]
|
||||
//change data label
|
||||
data.addDataSet(dataSet)
|
||||
return data
|
||||
}
|
||||
|
||||
typealias UIViewType = BarChartView
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct HeaderStatsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HeaderStatsView(entries: [
|
||||
//x - position of a bar, y - height of a bar
|
||||
BarChartDataEntry(x: 1, y: 1),
|
||||
BarChartDataEntry(x: 2, y: 4),
|
||||
BarChartDataEntry(x: 3, y: 3),
|
||||
BarChartDataEntry(x: 4, y: 2),
|
||||
BarChartDataEntry(x: 5, y: 1)
|
||||
]).frame(minHeight: 85, maxHeight: 90)
|
||||
}
|
||||
}
|
||||
142
Shared/views/SettingsView.swift
Normal file
142
Shared/views/SettingsView.swift
Normal file
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/8/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@State private var currentDate = Date() {
|
||||
didSet {
|
||||
if self.showReminder {
|
||||
LocalNotification.scheduleReminder(atTime: self.currentDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State private var showReminder: Bool = false {
|
||||
didSet {
|
||||
if self.showReminder {
|
||||
LocalNotification.testIfEnabled(completion: { result in
|
||||
switch result{
|
||||
case .success(_):
|
||||
LocalNotification.scheduleReminder(atTime: self.currentDate)
|
||||
case .failure(_):
|
||||
// show error
|
||||
break
|
||||
}
|
||||
})
|
||||
} else {
|
||||
LocalNotification.removeNotificaiton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View { ZStack {
|
||||
Color(UIColor.secondarySystemBackground)
|
||||
|
||||
VStack {
|
||||
closeButtonView
|
||||
.padding()
|
||||
notificationCell
|
||||
randomShitCell
|
||||
addTestDataCell
|
||||
clearDB
|
||||
whyBackgroundMode
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
private var closeButtonView: some View {
|
||||
HStack{
|
||||
Spacer()
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text("Exit")
|
||||
.font(.body)
|
||||
.foregroundColor(Color(UIColor.systemBlue))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private var notificationCell: some View {
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
VStack {
|
||||
Toggle("Would you like to be reminded?", isOn: $showReminder)
|
||||
.padding()
|
||||
DatePicker("", selection: $currentDate, displayedComponents: .hourAndMinute)
|
||||
.disabled(showReminder == false)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
|
||||
}
|
||||
|
||||
private var randomShitCell: some View {
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
VStack {
|
||||
Text("random shit")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
|
||||
}
|
||||
|
||||
private var addTestDataCell: some View {
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
Button(action: {
|
||||
PersistenceController.shared.populateTestData()
|
||||
}, label: {
|
||||
Text("Add test data")
|
||||
})
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
|
||||
}
|
||||
|
||||
private var clearDB: some View {
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
Button(action: {
|
||||
PersistenceController.shared.clearDB()
|
||||
}, label: {
|
||||
Text("Clear DB")
|
||||
})
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
|
||||
}
|
||||
|
||||
private var whyBackgroundMode: some View {
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
Text("we do bg mode b/c we can")
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView()
|
||||
|
||||
SettingsView()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user