fix #80 - improvements nightscout settings

- a new row in the NightScout settings which allow to test the url and api key
- by default set url https://yoursitename.herokuapp.com
This commit is contained in:
Johan Degraeve 2020-04-09 23:53:37 +02:00
parent 820166dcca
commit 710932352b
21 changed files with 250 additions and 19 deletions

View File

@ -124,5 +124,7 @@ enum ConstantsLog {
/// bluetoothPeripheralViewController
static let bluetoothPeripheralViewController = "blePeripheralViewController "
static let nightScoutSettingsViewModel = "nightScoutSettingsViewModel "
}

View File

@ -2,4 +2,9 @@ enum ConstantsNightScout {
/// maximum number of days to upload
static let maxDaysToUpload = 7
/// - default nightscout url
/// - used in settings, when setting first time nightscout url
static let defaultNightScoutUrl = "https://yoursitename.herokuapp.com"
}

View File

@ -1,3 +1,5 @@
"nightscouttestresult_verificationsuccessfulalerttitle" = "Verification Successful";
"nightscouttestresult_verificationsuccessfulalertbody" = "Your Nightscout account was verified successfully";
"nightscouttestresult_verificationerroralerttitle" = "Verification Error";
"warningAPIKeyOrURLIsnil" = "Url and API Key must be set";
"nightScoutAPIKeyAndURLStarted" = "Test started";

View File

@ -66,3 +66,4 @@
"displayDeltaInCalendarEvent" = "Display Delta";
"infoCalendarAccessDeniedByUser" = "You previously denied access to your Calendar.\n\nTo enable it go to your device settings, privacy, calendars and turn on";
"infoCalendarAccessRestricted" = "You can not authorization the app to access your Calendar, possibly due to active restrictions such as parental controls being in place.";
"testUrlAndAPIKey" = "Test Url and API Key";

View File

@ -16,4 +16,12 @@ class Texts_NightScoutTestResult {
return NSLocalizedString("nightscouttestresult_verificationerroralerttitle", tableName: filename, bundle: Bundle.main, value: "Verification Error", comment: "POP up after verifying nightscout credentials, to say that verification of url and api key was not successful - this is the title")
}()
static let warningAPIKeyOrURLIsnil: String = {
return NSLocalizedString("warningAPIKeyOrURLIsnil", tableName: filename, bundle: Bundle.main, value: "Url and API Key must be set", comment: "in settings screen, user tries to test url and API Key but one of them is not set")
}()
static let nightScoutAPIKeyAndURLStarted : String = {
return NSLocalizedString("nightScoutAPIKeyAndURLStarted", tableName: filename, bundle: Bundle.main, value: "Test started", comment: "in settings screen, user clicked test button for nightscout url and apikey")
}()
}

View File

