Add reminders to iOS

This commit is contained in:
2020-12-05 12:25:58 +01:00
parent 68c43af4e9
commit c390c014e4
8 changed files with 191 additions and 48 deletions

View File

@@ -43,7 +43,7 @@ class RemindersViewModel internal constructor(
val circulars: LiveData<List<Circular>> = val circulars: LiveData<List<Circular>> =
Transformations.switchMap(DoubleTrigger(query, schoolID)) { input -> Transformations.switchMap(DoubleTrigger(query, schoolID)) { input ->
if (input.first == null || input.first == "") { if (input.first == null || input.first == "") {
circularRepository.circularDao.getReminders(input.second ?: 0).asLiveData() circularRepository.circularDao.getFlowReminders(input.second ?: 0).asLiveData()
} else { } else {
circularRepository.circularDao.searchReminders( circularRepository.circularDao.searchReminders(
"%${input.first}%", "%${input.first}%",

View File

@@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
9512D3C1257AB4F60023C3A1 /* NewReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9512D3C0257AB4F60023C3A1 /* NewReminderView.swift */; };
952DEDDF2576F8DC001DF85D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952DEDDE2576F8DC001DF85D /* SceneDelegate.swift */; }; 952DEDDF2576F8DC001DF85D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952DEDDE2576F8DC001DF85D /* SceneDelegate.swift */; };
9547205B2573B688005AA401 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 9547205A2573B688005AA401 /* Settings.bundle */; }; 9547205B2573B688005AA401 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 9547205A2573B688005AA401 /* Settings.bundle */; };
954E68352574E3890034EBA8 /* iOSServerApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954E68342574E3890034EBA8 /* iOSServerApi.swift */; }; 954E68352574E3890034EBA8 /* iOSServerApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954E68342574E3890034EBA8 /* iOSServerApi.swift */; };
@@ -41,6 +42,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
892B265487980F6A344AC2A7 /* Pods-circolapp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-circolapp.debug.xcconfig"; path = "Target Support Files/Pods-circolapp/Pods-circolapp.debug.xcconfig"; sourceTree = "<group>"; }; 892B265487980F6A344AC2A7 /* Pods-circolapp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-circolapp.debug.xcconfig"; path = "Target Support Files/Pods-circolapp/Pods-circolapp.debug.xcconfig"; sourceTree = "<group>"; };
8BEA78E7C5BBEF0119834B33 /* Pods_circolapp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_circolapp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8BEA78E7C5BBEF0119834B33 /* Pods_circolapp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_circolapp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9512D3C0257AB4F60023C3A1 /* NewReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewReminderView.swift; sourceTree = "<group>"; };
952C5954255C57650018C010 /* shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = shared.framework; path = "../../shared/build/xcode-frameworks/shared.framework"; sourceTree = "<group>"; }; 952C5954255C57650018C010 /* shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = shared.framework; path = "../../shared/build/xcode-frameworks/shared.framework"; sourceTree = "<group>"; };
952DEDDE2576F8DC001DF85D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; 952DEDDE2576F8DC001DF85D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
9547205A2573B688005AA401 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; }; 9547205A2573B688005AA401 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
@@ -97,6 +99,7 @@
95CA31B1255C1EDE00AC095B /* ContentView.swift */, 95CA31B1255C1EDE00AC095B /* ContentView.swift */,
95CA31BF255C28C300AC095B /* CircularView.swift */, 95CA31BF255C28C300AC095B /* CircularView.swift */,
95BC3BF72572BCF900F24400 /* AttachmentView.swift */, 95BC3BF72572BCF900F24400 /* AttachmentView.swift */,
9512D3C0257AB4F60023C3A1 /* NewReminderView.swift */,
); );
path = View; path = View;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -278,6 +281,7 @@
954E68352574E3890034EBA8 /* iOSServerApi.swift in Sources */, 954E68352574E3890034EBA8 /* iOSServerApi.swift in Sources */,
95CA31B0255C1EDE00AC095B /* AppDelegate.swift in Sources */, 95CA31B0255C1EDE00AC095B /* AppDelegate.swift in Sources */,
95BC3BF82572BCF900F24400 /* AttachmentView.swift in Sources */, 95BC3BF82572BCF900F24400 /* AttachmentView.swift in Sources */,
9512D3C1257AB4F60023C3A1 /* NewReminderView.swift in Sources */,
954E683D2574ED9E0034EBA8 /* UserDefaultsExtensions.swift in Sources */, 954E683D2574ED9E0034EBA8 /* UserDefaultsExtensions.swift in Sources */,
95C46A51255D3A34007A75E5 /* CircularViewModel.swift in Sources */, 95C46A51255D3A34007A75E5 /* CircularViewModel.swift in Sources */,
); );

