adding option to send trace file

This commit is contained in:
Johan Degraeve 2020-05-02 22:54:03 +02:00
parent e367154b0a
commit 139cbefdcf
31 changed files with 864 additions and 106 deletions

View File

@ -161,6 +161,8 @@
F8AC426721ADEBD70078C348 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F8AC426621ADEBD70078C348 /* Assets.xcassets */; };
F8AC426A21ADEBD70078C348 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8AC426821ADEBD70078C348 /* LaunchScreen.storyboard */; };
F8AC42A121B31F170078C348 /* xdrip.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F8AC429F21B31F160078C348 /* xdrip.xcdatamodeld */; };
F8AF36152455C6F700B5977B /* ConstantsTrace.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AF36142455C6F700B5977B /* ConstantsTrace.swift */; };
F8AF361B245D93EE00B5977B /* Int16.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AF361A245D93ED00B5977B /* Int16.swift */; };
F8B3A783225D37F2004BA588 /* TextsNightScoutTestResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A782225D37F2004BA588 /* TextsNightScoutTestResult.swift */; };
F8B3A786225D4473004BA588 /* NightScoutTestResult.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8B3A788225D4473004BA588 /* NightScoutTestResult.strings */; };
F8B3A78B225D473D004BA588 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A78A225D473D004BA588 /* UIAlertController.swift */; };
@ -254,6 +256,7 @@
F8E51D63244B3386001C9E5A /* MiaoMiaoResponseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D62244B3386001C9E5A /* MiaoMiaoResponseType.swift */; };
F8E51D65244BA790001C9E5A /* WatlaaBluetoothPeripheralViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D64244BA790001C9E5A /* WatlaaBluetoothPeripheralViewModel.swift */; };
F8E51D67244BAE0E001C9E5A /* WatlaaBluetoothTransmitterMaster+CGMTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D66244BAE0E001C9E5A /* WatlaaBluetoothTransmitterMaster+CGMTransmitter.swift */; };
F8E51D6924549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D6824549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift */; };
F8EA6C8221B723BC0082976B /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8EA6C8121B723BC0082976B /* Date.swift */; };
F8EA6CA921BBE3010082976B /* UniqueId.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8EA6CA821BBE3010082976B /* UniqueId.swift */; };
F8EEDD5422FF685400D2D610 /* NSMutableURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8EEDD5322FF685400D2D610 /* NSMutableURLRequest.swift */; };
@ -494,6 +497,8 @@
F8AC426921ADEBD70078C348 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
F8AC426B21ADEBD70078C348 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F8AC42A021B31F160078C348 /* xdrip.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = xdrip.xcdatamodel; sourceTree = "<group>"; };
F8AF36142455C6F700B5977B /* ConstantsTrace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsTrace.swift; sourceTree = "<group>"; };
F8AF361A245D93ED00B5977B /* Int16.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Int16.swift; sourceTree = "<group>"; };
F8B3A782225D37F2004BA588 /* TextsNightScoutTestResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsNightScoutTestResult.swift; sourceTree = "<group>"; };
F8B3A787225D4473004BA588 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/NightScoutTestResult.strings; sourceTree = "<group>"; };
F8B3A78A225D473D004BA588 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = "<group>"; };
@ -656,6 +661,7 @@
F8E51D62244B3386001C9E5A /* MiaoMiaoResponseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiaoMiaoResponseType.swift; sourceTree = "<group>"; };
F8E51D64244BA790001C9E5A /* WatlaaBluetoothPeripheralViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatlaaBluetoothPeripheralViewModel.swift; sourceTree = "<group>"; };
F8E51D66244BAE0E001C9E5A /* WatlaaBluetoothTransmitterMaster+CGMTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WatlaaBluetoothTransmitterMaster+CGMTransmitter.swift"; sourceTree = "<group>"; };
F8E51D6824549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTraceSettingsViewModel.swift; sourceTree = "<group>"; };
F8EA6C8121B723BC0082976B /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
F8EA6CA821BBE3010082976B /* UniqueId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniqueId.swift; sourceTree = "<group>"; };
F8EBB030239701DA0058B0D4 /* xdrip v6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v6.xcdatamodel"; sourceTree = "<group>"; };
@ -788,6 +794,7 @@
F808592C23677D6A00F3829D /* ChartPoint.swift */,
F83098FD23AD3F84005741DF /* UITabBarController.swift */,
F8E51D602448E695001C9E5A /* Bundle.swift */,
F8AF361A245D93ED00B5977B /* Int16.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1131,17 +1138,17 @@
F85DC2F621D25E3A00B9F74A /* Managers */ = {
isa = PBXGroup;
children = (
F8E51D5B2448D8A3001C9E5A /* Loop */,
F8E3A2A723D906B600E5E98A /* Watch */,
F8297F42238DC4AC00D74D66 /* BluetoothPeripheral */,
F8BECB03235CE5970060DAE1 /* Charts */,
F81D6D4622BD5F43005EFAE2 /* DexcomShare */,
F821CF9922AEF2DF005C1E43 /* Speak */,
F821CF9122ADB064005C1E43 /* HealthKit */,
F821CF48229BF43A005C1E43 /* Alerts */,
F821CF54229BF43A005C1E43 /* Application */,
F8297F42238DC4AC00D74D66 /* BluetoothPeripheral */,
F8BECB03235CE5970060DAE1 /* Charts */,
F821CF4D229BF43A005C1E43 /* CoreData */,
F81D6D4622BD5F43005EFAE2 /* DexcomShare */,
F821CF9122ADB064005C1E43 /* HealthKit */,
F8E51D5B2448D8A3001C9E5A /* Loop */,
F821CF4F229BF43A005C1E43 /* NightScout */,
F821CF9922AEF2DF005C1E43 /* Speak */,
F8E3A2A723D906B600E5E98A /* Watch */,
);
path = Managers;
sourceTree = "<group>";
@ -1363,6 +1370,7 @@
F8A389EA233175A10010F405 /* SettingsViewM5StackSettingsViewModel.swift */,
F8B3A83B227F090D004BA588 /* SettingsViewNightScoutSettingsViewModel.swift */,
F8B3A83D227F090D004BA588 /* SettingsViewSpeakSettingsViewModel.swift */,
F8E51D6824549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift */,
);
path = SettingsViewModels;
sourceTree = "<group>";
@ -1542,6 +1550,7 @@
F8A1586E22EDC7EE007F5B5D /* ConstantsSuspensionPrevention.swift */,
F8E3A2AA23DA520B00E5E98A /* ConstantsWatch.swift */,
F8E51D5E2448E2E8001C9E5A /* ConstantsShareWithLoop.swift */,
F8AF36142455C6F700B5977B /* ConstantsTrace.swift */,
);
name = Constants;
path = xdrip/Constants;
@ -2196,6 +2205,7 @@
F830992823C32A13005741DF /* TextsWatlaaView.swift in Sources */,
F8B3A79722635A25004BA588 /* AlertEntry+CoreDataProperties.swift in Sources */,
F830992023C291E2005741DF /* WatlaaBluetoothTransmitter.swift in Sources */,
F8AF36152455C6F700B5977B /* ConstantsTrace.swift in Sources */,
F8F971B723A5914D00C3F17D /* BluetoothPeripheralType.swift in Sources */,
F80610C4222D4E4D00D8F236 /* ActionClosureable-extension.swift in Sources */,
F8B3A835227F08AC004BA588 /* PickerViewController.swift in Sources */,
@ -2281,7 +2291,9 @@
F8B3A78E22622954004BA588 /* AlertType+CoreDataClass.swift in Sources */,
F8F9721D23A5915900C3F17D /* LibreDerivedAlgorithmRunner.swift in Sources */,
F821CF5A229BF43A005C1E43 /* CoreDataManager.swift in Sources */,
F8E51D6924549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift in Sources */,
F85DC2F321CFE3D400B9F74A /* Calibration+CoreDataClass.swift in Sources */,
F8AF361B245D93EE00B5977B /* Int16.swift in Sources */,
F8E3A2AB23DA520B00E5E98A /* ConstantsWatch.swift in Sources */,
F821CF7B22A1D359005C1E43 /* NightScoutFollowerDelegate.swift in Sources */,
F8F9721523A5915900C3F17D /* PairRequestTxMessage.swift in Sources */,
@ -2706,7 +2718,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3009;
DEVELOPMENT_TEAM = 92C77A2942;
DEVELOPMENT_TEAM = RNX44PP998;
INFOPLIST_FILE = "$(SRCROOT)/xdrip/Supporting Files/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
@ -2714,7 +2726,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.10.6;
PRODUCT_BUNDLE_IDENTIFIER = com.paulplant.xdriptest;
PRODUCT_BUNDLE_IDENTIFIER = net.johandegraeve.xdripswift;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "xdrip/xdrip-Bridging-Header.h";
@ -2735,7 +2747,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3009;
DEVELOPMENT_TEAM = 92C77A2942;
DEVELOPMENT_TEAM = RNX44PP998;
INFOPLIST_FILE = "$(SRCROOT)/xdrip/Supporting Files/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
@ -2743,7 +2755,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.10.6;
PRODUCT_BUNDLE_IDENTIFIER = com.paulplant.xdriptest;
PRODUCT_BUNDLE_IDENTIFIER = net.johandegraeve.xdripswift;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "xdrip/xdrip-Bridging-Header.h";