@ -205,6 +205,10 @@ class Texts_SettingsView {
static let uploadSensorStartTime: String = {
return NSLocalizedString("uploadSensorStartTime", tableName: filename, bundle: Bundle.main, value: "Upload Sensor start time", comment: "nightscout settings, title of row")
}()
static let testUrlAndAPIKey: String = {
return NSLocalizedString("testUrlAndAPIKey", tableName: filename, bundle: Bundle.main, value: "Test Url and API Key", comment: "nightscout settings, when clicking the cell, test the url and api key")
}()
// MARK: - Section Speak

View File

@ -57,6 +57,13 @@ protocol SettingsViewModelProtocol {
/// This function will verify if complete reload is needed or not
func completeSettingsViewRefreshNeeded(index:Int) -> Bool
/// a view model may want to pass information back to the viewcontroller asynchronously. Example SettingsViewNightScoutSettingsViewModel will initiate a credential test. The response will come asynchronously and a text needs to return to the viewcontroller, to be shown to the user.
///
/// The viewmodel must call the messageHandler on the main thread.
/// - parameters:
/// - two strings, a title and a message.
func storeMessageHandler(messageHandler : @escaping ((String, String) -> Void))
}
/// to make the coding a bit easier, just one function defined for now, which is to get the viewModel for a specific setting

View File

@ -9,6 +9,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewM5StackBluetoothSettingsViewModel: SettingsViewModelProtocol {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func sectionTitle() -> String? {
return Texts_SettingsView.m5StackSectionTitleBluetooth
}

View File

@ -9,6 +9,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewM5StackGeneralSettingsViewModel: SettingsViewModelProtocol {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func sectionTitle() -> String? {
return Texts_SettingsView.sectionTitleGeneral
}

View File

@ -24,6 +24,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewM5StackWiFiSettingsViewModel: SettingsViewModelProtocol {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func sectionTitle() -> String? {
return Texts_Common.WiFi
}

View File

@ -15,6 +15,12 @@ final class SettingsViewController: UIViewController {
/// reference to soundPlayer
private var soundPlayer:SoundPlayer?
/// will show pop up with title and message
private var messageHandler: ((String, String) -> Void)?
/// UIAlertController used by messageHandler
private var messageHandlerUiAlertController: UIAlertController?
// MARK:- public functions
/// configure
@ -23,16 +29,49 @@ final class SettingsViewController: UIViewController {
self.coreDataManager = coreDataManager
self.soundPlayer = soundPlayer
// create messageHandler
messageHandler = {
(title, message) in
// piece of code that we need two times
let createAndPresentMessageHandlerUIAlertController = {
self.messageHandlerUiAlertController = UIAlertController(title: title, message: message, actionHandler: nil)
if let messageHandlerUiAlertController = self.messageHandlerUiAlertController {
self.present(messageHandlerUiAlertController, animated: true, completion: nil)
}
}
// first check if messageHandlerUiAlertController is not nil and is presenting. If it is, dismiss it and when completed call createAndPresentMessageHandlerUIAlertController
if let messageHandlerUiAlertController = self.messageHandlerUiAlertController {
if messageHandlerUiAlertController.isBeingPresented {
messageHandlerUiAlertController.dismiss(animated: true, completion: createAndPresentMessageHandlerUIAlertController)
return
}
}
// we're here which means there wasn't a messageHandlerUiAlertController being presented, so present it now
createAndPresentMessageHandlerUIAlertController()
}
}
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
title = Texts_SettingsView.screenTitle
setupView()
}
// MARK: - other overriden functions
@ -84,7 +123,7 @@ final class SettingsViewController: UIViewController {
tableView.delegate = self
}
}
}
extension SettingsViewController:UITableViewDataSource, UITableViewDelegate {
@ -195,6 +234,10 @@ extension SettingsViewController:UITableViewDataSource, UITableViewDelegate {
let viewModel = section.viewModel()
if let messageHandler = messageHandler {
viewModel.storeMessageHandler(messageHandler: messageHandler)
}
if viewModel.isEnabled(index: indexPath.row) {
let selectedRowAction = viewModel.onRowSelect(index: indexPath.row)

View File

@ -10,6 +10,10 @@ fileprivate enum Setting:Int, CaseIterable {
/// conforms to SettingsViewModelProtocol for all alert settings in the first sections screen
struct SettingsViewAlertSettingsViewModel:SettingsViewModelProtocol {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func completeSettingsViewRefreshNeeded(index: Int) -> Bool {
return false
}

View File

@ -30,6 +30,10 @@ class SettingsViewAppleWatchSettingsViewModel: SettingsViewModelProtocol {
/// used for requesting authorization to access calendar
let eventStore = EKEventStore()
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func sectionTitle() -> String? {
return Texts_SettingsView.appleWatchSectionTitle
}

View File

@ -12,6 +12,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func sectionTitle() -> String? {
return "Developer Settings"
}

View File

@ -21,6 +21,10 @@ fileprivate enum Setting:Int, CaseIterable {
/// conforms to SettingsViewModelProtocol for all Dexcom settings in the first sections screen
class SettingsViewDexcomSettingsViewModel:SettingsViewModelProtocol {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func completeSettingsViewRefreshNeeded(index: Int) -> Bool {
return true
}

View File

@ -28,6 +28,10 @@ fileprivate enum Setting:Int, CaseIterable {
/// conforms to SettingsViewModelProtocol for all general settings in the first sections screen
struct SettingsViewGeneralSettingsViewModel:SettingsViewModelProtocol {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func completeSettingsViewRefreshNeeded(index: Int) -> Bool {
// changing follower to master or master to follower requires changing ui for nightscout settings and transmitter type settings

View File

@ -12,6 +12,10 @@ class SettingsViewHealthKitSettingsViewModel:SettingsViewModelProtocol {
// MARK: - functions in protocol SettingsViewModelProtocol
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func completeSettingsViewRefreshNeeded(index: Int) -> Bool {
return false
}

View File

@ -15,6 +15,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewInfoViewModel:SettingsViewModelProtocol {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func sectionTitle() -> String? {
return Texts_HomeView.info
}

View File

@ -9,7 +9,11 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewM5StackSettingsViewModel: SettingsViewModelProtocol {
func sectionTitle() -> String? {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func sectionTitle() -> String? {
return "M5Stack"
}

View File

@ -1,4 +1,5 @@
import UIKit
import os
fileprivate enum Setting:Int, CaseIterable {
@ -11,21 +12,111 @@ fileprivate enum Setting:Int, CaseIterable {
/// nightscout api key
case nightScoutAPIKey = 2
/// to allow testing explicitly
case testUrlAndAPIKey = 3
/// should sensor start time be uploaded to NS yes or no
case uploadSensorStartTime = 3
case uploadSensorStartTime = 4
/// use nightscout schedule or not
case useSchedule = 4
case useSchedule = 5
/// open uiviewcontroller to edit schedule
case schedule = 5
case schedule = 6
}
/// conforms to SettingsViewModelProtocol for all nightscout settings in the first sections screen
class SettingsViewNightScoutSettingsViewModel:SettingsViewModelProtocol {
class SettingsViewNightScoutSettingsViewModel {
func completeSettingsViewRefreshNeeded(index: Int) -> Bool {
// MARK: - properties
/// in case info message or errors occur like credential check error, then this closure will be called with title and message
/// - parameters:
/// - first parameter is title
/// - second parameter is the message
///
/// the viewcontroller sets it by calling storeMessageHandler
private var messageHandler: ((String, String) -> Void)?
/// path to test API Secret
private let nightScoutAuthTestPath = "/api/v1/experiments/test"
/// for trace
private let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryCGMG5)
// MARK: - private functions
/// test the nightscout url and api key and send result to messageHandler
private func testNightScoutCredentials() {
// unwrap siteUrl and apiKey
guard let siteUrl = UserDefaults.standard.nightScoutUrl, let apiKey = UserDefaults.standard.nightScoutAPIKey else {return}
if let url = URL(string: siteUrl) {
let testURL = url.appendingPathComponent(nightScoutAuthTestPath)
var request = URLRequest(url: testURL)
request.setValue("application/json", forHTTPHeaderField:"Content-Type")
request.setValue("application/json", forHTTPHeaderField:"Accept")
request.setValue(apiKey.sha1(), forHTTPHeaderField:"api-secret")
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if let error = error {
trace("in testNightScoutCredentials, error = %{public}@", log: self.log, category: ConstantsLog.nightScoutSettingsViewModel, type: .info, error.localizedDescription)
self.callMessageHandlerInMainThread(title: Texts_NightScoutTestResult.verificationErrorAlertTitle, message: error.localizedDescription)
return
}
if let httpResponse = response as? HTTPURLResponse ,
httpResponse.statusCode != 200, let data = data {
let errorMessage = String(data: data, encoding: String.Encoding.utf8)!
trace("in testNightScoutCredentials, error = %{public}@", log: self.log, category: ConstantsLog.nightScoutSettingsViewModel, type: .info, errorMessage)
self.callMessageHandlerInMainThread(title: Texts_NightScoutTestResult.verificationErrorAlertTitle, message: errorMessage)
} else {
trace("in testNightScoutCredentials, successful", log: self.log, category: ConstantsLog.nightScoutSettingsViewModel, type: .info)
self.callMessageHandlerInMainThread(title: Texts_NightScoutTestResult.verificationSuccessFulAlertTitle, message: Texts_NightScoutTestResult.verificationSuccessFulAlertBody)
}
})
trace("in testNightScoutCredentials, url and apikey test started", log: log, category: ConstantsLog.nightScoutSettingsViewModel, type: .info)
task.resume()
}
}
private func callMessageHandlerInMainThread(title: String, message: String) {
// unwrap messageHandler
guard let messageHandler = messageHandler else {return}
DispatchQueue.main.async {
messageHandler(title, message)
}
}
}
/// conforms to SettingsViewModelProtocol for all nightscout settings in the first sections screen
extension SettingsViewNightScoutSettingsViewModel: SettingsViewModelProtocol {
func storeMessageHandler(messageHandler: @escaping ((String, String) -> Void)) {
self.messageHandler = messageHandler
}
func completeSettingsViewRefreshNeeded(index: Int) -> Bool {
return false
}
@ -43,24 +134,37 @@ class SettingsViewNightScoutSettingsViewModel:SettingsViewModelProtocol {
return SettingsSelectedRowAction.nothing
case .nightScoutUrl:
return SettingsSelectedRowAction.askText(title: Texts_SettingsView.labelNightScoutUrl, message: Texts_SettingsView.giveNightScoutUrl, keyboardType: .URL, text: UserDefaults.standard.nightScoutUrl, placeHolder: "yoursitename", actionTitle: nil, cancelTitle: nil, actionHandler: {(nightscouturl:String) in
return SettingsSelectedRowAction.askText(title: Texts_SettingsView.labelNightScoutUrl, message: Texts_SettingsView.giveNightScoutUrl, keyboardType: .URL, text: UserDefaults.standard.nightScoutUrl != nil ? UserDefaults.standard.nightScoutUrl : ConstantsNightScout.defaultNightScoutUrl, placeHolder: nil, actionTitle: nil, cancelTitle: nil, actionHandler: {(nightscouturl:String) in
// if user gave empty string then set to nil
// if not nil, and if not starting with http, add https, and remove ending /
UserDefaults.standard.nightScoutUrl = nightscouturl.toNilIfLength0().addHttpsIfNeeded()
if let url = UserDefaults.standard.nightScoutUrl {
debuglogging("url = " + url)
} else {
debuglogging("url is nil")
}
}, cancelHandler: nil, inputValidator: nil)
case .nightScoutAPIKey:
return SettingsSelectedRowAction.askText(title: Texts_SettingsView.labelNightScoutAPIKey, message: Texts_SettingsView.giveNightScoutAPIKey, keyboardType: .default, text: UserDefaults.standard.nightScoutAPIKey, placeHolder: nil, actionTitle: nil, cancelTitle: nil, actionHandler: {(apiKey:String) in
UserDefaults.standard.nightScoutAPIKey = apiKey.toNilIfLength0()}, cancelHandler: nil, inputValidator: nil)
case .testUrlAndAPIKey:
if UserDefaults.standard.nightScoutAPIKey != nil && UserDefaults.standard.nightScoutUrl != nil {
// show info that test is started, through the messageHandler
if let messageHandler = messageHandler {
messageHandler(Texts_HomeView.info, Texts_NightScoutTestResult.nightScoutAPIKeyAndURLStarted)
}
self.testNightScoutCredentials()
return .nothing
} else {
return .showInfoText(title: Texts_Common.warning, message: Texts_NightScoutTestResult.warningAPIKeyOrURLIsnil)
}
case .useSchedule:
return .nothing
@ -116,7 +220,8 @@ class SettingsViewNightScoutSettingsViewModel:SettingsViewModelProtocol {
return Texts_SettingsView.schedule
case .uploadSensorStartTime:
return Texts_SettingsView.uploadSensorStartTime
case .testUrlAndAPIKey:
return Texts_SettingsView.testUrlAndAPIKey
}
}
@ -136,6 +241,8 @@ class SettingsViewNightScoutSettingsViewModel:SettingsViewModelProtocol {
return UITableViewCell.AccessoryType.disclosureIndicator
case .uploadSensorStartTime:
return UITableViewCell.AccessoryType.none
case .testUrlAndAPIKey:
return .none
}
}
@ -155,7 +262,8 @@ class SettingsViewNightScoutSettingsViewModel:SettingsViewModelProtocol {
return nil
case .uploadSensorStartTime:
return nil
case .testUrlAndAPIKey:
return nil
}
}
@ -182,6 +290,9 @@ class SettingsViewNightScoutSettingsViewModel:SettingsViewModelProtocol {
case .uploadSensorStartTime:
return UISwitch(isOn: UserDefaults.standard.uploadSensorStartTimeToNS, action: {(isOn:Bool) in UserDefaults.standard.uploadSensorStartTimeToNS = isOn})
case .testUrlAndAPIKey:
return nil
}
}

View File

@ -22,7 +22,11 @@ fileprivate enum Setting:Int, CaseIterable {
/// conforms to SettingsViewModelProtocol for all speak settings in the first sections screen
class SettingsViewSpeakSettingsViewModel:SettingsViewModelProtocol {
func completeSettingsViewRefreshNeeded(index: Int) -> Bool {
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
func completeSettingsViewRefreshNeeded(index: Int) -> Bool {
return false
}