View File

@@ -24,26 +24,26 @@ import Firebase
class AppDelegate: NSObject, UIApplicationDelegate { class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure() FirebaseApp.configure()
// For iOS 10 display notification (sent via APNS) // For iOS 10 display notification (sent via APNS)
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization( UNUserNotificationCenter.current().requestAuthorization(
options: authOptions, options: authOptions,
completionHandler: {_, _ in }) completionHandler: {_, _ in })
application.registerForRemoteNotifications() application.registerForRemoteNotifications()
return true return true
} }
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created. // Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with. // Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
} }
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session. // Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
@@ -52,31 +52,57 @@ class AppDelegate: NSObject, UIApplicationDelegate {
} }
extension AppDelegate : UNUserNotificationCenterDelegate { extension AppDelegate : UNUserNotificationCenterDelegate {
// Receive displayed notifications for iOS 10 devices.
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler:
{ result, error in
if let errorReal = error {
print(errorReal.localizedDescription)
}
})
// UI is updated automatically // Receive displayed notifications for iOS 10 devices.
} func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
func userNotificationCenter(_ center: UNUserNotificationCenter, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
didReceive response: UNNotificationResponse, let userInfo = notification.request.content.userInfo
withCompletionHandler completionHandler: @escaping () -> Void) {
iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler: // Handle reminder
{ result, error in if ((userInfo["reminder"]) != nil) {
if let errorReal = error { let circularDao = iOSRepository.getCircularDao()
print(errorReal.localizedDescription) let circular = circularDao.getCircular(id: userInfo["id"] as! Int64, school: userInfo["school"] as! Int32)
} iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: false, completionHandler: {_,_ in })
})
// Show notification
completionHandler() completionHandler([.alert, .sound, .badge])
} return
}
// Handle new circular notification
iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler:
{ result, error in
if let errorReal = error {
print(errorReal.localizedDescription)
}
})
// UI is updated automatically
completionHandler([])
}
// Handle user action
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// Do nothing if it's a reminder
if ((userInfo["reminder"]) != nil) {
completionHandler()
return
}
iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler:
{ result, error in
if let errorReal = error {
print(errorReal.localizedDescription)
}
})
completionHandler()
}
} }

View File

@@ -73,7 +73,7 @@ class CircularViewModel: ObservableObject {
func startObservingAlarms() { func startObservingAlarms() {
stopObserving() stopObserving()
circularWatcher = repository.circularDao.getRemindersC(school: Int32(schoolID)).watch { circulars in circularWatcher = repository.circularDao.getCFlowReminders(school: Int32(schoolID)).watch { circulars in
self.circulars = circulars as! Array<Circular>; self.circulars = circulars as! Array<Circular>;
} }
} }

View File

@@ -9,17 +9,17 @@ import UIKit
import SwiftUI import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate { class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow? var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents. // Create the SwiftUI view that provides the window contents.
let contentView = ContentView() let contentView = ContentView()
// Use a UIHostingController as window root view controller. // Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene { if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene) let window = UIWindow(windowScene: windowScene)
@@ -28,29 +28,49 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
window.makeKeyAndVisible() window.makeKeyAndVisible()
} }
} }
func sceneDidDisconnect(_ scene: UIScene) { func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system. // Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded. // This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects. // Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
} }
func sceneDidBecomeActive(_ scene: UIScene) { func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state. // Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
} }
func sceneWillResignActive(_ scene: UIScene) { func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state. // Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call). // This may occur due to temporary interruptions (ex. an incoming phone call).
} }
func sceneWillEnterForeground(_ scene: UIScene) { func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground. // Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background. // Use this method to undo the changes made on entering the background.
}
let schoolID = UserDefaults.standard.integer(forKey: "school")
let reminders = iOSRepository.getCircularDao().getReminders(school: Int32(schoolID))
// Remove reminder flag if notification was delivered
UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in
// This function has to be called from the main thread because it is where database is accessible
DispatchQueue.main.async {
loop:
for circular in reminders {
for notification in notifications {
if (String(circular.id) == notification.identifier) {
continue loop
}
}
iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: false, completionHandler: {_,_ in })
}
}
}
}
func sceneDidEnterBackground(_ scene: UIScene) { func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background. // Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information // Use this method to save data, release shared resources, and store enough scene-specific state information

View File

@@ -21,6 +21,7 @@ import UIKit
import Shared import Shared
struct CircularView: View { struct CircularView: View {
@State private var creatingReminder: Bool = false
@State private var showDetail = false @State private var showDetail = false
var circular: Circular var circular: Circular
@@ -99,7 +100,16 @@ struct CircularView: View {
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
Button(action: { Button(action: {
iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: !circular.reminder, completionHandler: {_,_ in }) if circular.reminder {
let center = UNUserNotificationCenter.current()
center.removePendingNotificationRequests(withIdentifiers: [String(circular.id)])
iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: false, completionHandler: {_,_ in })
return
}
self.creatingReminder = true
}) { }) {
Image(systemName: circular.reminder ? "alarm.fill" : "alarm") Image(systemName: circular.reminder ? "alarm.fill" : "alarm")
.foregroundColor(.blue) .foregroundColor(.blue)
@@ -107,10 +117,13 @@ struct CircularView: View {
.padding() .padding()
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
.sheet(isPresented: self.$creatingReminder) {
NewReminderView(circular: circular)
}
} }
ForEach(0..<circular.attachmentsNames.count, id: \.self) { index in ForEach(0..<circular.attachmentsNames.count, id: \.self) { index in
AttachmentView(attachmentName: circular.attachmentsNames[index] as! String, attachmentUrl: circular.attachmentsUrls[index] as! String) AttachmentView(attachmentName: circular.attachmentsNames[index] as! String, attachmentUrl: circular.attachmentsUrls[index] as! String)
} }
} }
} }