View File

@ -122,9 +122,10 @@ enum ConstantsLog {
static let categoryWatchManager = "WatchManager "
/// bluetoothPeripheralViewController
static let bluetoothPeripheralViewController = "blePeripheralViewController "
static let categoryBluetoothPeripheralViewController = "blePeripheralViewController "
static let nightScoutSettingsViewModel = "nightScoutSettingsViewModel "
static let categoryNightScoutSettingsViewModel = "nightScoutSettingsViewModel "
static let categoryTraceSettingsViewModel = "TraceSettingsViewModel"
}

View File

@ -0,0 +1,14 @@
import Foundation
enum ConstantsTrace {
/// email address to which to send trace file
static let traceFileDestinationAddress = "xdrip@proximus.be"
/// will be used as filename to store traces on disk, and attachment file name when sending trace via e-mail
static let traceFileName = "xdriptrace.txt"
/// will be used as filename to app info (configured transmitters etc ...)
static let appInfoFileName = "appinfo.txt"
}

View File

@ -45,4 +45,8 @@ public class AlertType: NSManagedObject {
super.init(entity: entity, insertInto: context)
}
public override var description: String {
return name
}
}

View File

@ -0,0 +1,11 @@
import Foundation
extension Int16 {
/// example value 320 minutes is 5 hours and 20 minutes, would be converted to 05:20
func convertMinutesToTimeAsString() -> String {
return Int(self).convertMinutesToTimeAsString()
}
}

View File

@ -224,3 +224,13 @@ extension Optional where Wrapped == String {
}
}
extension String {
mutating func appendStringAndNewLine(_ stringToAdd: String) {
self = self + stringToAdd + "\n"
}
}

View File

@ -6,4 +6,10 @@ extension UInt16 {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt16>.size)
}
/// example value 320 minutes is 5 hours and 20 minutes, would be converted to 05:20
func convertMinutesToTimeAsString() -> String {
return Int(self).convertMinutesToTimeAsString()
}
}

View File

@ -139,6 +139,9 @@ extension UserDefaults {
/// - alertmanager will observe that value and when changed, verify if missed reading alert needs to be changed
case missedReadingAlertChanged = "missedReadingAlertChanged"
/// when was the app launched, used in trace info that is sent via email. Just to be able to see afterwards if the app ever crashed. Because sometimes users say it crashed, but maybe it just stopped receiving readings and restarted by opening the app, but didn't really crash
case timeStampAppLaunch = "timeStampAppLaunch"
// Nightscout
/// timestamp lastest reading uploaded to NightScout
case timeStampLatestNSUploadedBgReadingToNightScout = "timeStampLatestUploadedBgReading"
@ -172,6 +175,9 @@ extension UserDefaults {
/// OSLogEnabled enabled or not
case OSLogEnabled = "OSLogEnabled"
/// write trace to file enabled or not
case writeTraceToFile = "writeTraceToFile"
/// should libre oop value smoothing be done , default is on, means default libreValueSmoothingIsOff = false
case libreValueSmoothingIsOff = "libreValueSmoothingIsOff"
@ -714,6 +720,16 @@ extension UserDefaults {
}
}
/// when was the app launched, used in trace info that is sent via email. Just to be able to see afterwards if the app ever crashed. Because sometimes users say it crashed, but maybe it just stopped receiving readings and restarted by opening the app, but didn't really crash
var timeStampAppLaunch:Date? {
get {
return object(forKey: Key.timeStampAppLaunch.rawValue) as? Date
}
set {
set(newValue, forKey: Key.timeStampAppLaunch.rawValue)
}
}
/// timestamp lastest reading uploaded to NightScout
var timeStampLatestNightScoutUploadedBgReading:Date? {
get {
@ -814,6 +830,16 @@ extension UserDefaults {
set(newValue, forKey: Key.OSLogEnabled.rawValue)
}
}
/// write trace to file enabled or not
var writeTraceToFile: Bool {
get {
return bool(forKey: Key.writeTraceToFile.rawValue)
}
set {
set(newValue, forKey: Key.writeTraceToFile.rawValue)
}
}
/// should libre oop value smoothing be done , default is on, means default libreValueSmoothingIsOff = false
var libreValueSmoothingIsOff: Bool {

View File

@ -71,3 +71,9 @@
"displayDeltaInCalendarEvent" = "Display Delta?";
"infoCalendarAccessDeniedByUser" = "You previously denied access to your Calendar.\n\nTo enable it go to your device settings, privacy, calendars and enable it.";
"infoCalendarAccessRestricted" = "You cannot give authorization to xDrip4iOS to access your calendar. This is possibly due to active restrictions such as parental controls being in place.";
"writeTraceToFile" = "Write trace to file";
"sectionTitleTrace" = "Trace";
"sendTraceFile" = "Send trace file";
"describeProblem" = "Explain why you send the trace file as detailed as possible. If you have reported your problem on facebook group 'Xdrip for IOS', then mention your facebook name in the e-mail";
"emailbodyText" = "Problem description: ";
"failedToSendEmail" = "In case user tries to send trace file via email but error occurs.";

View File

@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>3806</string>
<string>3861</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>

View File

@ -322,5 +322,37 @@ class Texts_SettingsView {
return NSLocalizedString("infoCalendarAccessRestricted", tableName: filename, bundle: Bundle.main, value: "You cannot give authorization to xDrip4iOS to access your calendar. This is possibly due to active restrictions such as parental controls being in place.", comment: "If user is not allowed to give any app access to the Calendar, due to restrictions. And then tries to activate creation of events in calendar, this message will be shown")
}()
static let writeTraceToFile: String = {
return NSLocalizedString("writeTraceToFile", tableName: filename, bundle: Bundle.main, value: "Write trace to file", comment: "Should write trace to file be enabled or not")
}()
static let sectionTitleTrace: String = {
return NSLocalizedString("sectionTitleTrace", tableName: filename, bundle: Bundle.main, value: "Trace", comment: "in Settings, section title for Trace")
}()
static let sendTraceFile: String = {
return NSLocalizedString("sendTraceFile", tableName: filename, bundle: Bundle.main, value: "Send trace file", comment: "in Settings, row title to send settings")
}()
static let warningWriteTraceToFile: String = {
return NSLocalizedString("warningWriteTraceToFile", tableName: filename, bundle: Bundle.main, value: "Write trace to file is not enabled. Enable this first, then do your test, then send trace file", comment: "in Settings, when user clicks send trace to file, but write trace to file is not enabled")
}()
static let describeProblem: String = {
return NSLocalizedString("describeProblem", tableName: filename, bundle: Bundle.main, value: "Explain why you send the trace file as detailed as possible. If you have reported your problem on facebook group 'Xdrip for IOS', then mention your facebook name in the e-mail", comment: "Text in pop up shown when user wants to send the trace file")
}()
static let emailNotConfigured: String = {
return NSLocalizedString("emailNotConfigured", tableName: filename, bundle: Bundle.main, value: "You must configure an e-mail account on your iOS device.", comment: "user tries to send trace file but there's no native email account configured")
}()
static let emailbodyText: String = {
return NSLocalizedString("emailbodyText", tableName: filename, bundle: Bundle.main, value: "Problem description: ", comment: "default text in email body, when user wants to send trace file.")
}()
static let failedToSendEmail: String = {
return NSLocalizedString("failedToSendEmail", tableName: filename, bundle: Bundle.main, value: "Failed to send email", comment: "In case user tries to send trace file via email but error occurs.")
}()
}

View File

@ -20,6 +20,22 @@ func debuglogging(_ logtext:String) {
os_log("%{public}@", log: log, type: .debug, logtext)
}
/// finds the path to where xdrip can save files
fileprivate func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
/// trace file, in case tracing needs to be stored on file
var traceFileName:URL = {
getDocumentsDirectory().appendingPathComponent(ConstantsTrace.traceFileName)
}()
// appinfo filename
var appInfoFileName:URL = {
getDocumentsDirectory().appendingPathComponent(ConstantsTrace.appInfoFileName)
}()
/// function to be used for logging, takes same parameters as os_log but in a next phase also NSLog can be added, or writing to disk to send later via e-mail ..
/// - message : the text, same format as in os_log with %{private} and %{public} to either keep variables private or public , for NSLog, only 3 String formatters are suppored "@" for String, "d" for Int, "f" for double.
/// - log : is the name of the category that will be used in OSLog
@ -63,8 +79,8 @@ func trace(_ message: StaticString, log:OSLog, category: String, type: OSLogType
}
// if nslog is enabled in settings, then do nslog
if UserDefaults.standard.NSLogEnabled {
// if nslog or write trace to file is enabled in settings, then calculate first string to log
if UserDefaults.standard.NSLogEnabled || UserDefaults.standard.writeTraceToFile {
var argumentsCounter: Int = 0
@ -129,8 +145,288 @@ func trace(_ message: StaticString, log:OSLog, category: String, type: OSLogType
}
NSLog("%@", ConstantsLog.tracePrefix + " " + dateFormatNSLog.string(from: Date()) + " " + category + " " + actualMessage)
// create timeStamp to use in NSLog and tracefile
let timeStamp = dateFormatNSLog.string(from: Date())
// nslog if enabled
if UserDefaults.standard.NSLogEnabled {
NSLog("%@", ConstantsLog.tracePrefix + " " + timeStamp + " " + category + " " + actualMessage)
}
// if trace to file enabled, then write trace to file
if UserDefaults.standard.writeTraceToFile {
do {
let textToWrite = timeStamp + " " + category + " " + actualMessage + "\n"
if let fileHandle = FileHandle(forWritingAtPath: traceFileName.path) {
// file already exists, go to end of file and append text
fileHandle.seekToEndOfFile()
fileHandle.write(textToWrite.data(using: .utf8)!)
} else {
// file doesn't exist yet
try textToWrite.write(to: traceFileName, atomically: true, encoding: String.Encoding.utf8)
}
} catch {
NSLog("%@", ConstantsLog.tracePrefix + " " + dateFormatNSLog.string(from: Date()) + " write trace to file failed")
}
}
}
}
/// deletes the tracefile from disk
func deleteTraceFile() {
do {
let fileManager = FileManager.default
try fileManager.removeItem(at: traceFileName)
} catch {
//
}
}
class Trace {
// MARK: - private properties
/// CoreDataManager to use
private static var coreDataManager:CoreDataManager?
/// BluetoothPeripheralManager to use
private static var bluetoothPeripheralManager: BluetoothPeripheralManager?
private static let paragraphSeperator = "\n\n===================================================\n\n"
// MARK: - initializer
static func initialize(coreDataManager: CoreDataManager?) {
self.coreDataManager = coreDataManager
}
// MARK: - public static functions
/// creates file on disk with app info
static func createAppInfoFile() {
var traceInfo = ""
// Info from User Defaults
// timestamp app launch
// unwrap timeStampAppLaunch
if let timeStampAppLaunch = UserDefaults.standard.timeStampAppLaunch {
traceInfo.appendStringAndNewLine("App launched at " + timeStampAppLaunch.toShortString() + " local time.")
}
// cgm transmitter type from UserDefaults
if let cgmTransmitterTypeAsString = UserDefaults.standard.cgmTransmitterTypeAsString {
traceInfo.appendStringAndNewLine("Transmitter type = " + cgmTransmitterTypeAsString + "\n")
traceInfo.appendStringAndNewLine(paragraphSeperator)
}
// Info from coredata
if let coreDataManager = coreDataManager {
// accessors
let bLEPeripheralAccessor = BLEPeripheralAccessor(coreDataManager: coreDataManager)
let alertEntriesAccessor = AlertEntriesAccessor(coreDataManager: coreDataManager)
let alertTypesAccessor = AlertTypesAccessor(coreDataManager: coreDataManager)
// all bluetooth transmitters
traceInfo.appendStringAndNewLine("List of bluetooth peripherals:\n")
for blePeripheral in bLEPeripheralAccessor.getBLEPeripherals() {
traceInfo.appendStringAndNewLine(" Name : " + blePeripheral.name)
traceInfo.appendStringAndNewLine(" Address : " + blePeripheral.address)
if let alias = blePeripheral.alias {
traceInfo.appendStringAndNewLine(" Alias : " + alias)
}
traceInfo.appendStringAndNewLine(" xDrip will " + (blePeripheral.shouldconnect ? "try ":"not try") + " to connect to this peripheral")
for bluetoothPeripheralType in BluetoothPeripheralType.allCases {
switch bluetoothPeripheralType {
case .M5StackType:
if let m5Stack = blePeripheral.m5Stack, !m5Stack.isM5StickC {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
traceInfo.appendStringAndNewLine(" battery level = " + m5Stack.batteryLevel.description)
// if needed additional specific info can be added
}
case .M5StickCType:
if let m5Stack = blePeripheral.m5Stack, m5Stack.isM5StickC {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
traceInfo.appendStringAndNewLine(" battery level = " + m5Stack.batteryLevel.description)
}
case .DexcomG4Type:
if let dexcomG4 = blePeripheral.dexcomG4 {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
// if needed additional specific info can be added
traceInfo.appendStringAndNewLine(" batterylevel : " + dexcomG4.batteryLevel.description)
}
case .DexcomG5Type:
if let dexcomG5 = blePeripheral.dexcomG5, !dexcomG5.isDexcomG6 {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
// if needed additional specific info can be added
traceInfo.appendStringAndNewLine(" voltageA : " + dexcomG5.voltageA.description)
traceInfo.appendStringAndNewLine(" voltageB : " + dexcomG5.voltageB.description)
}
case .DexcomG6Type:
if let dexcomG6 = blePeripheral.dexcomG5, dexcomG6.isDexcomG6 {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
// if needed additional specific info can be added
traceInfo.appendStringAndNewLine(" voltageA : " + dexcomG6.voltageA.description)
traceInfo.appendStringAndNewLine(" voltageB : " + dexcomG6.voltageB.description)
}
case .BluconType:
if let blucon = blePeripheral.blucon {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
// if needed additional specific info can be added
traceInfo.appendStringAndNewLine(" batteryLevel : " + blucon.batteryLevel.description)
}
case .BlueReaderType:
if blePeripheral.blueReader != nil {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
}
case .BubbleType:
if let bubble = blePeripheral.bubble {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
traceInfo.appendStringAndNewLine(" batteryLevel : " + bubble.batteryLevel.description)
}
case .DropletType:
if let droplet = blePeripheral.droplet {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
traceInfo.appendStringAndNewLine(" batteryLevel : " + droplet.batteryLevel.description)
}
case .GNSentryType:
if let gNSEntry = blePeripheral.gNSEntry {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
traceInfo.appendStringAndNewLine(" batteryLevel : " + gNSEntry.batteryLevel.description)
}
case .MiaoMiaoType:
if let miaoMiao = blePeripheral.miaoMiao {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
traceInfo.appendStringAndNewLine(" batteryLevel : " + miaoMiao.batteryLevel.description)
}
case .WatlaaType:
if let watlaa = blePeripheral.watlaa {
traceInfo.appendStringAndNewLine(" type = " + bluetoothPeripheralType.rawValue)
traceInfo.appendStringAndNewLine(" batteryLevel : " + watlaa.watlaaBatteryLevel.description)
}
}
}
traceInfo.appendStringAndNewLine("")
}
// all alertentries
traceInfo.appendStringAndNewLine("List of alerts:\n")
for alertKind in AlertKind.allCases {
traceInfo.appendStringAndNewLine(" alert kind : " + alertKind.descriptionForLogging())
let alertEntries = alertEntriesAccessor.getAllEntries(forAlertKind: alertKind, alertTypesAccessor: alertTypesAccessor)
for alertEntry in alertEntries {
traceInfo.appendStringAndNewLine(" start " + alertEntry.start.convertMinutesToTimeAsString() + " - value " + alertEntry.value.description + " - alert type : " + alertEntry.alertType.description)
}
traceInfo.appendStringAndNewLine("")
}
// all alert types
traceInfo.appendStringAndNewLine("List of alert types:\n")
for alertType in alertTypesAccessor.getAllAlertTypes() {
traceInfo.appendStringAndNewLine(" alert type : " + alertType.description)
traceInfo.appendStringAndNewLine(" name : " + alertType.name)
traceInfo.appendStringAndNewLine(" enabled : " + alertType.enabled.description)
traceInfo.appendStringAndNewLine(" overridemute : " + alertType.overridemute.description)
traceInfo.appendStringAndNewLine(" snooze via notification : " + alertType.snooze.description)
traceInfo.appendStringAndNewLine(" default snooze period : " + alertType.snoozeperiod.description)
if let soundname = alertType.soundname {
traceInfo.appendStringAndNewLine(" sound : " + soundname)
} else {
traceInfo.appendStringAndNewLine(" sound : " + "default iOS sound")
}
traceInfo.appendStringAndNewLine("")
}
}
do {
try traceInfo.write(to: appInfoFileName, atomically: true, encoding: String.Encoding.utf8)
} catch {
NSLog("%@", ConstantsLog.tracePrefix + " " + dateFormatNSLog.string(from: Date()) + " write appinfo to file failed")
}
}
}

View File

@ -102,7 +102,7 @@ class BluetoothPeripheralViewController: UIViewController {
private var infoAlertWhenScanningStarts: UIAlertController?
/// for trace
private let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.bluetoothPeripheralViewController)
private let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryBluetoothPeripheralViewController)
/// to keep track of scanning result
private var previousScanningResult: BluetoothTransmitter.startScanningResult?
@ -450,7 +450,7 @@ class BluetoothPeripheralViewController: UIViewController {
case .alreadyScanning, .alreadyConnected, .connecting :
trace("in handleScanningResult, scanning not started. Scanning result = %{public}@", log: log, category: ConstantsLog.bluetoothPeripheralViewController, type: .error, startScanningResult.description())
trace("in handleScanningResult, scanning not started. Scanning result = %{public}@", log: log, category: ConstantsLog.categoryBluetoothPeripheralViewController, type: .error, startScanningResult.description())
// no further processing, should normally not happen,
// set isScanning false, although it should already be false
@ -458,7 +458,7 @@ class BluetoothPeripheralViewController: UIViewController {
case .poweredOff:
trace("in handleScanningResult, scanning not started. Bluetooth is not on", log: log, category: ConstantsLog.bluetoothPeripheralViewController, type: .error)
trace("in handleScanningResult, scanning not started. Bluetooth is not on", log: log, category: ConstantsLog.categoryBluetoothPeripheralViewController, type: .error)
// show info that user should switch on bluetooth
self.infoAlertWhenScanningStarts = UIAlertController(title: Texts_Common.warning, message: Texts_HomeView.bluetoothIsNotOn, actionHandler: nil)
@ -466,12 +466,12 @@ class BluetoothPeripheralViewController: UIViewController {
case .other(let reason):
trace("in handleScanningResult, scanning not started. Scanning result = %{public}@", log: log, category: ConstantsLog.bluetoothPeripheralViewController, type: .error, reason)
trace("in handleScanningResult, scanning not started. Scanning result = %{public}@", log: log, category: ConstantsLog.categoryBluetoothPeripheralViewController, type: .error, reason)
// no further processing, should normally not happen,
case .unauthorized:
trace("in handleScanningResult, scanning not started. Scanning result = unauthorized", log: log, category: ConstantsLog.bluetoothPeripheralViewController, type: .error)
trace("in handleScanningResult, scanning not started. Scanning result = unauthorized", log: log, category: ConstantsLog.categoryBluetoothPeripheralViewController, type: .error)
// show info that user should switch on bluetooth
self.infoAlertWhenScanningStarts = UIAlertController(title: Texts_Common.warning, message: Texts_HomeView.bluetoothIsNotAuthorized, actionHandler: nil)
@ -479,7 +479,7 @@ class BluetoothPeripheralViewController: UIViewController {
case .unknown:
trace("in handleScanningResult, scanning not started. Scanning result = unknown - this is always occuring when a BluetoothTransmitter starts scanning the first time. You should see now a new call to handleScanningResult", log: log, category: ConstantsLog.bluetoothPeripheralViewController, type: .info)
trace("in handleScanningResult, scanning not started. Scanning result = unknown - this is always occuring when a BluetoothTransmitter starts scanning the first time. You should see now a new call to handleScanningResult", log: log, category: ConstantsLog.categoryBluetoothPeripheralViewController, type: .info)
}

View File

@ -64,6 +64,14 @@ protocol SettingsViewModelProtocol {
/// - two strings, a title and a message.
func storeMessageHandler(messageHandler : @escaping ((String, String) -> Void))
/// store uiviewcontroller that created the model, in case it's needed
func storeUIViewController(uIViewController: UIViewController)
/// closure to call to reload a row specified by index in the section that the viewmodel is implementing (ie not in another section)
///
/// just an additional method to force row reloads, (there's also the method completeSettingsViewRefreshNeeded which may return true or false depending on row number and which will be called from within the SettingsViewController. The rowReloadClosure is useful when the reload needs to be handled asynchronously
func storeRowReloadClosure(rowReloadClosure: @escaping ((Int) -> 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

@ -350,6 +350,9 @@ final class RootViewController: UIViewController {
// creates activeSensor, bgreadingsAccessor, calibrationsAccessor, NightScoutUploadManager, soundPlayer, dexcomShareUploadManager, nightScoutFollowManager, alertManager, healthKitManager, bgReadingSpeaker, bluetoothPeripheralManager, watchManager
private func setupApplicationData() {
// setup Trace
Trace.initialize(coreDataManager: coreDataManager)
// if coreDataManager is nil then there's no reason to continue
guard let coreDataManager = coreDataManager else {
fatalError("In setupApplicationData but coreDataManager == nil")
@ -654,6 +657,9 @@ final class RootViewController: UIViewController {
/// should be called only once immediately after app start, ie in viewdidload
private func setupUpdateLabelsAndChartTimer() {
// set timeStampAppLaunch to now
UserDefaults.standard.timeStampAppLaunch = Date()
// this is the actual timer
var updateLabelsAndChartTimer:Timer?

View File

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

View File

@ -9,6 +9,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewM5StackGeneralSettingsViewModel: SettingsViewModelProtocol {
func storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}

View File

@ -24,6 +24,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewM5StackWiFiSettingsViewModel: SettingsViewModelProtocol {
func storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}

View File

@ -21,6 +21,76 @@ final class SettingsViewController: UIViewController {
/// UIAlertController used by messageHandler
private var messageHandlerUiAlertController: UIAlertController?
/// array of viewmodels, one per section
private var viewModels = [SettingsViewModelProtocol]()
private enum Section: Int, CaseIterable, SettingsProtocol {
///General settings - language, glucose unit, high and low value
case general
/// alarms
case alarms
///nightscout settings
case nightscout
///dexcom share settings
case dexcom
/// healthkit
case healthkit
/// store bg values in healthkit
case speak
/// M5 stack settings
case M5stack
/// Apple Watch settings
case AppleWatch
/// tracing
case trace
/// info
case info
/// developper settings
case developer
func viewModel() -> SettingsViewModelProtocol {
switch self {
case .general:
return SettingsViewGeneralSettingsViewModel()
case .alarms:
return SettingsViewAlertSettingsViewModel()
case .nightscout:
return SettingsViewNightScoutSettingsViewModel()
case .dexcom:
return SettingsViewDexcomSettingsViewModel()
case .healthkit:
return SettingsViewHealthKitSettingsViewModel()
case .speak:
return SettingsViewSpeakSettingsViewModel()
case .M5stack:
return SettingsViewM5StackSettingsViewModel()
case .info:
return SettingsViewInfoViewModel()
case .developer:
return SettingsViewDevelopmentSettingsViewModel()
case .AppleWatch:
return SettingsViewAppleWatchSettingsViewModel()
case .trace:
return SettingsViewTraceSettingsViewModel()
}
}
}
// MARK:- public functions
/// configure
@ -29,6 +99,32 @@ final class SettingsViewController: UIViewController {
self.coreDataManager = coreDataManager
self.soundPlayer = soundPlayer
// initialize viewModels
for section in Section.allCases {
// get a viewModel for the section
let viewModel = section.viewModel()
// unwrap messageHandler and store in the viewModel
if let messageHandler = messageHandler {
viewModel.storeMessageHandler(messageHandler: messageHandler)
}
// store self as uiViewController in the viewModel
viewModel.storeUIViewController(uIViewController: self)
// store reload closure in the viewModel
viewModel.storeRowReloadClosure(rowReloadClosure: {row in
self.tableView.reloadRows(at: [IndexPath(row: row, section: section.rawValue)], with: .none)
})
// store the viewModel
self.viewModels.append(viewModel)
}
// create messageHandler
messageHandler = {
(title, message) in
@ -128,74 +224,11 @@ final class SettingsViewController: UIViewController {
extension SettingsViewController:UITableViewDataSource, UITableViewDelegate {
private enum Section: Int, CaseIterable, SettingsProtocol {
///General settings - language, glucose unit, high and low value
case general
/// alarms
case alarms
///nightscout settings
case nightscout
///dexcom share settings
case dexcom
/// healthkit
case healthkit
/// store bg values in healthkit
case speak
/// M5 stack settings
case M5stack
/// Apple Watch settings
case AppleWatch
/// info
case info
/// developper settings
case developer
func viewModel() -> SettingsViewModelProtocol {
switch self {
case .general:
return SettingsViewGeneralSettingsViewModel()
case .alarms:
return SettingsViewAlertSettingsViewModel()
case .nightscout:
return SettingsViewNightScoutSettingsViewModel()
case .dexcom:
return SettingsViewDexcomSettingsViewModel()
case .healthkit:
return SettingsViewHealthKitSettingsViewModel()
case .speak:
return SettingsViewSpeakSettingsViewModel()
case .M5stack:
return SettingsViewM5StackSettingsViewModel()
case .info:
return SettingsViewInfoViewModel()
case .developer:
return SettingsViewDevelopmentSettingsViewModel()
case .AppleWatch:
return SettingsViewAppleWatchSettingsViewModel()
}
}
}
// MARK: - UITableViewDataSource protocol Methods
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let section = Section(rawValue: section) else { fatalError("Unexpected Section") }
return section.viewModel().sectionTitle()
return viewModels[section].sectionTitle()
}
@ -205,21 +238,17 @@ extension SettingsViewController:UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = Section(rawValue: section) else { fatalError("Unexpected Section") }
return section.viewModel().numberOfRows()
return viewModels[section].numberOfRows()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else { fatalError("Unexpected Section") }
guard var cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTableViewCell else { fatalError("Unexpected Table View Cell") }
// Configure Cell
SettingsViewUtilities.configureSettingsCell(cell: &cell, forRowWithIndex: indexPath.row, forSectionWithIndex: indexPath.section, withViewModel: section.viewModel(), tableView: tableView)
SettingsViewUtilities.configureSettingsCell(cell: &cell, forRowWithIndex: indexPath.row, forSectionWithIndex: indexPath.section, withViewModel: viewModels[indexPath.section], tableView: tableView)
return cell
}
@ -230,13 +259,7 @@ extension SettingsViewController:UITableViewDataSource, UITableViewDelegate {
tableView.deselectRow(at: indexPath, animated: true)
guard let section = Section(rawValue: indexPath.section) else { fatalError("Unexpected Section") }
let viewModel = section.viewModel()
if let messageHandler = messageHandler {
viewModel.storeMessageHandler(messageHandler: messageHandler)
}
let viewModel = viewModels[indexPath.section]
if viewModel.isEnabled(index: indexPath.row) {

View File

@ -10,6 +10,8 @@ fileprivate enum Setting:Int, CaseIterable {
/// conforms to SettingsViewModelProtocol for all alert settings in the first sections screen
struct SettingsViewAlertSettingsViewModel:SettingsViewModelProtocol {
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}
@ -33,6 +35,8 @@ struct SettingsViewAlertSettingsViewModel:SettingsViewModelProtocol {
}
}
func storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {}
func sectionTitle() -> String? {
return Texts_SettingsView.sectionTitleAlerting
}

View File

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

View File

@ -15,6 +15,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol {
func storeRowReloadClosure(rowReloadClosure: @escaping ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}

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 storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}

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 storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}

View File

@ -12,6 +12,10 @@ class SettingsViewHealthKitSettingsViewModel:SettingsViewModelProtocol {
// MARK: - functions in protocol SettingsViewModelProtocol
func storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}

View File

@ -18,6 +18,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewInfoViewModel:SettingsViewModelProtocol {
func storeRowReloadClosure(rowReloadClosure: @escaping ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}

View File

@ -9,6 +9,10 @@ fileprivate enum Setting:Int, CaseIterable {
struct SettingsViewM5StackSettingsViewModel: SettingsViewModelProtocol {
func storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}

View File

@ -64,7 +64,7 @@ class SettingsViewNightScoutSettingsViewModel {
if let error = error {
trace("in testNightScoutCredentials, error = %{public}@", log: self.log, category: ConstantsLog.nightScoutSettingsViewModel, type: .info, error.localizedDescription)
trace("in testNightScoutCredentials, error = %{public}@", log: self.log, category: ConstantsLog.categoryNightScoutSettingsViewModel, type: .info, error.localizedDescription)
self.callMessageHandlerInMainThread(title: Texts_NightScoutTestResult.verificationErrorAlertTitle, message: error.localizedDescription)
@ -77,20 +77,20 @@ class SettingsViewNightScoutSettingsViewModel {
let errorMessage = String(data: data, encoding: String.Encoding.utf8)!
trace("in testNightScoutCredentials, error = %{public}@", log: self.log, category: ConstantsLog.nightScoutSettingsViewModel, type: .info, errorMessage)
trace("in testNightScoutCredentials, error = %{public}@", log: self.log, category: ConstantsLog.categoryNightScoutSettingsViewModel, type: .info, errorMessage)
self.callMessageHandlerInMainThread(title: Texts_NightScoutTestResult.verificationErrorAlertTitle, message: errorMessage)
} else {
trace("in testNightScoutCredentials, successful", log: self.log, category: ConstantsLog.nightScoutSettingsViewModel, type: .info)
trace("in testNightScoutCredentials, successful", log: self.log, category: ConstantsLog.categoryNightScoutSettingsViewModel, 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)
trace("in testNightScoutCredentials, url and apikey test started", log: log, category: ConstantsLog.categoryNightScoutSettingsViewModel, type: .info)
task.resume()
}
@ -112,6 +112,10 @@ class SettingsViewNightScoutSettingsViewModel {
/// conforms to SettingsViewModelProtocol for all nightscout settings in the first sections screen
extension SettingsViewNightScoutSettingsViewModel: SettingsViewModelProtocol {
func storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: @escaping ((String, String) -> Void)) {
self.messageHandler = messageHandler
}

View File

@ -22,6 +22,10 @@ fileprivate enum Setting:Int, CaseIterable {
/// conforms to SettingsViewModelProtocol for all speak settings in the first sections screen
class SettingsViewSpeakSettingsViewModel:SettingsViewModelProtocol {
func storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {}
func storeUIViewController(uIViewController: UIViewController) {}
func storeMessageHandler(messageHandler: ((String, String) -> Void)) {
// this ViewModel does need to send back messages to the viewcontroller asynchronously
}

View File

@ -0,0 +1,253 @@
import UIKit
import MessageUI
import os
fileprivate enum Setting:Int, CaseIterable {
/// write trace to file enabled or not
case writeTraceToFile = 0
/// to send trace file
case sendTraceFile = 1
}
class SettingsViewTraceSettingsViewModel: NSObject {
/// need to present MFMailComposeViewController
private var uIViewController: UIViewController?
/// to force a row reload
private var rowReloadClosure: ((Int) -> Void)?
/// for logging
private var log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryTraceSettingsViewModel)
/// 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)?
}
extension SettingsViewTraceSettingsViewModel: SettingsViewModelProtocol {
func storeRowReloadClosure(rowReloadClosure: @escaping ((Int) -> Void)) {
self.rowReloadClosure = rowReloadClosure
}
func storeUIViewController(uIViewController: UIViewController) {
self.uIViewController = uIViewController
}
func storeMessageHandler(messageHandler: @escaping ((String, String) -> Void)) {
self.messageHandler = messageHandler
}
func sectionTitle() -> String? {
return Texts_SettingsView.sectionTitleTrace
}
func settingsRowText(index: Int) -> String {
guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") }
switch setting {
case .writeTraceToFile:
return Texts_SettingsView.writeTraceToFile
case .sendTraceFile:
return Texts_SettingsView.sendTraceFile
}
}
func accessoryType(index: Int) -> UITableViewCell.AccessoryType {
guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") }
switch setting {
case .writeTraceToFile:
return .none
case .sendTraceFile:
return .disclosureIndicator
}
}
func detailedText(index: Int) -> String? {
guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") }
switch setting {
case .writeTraceToFile:
return nil
case .sendTraceFile:
return nil
}
}
func uiView(index: Int) -> UIView? {
guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") }
switch setting {
case .writeTraceToFile:
return UISwitch(isOn: UserDefaults.standard.writeTraceToFile, action: {
(isOn:Bool) in
if isOn {
// set writeTraceToFile to true before logging in trace that it is enabled
UserDefaults.standard.writeTraceToFile = true
trace("Trace to file enabled", log: self.log, category: ConstantsLog.categoryTraceSettingsViewModel, type: .info)
} else {
trace("Trace to file disabled", log: self.log, category: ConstantsLog.categoryTraceSettingsViewModel, type: .info)
// set writeTraceToFile to false after logging in trace that it is enabled
UserDefaults.standard.writeTraceToFile = false
}
})
case .sendTraceFile:
return nil
}
}
func numberOfRows() -> Int {
return Setting.allCases.count
}
func onRowSelect(index: Int) -> SettingsSelectedRowAction {
guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") }
switch setting {
case .writeTraceToFile:
return .nothing
case .sendTraceFile:
if !UserDefaults.standard.writeTraceToFile {
return .showInfoText(title: Texts_Common.warning, message: Texts_SettingsView.warningWriteTraceToFile)
} else {
guard let uIViewController = uIViewController else {fatalError("in SettingsViewTraceSettingsViewModel, onRowSelect, uIViewController is nil")}
do {
// create traceFile info as data
let fileData = try Data(contentsOf: traceFileName)
// create app info as data
Trace.createAppInfoFile()
let appInfoData = try Data(contentsOf: appInfoFileName)
if MFMailComposeViewController.canSendMail() {
return .askConfirmation(title: Texts_HomeView.info, message: Texts_SettingsView.describeProblem, actionHandler: {
let mail = MFMailComposeViewController()
mail.mailComposeDelegate = self
mail.setToRecipients([ConstantsTrace.traceFileDestinationAddress])
mail.setMessageBody(Texts_SettingsView.emailbodyText, isHTML: true)
mail.addAttachmentData(fileData as Data, mimeType: "text/txt", fileName: ConstantsTrace.traceFileName)
mail.addAttachmentData(appInfoData as Data, mimeType: "text/txt", fileName: ConstantsTrace.appInfoFileName)
uIViewController.present(mail, animated: true)
}, cancelHandler: nil)
} else {
return .showInfoText(title: Texts_Common.warning, message: Texts_SettingsView.emailNotConfigured)
}
} catch {
// should never get here ?
return .showInfoText(title: Texts_Common.warning, message: "Failed to create attachment")
}
}
}
}
func isEnabled(index: Int) -> Bool {
return true
}
func completeSettingsViewRefreshNeeded(index: Int) -> Bool {
return false
}
}
extension SettingsViewTraceSettingsViewModel: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
switch result {
case .cancelled:
break
case .sent, .saved:
// delete the file as it's been sent successfully
deleteTraceFile()
// disable tracing, to avoid user forgets turning it off
UserDefaults.standard.writeTraceToFile = false
// as setting writeTraceToFile has been changed to false, the row with that setting needs to be reloaded
if let rowReloadClosure = rowReloadClosure {
rowReloadClosure(Setting.writeTraceToFile.rawValue)
}
case .failed:
if let messageHandler = messageHandler {
messageHandler(Texts_Common.warning, Texts_SettingsView.failedToSendEmail)
}
@unknown default:
break
}
}
}

View File

@ -172,6 +172,4 @@ class SettingsViewUtilities {
}
}
}