View File

@@ -0,0 +1,79 @@
/*
* Circolapp
* Copyright (C) 2019-2020 Matteo Schiff
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import SwiftUI
import Shared
struct NewReminderView: View {
@State private var reminderDate = Date()
@Environment(\.presentationMode) var presentationMode
var circular: Circular
var body: some View {
NavigationView {
DatePicker("Pick a date for the reminder", selection: $reminderDate, in: Date()...)
.datePickerStyle(WheelDatePickerStyle())
.labelsHidden()
.padding()
.navigationBarTitle(Text("New reminder"), displayMode: .inline)
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}, trailing: Button(action: {
createReminder()
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
})
}
}
func createReminder() {
iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: true, completionHandler: {_,_ in })
let center = UNUserNotificationCenter.current()
let content = UNMutableNotificationContent()
content.title = "Circular number " + String(circular.id)
content.body = circular.name
content.sound = UNNotificationSound.default
content.userInfo["reminder"] = true
content.userInfo["id"] = circular.id
content.userInfo["school"] = circular.school
let comps = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: reminderDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: false)
let request = UNNotificationRequest(identifier: String(circular.id), content: content, trigger: trigger)
center.add(request) { (error) in
if error != nil {
print("Error \(String(describing: error))")
}
}
}
}
struct NewReminderView_Previews: PreviewProvider {
static var previewCircular = Circular(id: 1, school: 0, name: "This is a circular", url: "http://example.com", date: "19/11/2020", favourite: false, reminder: false, attachmentsNames: [], attachmentsUrls: [])
static var previews: some View {
NewReminderView(circular: previewCircular)
}
}

View File

@@ -83,9 +83,10 @@ class CircularDao(
.mapToList() .mapToList()
fun searchFavouritesC(query: String, school: Int) = searchFavourites(query, school).wrap() fun searchFavouritesC(query: String, school: Int) = searchFavourites(query, school).wrap()
fun getReminders(school: Int) = fun getReminders(school: Int) = appDatabaseQueries.getReminders(school.toLong(), circularMapper).executeAsList()
fun getFlowReminders(school: Int) =
appDatabaseQueries.getReminders(school.toLong(), circularMapper).asFlow().mapToList() appDatabaseQueries.getReminders(school.toLong(), circularMapper).asFlow().mapToList()
fun getRemindersC(school: Int) = getReminders(school).wrap() fun getCFlowReminders(school: Int) = getFlowReminders(school).wrap()
fun searchReminders(query: String, school: Int) = fun searchReminders(query: String, school: Int) =
appDatabaseQueries.searchReminders(school.toLong(), query, circularMapper).asFlow() appDatabaseQueries.searchReminders(school.toLong(), query, circularMapper).asFlow()