diff --git a/.github/workflows/build_LoopFollow.yml b/.github/workflows/build_LoopFollow.yml index 443f7bf0c..e6e1a5dda 100644 --- a/.github/workflows/build_LoopFollow.yml +++ b/.github/workflows/build_LoopFollow.yml @@ -131,10 +131,10 @@ jobs: if: steps.workflow-permission.outputs.has_permission != 'true' run: | echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY - echo "You have not yet configured the scheduled sync and build for LoopFollow's browser build." >> $GITHUB_STEP_SUMMARY - echo "Synchronizing your fork of LoopFollow with the upstream repository loopandlearn/LoopFollow will be skipped." >> $GITHUB_STEP_SUMMARY - echo "If you want to enable automatic builds and updates for your LoopFollow, please follow the instructions \ - under the following path LoopFollow/fastlane/testflight.md." >> $GITHUB_STEP_SUMMARY + echo "You have not yet configured the scheduled sync and build for ${{ github.event.repository.name }}'s browser build." >> $GITHUB_STEP_SUMMARY + echo "Synchronizing your fork of ${{ github.event.repository.name }} with the upstream repository ${{ env.UPSTREAM_REPO }} will be skipped." >> $GITHUB_STEP_SUMMARY + echo "If you want to enable automatic builds and updates for your ${{ github.event.repository.name }}, please follow the instructions \ + under the following path ${{ github.event.repository.name }}/fastlane/testflight.md." >> $GITHUB_STEP_SUMMARY # Set a logic flag if this is the second instance of this day-of-week in this month - name: Check if this is the second time this day-of-week happens this month @@ -148,6 +148,30 @@ jobs: echo "is_second_instance=false" >> "$GITHUB_OUTPUT" fi + # When a scheduled run finds nothing new to build, cancel the run so it shows + # as "cancelled" instead of "success" — a green check should always mean a + # new build was made. + cancel_when_no_build: + needs: check_status + name: Cancel run when there is nothing to build + runs-on: ubuntu-latest + permissions: + actions: write + if: | + github.event_name == 'schedule' && + !(vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') && + !(vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true') + steps: + - name: Cancel the run + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "Repository is up to date and no monthly build is due — cancelling this run so it is not reported as a successful build." >> "$GITHUB_STEP_SUMMARY" + gh run cancel ${{ github.run_id }} --repo ${{ github.repository }} + # Keep the job alive until the cancellation lands, so the run ends as + # "cancelled" rather than completing successfully first. + sleep 300 + # Checks if Distribution certificate is present and valid, optionally nukes and # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true' # only run if a build is planned @@ -161,7 +185,7 @@ jobs: (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') || (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' ) - # Builds LoopFollow + # Builds the app build: name: Build needs: [check_certs, check_status] @@ -203,7 +227,7 @@ jobs: - name: Sync clock run: sudo sntp -sS time.windows.com - # Build signed LoopFollow IPA file + # Build signed IPA file - name: Fastlane Build & Archive run: bundle exec fastlane build_LoopFollow env: diff --git a/.github/workflows/tag_on_main.yml b/.github/workflows/tag_on_main.yml index 4fa3d1714..388a2b537 100644 --- a/.github/workflows/tag_on_main.yml +++ b/.github/workflows/tag_on_main.yml @@ -17,6 +17,10 @@ on: push: branches: - main + # Manual fallback: if the push-triggered run was ever skipped (e.g. a commit + # with [skip ci] landing on main), the tag can be created by dispatching this + # workflow against main. It is idempotent — it skips if the tag already exists. + workflow_dispatch: jobs: tag: diff --git a/Config.xcconfig b/Config.xcconfig index 9b34855d4..275e32170 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -6,4 +6,4 @@ unique_id = ${DEVELOPMENT_TEAM} //Version (DEFAULT) -LOOP_FOLLOW_MARKETING_VERSION = 6.2.0 +LOOP_FOLLOW_MARKETING_VERSION = 7.0.0 diff --git a/Gemfile b/Gemfile index 0eb90cb08..502d6331b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,2 @@ source "https://rubygems.org" -gem "fastlane", "2.235.0" +gem "fastlane", "2.237.0" diff --git a/Gemfile.lock b/Gemfile.lock index d255873eb..8e70830ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1254.0) - aws-sdk-core (3.250.0) + aws-partitions (1.1267.0) + aws-sdk-core (3.253.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -17,10 +17,10 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.128.0) + aws-sdk-kms (1.129.0) aws-sdk-core (~> 3, >= 3.248.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.224.0) + aws-sdk-s3 (1.226.0) aws-sdk-core (~> 3, >= 3.248.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -42,8 +42,9 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.112.0) - faraday (1.10.5) + excon (1.5.0) + logger + faraday (1.10.6) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -72,10 +73,10 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.1) - fastlane (2.235.0) + fastlane (2.237.0) CFPropertyList (>= 2.3, < 5.0.0) abbrev (~> 0.1) - addressable (>= 2.8, < 3.0.0) + addressable (>= 2.9.0, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.197) babosa (>= 1.0.3, < 2.0.0) @@ -87,7 +88,7 @@ GEM csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) + excon (>= 0.71.0, < 2.0.0) faraday (~> 1.0) faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) @@ -101,9 +102,10 @@ GEM highline (~> 2.0) http-cookie (~> 1.0.5) json (< 3.0.0) - jwt (>= 2.1.0, < 4) + jwt (>= 2.10.3, < 4) logger (>= 1.6, < 2.0) mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) multipart-post (>= 2.0.0, < 3.0.0) mutex_m (~> 0.3) naturally (~> 2.2) @@ -124,7 +126,7 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-sirp (1.1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.101.0) + google-apis-androidpublisher_v3 (0.104.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) @@ -134,20 +136,20 @@ GEM mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - google-apis-iamcredentials_v1 (0.27.0) + google-apis-iamcredentials_v1 (0.28.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.17.0) + google-apis-playcustomapp_v1 (0.18.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.62.0) + google-apis-storage_v1 (0.64.0) google-apis-core (>= 0.15.0, < 2.a) - google-cloud-core (1.8.0) + google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (2.2.2) base64 (~> 0.2) faraday (>= 1.0, < 3.a) - google-cloud-errors (1.6.0) - google-cloud-storage (1.60.0) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-core (>= 0.18, < 2) @@ -157,13 +159,13 @@ GEM googleauth (~> 1.9) mini_mime (~> 1.0) google-logging-utils (0.2.0) - googleauth (1.16.2) + googleauth (1.17.1) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.2) google-logging-utils (~> 0.1) jwt (>= 1.4, < 4.0) - multi_json (~> 1.11) os (>= 0.9, < 2.0) + pstore (~> 0.1) signet (>= 0.16, < 2.a) highline (2.0.3) http-cookie (1.0.8) @@ -171,7 +173,7 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.19.7) + json (2.20.0) jwt (3.2.0) base64 logger (1.7.0) @@ -182,11 +184,12 @@ GEM mutex_m (0.3.0) nanaimo (0.4.0) naturally (2.3.0) - nkf (0.2.0) + nkf (0.3.0) optparse (0.8.1) os (1.1.4) ostruct (0.6.3) plist (3.7.2) + pstore (0.2.1) public_suffix (7.0.5) rake (13.4.2) representable (3.2.0) @@ -199,11 +202,10 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.4.1) security (0.1.5) - signet (0.21.0) + signet (0.22.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 4.0) - multi_json (~> 1.10) simctl (1.6.10) CFPropertyList naturally @@ -218,12 +220,14 @@ GEM uber (0.1.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.27.0) + xcodeproj (1.28.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) + base64 claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.4.0) + nkf rexml (>= 3.3.6, < 4.0) xcpretty (0.4.1) rouge (~> 3.28.0) @@ -235,7 +239,7 @@ PLATFORMS ruby DEPENDENCIES - fastlane (= 2.235.0) + fastlane (= 2.237.0) BUNDLED WITH 4.0.12 diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index b162633e0..560cd2f8e 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -7,440 +7,16 @@ objects = { /* Begin PBXBuildFile section */ - B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */; }; - B500000000000000000000A4 /* QuickPickBolusesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A3 /* QuickPickBolusesManager.swift */; }; - B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */; }; - B500000000000000000000B4 /* QuickPickMealsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B3 /* QuickPickMealsManager.swift */; }; - 2D8068C66833EEAED7B4BEB8 /* FutureCarbsCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */; }; - 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77982F5BD8AB00E96858 /* APNSClient.swift */; }; - 374A77A52F5BE17000E96858 /* AppGroupID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A779F2F5BE17000E96858 /* AppGroupID.swift */; }; - 374A77A62F5BE17000E96858 /* GlucoseSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77A12F5BE17000E96858 /* GlucoseSnapshot.swift */; }; - 374A77A72F5BE17000E96858 /* LAAppGroupSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77A32F5BE17000E96858 /* LAAppGroupSettings.swift */; }; - 374A77A82F5BE17000E96858 /* GlucoseLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77A02F5BE17000E96858 /* GlucoseLiveActivityAttributes.swift */; }; - 374A77AA2F5BE17000E96858 /* AppGroupID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A779F2F5BE17000E96858 /* AppGroupID.swift */; }; - 374A77AB2F5BE17000E96858 /* GlucoseSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77A12F5BE17000E96858 /* GlucoseSnapshot.swift */; }; - 374A77AC2F5BE17000E96858 /* LAAppGroupSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77A32F5BE17000E96858 /* LAAppGroupSettings.swift */; }; - 374A77AD2F5BE17000E96858 /* GlucoseLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77A02F5BE17000E96858 /* GlucoseLiveActivityAttributes.swift */; }; - 374A77B42F5BE1AC00E96858 /* StorageCurrentGlucoseStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77B32F5BE1AC00E96858 /* StorageCurrentGlucoseStateProvider.swift */; }; - 374A77B52F5BE1AC00E96858 /* GlucoseSnapshotBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77AE2F5BE1AC00E96858 /* GlucoseSnapshotBuilder.swift */; }; - 374A77B72F5BE1AC00E96858 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77B12F5BE1AC00E96858 /* LiveActivityManager.swift */; }; - 374A77B82F5BE1AC00E96858 /* GlucoseSnapshotStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77AF2F5BE1AC00E96858 /* GlucoseSnapshotStore.swift */; }; - 374A77B92F5BE1AC00E96858 /* PreferredGlucoseUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77B22F5BE1AC00E96858 /* PreferredGlucoseUnit.swift */; }; - 379BECAA2F6588300069DC62 /* RestartLiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379BECA92F6588300069DC62 /* RestartLiveActivityIntent.swift */; }; - 379BECB02F65DA4B0069DC62 /* LiveActivitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379BECAF2F65DA4B0069DC62 /* LiveActivitySettingsView.swift */; }; 37A4BDDB2F5B6B4A00EEB289 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37A4BDDA2F5B6B4A00EEB289 /* WidgetKit.framework */; }; 37A4BDDD2F5B6B4A00EEB289 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37A4BDDC2F5B6B4A00EEB289 /* SwiftUI.framework */; }; 37A4BDE82F5B6B4C00EEB289 /* LoopFollowLAExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 37A4BDD92F5B6B4A00EEB289 /* LoopFollowLAExtensionExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 37E4DD0D2F7E0967000511C8 /* LALivenessStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E4DD0C2F7E0967000511C8 /* LALivenessStore.swift */; }; - 37E4DD0E2F7E097D000511C8 /* LALivenessStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E4DD0C2F7E0967000511C8 /* LALivenessStore.swift */; }; - 37E4DD112F7E0D35000511C8 /* LALivenessMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E4DD0F2F7E0985000511C8 /* LALivenessMarker.swift */; }; 3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; }; - 5544D8C363FB5D3B9BF8CE4A /* APNSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7C2676561D686C6459CAA2D /* APNSettingsView.swift */; }; - 654132E72E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */; }; - 654132EA2E19F24800BDBE08 /* TOTPGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E92E19F24800BDBE08 /* TOTPGenerator.swift */; }; - 654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134172E1DC09700BDBE08 /* OverridePresetsView.swift */; }; - 6541341A2E1DC27900BDBE08 /* OverridePresetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134192E1DC27900BDBE08 /* OverridePresetData.swift */; }; - 6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; }; - 656F8C102E49F36F0008DC1D /* QRCodeDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */; }; - 656F8C122E49F3780008DC1D /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */; }; - 656F8C142E49F3D20008DC1D /* RemoteCommandSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */; }; - 657F98182F043D8100F732BD /* HomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657F98172F043D8100F732BD /* HomeContentView.swift */; }; - 657F99E92F0BC81500F732BD /* OTPSecureMessenger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657F99E82F0BC81500F732BD /* OTPSecureMessenger.swift */; }; - 6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.swift */; }; - 6589CC622E9E7D1600BB18FE /* ImportExportSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC532E9E7D1600BB18FE /* ImportExportSettingsView.swift */; }; - 6589CC632E9E7D1600BB18FE /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5D2E9E7D1600BB18FE /* GeneralSettingsView.swift */; }; - 6589CC642E9E7D1600BB18FE /* ContactSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC592E9E7D1600BB18FE /* ContactSettingsView.swift */; }; - 6589CC652E9E7D1600BB18FE /* DexcomSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5C2E9E7D1600BB18FE /* DexcomSettingsViewModel.swift */; }; - 6589CC662E9E7D1600BB18FE /* AdvancedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC562E9E7D1600BB18FE /* AdvancedSettingsView.swift */; }; - 6589CC672E9E7D1600BB18FE /* ImportExportSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC542E9E7D1600BB18FE /* ImportExportSettingsViewModel.swift */; }; - 6589CC682E9E7D1600BB18FE /* ExportableSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC522E9E7D1600BB18FE /* ExportableSettings.swift */; }; - 6589CC692E9E7D1600BB18FE /* ContactSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5A2E9E7D1600BB18FE /* ContactSettingsViewModel.swift */; }; - 6589CC6A2E9E7D1600BB18FE /* DexcomSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5B2E9E7D1600BB18FE /* DexcomSettingsView.swift */; }; - 6589CC6B2E9E7D1600BB18FE /* TabCustomizationModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */; }; - 6589CC6C2E9E7D1600BB18FE /* GraphSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */; }; - 6589CC6D2E9E7D1600BB18FE /* CalendarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC582E9E7D1600BB18FE /* CalendarSettingsView.swift */; }; - 6589CC6E2E9E7D1600BB18FE /* SettingsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */; }; - 6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC572E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift */; }; - 6589CC712E9E7D1600BB18FE /* ShareLogNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */; }; - 6589CC712E9E814F00BB18FE /* AlarmSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */; }; - 6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */; }; - 65A100012F5AA00000AA1001 /* UnitsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */; }; - 65A100032F5AA00000AA1002 /* UnitsConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A100022F5AA00000AA1002 /* UnitsConfigurationView.swift */; }; - 65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; }; - 65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */; }; - 66E3D12E66AA4534A144A54B /* BackgroundRefreshManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8CA8BE0B3D247408FE088B4 /* BackgroundRefreshManager.swift */; }; - ACE7F6DE0D065BEB52CDC0DB /* FutureCarbsAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D2A4EFD18B7B7748B6669E /* FutureCarbsAlarmEditor.swift */; }; - DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; }; - DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; }; - DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */; }; - DD026E5B2EA2C9C300A39CB5 /* InsulinFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD026E5A2EA2C9C300A39CB5 /* InsulinFormatter.swift */; }; - DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */; }; - DD0650EB2DCE8385004D3B41 /* LowBGCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */; }; - DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */; }; - DD0650EF2DCE96FF004D3B41 /* HighBGCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */; }; - DD0650F12DCE9A9E004D3B41 /* MissedReadingCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F02DCE9A9E004D3B41 /* MissedReadingCondition.swift */; }; - DD0650F32DCE9B3D004D3B41 /* MissedReadingEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */; }; - DD0650F52DCF303F004D3B41 /* AlarmStepperSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */; }; - DD0650F72DCFDA26004D3B41 /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */; }; - DD0650F92DCFE7BE004D3B41 /* FastDropAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F82DCFE7BE004D3B41 /* FastDropAlarmEditor.swift */; }; - DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; }; - DD0B9D562DE1EC8A0090C337 /* AlarmType+Snooze.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0B9D552DE1EC8A0090C337 /* AlarmType+Snooze.swift */; }; - DD0B9D582DE1F3B20090C337 /* AlarmType+canAcknowledge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0B9D572DE1F3B20090C337 /* AlarmType+canAcknowledge.swift */; }; - DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */; }; - DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C612C4175FD00DBADDF /* NSProfile.swift */; }; - DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */; }; - DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */; }; - DD0C0C682C48529400DBADDF /* Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C672C48529400DBADDF /* Metric.swift */; }; - DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */; }; - DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */; }; - DD4E5F6A7B8C9D0E2F2A3B4C /* RemoteContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */; }; - AA1B2C3D4E5F6A7B8C9D0E2F /* LoopFollowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1B2C3D4E5F6A7B8C9D0E1F /* LoopFollowApp.swift */; }; - BB2C3D4E5F6A7B8C9D0E2F2A /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB2C3D4E5F6A7B8C9D0E1F2A /* MainTabView.swift */; }; - DD12D4872E1705E6004E0112 /* AlarmsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD12D4862E1705E6004E0112 /* AlarmsContainerView.swift */; }; - DD13BC752C3FD6210062313B /* InfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC742C3FD6200062313B /* InfoType.swift */; }; - DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC762C3FD64E0062313B /* InfoData.swift */; }; - DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC782C3FE63A0062313B /* InfoManager.swift */; }; - DD13BC7B2C3FE64A0062313B /* InfoTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC7A2C3FE64A0062313B /* InfoTableView.swift */; }; - DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */; }; - DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */; }; - DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF102C997B4600FB655A /* LoadingButtonView.swift */; }; - B500000000000000000000C2 /* QuickPickSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000C1 /* QuickPickSectionHeader.swift */; }; - DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52B82E1EB5DC00432050 /* TabPosition.swift */; }; - CC3D4E5F6A7B8C9D0E2F2A3B /* MoreMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3D4E5F6A7B8C9D0E1F2A3B /* MoreMenuView.swift */; }; - DD7A3B5D2F1E8D9A00B4C6E1 /* BGDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7A3B5C2F1E8D9A00B4C6E1 /* BGDisplayView.swift */; }; - DD7A3B5F2F1E8DA000B4C6E1 /* LineChartWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7A3B5E2F1E8DA000B4C6E1 /* LineChartWrapper.swift */; }; - DD7A3B612F1E8DA600B4C6E1 /* MainHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7A3B602F1E8DA600B4C6E1 /* MainHomeView.swift */; }; - EE5F6A7B8C9D0E2F2A3B4C5D /* NightscoutContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5F6A7B8C9D0E1F2A3B4C5D /* NightscoutContentView.swift */; }; - DD1D52C02E4C100000000001 /* AppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BF2E4C100000000001 /* AppearanceMode.swift */; }; - DD1D52C22E4C100000000002 /* PredictionDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52C12E4C100000000002 /* PredictionDisplayType.swift */; }; - DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */; }; - DD2C2E512D3B8B0C006413A5 /* NightscoutSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */; }; - DD485F142E454B2600CE8CBF /* SecureMessenger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD485F132E454B2600CE8CBF /* SecureMessenger.swift */; }; - DD4878032C7B297E0048F05C /* StorageValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878022C7B297E0048F05C /* StorageValue.swift */; }; - DD4878052C7B2C970048F05C /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878042C7B2C970048F05C /* Storage.swift */; }; - DD4878082C7B30BF0048F05C /* RemoteSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878072C7B30BF0048F05C /* RemoteSettingsView.swift */; }; - DD48780A2C7B30D40048F05C /* RemoteSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878092C7B30D40048F05C /* RemoteSettingsViewModel.swift */; }; - AB1CD0012C7B30D40048F05C /* RemoteDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1CD0022C7B30D40048F05C /* RemoteDiagnostics.swift */; }; - DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48780D2C7B74A40048F05C /* TrioRemoteControlViewModel.swift */; }; - DD4878102C7B74BF0048F05C /* TrioRemoteControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48780F2C7B74BF0048F05C /* TrioRemoteControlView.swift */; }; - DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878122C7B750D0048F05C /* TempTargetView.swift */; }; - DD4878152C7B75230048F05C /* MealView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878142C7B75230048F05C /* MealView.swift */; }; - DD4878172C7B75350048F05C /* BolusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878162C7B75350048F05C /* BolusView.swift */; }; - DD48781E2C7DAF2F0048F05C /* PushNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48781D2C7DAF2F0048F05C /* PushNotificationManager.swift */; }; - DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48781F2C7DAF890048F05C /* PushMessage.swift */; }; - DD493AD52ACF2109009A6922 /* ResumePump.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AD42ACF2109009A6922 /* ResumePump.swift */; }; - DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AD62ACF2139009A6922 /* SuspendPump.swift */; }; - DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AD82ACF2171009A6922 /* Carbs.swift */; }; - DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493ADA2ACF21A3009A6922 /* Bolus.swift */; }; - DD493ADD2ACF21E0009A6922 /* Basals.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493ADC2ACF21E0009A6922 /* Basals.swift */; }; - DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493ADE2ACF22BB009A6922 /* SAge.swift */; }; - DD493AE12ACF22FE009A6922 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE02ACF22FE009A6922 /* Profile.swift */; }; - DD493AE32ACF2358009A6922 /* CAge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE22ACF2358009A6922 /* CAge.swift */; }; - DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE42ACF2383009A6922 /* Treatments.swift */; }; - DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */; }; - DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; }; - DD4A407E2E6AFEE6007B318B /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A407D2E6AFEE6007B318B /* AuthService.swift */; }; - DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */; }; - DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */; }; - DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */; }; - DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */; }; - DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */; }; - DD4AFB6B2DB6BF2A00BB593F /* Binding+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB6A2DB6BF2A00BB593F /* Binding+Optional.swift */; }; - DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */; }; - DD5334212C60EBEE00062F9D /* InsulinCartridgeChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5334202C60EBEE00062F9D /* InsulinCartridgeChange.swift */; }; - DD5334232C60ED3600062F9D /* IAge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5334222C60ED3600062F9D /* IAge.swift */; }; - DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5334262C61668700062F9D /* InfoDisplaySettingsViewModel.swift */; }; - DD5334292C6166A500062F9D /* InfoDisplaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5334282C6166A500062F9D /* InfoDisplaySettingsView.swift */; }; - DD5334B02D1447C500CDD6EA /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5334AF2D1447C500CDD6EA /* BLEManager.swift */; }; - DD5817172D2710E90041FB98 /* BLEDeviceSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5817162D2710E50041FB98 /* BLEDeviceSelectionView.swift */; }; - DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5817192D299EF40041FB98 /* DexcomHeartbeatBluetoothDevice.swift */; }; - DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58171B2D299F8D0041FB98 /* BluetoothDevice.swift */; }; - DD58171E2D299FCA0041FB98 /* BluetoothDeviceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58171D2D299FC50041FB98 /* BluetoothDeviceDelegate.swift */; }; - DD5DA27C2DC930D6003D44FC /* GlucoseValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */; }; - DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */; }; - DD608A0A2C23593900F91132 /* SMB.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A092C23593900F91132 /* SMB.swift */; }; - DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */; }; - DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */; }; - DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7B0D432D730A320063DCB6 /* CycleHelper.swift */; }; - DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19832ACDA50C00DBD158 /* Overrides.swift */; }; - DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19852ACDA59700DBD158 /* BGCheck.swift */; }; - DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19872ACDA5DA00DBD158 /* Notes.swift */; }; - DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19892ACDA62600DBD158 /* SensorStart.swift */; }; - DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */; }; - DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */; }; - DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */; }; - DD7F4BC52DD3CE0700D449E9 /* AlarmBGLimitSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */; }; - DD7F4BC72DD473A600D449E9 /* FastDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */; }; - DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */; }; - DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */; }; - DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */; }; - DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */; }; - DD7F4C0B2DD51C5500D449E9 /* OverrideEndCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */; }; - DD7F4C0D2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */; }; - DD7F4C0F2DD51EC200D449E9 /* TempTargetStartCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */; }; - DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */; }; - DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */; }; - DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */; }; - DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */; }; - DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */; }; - DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */; }; - DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */; }; - DD7F4C1F2DD6648B00D449E9 /* FastRiseCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */; }; - DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */; }; - DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */; }; - DD7F4C252DD7B20700D449E9 /* AlarmType+timeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */; }; - DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; - DD8316442DE47CA9004467AA /* BGPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316432DE47CA9004467AA /* BGPicker.swift */; }; - DD8316482DE49EE5004467AA /* Storage+Migrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316472DE49EE5004467AA /* Storage+Migrate.swift */; }; - DD83164A2DE4C504004467AA /* SettingsStepperRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316492DE4C504004467AA /* SettingsStepperRow.swift */; }; - DD83164C2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */; }; - DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; - DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; - DD91E4DE2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; - DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */; }; - DD9ACA042D32821400415D8A /* DeviceStatusTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ACA032D32821200415D8A /* DeviceStatusTask.swift */; }; - DD9ACA062D32AF7900415D8A /* TreatmentsTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ACA052D32AF6E00415D8A /* TreatmentsTask.swift */; }; - DD9ACA082D32F68B00415D8A /* BGTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ACA072D32F68900415D8A /* BGTask.swift */; }; - DD9ACA0A2D33095600415D8A /* MinAgoTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ACA092D33095500415D8A /* MinAgoTask.swift */; }; - DD9ACA0C2D33BB8600415D8A /* CalendarTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ACA0B2D33BB8400415D8A /* CalendarTask.swift */; }; - DD9ACA0E2D340BFF00415D8A /* AlarmTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ACA0D2D340BFE00415D8A /* AlarmTask.swift */; }; - DD9ACA102D34129200415D8A /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ACA0F2D34128600415D8A /* Task.swift */; }; - DD9ED0C82D355244000D2A63 /* LogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0C72D35523F000D2A63 /* LogViewModel.swift */; }; - DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0C92D355256000D2A63 /* LogView.swift */; }; - DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */; }; - DD9ED0CE2D35587A000D2A63 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CD2D355879000D2A63 /* LogEntry.swift */; }; - DDA9ACA62D6A66D000E6F1A9 /* ContactColorMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */; }; - DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */; }; - DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */; }; - DDA9ACAC2D6B317100E6F1A9 /* ContactType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */; }; - DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */; }; - DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */; }; + DD50C10A2F60A00000000001 /* SocketIO in Frameworks */ = {isa = PBXBuildFile; productRef = DD50C10A2F60A00000000003 /* SocketIO */; }; DDB0AF552BB1B24A00AFA48B /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */; }; - DDB9FC7B2DDB573F00EFAA76 /* IOBCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */; }; - DDB9FC7D2DDB575300EFAA76 /* IOBAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */; }; - DDB9FC7F2DDB584500EFAA76 /* BolusEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */; }; - DDBD19962DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBD19952DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift */; }; - DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */; }; - DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */; }; - DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */; }; - DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */; }; - DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */; }; - DDC6CA452DD8D8E60060EE25 /* PumpChangeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */; }; - DDC6CA472DD8D9010060EE25 /* PumpChangeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */; }; - DDC6CA492DD8E47A0060EE25 /* PumpVolumeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */; }; - DDC6CA4B2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */; }; - DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */; }; - DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */; }; - DDC7E5422DBD8A1600EB1127 /* AlarmGeneralSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5392DBD8A1600EB1127 /* AlarmGeneralSection.swift */; }; - DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */; }; - DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */; }; - DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */; }; - DDC7E5472DBD8A1600EB1127 /* AlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */; }; - DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */; }; - DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */; }; - DDCC3A4D2DDBB77C006F1C10 /* PhoneBatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4C2DDBB77C006F1C10 /* PhoneBatteryAlarmEditor.swift */; }; - DDCC3A4F2DDC5B54006F1C10 /* BatteryDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */; }; - DDCC3A502DDED000006F1C10 /* PumpBatteryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A512DDED000006F1C10 /* PumpBatteryCondition.swift */; }; - DDCC3A542DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */; }; - DDCC3A562DDC9617006F1C10 /* MissedBolusCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */; }; - DDCC3A582DDC9655006F1C10 /* MissedBolusAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */; }; - DDCC3A5A2DDC988F006F1C10 /* CarbSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A592DDC988F006F1C10 /* CarbSample.swift */; }; - DDCC3A5B2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A5C2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift */; }; - DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */; }; - DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A812D85FD14004DF4DD /* AlarmType.swift */; }; - DDCF9A882D85FD33004DF4DD /* AlarmData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A872D85FD33004DF4DD /* AlarmData.swift */; }; - DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */; }; DDCFCAF22B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */; }; - DDD10EFF2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10EFE2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift */; }; - DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */; }; - DDD10F032C518A6500D76A8E /* TreatmentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F022C518A6500D76A8E /* TreatmentResponse.swift */; }; - DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F042C529DA200D76A8E /* ObservableValue.swift */; }; - DDD10F072C529DE800D76A8E /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F062C529DE800D76A8E /* Observable.swift */; }; - DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */; }; - DDDB86F12DF7223C00AADDAC /* DeleteAlarmSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */; }; - DDDC01DD2E244B3100D9975C /* JWTManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC01DC2E244B3100D9975C /* JWTManager.swift */; }; - A1A1A10002000000A0CFEED1 /* LogRedactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A1A10002000000A0CFEED2 /* LogRedactor.swift */; }; - DDDC31CC2E13A7DF009EA0F3 /* AddAlarmSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */; }; - DDDC31CE2E13A811009EA0F3 /* AlarmTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */; }; - DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE69ED12C7256260013EAEC /* RemoteType.swift */; }; - DDE75D232DE5E505007C1FC1 /* Glyph.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D222DE5E505007C1FC1 /* Glyph.swift */; }; - DDE75D272DE5E539007C1FC1 /* ActionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D262DE5E539007C1FC1 /* ActionRow.swift */; }; - DDE75D292DE5E56C007C1FC1 /* LinkRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */; }; - DDE75D2B2DE5E613007C1FC1 /* NavigationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D2A2DE5E613007C1FC1 /* NavigationRow.swift */; }; - DDE75D2D2DE71401007C1FC1 /* TogglableSecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D2C2DE71401007C1FC1 /* TogglableSecureInput.swift */; }; - DDEF503A2D31615000999A5D /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF50392D31614200999A5D /* LogManager.swift */; }; - DDEF503C2D31BE2D00999A5D /* TaskScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF503B2D31BE2A00999A5D /* TaskScheduler.swift */; }; - DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF503E2D32754A00999A5D /* ProfileTask.swift */; }; - DDEF50402D479B8A00884336 /* LoopAPNSService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF503F2D479B8A00884336 /* LoopAPNSService.swift */; }; - DDEF50422D479BAA00884336 /* LoopAPNSCarbsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF50412D479BAA00884336 /* LoopAPNSCarbsView.swift */; }; - DDEF50432D479BBA00884336 /* LoopAPNSBolusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF50422D479BBA00884336 /* LoopAPNSBolusView.swift */; }; - DDEF50452D479BDA00884336 /* LoopAPNSRemoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF50442D479BDA00884336 /* LoopAPNSRemoteView.swift */; }; - DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */; }; - DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */; }; DDF2C0142BEFD468007A20E6 /* blacklisted-versions.json in Resources */ = {isa = PBXBuildFile; fileRef = DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */; }; - DDF699962C5582290058A8D9 /* TextFieldWithToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF699952C5582290058A8D9 /* TextFieldWithToolBar.swift */; }; - DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF699982C5AA3060058A8D9 /* TempTargetPresetManager.swift */; }; - DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6999A2C5AA32E0058A8D9 /* TempTargetPreset.swift */; }; - DDF6999E2C5AAA640058A8D9 /* ErrorMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6999D2C5AAA640058A8D9 /* ErrorMessageView.swift */; }; - DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF9676D2AD08C6E00C5EB95 /* SiteChange.swift */; }; - DDFD5C532CB167DA00D3FD68 /* TRCCommandType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFD5C522CB167DA00D3FD68 /* TRCCommandType.swift */; }; - DDFF3D7F2D1414A200BF9D9E /* BLEDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF3D7E2D1414A200BF9D9E /* BLEDevice.swift */; }; - DDFF3D852D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF3D842D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift */; }; - DDFF3D872D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF3D862D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift */; }; - DDFF3D892D1429AB00BF9D9E /* BackgroundRefreshType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF3D882D1429AB00BF9D9E /* BackgroundRefreshType.swift */; }; - F19449721F3B792730A0F4FD /* PendingFutureCarb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9BEC26E4E48EF9B811A372 /* PendingFutureCarb.swift */; }; - FC16A97A24996673003D6245 /* NightScout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97924996673003D6245 /* NightScout.swift */; }; - FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */; }; - FC16A97D24996747003D6245 /* SpeakBG.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97C24996747003D6245 /* SpeakBG.swift */; }; - FC16A97F249969E2003D6245 /* Graphs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97E249969E2003D6245 /* Graphs.swift */; }; - FC16A98124996C07003D6245 /* DateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A98024996C07003D6245 /* DateTime.swift */; }; - FC1BDD2B24A22650001B652C /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2A24A22650001B652C /* Stats.swift */; }; - FC1BDD2D24A23204001B652C /* MainViewController+updateStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */; }; - A1B2C3D4E5F6A7B8C9D0E1F3 /* StatsDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F2 /* StatsDisplayModel.swift */; }; - A1B2C3D4E5F6A7B8C9D0E1F5 /* StatsDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F4 /* StatsDisplayView.swift */; }; - FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2E24A232A3001B652C /* DataStructs.swift */; }; - FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FC3AE7B3249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld */; }; - FC3CAB022493B6220068A152 /* BackgroundTaskAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC688592489554800A0279D /* BackgroundTaskAudio.swift */; }; - A1A1A10001000000A0CFEED1 /* APNsCredentialValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A1A10001000000A0CFEED2 /* APNsCredentialValidator.swift */; }; FC5A5C3D2497B229009C550E /* Config.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = FC5A5C3C2497B229009C550E /* Config.xcconfig */; }; - FC7CE518248ABE37001F83B8 /* Siri_Alert_Calibration_Needed.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4A9248ABE2B001F83B8 /* Siri_Alert_Calibration_Needed.caf */; }; - FC7CE519248ABE37001F83B8 /* Rise_And_Shine.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4AA248ABE2B001F83B8 /* Rise_And_Shine.caf */; }; - FC7CE51A248ABE37001F83B8 /* Jingle_All_The_Way.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4AB248ABE2B001F83B8 /* Jingle_All_The_Way.caf */; }; - FC7CE51B248ABE37001F83B8 /* Thunder_Sound_FX.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4AC248ABE2B001F83B8 /* Thunder_Sound_FX.caf */; }; - FC7CE51C248ABE37001F83B8 /* Sci-Fi_Spaceship_Warm_Up.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4AD248ABE2B001F83B8 /* Sci-Fi_Spaceship_Warm_Up.caf */; }; - FC7CE51D248ABE37001F83B8 /* Not_Kiddin.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4AE248ABE2B001F83B8 /* Not_Kiddin.caf */; }; - FC7CE51E248ABE37001F83B8 /* Computer_Magic.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4AF248ABE2B001F83B8 /* Computer_Magic.caf */; }; - FC7CE51F248ABE37001F83B8 /* Cartoon_Tip_Toe_Sneaky_Walk.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B0248ABE2B001F83B8 /* Cartoon_Tip_Toe_Sneaky_Walk.caf */; }; - FC7CE520248ABE37001F83B8 /* Burglar_Alarm_Siren_1.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B1248ABE2C001F83B8 /* Burglar_Alarm_Siren_1.caf */; }; - FC7CE521248ABE37001F83B8 /* Emergency_Alarm_Carbon_Monoxide.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B2248ABE2C001F83B8 /* Emergency_Alarm_Carbon_Monoxide.caf */; }; - FC7CE522248ABE37001F83B8 /* Signature_Corporate.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B3248ABE2C001F83B8 /* Signature_Corporate.caf */; }; - FC7CE523248ABE37001F83B8 /* Wake_Up_Will_You.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B4248ABE2C001F83B8 /* Wake_Up_Will_You.caf */; }; - FC7CE524248ABE37001F83B8 /* Marimba_Flutter_or_Shake.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B5248ABE2C001F83B8 /* Marimba_Flutter_or_Shake.caf */; }; - FC7CE525248ABE37001F83B8 /* Sci-Fi_Warning.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B6248ABE2C001F83B8 /* Sci-Fi_Warning.caf */; }; - FC7CE526248ABE37001F83B8 /* Sci-Fi_Alarm_Loop_3.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B7248ABE2C001F83B8 /* Sci-Fi_Alarm_Loop_3.caf */; }; - FC7CE527248ABE37001F83B8 /* Alert_Tone_Ringtone_2.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B8248ABE2C001F83B8 /* Alert_Tone_Ringtone_2.caf */; }; - FC7CE528248ABE37001F83B8 /* Cartoon_Bounce_To_Ceiling.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4B9248ABE2D001F83B8 /* Cartoon_Bounce_To_Ceiling.caf */; }; - FC7CE529248ABE37001F83B8 /* Laser_Shoot.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4BA248ABE2D001F83B8 /* Laser_Shoot.caf */; }; - FC7CE52A248ABE37001F83B8 /* Nightguard.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4BB248ABE2D001F83B8 /* Nightguard.caf */; }; - FC7CE52B248ABE37001F83B8 /* Machine_Charge.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4BC248ABE2D001F83B8 /* Machine_Charge.caf */; }; - FC7CE52C248ABE37001F83B8 /* Cuckoo_Clock.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4BD248ABE2D001F83B8 /* Cuckoo_Clock.caf */; }; - FC7CE52D248ABE37001F83B8 /* Siri_Urgent_High_Glucose.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4BE248ABE2D001F83B8 /* Siri_Urgent_High_Glucose.caf */; }; - FC7CE52E248ABE37001F83B8 /* Cartoon_Villain_Horns.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4BF248ABE2D001F83B8 /* Cartoon_Villain_Horns.caf */; }; - FC7CE52F248ABE37001F83B8 /* Siri_Glucose_Rising_Fast.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C0248ABE2D001F83B8 /* Siri_Glucose_Rising_Fast.caf */; }; - FC7CE530248ABE37001F83B8 /* Dhol_Shuffleloop.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C1248ABE2E001F83B8 /* Dhol_Shuffleloop.caf */; }; - FC7CE531248ABE37001F83B8 /* Sci-Fi_Computer_Console_Alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C2248ABE2E001F83B8 /* Sci-Fi_Computer_Console_Alarm.caf */; }; - FC7CE532248ABE37001F83B8 /* Siri_High_Glucose.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C3248ABE2E001F83B8 /* Siri_High_Glucose.caf */; }; - FC7CE533248ABE37001F83B8 /* Ending_Reached.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C4248ABE2E001F83B8 /* Ending_Reached.caf */; }; - FC7CE534248ABE37001F83B8 /* Siri_Missed_Readings.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C5248ABE2E001F83B8 /* Siri_Missed_Readings.caf */; }; - FC7CE535248ABE37001F83B8 /* Cartoon_Ascend_Climb_Sneaky.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C6248ABE2E001F83B8 /* Cartoon_Ascend_Climb_Sneaky.caf */; }; - FC7CE536248ABE37001F83B8 /* Fly.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C7248ABE2E001F83B8 /* Fly.caf */; }; - FC7CE537248ABE37001F83B8 /* Open_Your_Eyes_And_See.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C8248ABE2E001F83B8 /* Open_Your_Eyes_And_See.caf */; }; - FC7CE538248ABE37001F83B8 /* 20ms-of-silence.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4C9248ABE2F001F83B8 /* 20ms-of-silence.caf */; }; - FC7CE539248ABE37001F83B8 /* Remembers_Me_Of_Asia.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4CA248ABE2F001F83B8 /* Remembers_Me_Of_Asia.caf */; }; - FC7CE53A248ABE37001F83B8 /* Alarm_Clock.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4CB248ABE2F001F83B8 /* Alarm_Clock.caf */; }; - FC7CE53B248ABE37001F83B8 /* Cartoon_Dreamy_Glissando_Harp.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4CC248ABE2F001F83B8 /* Cartoon_Dreamy_Glissando_Harp.caf */; }; - FC7CE53C248ABE37001F83B8 /* Early_Sunrise.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4CD248ABE2F001F83B8 /* Early_Sunrise.caf */; }; - FC7CE53D248ABE37001F83B8 /* 1-millisecond-of-silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4CE248ABE2F001F83B8 /* 1-millisecond-of-silence.mp3 */; }; - FC7CE53E248ABE37001F83B8 /* Soft_Warm_Airy_Optimistic.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4CF248ABE30001F83B8 /* Soft_Warm_Airy_Optimistic.caf */; }; - FC7CE53F248ABE37001F83B8 /* Siri_Alert_Glucose_Dropping_Fast.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D0248ABE30001F83B8 /* Siri_Alert_Glucose_Dropping_Fast.caf */; }; - FC7CE540248ABE37001F83B8 /* Two_Turtle_Doves.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D1248ABE30001F83B8 /* Two_Turtle_Doves.caf */; }; - FC7CE541248ABE37001F83B8 /* Discreet.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D2248ABE30001F83B8 /* Discreet.caf */; }; - FC7CE542248ABE37001F83B8 /* Alien_Siren.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D3248ABE30001F83B8 /* Alien_Siren.caf */; }; - FC7CE543248ABE37001F83B8 /* Pager_Beeps.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D4248ABE30001F83B8 /* Pager_Beeps.caf */; }; - FC7CE544248ABE37001F83B8 /* Sci-Fi_Alarm_Loop_1.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D5248ABE31001F83B8 /* Sci-Fi_Alarm_Loop_1.caf */; }; - FC7CE545248ABE37001F83B8 /* Indeed.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D6248ABE31001F83B8 /* Indeed.caf */; }; - FC7CE546248ABE37001F83B8 /* Cartoon_Uh_Oh.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D7248ABE31001F83B8 /* Cartoon_Uh_Oh.caf */; }; - FC7CE547248ABE37001F83B8 /* Sci-Fi_Air_Raid_Alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D8248ABE31001F83B8 /* Sci-Fi_Air_Raid_Alarm.caf */; }; - FC7CE548248ABE37001F83B8 /* Store_Door_Chime.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4D9248ABE31001F83B8 /* Store_Door_Chime.caf */; }; - FC7CE549248ABE37001F83B8 /* Magical_Twinkle.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4DA248ABE31001F83B8 /* Magical_Twinkle.caf */; }; - FC7CE54A248ABE37001F83B8 /* Siri_Alert_Urgent_High_Glucose.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4DB248ABE31001F83B8 /* Siri_Alert_Urgent_High_Glucose.caf */; }; - FC7CE54B248ABE37001F83B8 /* Siri_Alert_Device_Muted.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4DC248ABE32001F83B8 /* Siri_Alert_Device_Muted.caf */; }; - FC7CE54C248ABE37001F83B8 /* Emergency_Alarm_Siren.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4DD248ABE32001F83B8 /* Emergency_Alarm_Siren.caf */; }; - FC7CE54D248ABE37001F83B8 /* Analog_Watch_Alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4DE248ABE32001F83B8 /* Analog_Watch_Alarm.caf */; }; - FC7CE54E248ABE37001F83B8 /* Hell_Yeah_Somewhat_Calmer.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4DF248ABE32001F83B8 /* Hell_Yeah_Somewhat_Calmer.caf */; }; - FC7CE54F248ABE37001F83B8 /* Cartoon_Machine_Clumsy_Loop.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E0248ABE32001F83B8 /* Cartoon_Machine_Clumsy_Loop.caf */; }; - FC7CE550248ABE37001F83B8 /* Siri_Alert_High_Glucose.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E1248ABE32001F83B8 /* Siri_Alert_High_Glucose.caf */; }; - FC7CE551248ABE37001F83B8 /* Marimba_Descend.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E2248ABE32001F83B8 /* Marimba_Descend.caf */; }; - FC7CE552248ABE37001F83B8 /* Siri_Alert_Urgent_Low_Glucose.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E3248ABE32001F83B8 /* Siri_Alert_Urgent_Low_Glucose.caf */; }; - FC7CE553248ABE37001F83B8 /* Sci-Fi_Eerie_Alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E4248ABE32001F83B8 /* Sci-Fi_Eerie_Alarm.caf */; }; - FC7CE554248ABE37001F83B8 /* Siri_Alert_Glucose_Rising_Fast.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E5248ABE32001F83B8 /* Siri_Alert_Glucose_Rising_Fast.caf */; }; - FC7CE555248ABE37001F83B8 /* Sci-Fi_Incoming_Message_Alert.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E6248ABE32001F83B8 /* Sci-Fi_Incoming_Message_Alert.caf */; }; - FC7CE556248ABE37001F83B8 /* Sci-Fi_Console_Alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E7248ABE33001F83B8 /* Sci-Fi_Console_Alarm.caf */; }; - FC7CE557248ABE37001F83B8 /* Soft_Marimba_Pad_Positive.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E8248ABE33001F83B8 /* Soft_Marimba_Pad_Positive.caf */; }; - FC7CE558248ABE37001F83B8 /* Rush.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4E9248ABE33001F83B8 /* Rush.caf */; }; - FC7CE559248ABE37001F83B8 /* Martian_Scanner.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4EA248ABE33001F83B8 /* Martian_Scanner.caf */; }; - FC7CE55A248ABE37001F83B8 /* Alert_Tone_Ringtone_1.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4EB248ABE33001F83B8 /* Alert_Tone_Ringtone_1.caf */; }; - FC7CE55B248ABE37001F83B8 /* 500ms-of-silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4EC248ABE33001F83B8 /* 500ms-of-silence.mp3 */; }; - FC7CE55C248ABE37001F83B8 /* Big_Clock_Ticking.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4ED248ABE33001F83B8 /* Big_Clock_Ticking.caf */; }; - FC7CE55D248ABE37001F83B8 /* Metallic.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4EE248ABE33001F83B8 /* Metallic.caf */; }; - FC7CE55E248ABE37001F83B8 /* Cell_Phone_Ring_Tone.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4EF248ABE33001F83B8 /* Cell_Phone_Ring_Tone.caf */; }; - FC7CE55F248ABE37001F83B8 /* Siri_Alert_Transmitter_Battery_Low.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F0248ABE33001F83B8 /* Siri_Alert_Transmitter_Battery_Low.caf */; }; - FC7CE560248ABE37001F83B8 /* Burglar_Alarm_Siren_2.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F1248ABE33001F83B8 /* Burglar_Alarm_Siren_2.caf */; }; - FC7CE561248ABE37001F83B8 /* Wrong_Answer.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F2248ABE34001F83B8 /* Wrong_Answer.caf */; }; - FC7CE562248ABE37001F83B8 /* Soft_Warm_Airy_Reassuring.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F3248ABE34001F83B8 /* Soft_Warm_Airy_Reassuring.caf */; }; - FC7CE563248ABE37001F83B8 /* CSFX-2_Alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F4248ABE34001F83B8 /* CSFX-2_Alarm.caf */; }; - FC7CE564248ABE37001F83B8 /* Siri_Low_Glucose.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F5248ABE34001F83B8 /* Siri_Low_Glucose.caf */; }; - FC7CE565248ABE37001F83B8 /* Sci-Fi_Spaceship_Message.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F6248ABE34001F83B8 /* Sci-Fi_Spaceship_Message.caf */; }; - FC7CE566248ABE37001F83B8 /* In_A_Hurry.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F7248ABE34001F83B8 /* In_A_Hurry.caf */; }; - FC7CE567248ABE37001F83B8 /* Orchestral_Horns.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F8248ABE34001F83B8 /* Orchestral_Horns.caf */; }; - FC7CE568248ABE37001F83B8 /* Sci-Fi_Engine_Shut_Down.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4F9248ABE34001F83B8 /* Sci-Fi_Engine_Shut_Down.caf */; }; - FC7CE569248ABE37001F83B8 /* silence-1sec.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4FA248ABE34001F83B8 /* silence-1sec.mp3 */; }; - FC7CE56A248ABE37001F83B8 /* Cartoon_Siren.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4FB248ABE34001F83B8 /* Cartoon_Siren.caf */; }; - FC7CE56B248ABE37001F83B8 /* Cartoon_Ascend_Then_Descend.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4FC248ABE34001F83B8 /* Cartoon_Ascend_Then_Descend.caf */; }; - FC7CE56C248ABE37001F83B8 /* Time_Has_Come.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4FD248ABE35001F83B8 /* Time_Has_Come.caf */; }; - FC7CE56D248ABE37001F83B8 /* Siri_Alert_Low_Glucose.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4FE248ABE35001F83B8 /* Siri_Alert_Low_Glucose.caf */; }; - FC7CE56E248ABE37001F83B8 /* Ambulance.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4FF248ABE35001F83B8 /* Ambulance.caf */; }; - FC7CE56F248ABE37001F83B8 /* Ghost_Hover.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE500248ABE35001F83B8 /* Ghost_Hover.caf */; }; - FC7CE570248ABE37001F83B8 /* Insistently.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE501248ABE35001F83B8 /* Insistently.caf */; }; - FC7CE571248ABE37001F83B8 /* Sunny.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE502248ABE35001F83B8 /* Sunny.caf */; }; - FC7CE572248ABE37001F83B8 /* Siri_Calibration_Needed.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE503248ABE35001F83B8 /* Siri_Calibration_Needed.caf */; }; - FC7CE573248ABE37001F83B8 /* Marching_Heavy_Footed_Fat_Elephants.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE504248ABE35001F83B8 /* Marching_Heavy_Footed_Fat_Elephants.caf */; }; - FC7CE574248ABE37001F83B8 /* Chimes_Glassy.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE505248ABE35001F83B8 /* Chimes_Glassy.caf */; }; - FC7CE575248ABE37001F83B8 /* Siri_Device_Muted.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE506248ABE35001F83B8 /* Siri_Device_Muted.caf */; }; - FC7CE576248ABE37001F83B8 /* Alarm_Buzzer.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE507248ABE35001F83B8 /* Alarm_Buzzer.caf */; }; - FC7CE577248ABE37001F83B8 /* Cartoon_Fail_Strings_Trumpet.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE508248ABE35001F83B8 /* Cartoon_Fail_Strings_Trumpet.caf */; }; - FC7CE578248ABE37001F83B8 /* Siri_Alert_Missed_Readings.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE509248ABE36001F83B8 /* Siri_Alert_Missed_Readings.caf */; }; - FC7CE579248ABE37001F83B8 /* Good_Morning.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE50A248ABE36001F83B8 /* Good_Morning.caf */; }; - FC7CE57A248ABE37001F83B8 /* Emergency_Alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE50B248ABE36001F83B8 /* Emergency_Alarm.caf */; }; - FC7CE57B248ABE37001F83B8 /* Alert_Tone_Busy.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE50C248ABE36001F83B8 /* Alert_Tone_Busy.caf */; }; - FC7CE57C248ABE37001F83B8 /* Tornado_Siren.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE50D248ABE36001F83B8 /* Tornado_Siren.caf */; }; - FC7CE57D248ABE37001F83B8 /* Win_Gain.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE50E248ABE36001F83B8 /* Win_Gain.caf */; }; - FC7CE57E248ABE37001F83B8 /* Unpaved.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE50F248ABE36001F83B8 /* Unpaved.caf */; }; - FC7CE57F248ABE37001F83B8 /* Sci-Fi_Alarm_Loop_4.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE510248ABE36001F83B8 /* Sci-Fi_Alarm_Loop_4.caf */; }; - FC7CE580248ABE37001F83B8 /* Sci-Fi_Alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE511248ABE36001F83B8 /* Sci-Fi_Alarm.caf */; }; - FC7CE581248ABE37001F83B8 /* Siri_Urgent_Low_Glucose.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE512248ABE36001F83B8 /* Siri_Urgent_Low_Glucose.caf */; }; - FC7CE582248ABE37001F83B8 /* Siri_Transmitter_Battery_Low.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE513248ABE36001F83B8 /* Siri_Transmitter_Battery_Low.caf */; }; - FC7CE583248ABE37001F83B8 /* Sci-Fi_Alarm_Loop_2.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE514248ABE36001F83B8 /* Sci-Fi_Alarm_Loop_2.caf */; }; - FC7CE584248ABE37001F83B8 /* Oringz.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE515248ABE37001F83B8 /* Oringz.caf */; }; - FC7CE585248ABE37001F83B8 /* Siri_Glucose_Dropping_Fast.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE516248ABE37001F83B8 /* Siri_Glucose_Dropping_Fast.caf */; }; - FC7CE586248ABE37001F83B8 /* Martian_Gun.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE517248ABE37001F83B8 /* Martian_Gun.caf */; }; - FC7CE58D248ABEF2001F83B8 /* alarm.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE58B248ABEF1001F83B8 /* alarm.mp3 */; }; - FC7CE58E248ABEF2001F83B8 /* alarm-notification.m4a in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE58C248ABEF1001F83B8 /* alarm-notification.m4a */; }; - FC7CE59C248D33A9001F83B8 /* dragbar.png in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE59B248D33A9001F83B8 /* dragbar.png */; }; - FC8589BF252B54F500C8FC73 /* Mobileprovision.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */; }; - FC9788182485969B00A7906C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC9788172485969B00A7906C /* AppDelegate.swift */; }; - FC97881C2485969B00A7906C /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC97881B2485969B00A7906C /* MainViewController.swift */; }; - FC97881E2485969B00A7906C /* NightScoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC97881D2485969B00A7906C /* NightScoutViewController.swift */; }; - FC9788262485969C00A7906C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FC9788252485969C00A7906C /* Assets.xcassets */; }; - FC9788292485969C00A7906C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC9788272485969C00A7906C /* LaunchScreen.storyboard */; }; - FCA2DDE62501095000254A8C /* Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCA2DDE52501095000254A8C /* Timers.swift */; }; - FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */; }; - FCC6885C2489559400A0279D /* blank.wav in Resources */ = {isa = PBXBuildFile; fileRef = FCC6885B2489559400A0279D /* blank.wav */; }; - FCC6885E24896A6C00A0279D /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FCC6885D24896A6C00A0279D /* silence.mp3 */; }; - FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886624898F8000A0279D /* UserDefaultsValue.swift */; }; - FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */; }; - FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886A24898FD800A0279D /* ObservationToken.swift */; }; - FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886C2489909D00A0279D /* AnyConvertible.swift */; }; - FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886E2489A53800A0279D /* AppConstants.swift */; }; - 9C7FB98C98BE4FF98F4815EE /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFBE69CEF18416D84959974 /* Telemetry.swift */; }; - FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD2A27C24C9D044009F7B7B /* Globals.swift */; }; - FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */; }; - FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEF87AA24A1417900AE6FA0 /* Localizer.swift */; }; FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCFEEC9D2486E68E00402A7F /* WebKit.framework */; }; - FCFEECA02488157B00402A7F /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEEC9F2488157B00402A7F /* Chart.swift */; }; - DD50C10A2F60A00000000001 /* SocketIO in Frameworks */ = {isa = PBXBuildFile; productRef = DD50C10A2F60A00000000003 /* SocketIO */; }; - DD50C10A2F60B00000000002 /* NightscoutSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */; }; - DD50C10A2F60B00000000004 /* NightscoutSocketDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -475,450 +51,73 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteBolusHistoryEntry.swift; sourceTree = ""; }; - B500000000000000000000A3 /* QuickPickBolusesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickBolusesManager.swift; sourceTree = ""; }; - B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMealHistoryEntry.swift; sourceTree = ""; }; - B500000000000000000000B3 /* QuickPickMealsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickMealsManager.swift; sourceTree = ""; }; 059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = ""; }; - 2B9BEC26E4E48EF9B811A372 /* PendingFutureCarb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingFutureCarb.swift; sourceTree = ""; }; - 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureCarbsCondition.swift; sourceTree = ""; }; - 374A77982F5BD8AB00E96858 /* APNSClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSClient.swift; sourceTree = ""; }; - 374A779F2F5BE17000E96858 /* AppGroupID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupID.swift; sourceTree = ""; }; - 374A77A02F5BE17000E96858 /* GlucoseLiveActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseLiveActivityAttributes.swift; sourceTree = ""; }; - 374A77A12F5BE17000E96858 /* GlucoseSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSnapshot.swift; sourceTree = ""; }; - 374A77A32F5BE17000E96858 /* LAAppGroupSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAAppGroupSettings.swift; sourceTree = ""; }; - 374A77AE2F5BE1AC00E96858 /* GlucoseSnapshotBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSnapshotBuilder.swift; sourceTree = ""; }; - 374A77AF2F5BE1AC00E96858 /* GlucoseSnapshotStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSnapshotStore.swift; sourceTree = ""; }; - 374A77B12F5BE1AC00E96858 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = ""; }; - 374A77B22F5BE1AC00E96858 /* PreferredGlucoseUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferredGlucoseUnit.swift; sourceTree = ""; }; - 374A77B32F5BE1AC00E96858 /* StorageCurrentGlucoseStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageCurrentGlucoseStateProvider.swift; sourceTree = ""; }; 374DAACA2F5B924B00BB663B /* LoopFollowLAExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LoopFollowLAExtensionExtension.entitlements; sourceTree = ""; }; - 379BECA92F6588300069DC62 /* RestartLiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntent.swift; sourceTree = ""; }; - 379BECAF2F65DA4B0069DC62 /* LiveActivitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsView.swift; sourceTree = ""; }; 37A4BDD92F5B6B4A00EEB289 /* LoopFollowLAExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LoopFollowLAExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 37A4BDDA2F5B6B4A00EEB289 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = /System/Library/Frameworks/WidgetKit.framework; sourceTree = ""; }; 37A4BDDC2F5B6B4A00EEB289 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = /System/Library/Frameworks/SwiftUI.framework; sourceTree = ""; }; - 37E4DD0C2F7E0967000511C8 /* LALivenessStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LALivenessStore.swift; sourceTree = ""; }; - 37E4DD0F2F7E0985000511C8 /* LALivenessMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LALivenessMarker.swift; sourceTree = ""; }; - 654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleQRCodeScannerView.swift; sourceTree = ""; }; - 654132E92E19F24800BDBE08 /* TOTPGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPGenerator.swift; sourceTree = ""; }; - 654134172E1DC09700BDBE08 /* OverridePresetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetsView.swift; sourceTree = ""; }; - 654134192E1DC27900BDBE08 /* OverridePresetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetData.swift; sourceTree = ""; }; - 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; }; - 656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeDisplayView.swift; sourceTree = ""; }; - 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = ""; }; - 656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCommandSettings.swift; sourceTree = ""; }; - 657F98172F043D8100F732BD /* HomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeContentView.swift; sourceTree = ""; }; - 657F99E82F0BC81500F732BD /* OTPSecureMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPSecureMessenger.swift; sourceTree = ""; }; - 6584B1002E4A263900135D4D /* TOTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPService.swift; sourceTree = ""; }; - 6589CC522E9E7D1600BB18FE /* ExportableSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableSettings.swift; sourceTree = ""; }; - 6589CC532E9E7D1600BB18FE /* ImportExportSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportSettingsView.swift; sourceTree = ""; }; - 6589CC542E9E7D1600BB18FE /* ImportExportSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportSettingsViewModel.swift; sourceTree = ""; }; - 6589CC562E9E7D1600BB18FE /* AdvancedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsView.swift; sourceTree = ""; }; - 6589CC572E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsViewModel.swift; sourceTree = ""; }; - 6589CC582E9E7D1600BB18FE /* CalendarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarSettingsView.swift; sourceTree = ""; }; - 6589CC592E9E7D1600BB18FE /* ContactSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactSettingsView.swift; sourceTree = ""; }; - 6589CC5A2E9E7D1600BB18FE /* ContactSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactSettingsViewModel.swift; sourceTree = ""; }; - 6589CC5B2E9E7D1600BB18FE /* DexcomSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSettingsView.swift; sourceTree = ""; }; - 6589CC5C2E9E7D1600BB18FE /* DexcomSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSettingsViewModel.swift; sourceTree = ""; }; - 6589CC5D2E9E7D1600BB18FE /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; - 6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsView.swift; sourceTree = ""; }; - 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMenuView.swift; sourceTree = ""; }; - 6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationModal.swift; sourceTree = ""; }; - 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogNoticeView.swift; sourceTree = ""; }; - 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSelectionView.swift; sourceTree = ""; }; - 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationManager.swift; sourceTree = ""; }; - 65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsSettingsView.swift; sourceTree = ""; }; - 65A100022F5AA00000AA1002 /* UnitsConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsConfigurationView.swift; sourceTree = ""; }; - 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = ""; }; - 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonHandler.swift; sourceTree = ""; }; A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - A8CA8BE0B3D247408FE088B4 /* BackgroundRefreshManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundRefreshManager.swift; sourceTree = ""; }; - B7D2A4EFD18B7B7748B6669E /* FutureCarbsAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureCarbsAlarmEditor.swift; sourceTree = ""; }; - DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = ""; }; - DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = ""; }; - DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinPrecisionManager.swift; sourceTree = ""; }; - DD026E5A2EA2C9C300A39CB5 /* InsulinFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinFormatter.swift; sourceTree = ""; }; - DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGSection.swift; sourceTree = ""; }; - DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBGCondition.swift; sourceTree = ""; }; - DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = ""; }; - DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBGCondition.swift; sourceTree = ""; }; - DD0650F02DCE9A9E004D3B41 /* MissedReadingCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedReadingCondition.swift; sourceTree = ""; }; - DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedReadingEditor.swift; sourceTree = ""; }; - DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmStepperSection.swift; sourceTree = ""; }; - DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = ""; }; - DD0650F82DCFE7BE004D3B41 /* FastDropAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastDropAlarmEditor.swift; sourceTree = ""; }; - DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; - DD0B9D552DE1EC8A0090C337 /* AlarmType+Snooze.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+Snooze.swift"; sourceTree = ""; }; - DD0B9D572DE1F3B20090C337 /* AlarmType+canAcknowledge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+canAcknowledge.swift"; sourceTree = ""; }; - DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; - DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; - DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKUnit+Extensions.swift"; sourceTree = ""; }; - DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDataSeparator.swift; sourceTree = ""; }; - DD0C0C672C48529400DBADDF /* Metric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metric.swift; sourceTree = ""; }; - DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinMetric.swift; sourceTree = ""; }; - DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbMetric.swift; sourceTree = ""; }; - DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteContentView.swift; sourceTree = ""; }; - AA1B2C3D4E5F6A7B8C9D0E1F /* LoopFollowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopFollowApp.swift; sourceTree = ""; }; - BB2C3D4E5F6A7B8C9D0E1F2A /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; - DD12D4862E1705E6004E0112 /* AlarmsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmsContainerView.swift; sourceTree = ""; }; - DD13BC742C3FD6200062313B /* InfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoType.swift; sourceTree = ""; }; - DD13BC762C3FD64E0062313B /* InfoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoData.swift; sourceTree = ""; }; - DD13BC782C3FE63A0062313B /* InfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoManager.swift; sourceTree = ""; }; - DD13BC7A2C3FE64A0062313B /* InfoTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoTableView.swift; sourceTree = ""; }; - DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorageValue.swift; sourceTree = ""; }; - DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKQuantityInputView.swift; sourceTree = ""; }; - DD16AF102C997B4600FB655A /* LoadingButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonView.swift; sourceTree = ""; }; - B500000000000000000000C1 /* QuickPickSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickSectionHeader.swift; sourceTree = ""; }; - DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = ""; }; - CC3D4E5F6A7B8C9D0E1F2A3B /* MoreMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuView.swift; sourceTree = ""; }; - DD7A3B5C2F1E8D9A00B4C6E1 /* BGDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGDisplayView.swift; sourceTree = ""; }; - DD7A3B5E2F1E8DA000B4C6E1 /* LineChartWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartWrapper.swift; sourceTree = ""; }; - DD7A3B602F1E8DA600B4C6E1 /* MainHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeView.swift; sourceTree = ""; }; - EE5F6A7B8C9D0E1F2A3B4C5D /* NightscoutContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutContentView.swift; sourceTree = ""; }; - DD1D52BF2E4C100000000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = ""; }; - DD1D52C12E4C100000000002 /* PredictionDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredictionDisplayType.swift; sourceTree = ""; }; - DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsView.swift; sourceTree = ""; }; - DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsViewModel.swift; sourceTree = ""; }; - DD485F132E454B2600CE8CBF /* SecureMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureMessenger.swift; sourceTree = ""; }; - DD4878022C7B297E0048F05C /* StorageValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageValue.swift; sourceTree = ""; }; - DD4878042C7B2C970048F05C /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; - DD4878072C7B30BF0048F05C /* RemoteSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSettingsView.swift; sourceTree = ""; }; - DD4878092C7B30D40048F05C /* RemoteSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSettingsViewModel.swift; sourceTree = ""; }; - AB1CD0022C7B30D40048F05C /* RemoteDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteDiagnostics.swift; sourceTree = ""; }; - DD48780D2C7B74A40048F05C /* TrioRemoteControlViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControlViewModel.swift; sourceTree = ""; }; - DD48780F2C7B74BF0048F05C /* TrioRemoteControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControlView.swift; sourceTree = ""; }; - DD4878122C7B750D0048F05C /* TempTargetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetView.swift; sourceTree = ""; }; - DD4878142C7B75230048F05C /* MealView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealView.swift; sourceTree = ""; }; - DD4878162C7B75350048F05C /* BolusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusView.swift; sourceTree = ""; }; - DD48781D2C7DAF2F0048F05C /* PushNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationManager.swift; sourceTree = ""; }; - DD48781F2C7DAF890048F05C /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = ""; }; - DD493AD42ACF2109009A6922 /* ResumePump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumePump.swift; sourceTree = ""; }; - DD493AD62ACF2139009A6922 /* SuspendPump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendPump.swift; sourceTree = ""; }; - DD493AD82ACF2171009A6922 /* Carbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carbs.swift; sourceTree = ""; }; - DD493ADA2ACF21A3009A6922 /* Bolus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bolus.swift; sourceTree = ""; }; - DD493ADC2ACF21E0009A6922 /* Basals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Basals.swift; sourceTree = ""; }; - DD493ADE2ACF22BB009A6922 /* SAge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAge.swift; sourceTree = ""; }; - DD493AE02ACF22FE009A6922 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; - DD493AE22ACF2358009A6922 /* CAge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CAge.swift; sourceTree = ""; }; - DD493AE42ACF2383009A6922 /* Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Treatments.swift; sourceTree = ""; }; - DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = ""; }; - DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = ""; }; - DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketManager.swift; sourceTree = ""; }; - DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketDataHandler.swift; sourceTree = ""; }; - DD4A407D2E6AFEE6007B318B /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; - DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeOfDay.swift; sourceTree = ""; }; - DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmConfiguration.swift; sourceTree = ""; }; - DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingsView.swift; sourceTree = ""; }; - DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmListView.swift; sourceTree = ""; }; - DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UUID+Identifiable.swift"; sourceTree = ""; }; - DD4AFB6A2DB6BF2A00BB593F /* Binding+Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Optional.swift"; sourceTree = ""; }; - DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageUpdater.swift; sourceTree = ""; }; - DD5334202C60EBEE00062F9D /* InsulinCartridgeChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinCartridgeChange.swift; sourceTree = ""; }; - DD5334222C60ED3600062F9D /* IAge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAge.swift; sourceTree = ""; }; - DD5334262C61668700062F9D /* InfoDisplaySettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDisplaySettingsViewModel.swift; sourceTree = ""; }; - DD5334282C6166A500062F9D /* InfoDisplaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDisplaySettingsView.swift; sourceTree = ""; }; - DD5334AF2D1447C500CDD6EA /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; - DD5817162D2710E50041FB98 /* BLEDeviceSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEDeviceSelectionView.swift; sourceTree = ""; }; - DD5817192D299EF40041FB98 /* DexcomHeartbeatBluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomHeartbeatBluetoothDevice.swift; sourceTree = ""; }; - DD58171B2D299F8D0041FB98 /* BluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDevice.swift; sourceTree = ""; }; - DD58171D2D299FC50041FB98 /* BluetoothDeviceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDeviceDelegate.swift; sourceTree = ""; }; - DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseValue.swift; sourceTree = ""; }; - DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusLoop.swift; sourceTree = ""; }; - DD608A092C23593900F91132 /* SMB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMB.swift; sourceTree = ""; }; - DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAlertManager.swift; sourceTree = ""; }; - DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusOpenAPS.swift; sourceTree = ""; }; - DD7B0D432D730A320063DCB6 /* CycleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CycleHelper.swift; sourceTree = ""; }; - DD7E19832ACDA50C00DBD158 /* Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Overrides.swift; sourceTree = ""; }; - DD7E19852ACDA59700DBD158 /* BGCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGCheck.swift; sourceTree = ""; }; - DD7E19872ACDA5DA00DBD158 /* Notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notes.swift; sourceTree = ""; }; - DD7E19892ACDA62600DBD158 /* SensorStart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorStart.swift; sourceTree = ""; }; - DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmAudioSection.swift; sourceTree = ""; }; - DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmActiveSection.swift; sourceTree = ""; }; - DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSnoozeSection.swift; sourceTree = ""; }; - DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGLimitSection.swift; sourceTree = ""; }; - DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastDropCondition.swift; sourceTree = ""; }; - DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingAlarmEditor.swift; sourceTree = ""; }; - DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingCondition.swift; sourceTree = ""; }; - DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStartAlarmEditor.swift; sourceTree = ""; }; - DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStartCondition.swift; sourceTree = ""; }; - DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideEndCondition.swift; sourceTree = ""; }; - DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideEndAlarmEditor.swift; sourceTree = ""; }; - DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStartCondition.swift; sourceTree = ""; }; - DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStartAlarmEditor.swift; sourceTree = ""; }; - DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetEndCondition.swift; sourceTree = ""; }; - DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetEndAlarmEditor.swift; sourceTree = ""; }; - DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusCondition.swift; sourceTree = ""; }; - DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusAlarmEditor.swift; sourceTree = ""; }; - DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBCondition.swift; sourceTree = ""; }; - DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBAlarmEditor.swift; sourceTree = ""; }; - DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseCondition.swift; sourceTree = ""; }; - DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseAlarmEditor.swift; sourceTree = ""; }; - DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+SortDirection.swift"; sourceTree = ""; }; - DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+timeUnit.swift"; sourceTree = ""; }; - DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; - DD8316432DE47CA9004467AA /* BGPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGPicker.swift; sourceTree = ""; }; - DD8316472DE49EE5004467AA /* Storage+Migrate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Migrate.swift"; sourceTree = ""; }; - DD8316492DE4C504004467AA /* SettingsStepperRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStepperRow.swift; sourceTree = ""; }; - DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BinaryFloatingPoint+localized.swift"; sourceTree = ""; }; - DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; - DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; - DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = ""; }; - DD9ACA032D32821200415D8A /* DeviceStatusTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusTask.swift; sourceTree = ""; }; - DD9ACA052D32AF6E00415D8A /* TreatmentsTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentsTask.swift; sourceTree = ""; }; - DD9ACA072D32F68900415D8A /* BGTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGTask.swift; sourceTree = ""; }; - DD9ACA092D33095500415D8A /* MinAgoTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinAgoTask.swift; sourceTree = ""; }; - DD9ACA0B2D33BB8400415D8A /* CalendarTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarTask.swift; sourceTree = ""; }; - DD9ACA0D2D340BFE00415D8A /* AlarmTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmTask.swift; sourceTree = ""; }; - DD9ACA0F2D34128600415D8A /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; - DD9ED0C72D35523F000D2A63 /* LogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewModel.swift; sourceTree = ""; }; - DD9ED0C92D355256000D2A63 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = ""; }; - DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; - DD9ED0CD2D355879000D2A63 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = ""; }; - DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorMode.swift; sourceTree = ""; }; - DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorOption.swift; sourceTree = ""; }; - DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactIncludeOption.swift; sourceTree = ""; }; - DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactType.swift; sourceTree = ""; }; - DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkHeartbeatBluetoothDevice.swift; sourceTree = ""; }; DDB0AF502BB1A84500AFA48B /* capture-build-details.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "capture-build-details.sh"; sourceTree = ""; }; - DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = ""; }; DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = ""; }; - DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBCondition.swift; sourceTree = ""; }; - DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBAlarmEditor.swift; sourceTree = ""; }; - DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusEntry.swift; sourceTree = ""; }; - DDBD19952DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Alarm+byPriorityThenSpec.swift"; sourceTree = ""; }; - DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = ""; }; - DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryCondition.swift; sourceTree = ""; }; - DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryAlarmEditor.swift; sourceTree = ""; }; - DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorAgeAlarmEditor.swift; sourceTree = ""; }; - DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorAgeCondition.swift; sourceTree = ""; }; - DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpChangeCondition.swift; sourceTree = ""; }; - DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpChangeAlarmEditor.swift; sourceTree = ""; }; - DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpVolumeCondition.swift; sourceTree = ""; }; - DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpVolumeAlarmEditor.swift; sourceTree = ""; }; - DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = ""; }; - DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = isOnPhoneCall.swift; sourceTree = ""; }; - DDC7E5392DBD8A1600EB1127 /* AlarmGeneralSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmGeneralSection.swift; sourceTree = ""; }; - DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundFile.swift; sourceTree = ""; }; - DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireAlarmEditor.swift; sourceTree = ""; }; - DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBgAlarmEditor.swift; sourceTree = ""; }; - DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmEditor.swift; sourceTree = ""; }; - DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewModel.swift; sourceTree = ""; }; - DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryCondition.swift; sourceTree = ""; }; - DDCC3A4C2DDBB77C006F1C10 /* PhoneBatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneBatteryAlarmEditor.swift; sourceTree = ""; }; - DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropCondition.swift; sourceTree = ""; }; - DDCC3A512DDED000006F1C10 /* PumpBatteryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpBatteryCondition.swift; sourceTree = ""; }; - DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropAlarmEditor.swift; sourceTree = ""; }; - DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedBolusCondition.swift; sourceTree = ""; }; - DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedBolusAlarmEditor.swift; sourceTree = ""; }; - DDCC3A592DDC988F006F1C10 /* CarbSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbSample.swift; sourceTree = ""; }; - DDCC3A5C2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpBatteryAlarmEditor.swift; sourceTree = ""; }; DDCC3ABF2DDE10B0006F1C10 /* Testing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Testing.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/Testing.framework; sourceTree = DEVELOPER_DIR; }; DDCC3AD62DDE1790006F1C10 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarm.swift; sourceTree = ""; }; - DDCF9A812D85FD14004DF4DD /* AlarmType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmType.swift; sourceTree = ""; }; - DDCF9A872D85FD33004DF4DD /* AlarmData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmData.swift; sourceTree = ""; }; - DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmManager.swift; sourceTree = ""; }; DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = LoopFollowDisplayNameConfig.xcconfig; sourceTree = ""; }; - DDD10EFE2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableUserDefaultsValue.swift; sourceTree = ""; }; - DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableUserDefaults.swift; sourceTree = ""; }; - DDD10F022C518A6500D76A8E /* TreatmentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentResponse.swift; sourceTree = ""; }; - DDD10F042C529DA200D76A8E /* ObservableValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableValue.swift; sourceTree = ""; }; - DDD10F062C529DE800D76A8E /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; - DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryTarget.swift; sourceTree = ""; }; - DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAlarmSection.swift; sourceTree = ""; }; - DDDC01DC2E244B3100D9975C /* JWTManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWTManager.swift; sourceTree = ""; }; - A1A1A10002000000A0CFEED2 /* LogRedactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRedactor.swift; sourceTree = ""; }; - DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAlarmSheet.swift; sourceTree = ""; }; - DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmTile.swift; sourceTree = ""; }; - DDE69ED12C7256260013EAEC /* RemoteType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteType.swift; sourceTree = ""; }; - DDE75D222DE5E505007C1FC1 /* Glyph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glyph.swift; sourceTree = ""; }; - DDE75D262DE5E539007C1FC1 /* ActionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRow.swift; sourceTree = ""; }; - DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkRow.swift; sourceTree = ""; }; - DDE75D2A2DE5E613007C1FC1 /* NavigationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRow.swift; sourceTree = ""; }; - DDE75D2C2DE71401007C1FC1 /* TogglableSecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TogglableSecureInput.swift; sourceTree = ""; }; - DDEF50392D31614200999A5D /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; - DDEF503B2D31BE2A00999A5D /* TaskScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskScheduler.swift; sourceTree = ""; }; - DDEF503E2D32754A00999A5D /* ProfileTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTask.swift; sourceTree = ""; }; - DDEF503F2D479B8A00884336 /* LoopAPNSService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAPNSService.swift; sourceTree = ""; }; - DDEF50412D479BAA00884336 /* LoopAPNSCarbsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAPNSCarbsView.swift; sourceTree = ""; }; - DDEF50422D479BBA00884336 /* LoopAPNSBolusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAPNSBolusView.swift; sourceTree = ""; }; - DDEF50442D479BDA00884336 /* LoopAPNSRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAPNSRemoteView.swift; sourceTree = ""; }; - DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubService.swift; sourceTree = ""; }; - DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionManager.swift; sourceTree = ""; }; DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blacklisted-versions.json"; sourceTree = ""; }; - DDF699952C5582290058A8D9 /* TextFieldWithToolBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldWithToolBar.swift; sourceTree = ""; }; - DDF699982C5AA3060058A8D9 /* TempTargetPresetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetPresetManager.swift; sourceTree = ""; }; - DDF6999A2C5AA32E0058A8D9 /* TempTargetPreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetPreset.swift; sourceTree = ""; }; - DDF6999D2C5AAA640058A8D9 /* ErrorMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessageView.swift; sourceTree = ""; }; - DDF9676D2AD08C6E00C5EB95 /* SiteChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteChange.swift; sourceTree = ""; }; - DDFD5C522CB167DA00D3FD68 /* TRCCommandType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TRCCommandType.swift; sourceTree = ""; }; - DDFF3D7E2D1414A200BF9D9E /* BLEDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEDevice.swift; sourceTree = ""; }; - DDFF3D842D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundRefreshSettingsView.swift; sourceTree = ""; }; - DDFF3D862D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundRefreshSettingsViewModel.swift; sourceTree = ""; }; - DDFF3D882D1429AB00BF9D9E /* BackgroundRefreshType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundRefreshType.swift; sourceTree = ""; }; - E7C2676561D686C6459CAA2D /* APNSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSettingsView.swift; sourceTree = ""; }; ECA3EFB4037410B4973BB632 /* Pods-LoopFollow.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.debug.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig"; sourceTree = ""; }; - FC16A97924996673003D6245 /* NightScout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightScout.swift; sourceTree = ""; }; - FC16A97C24996747003D6245 /* SpeakBG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakBG.swift; sourceTree = ""; }; - FC16A97E249969E2003D6245 /* Graphs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Graphs.swift; sourceTree = ""; }; - FC16A98024996C07003D6245 /* DateTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTime.swift; sourceTree = ""; }; - FC1BDD2A24A22650001B652C /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = ""; }; - FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+updateStats.swift"; sourceTree = ""; }; - A1B2C3D4E5F6A7B8C9D0E1F2 /* StatsDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDisplayModel.swift; sourceTree = ""; }; - A1B2C3D4E5F6A7B8C9D0E1F4 /* StatsDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDisplayView.swift; sourceTree = ""; }; - FC1BDD2E24A232A3001B652C /* DataStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStructs.swift; sourceTree = ""; }; - FC3AE7B4249E8E0E00AAE1E0 /* LoopFollow.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LoopFollow.xcdatamodel; sourceTree = ""; }; FC5A5C3C2497B229009C550E /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; - FC7CE4A9248ABE2B001F83B8 /* Siri_Alert_Calibration_Needed.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Calibration_Needed.caf; sourceTree = ""; }; - FC7CE4AA248ABE2B001F83B8 /* Rise_And_Shine.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Rise_And_Shine.caf; sourceTree = ""; }; - FC7CE4AB248ABE2B001F83B8 /* Jingle_All_The_Way.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Jingle_All_The_Way.caf; sourceTree = ""; }; - FC7CE4AC248ABE2B001F83B8 /* Thunder_Sound_FX.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Thunder_Sound_FX.caf; sourceTree = ""; }; - FC7CE4AD248ABE2B001F83B8 /* Sci-Fi_Spaceship_Warm_Up.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Spaceship_Warm_Up.caf"; sourceTree = ""; }; - FC7CE4AE248ABE2B001F83B8 /* Not_Kiddin.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Not_Kiddin.caf; sourceTree = ""; }; - FC7CE4AF248ABE2B001F83B8 /* Computer_Magic.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Computer_Magic.caf; sourceTree = ""; }; - FC7CE4B0248ABE2B001F83B8 /* Cartoon_Tip_Toe_Sneaky_Walk.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Tip_Toe_Sneaky_Walk.caf; sourceTree = ""; }; - FC7CE4B1248ABE2C001F83B8 /* Burglar_Alarm_Siren_1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Burglar_Alarm_Siren_1.caf; sourceTree = ""; }; - FC7CE4B2248ABE2C001F83B8 /* Emergency_Alarm_Carbon_Monoxide.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Emergency_Alarm_Carbon_Monoxide.caf; sourceTree = ""; }; - FC7CE4B3248ABE2C001F83B8 /* Signature_Corporate.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Signature_Corporate.caf; sourceTree = ""; }; - FC7CE4B4248ABE2C001F83B8 /* Wake_Up_Will_You.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Wake_Up_Will_You.caf; sourceTree = ""; }; - FC7CE4B5248ABE2C001F83B8 /* Marimba_Flutter_or_Shake.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Marimba_Flutter_or_Shake.caf; sourceTree = ""; }; - FC7CE4B6248ABE2C001F83B8 /* Sci-Fi_Warning.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Warning.caf"; sourceTree = ""; }; - FC7CE4B7248ABE2C001F83B8 /* Sci-Fi_Alarm_Loop_3.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Alarm_Loop_3.caf"; sourceTree = ""; }; - FC7CE4B8248ABE2C001F83B8 /* Alert_Tone_Ringtone_2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Alert_Tone_Ringtone_2.caf; sourceTree = ""; }; - FC7CE4B9248ABE2D001F83B8 /* Cartoon_Bounce_To_Ceiling.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Bounce_To_Ceiling.caf; sourceTree = ""; }; - FC7CE4BA248ABE2D001F83B8 /* Laser_Shoot.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Laser_Shoot.caf; sourceTree = ""; }; - FC7CE4BB248ABE2D001F83B8 /* Nightguard.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Nightguard.caf; sourceTree = ""; }; - FC7CE4BC248ABE2D001F83B8 /* Machine_Charge.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Machine_Charge.caf; sourceTree = ""; }; - FC7CE4BD248ABE2D001F83B8 /* Cuckoo_Clock.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cuckoo_Clock.caf; sourceTree = ""; }; - FC7CE4BE248ABE2D001F83B8 /* Siri_Urgent_High_Glucose.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Urgent_High_Glucose.caf; sourceTree = ""; }; - FC7CE4BF248ABE2D001F83B8 /* Cartoon_Villain_Horns.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Villain_Horns.caf; sourceTree = ""; }; - FC7CE4C0248ABE2D001F83B8 /* Siri_Glucose_Rising_Fast.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Glucose_Rising_Fast.caf; sourceTree = ""; }; - FC7CE4C1248ABE2E001F83B8 /* Dhol_Shuffleloop.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Dhol_Shuffleloop.caf; sourceTree = ""; }; - FC7CE4C2248ABE2E001F83B8 /* Sci-Fi_Computer_Console_Alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Computer_Console_Alarm.caf"; sourceTree = ""; }; - FC7CE4C3248ABE2E001F83B8 /* Siri_High_Glucose.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_High_Glucose.caf; sourceTree = ""; }; - FC7CE4C4248ABE2E001F83B8 /* Ending_Reached.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Ending_Reached.caf; sourceTree = ""; }; - FC7CE4C5248ABE2E001F83B8 /* Siri_Missed_Readings.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Missed_Readings.caf; sourceTree = ""; }; - FC7CE4C6248ABE2E001F83B8 /* Cartoon_Ascend_Climb_Sneaky.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Ascend_Climb_Sneaky.caf; sourceTree = ""; }; - FC7CE4C7248ABE2E001F83B8 /* Fly.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Fly.caf; sourceTree = ""; }; - FC7CE4C8248ABE2E001F83B8 /* Open_Your_Eyes_And_See.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Open_Your_Eyes_And_See.caf; sourceTree = ""; }; - FC7CE4C9248ABE2F001F83B8 /* 20ms-of-silence.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "20ms-of-silence.caf"; sourceTree = ""; }; - FC7CE4CA248ABE2F001F83B8 /* Remembers_Me_Of_Asia.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Remembers_Me_Of_Asia.caf; sourceTree = ""; }; - FC7CE4CB248ABE2F001F83B8 /* Alarm_Clock.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Alarm_Clock.caf; sourceTree = ""; }; - FC7CE4CC248ABE2F001F83B8 /* Cartoon_Dreamy_Glissando_Harp.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Dreamy_Glissando_Harp.caf; sourceTree = ""; }; - FC7CE4CD248ABE2F001F83B8 /* Early_Sunrise.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Early_Sunrise.caf; sourceTree = ""; }; - FC7CE4CE248ABE2F001F83B8 /* 1-millisecond-of-silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "1-millisecond-of-silence.mp3"; sourceTree = ""; }; - FC7CE4CF248ABE30001F83B8 /* Soft_Warm_Airy_Optimistic.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Soft_Warm_Airy_Optimistic.caf; sourceTree = ""; }; - FC7CE4D0248ABE30001F83B8 /* Siri_Alert_Glucose_Dropping_Fast.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Glucose_Dropping_Fast.caf; sourceTree = ""; }; - FC7CE4D1248ABE30001F83B8 /* Two_Turtle_Doves.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Two_Turtle_Doves.caf; sourceTree = ""; }; - FC7CE4D2248ABE30001F83B8 /* Discreet.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Discreet.caf; sourceTree = ""; }; - FC7CE4D3248ABE30001F83B8 /* Alien_Siren.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Alien_Siren.caf; sourceTree = ""; }; - FC7CE4D4248ABE30001F83B8 /* Pager_Beeps.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Pager_Beeps.caf; sourceTree = ""; }; - FC7CE4D5248ABE31001F83B8 /* Sci-Fi_Alarm_Loop_1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Alarm_Loop_1.caf"; sourceTree = ""; }; - FC7CE4D6248ABE31001F83B8 /* Indeed.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Indeed.caf; sourceTree = ""; }; - FC7CE4D7248ABE31001F83B8 /* Cartoon_Uh_Oh.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Uh_Oh.caf; sourceTree = ""; }; - FC7CE4D8248ABE31001F83B8 /* Sci-Fi_Air_Raid_Alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Air_Raid_Alarm.caf"; sourceTree = ""; }; - FC7CE4D9248ABE31001F83B8 /* Store_Door_Chime.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Store_Door_Chime.caf; sourceTree = ""; }; - FC7CE4DA248ABE31001F83B8 /* Magical_Twinkle.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Magical_Twinkle.caf; sourceTree = ""; }; - FC7CE4DB248ABE31001F83B8 /* Siri_Alert_Urgent_High_Glucose.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Urgent_High_Glucose.caf; sourceTree = ""; }; - FC7CE4DC248ABE32001F83B8 /* Siri_Alert_Device_Muted.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Device_Muted.caf; sourceTree = ""; }; - FC7CE4DD248ABE32001F83B8 /* Emergency_Alarm_Siren.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Emergency_Alarm_Siren.caf; sourceTree = ""; }; - FC7CE4DE248ABE32001F83B8 /* Analog_Watch_Alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Analog_Watch_Alarm.caf; sourceTree = ""; }; - FC7CE4DF248ABE32001F83B8 /* Hell_Yeah_Somewhat_Calmer.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Hell_Yeah_Somewhat_Calmer.caf; sourceTree = ""; }; - FC7CE4E0248ABE32001F83B8 /* Cartoon_Machine_Clumsy_Loop.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Machine_Clumsy_Loop.caf; sourceTree = ""; }; - FC7CE4E1248ABE32001F83B8 /* Siri_Alert_High_Glucose.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_High_Glucose.caf; sourceTree = ""; }; - FC7CE4E2248ABE32001F83B8 /* Marimba_Descend.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Marimba_Descend.caf; sourceTree = ""; }; - FC7CE4E3248ABE32001F83B8 /* Siri_Alert_Urgent_Low_Glucose.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Urgent_Low_Glucose.caf; sourceTree = ""; }; - FC7CE4E4248ABE32001F83B8 /* Sci-Fi_Eerie_Alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Eerie_Alarm.caf"; sourceTree = ""; }; - FC7CE4E5248ABE32001F83B8 /* Siri_Alert_Glucose_Rising_Fast.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Glucose_Rising_Fast.caf; sourceTree = ""; }; - FC7CE4E6248ABE32001F83B8 /* Sci-Fi_Incoming_Message_Alert.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Incoming_Message_Alert.caf"; sourceTree = ""; }; - FC7CE4E7248ABE33001F83B8 /* Sci-Fi_Console_Alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Console_Alarm.caf"; sourceTree = ""; }; - FC7CE4E8248ABE33001F83B8 /* Soft_Marimba_Pad_Positive.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Soft_Marimba_Pad_Positive.caf; sourceTree = ""; }; - FC7CE4E9248ABE33001F83B8 /* Rush.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Rush.caf; sourceTree = ""; }; - FC7CE4EA248ABE33001F83B8 /* Martian_Scanner.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Martian_Scanner.caf; sourceTree = ""; }; - FC7CE4EB248ABE33001F83B8 /* Alert_Tone_Ringtone_1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Alert_Tone_Ringtone_1.caf; sourceTree = ""; }; - FC7CE4EC248ABE33001F83B8 /* 500ms-of-silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "500ms-of-silence.mp3"; sourceTree = ""; }; - FC7CE4ED248ABE33001F83B8 /* Big_Clock_Ticking.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Big_Clock_Ticking.caf; sourceTree = ""; }; - FC7CE4EE248ABE33001F83B8 /* Metallic.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Metallic.caf; sourceTree = ""; }; - FC7CE4EF248ABE33001F83B8 /* Cell_Phone_Ring_Tone.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cell_Phone_Ring_Tone.caf; sourceTree = ""; }; - FC7CE4F0248ABE33001F83B8 /* Siri_Alert_Transmitter_Battery_Low.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Transmitter_Battery_Low.caf; sourceTree = ""; }; - FC7CE4F1248ABE33001F83B8 /* Burglar_Alarm_Siren_2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Burglar_Alarm_Siren_2.caf; sourceTree = ""; }; - FC7CE4F2248ABE34001F83B8 /* Wrong_Answer.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Wrong_Answer.caf; sourceTree = ""; }; - FC7CE4F3248ABE34001F83B8 /* Soft_Warm_Airy_Reassuring.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Soft_Warm_Airy_Reassuring.caf; sourceTree = ""; }; - FC7CE4F4248ABE34001F83B8 /* CSFX-2_Alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "CSFX-2_Alarm.caf"; sourceTree = ""; }; - FC7CE4F5248ABE34001F83B8 /* Siri_Low_Glucose.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Low_Glucose.caf; sourceTree = ""; }; - FC7CE4F6248ABE34001F83B8 /* Sci-Fi_Spaceship_Message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Spaceship_Message.caf"; sourceTree = ""; }; - FC7CE4F7248ABE34001F83B8 /* In_A_Hurry.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = In_A_Hurry.caf; sourceTree = ""; }; - FC7CE4F8248ABE34001F83B8 /* Orchestral_Horns.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Orchestral_Horns.caf; sourceTree = ""; }; - FC7CE4F9248ABE34001F83B8 /* Sci-Fi_Engine_Shut_Down.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Engine_Shut_Down.caf"; sourceTree = ""; }; - FC7CE4FA248ABE34001F83B8 /* silence-1sec.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "silence-1sec.mp3"; sourceTree = ""; }; - FC7CE4FB248ABE34001F83B8 /* Cartoon_Siren.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Siren.caf; sourceTree = ""; }; - FC7CE4FC248ABE34001F83B8 /* Cartoon_Ascend_Then_Descend.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Ascend_Then_Descend.caf; sourceTree = ""; }; - FC7CE4FD248ABE35001F83B8 /* Time_Has_Come.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Time_Has_Come.caf; sourceTree = ""; }; - FC7CE4FE248ABE35001F83B8 /* Siri_Alert_Low_Glucose.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Low_Glucose.caf; sourceTree = ""; }; - FC7CE4FF248ABE35001F83B8 /* Ambulance.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Ambulance.caf; sourceTree = ""; }; - FC7CE500248ABE35001F83B8 /* Ghost_Hover.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Ghost_Hover.caf; sourceTree = ""; }; - FC7CE501248ABE35001F83B8 /* Insistently.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Insistently.caf; sourceTree = ""; }; - FC7CE502248ABE35001F83B8 /* Sunny.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Sunny.caf; sourceTree = ""; }; - FC7CE503248ABE35001F83B8 /* Siri_Calibration_Needed.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Calibration_Needed.caf; sourceTree = ""; }; - FC7CE504248ABE35001F83B8 /* Marching_Heavy_Footed_Fat_Elephants.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Marching_Heavy_Footed_Fat_Elephants.caf; sourceTree = ""; }; - FC7CE505248ABE35001F83B8 /* Chimes_Glassy.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Chimes_Glassy.caf; sourceTree = ""; }; - FC7CE506248ABE35001F83B8 /* Siri_Device_Muted.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Device_Muted.caf; sourceTree = ""; }; - FC7CE507248ABE35001F83B8 /* Alarm_Buzzer.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Alarm_Buzzer.caf; sourceTree = ""; }; - FC7CE508248ABE35001F83B8 /* Cartoon_Fail_Strings_Trumpet.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Cartoon_Fail_Strings_Trumpet.caf; sourceTree = ""; }; - FC7CE509248ABE36001F83B8 /* Siri_Alert_Missed_Readings.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Missed_Readings.caf; sourceTree = ""; }; - FC7CE50A248ABE36001F83B8 /* Good_Morning.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Good_Morning.caf; sourceTree = ""; }; - FC7CE50B248ABE36001F83B8 /* Emergency_Alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Emergency_Alarm.caf; sourceTree = ""; }; - FC7CE50C248ABE36001F83B8 /* Alert_Tone_Busy.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Alert_Tone_Busy.caf; sourceTree = ""; }; - FC7CE50D248ABE36001F83B8 /* Tornado_Siren.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Tornado_Siren.caf; sourceTree = ""; }; - FC7CE50E248ABE36001F83B8 /* Win_Gain.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Win_Gain.caf; sourceTree = ""; }; - FC7CE50F248ABE36001F83B8 /* Unpaved.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Unpaved.caf; sourceTree = ""; }; - FC7CE510248ABE36001F83B8 /* Sci-Fi_Alarm_Loop_4.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Alarm_Loop_4.caf"; sourceTree = ""; }; - FC7CE511248ABE36001F83B8 /* Sci-Fi_Alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Alarm.caf"; sourceTree = ""; }; - FC7CE512248ABE36001F83B8 /* Siri_Urgent_Low_Glucose.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Urgent_Low_Glucose.caf; sourceTree = ""; }; - FC7CE513248ABE36001F83B8 /* Siri_Transmitter_Battery_Low.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Transmitter_Battery_Low.caf; sourceTree = ""; }; - FC7CE514248ABE36001F83B8 /* Sci-Fi_Alarm_Loop_2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sci-Fi_Alarm_Loop_2.caf"; sourceTree = ""; }; - FC7CE515248ABE37001F83B8 /* Oringz.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Oringz.caf; sourceTree = ""; }; - FC7CE516248ABE37001F83B8 /* Siri_Glucose_Dropping_Fast.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Glucose_Dropping_Fast.caf; sourceTree = ""; }; - FC7CE517248ABE37001F83B8 /* Martian_Gun.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Martian_Gun.caf; sourceTree = ""; }; - FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlarmSound.swift; sourceTree = ""; }; - FC7CE58B248ABEF1001F83B8 /* alarm.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm.mp3; sourceTree = ""; }; - FC7CE58C248ABEF1001F83B8 /* alarm-notification.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "alarm-notification.m4a"; sourceTree = ""; }; - FC7CE59B248D33A9001F83B8 /* dragbar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dragbar.png; sourceTree = ""; }; - FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mobileprovision.swift; sourceTree = ""; }; - FC8DEEE62485D1ED0075863F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FC9788142485969B00A7906C /* Loop Follow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Loop Follow.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - FC9788172485969B00A7906C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - FC97881B2485969B00A7906C /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; - FC97881D2485969B00A7906C /* NightScoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightScoutViewController.swift; sourceTree = ""; }; - FC9788252485969C00A7906C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - FC9788282485969C00A7906C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - FCA2DDE52501095000254A8C /* Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timers.swift; sourceTree = ""; }; - FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryKeyPath.swift; sourceTree = ""; }; - FCC688592489554800A0279D /* BackgroundTaskAudio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskAudio.swift; sourceTree = ""; }; - A1A1A10001000000A0CFEED2 /* APNsCredentialValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNsCredentialValidator.swift; sourceTree = ""; }; - FCC6885B2489559400A0279D /* blank.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = blank.wav; sourceTree = ""; }; - FCC6885D24896A6C00A0279D /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; - FCC6886624898F8000A0279D /* UserDefaultsValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsValue.swift; sourceTree = ""; }; - FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsValueGroups.swift; sourceTree = ""; }; - FCC6886A24898FD800A0279D /* ObservationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationToken.swift; sourceTree = ""; }; - FCC6886C2489909D00A0279D /* AnyConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyConvertible.swift; sourceTree = ""; }; - FCC6886E2489A53800A0279D /* AppConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; - BDFBE69CEF18416D84959974 /* Telemetry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = ""; }; - FCC688702489A57C00A0279D /* Loop Follow.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Loop Follow.entitlements"; sourceTree = ""; }; - FCD2A27C24C9D044009F7B7B /* Globals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; - FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = carbBolusArrays.swift; sourceTree = ""; }; FCE537C2249AAB2600F80BF8 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; - FCEF87AA24A1417900AE6FA0 /* Localizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localizer.swift; sourceTree = ""; }; FCFEEC9D2486E68E00402A7F /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; - FCFEEC9F2488157B00402A7F /* Chart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chart.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + D736FE5E8494B2581A4EA95F /* Exceptions for "LoopFollow" folder in "LoopFollow" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = FC9788132485969B00A7906C /* LoopFollow */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ - 37A4BDDE2F5B6B4A00EEB289 /* LoopFollowLAExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LoopFollowLAExtension; sourceTree = ""; }; - 65AC25F52ECFD5E800421360 /* Stats */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Stats; sourceTree = ""; }; - 65AC26702ED245DF00421360 /* Treatments */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Treatments; sourceTree = ""; }; - DDCC3AD72DDE1790006F1C10 /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = ""; }; + 37A4BDDE2F5B6B4A00EEB289 /* LoopFollowLAExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = LoopFollowLAExtension; + sourceTree = ""; + }; + 9555FE45F7FAEFA6142D04CA /* LoopFollow */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D736FE5E8494B2581A4EA95F /* Exceptions for "LoopFollow" folder in "LoopFollow" target */, + ); + path = LoopFollow; + sourceTree = ""; + }; + BC0FBFD3754955D028EF6631 /* Shared */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = Shared; + sourceTree = ""; + }; + DDCC3AD72DDE1790006F1C10 /* Tests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = Tests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -951,61 +150,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 376310762F5CD65100656488 /* LiveActivity */ = { - isa = PBXGroup; - children = ( - 37E4DD0F2F7E0985000511C8 /* LALivenessMarker.swift */, - 37E4DD0C2F7E0967000511C8 /* LALivenessStore.swift */, - 374A77AE2F5BE1AC00E96858 /* GlucoseSnapshotBuilder.swift */, - 374A77AF2F5BE1AC00E96858 /* GlucoseSnapshotStore.swift */, - 374A77B12F5BE1AC00E96858 /* LiveActivityManager.swift */, - 374A77B22F5BE1AC00E96858 /* PreferredGlucoseUnit.swift */, - 374A77B32F5BE1AC00E96858 /* StorageCurrentGlucoseStateProvider.swift */, - 374A779F2F5BE17000E96858 /* AppGroupID.swift */, - 374A77A02F5BE17000E96858 /* GlucoseLiveActivityAttributes.swift */, - 374A77A12F5BE17000E96858 /* GlucoseSnapshot.swift */, - 374A77A32F5BE17000E96858 /* LAAppGroupSettings.swift */, - 374A77982F5BD8AB00E96858 /* APNSClient.swift */, - ); - path = LiveActivity; - sourceTree = ""; - }; - 6589CC552E9E7D1600BB18FE /* ImportExport */ = { - isa = PBXGroup; - children = ( - 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */, - 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */, - 6589CC522E9E7D1600BB18FE /* ExportableSettings.swift */, - 6589CC532E9E7D1600BB18FE /* ImportExportSettingsView.swift */, - 6589CC542E9E7D1600BB18FE /* ImportExportSettingsViewModel.swift */, - ); - path = ImportExport; - sourceTree = ""; - }; - 6589CC612E9E7D1600BB18FE /* Settings */ = { - isa = PBXGroup; - children = ( - 6589CC552E9E7D1600BB18FE /* ImportExport */, - 6589CC562E9E7D1600BB18FE /* AdvancedSettingsView.swift */, - E7C2676561D686C6459CAA2D /* APNSettingsView.swift */, - 6589CC572E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift */, - 6589CC582E9E7D1600BB18FE /* CalendarSettingsView.swift */, - 6589CC592E9E7D1600BB18FE /* ContactSettingsView.swift */, - 6589CC5A2E9E7D1600BB18FE /* ContactSettingsViewModel.swift */, - 6589CC5B2E9E7D1600BB18FE /* DexcomSettingsView.swift */, - 6589CC5C2E9E7D1600BB18FE /* DexcomSettingsViewModel.swift */, - 6589CC5D2E9E7D1600BB18FE /* GeneralSettingsView.swift */, - 6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */, - 657F98172F043D8100F732BD /* HomeContentView.swift */, - 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */, - 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */, - 65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */, - 65A100022F5AA00000AA1002 /* UnitsConfigurationView.swift */, - 6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */, - ); - path = Settings; - sourceTree = ""; - }; 6A5880E0B811AF443B05AB02 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1028,227 +172,6 @@ path = Pods; sourceTree = ""; }; - DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { - isa = PBXGroup; - children = ( - DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */, - 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */, - DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */, - DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */, - DDCC3A512DDED000006F1C10 /* PumpBatteryCondition.swift */, - DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */, - DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */, - DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */, - DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */, - DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */, - DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */, - DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */, - DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */, - DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */, - DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */, - DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */, - DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */, - DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */, - DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */, - DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */, - DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */, - DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */, - DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */, - DD0650F02DCE9A9E004D3B41 /* MissedReadingCondition.swift */, - ); - path = AlarmCondition; - sourceTree = ""; - }; - DD0C0C692C4852A100DBADDF /* Metric */ = { - isa = PBXGroup; - children = ( - DD0C0C672C48529400DBADDF /* Metric.swift */, - DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */, - DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */, - ); - path = Metric; - sourceTree = ""; - }; - B500000000000000000000B5 /* QuickPickMeals */ = { - isa = PBXGroup; - children = ( - B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */, - B500000000000000000000B3 /* QuickPickMealsManager.swift */, - ); - path = QuickPickMeals; - sourceTree = ""; - }; - B500000000000000000000A5 /* QuickPickBoluses */ = { - isa = PBXGroup; - children = ( - B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */, - B500000000000000000000A3 /* QuickPickBolusesManager.swift */, - ); - path = QuickPickBoluses; - sourceTree = ""; - }; - DD0C0C6E2C4AFFB800DBADDF /* Remote */ = { - isa = PBXGroup; - children = ( - B500000000000000000000A5 /* QuickPickBoluses */, - B500000000000000000000B5 /* QuickPickMeals */, - DDEF503E2D479B8A00884336 /* LoopAPNS */, - DD4878112C7B74F90048F05C /* TRC */, - DD4878062C7B2E9E0048F05C /* Settings */, - DDF699972C5AA2E50058A8D9 /* TempTargetPreset */, - DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */, - DDE69ED12C7256260013EAEC /* RemoteType.swift */, - ); - path = Remote; - sourceTree = ""; - }; - DD13BC732C3FD60E0062313B /* InfoTable */ = { - isa = PBXGroup; - children = ( - DD13BC742C3FD6200062313B /* InfoType.swift */, - DD13BC762C3FD64E0062313B /* InfoData.swift */, - DD13BC782C3FE63A0062313B /* InfoManager.swift */, - DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */, - DD13BC7A2C3FE64A0062313B /* InfoTableView.swift */, - ); - path = InfoTable; - sourceTree = ""; - }; - DD2C2E4D2D3B8ACF006413A5 /* Nightscout */ = { - isa = PBXGroup; - children = ( - DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */, - DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */, - ); - path = Nightscout; - sourceTree = ""; - }; - DD4878062C7B2E9E0048F05C /* Settings */ = { - isa = PBXGroup; - children = ( - 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */, - 656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */, - DD4878072C7B30BF0048F05C /* RemoteSettingsView.swift */, - DD4878092C7B30D40048F05C /* RemoteSettingsViewModel.swift */, - AB1CD0022C7B30D40048F05C /* RemoteDiagnostics.swift */, - ); - path = Settings; - sourceTree = ""; - }; - DD4878112C7B74F90048F05C /* TRC */ = { - isa = PBXGroup; - children = ( - DD485F132E454B2600CE8CBF /* SecureMessenger.swift */, - DD48781F2C7DAF890048F05C /* PushMessage.swift */, - DD4878122C7B750D0048F05C /* TempTargetView.swift */, - DD48781D2C7DAF2F0048F05C /* PushNotificationManager.swift */, - DD4878142C7B75230048F05C /* MealView.swift */, - DD48780F2C7B74BF0048F05C /* TrioRemoteControlView.swift */, - DD48780D2C7B74A40048F05C /* TrioRemoteControlViewModel.swift */, - DD4878162C7B75350048F05C /* BolusView.swift */, - DDD10F022C518A6500D76A8E /* TreatmentResponse.swift */, - DDFD5C522CB167DA00D3FD68 /* TRCCommandType.swift */, - DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */, - ); - path = TRC; - sourceTree = ""; - }; - DD493AEA2ACF2761009A6922 /* Treatments */ = { - isa = PBXGroup; - children = ( - DD7E19872ACDA5DA00DBD158 /* Notes.swift */, - DD7E19832ACDA50C00DBD158 /* Overrides.swift */, - DD7E19852ACDA59700DBD158 /* BGCheck.swift */, - DD7E19892ACDA62600DBD158 /* SensorStart.swift */, - DD493AD62ACF2139009A6922 /* SuspendPump.swift */, - DD493AD82ACF2171009A6922 /* Carbs.swift */, - DD493ADC2ACF21E0009A6922 /* Basals.swift */, - DD493ADA2ACF21A3009A6922 /* Bolus.swift */, - DD493AD42ACF2109009A6922 /* ResumePump.swift */, - DDF9676D2AD08C6E00C5EB95 /* SiteChange.swift */, - DD608A092C23593900F91132 /* SMB.swift */, - DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */, - DD5334202C60EBEE00062F9D /* InsulinCartridgeChange.swift */, - ); - path = Treatments; - sourceTree = ""; - }; - DD50C74D2D0828250057AE6F /* Contact */ = { - isa = PBXGroup; - children = ( - DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */, - DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */, - DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */, - DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */, - DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */, - ); - path = Contact; - sourceTree = ""; - }; - DD5334252C61667700062F9D /* InfoDisplaySettings */ = { - isa = PBXGroup; - children = ( - DD5334262C61668700062F9D /* InfoDisplaySettingsViewModel.swift */, - DD5334282C6166A500062F9D /* InfoDisplaySettingsView.swift */, - ); - path = InfoDisplaySettings; - sourceTree = ""; - }; - DD7E19802ACDA0EA00DBD158 /* Nightscout */ = { - isa = PBXGroup; - children = ( - DD493AEA2ACF2761009A6922 /* Treatments */, - DD493ADE2ACF22BB009A6922 /* SAge.swift */, - DD493AE02ACF22FE009A6922 /* Profile.swift */, - DD493AE22ACF2358009A6922 /* CAge.swift */, - DD493AE42ACF2383009A6922 /* Treatments.swift */, - DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */, - DD493AE82ACF2445009A6922 /* BGData.swift */, - DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */, - DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */, - DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */, - DD0C0C612C4175FD00DBADDF /* NSProfile.swift */, - DD5334222C60ED3600062F9D /* IAge.swift */, - DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */, - DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */, - ); - path = Nightscout; - sourceTree = ""; - }; - DD98F54224BCEF190007425A /* Extensions */ = { - isa = PBXGroup; - children = ( - DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */, - DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */, - DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */, - DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */, - DD4AFB6A2DB6BF2A00BB593F /* Binding+Optional.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - DD9ED0C62D355225000D2A63 /* Log */ = { - isa = PBXGroup; - children = ( - DD9ED0CD2D355879000D2A63 /* LogEntry.swift */, - DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */, - DD9ED0C92D355256000D2A63 /* LogView.swift */, - DD9ED0C72D35523F000D2A63 /* LogViewModel.swift */, - DDEF50392D31614200999A5D /* LogManager.swift */, - ); - path = Log; - sourceTree = ""; - }; - DDAD16302D2EF98C0084BE10 /* Devices */ = { - isa = PBXGroup; - children = ( - DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */, - DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */, - DD5817192D299EF40041FB98 /* DexcomHeartbeatBluetoothDevice.swift */, - ); - path = Devices; - sourceTree = ""; - }; DDB0AF4F2BB1A81F00AFA48B /* Scripts */ = { isa = PBXGroup; children = ( @@ -1257,431 +180,22 @@ path = Scripts; sourceTree = ""; }; - DDC6CA3B2DD7B9050060EE25 /* AlarmType */ = { - isa = PBXGroup; - children = ( - DDCF9A812D85FD14004DF4DD /* AlarmType.swift */, - DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */, - DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */, - DD0B9D552DE1EC8A0090C337 /* AlarmType+Snooze.swift */, - DD0B9D572DE1F3B20090C337 /* AlarmType+canAcknowledge.swift */, - ); - path = AlarmType; - sourceTree = ""; - }; - DDC7E5142DBCE1B900EB1127 /* Snoozer */ = { - isa = PBXGroup; - children = ( - DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */, - DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */, - ); - path = Snoozer; - sourceTree = ""; - }; - DDC7E53B2DBD8A1600EB1127 /* Components */ = { - isa = PBXGroup; - children = ( - DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */, - DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */, - DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */, - DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */, - DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */, - DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */, - DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */, - DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */, - DDC7E5392DBD8A1600EB1127 /* AlarmGeneralSection.swift */, - DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */, - ); - path = Components; - sourceTree = ""; - }; - DDC7E53F2DBD8A1600EB1127 /* Editors */ = { - isa = PBXGroup; - children = ( - DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */, - B7D2A4EFD18B7B7748B6669E /* FutureCarbsAlarmEditor.swift */, - DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */, - DDCC3A4C2DDBB77C006F1C10 /* PhoneBatteryAlarmEditor.swift */, - DDCC3A5C2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift */, - DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */, - DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */, - DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */, - DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */, - DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */, - DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */, - DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */, - DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */, - DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */, - DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */, - DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */, - DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */, - DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */, - DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */, - DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */, - DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */, - DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */, - DD0650F82DCFE7BE004D3B41 /* FastDropAlarmEditor.swift */, - ); - path = Editors; - sourceTree = ""; - }; - DDC7E5412DBD8A1600EB1127 /* AlarmEditing */ = { - isa = PBXGroup; - children = ( - DDC7E53B2DBD8A1600EB1127 /* Components */, - DDC7E53F2DBD8A1600EB1127 /* Editors */, - DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */, - ); - path = AlarmEditing; - sourceTree = ""; - }; - DDC7E5CD2DC6637800EB1127 /* Storage */ = { - isa = PBXGroup; - children = ( - FCC688512489363F00A0279D /* Framework */, - DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */, - DDD10F062C529DE800D76A8E /* Observable.swift */, - DD4878042C7B2C970048F05C /* Storage.swift */, - DD8316472DE49EE5004467AA /* Storage+Migrate.swift */, - ); - path = Storage; - sourceTree = ""; - }; - DDCC3A502DDC5BD4006F1C10 /* DataStructs */ = { - isa = PBXGroup; - children = ( - DDCC3A592DDC988F006F1C10 /* CarbSample.swift */, - 2B9BEC26E4E48EF9B811A372 /* PendingFutureCarb.swift */, - DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */, - DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */, - ); - path = DataStructs; - sourceTree = ""; - }; - DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { - isa = PBXGroup; - children = ( - DD12D4862E1705E6004E0112 /* AlarmsContainerView.swift */, - DDDC31CA2E13A7D2009EA0F3 /* AddAlarm */, - DDCC3A502DDC5BD4006F1C10 /* DataStructs */, - DDC6CA3B2DD7B9050060EE25 /* AlarmType */, - DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */, - DDC7E5412DBD8A1600EB1127 /* AlarmEditing */, - DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */, - DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */, - DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */, - DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */, - DDCF9A872D85FD33004DF4DD /* AlarmData.swift */, - DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */, - DDBD19952DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift */, - ); - path = Alarm; - sourceTree = ""; - }; - DDDC31CA2E13A7D2009EA0F3 /* AddAlarm */ = { - isa = PBXGroup; - children = ( - DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */, - DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */, - ); - path = AddAlarm; - sourceTree = ""; - }; - DDEF503D2D32753A00999A5D /* Task */ = { - isa = PBXGroup; - children = ( - DD9ACA0F2D34128600415D8A /* Task.swift */, - DD9ACA0D2D340BFE00415D8A /* AlarmTask.swift */, - DD9ACA0B2D33BB8400415D8A /* CalendarTask.swift */, - DD9ACA092D33095500415D8A /* MinAgoTask.swift */, - DD9ACA072D32F68900415D8A /* BGTask.swift */, - DD9ACA052D32AF6E00415D8A /* TreatmentsTask.swift */, - DD9ACA032D32821200415D8A /* DeviceStatusTask.swift */, - DDEF503E2D32754A00999A5D /* ProfileTask.swift */, - DDEF503B2D31BE2A00999A5D /* TaskScheduler.swift */, - ); - path = Task; - sourceTree = ""; - }; - DDEF503E2D479B8A00884336 /* LoopAPNS */ = { - isa = PBXGroup; - children = ( - 657F99E82F0BC81500F732BD /* OTPSecureMessenger.swift */, - 6584B1002E4A263900135D4D /* TOTPService.swift */, - DDEF503F2D479B8A00884336 /* LoopAPNSService.swift */, - DDEF50412D479BAA00884336 /* LoopAPNSCarbsView.swift */, - DDEF50422D479BBA00884336 /* LoopAPNSBolusView.swift */, - 654134172E1DC09700BDBE08 /* OverridePresetsView.swift */, - 654134192E1DC27900BDBE08 /* OverridePresetData.swift */, - DDEF50442D479BDA00884336 /* LoopAPNSRemoteView.swift */, - ); - path = LoopAPNS; - sourceTree = ""; - }; - DDF699972C5AA2E50058A8D9 /* TempTargetPreset */ = { - isa = PBXGroup; - children = ( - DDF699982C5AA3060058A8D9 /* TempTargetPresetManager.swift */, - DDF6999A2C5AA32E0058A8D9 /* TempTargetPreset.swift */, - ); - path = TempTargetPreset; - sourceTree = ""; - }; - DDF6999C2C5AAA4C0058A8D9 /* Views */ = { - isa = PBXGroup; - children = ( - 656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */, - DDE75D2C2DE71401007C1FC1 /* TogglableSecureInput.swift */, - DDE75D222DE5E505007C1FC1 /* Glyph.swift */, - DD8316492DE4C504004467AA /* SettingsStepperRow.swift */, - DD8316432DE47CA9004467AA /* BGPicker.swift */, - DDF6999D2C5AAA640058A8D9 /* ErrorMessageView.swift */, - DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */, - DD16AF102C997B4600FB655A /* LoadingButtonView.swift */, - B500000000000000000000C1 /* QuickPickSectionHeader.swift */, - DDE75D262DE5E539007C1FC1 /* ActionRow.swift */, - 654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */, - DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */, - DDE75D2A2DE5E613007C1FC1 /* NavigationRow.swift */, - ); - path = Views; - sourceTree = ""; - }; - DDFF3D792D140F1800BF9D9E /* BackgroundRefresh */ = { - isa = PBXGroup; - children = ( - DDFF3D7A2D140F4200BF9D9E /* BT */, - DDFF3D842D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift */, - DDFF3D862D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift */, - DDFF3D882D1429AB00BF9D9E /* BackgroundRefreshType.swift */, - ); - path = BackgroundRefresh; - sourceTree = ""; - }; - DDFF3D7A2D140F4200BF9D9E /* BT */ = { - isa = PBXGroup; - children = ( - DDAD16302D2EF98C0084BE10 /* Devices */, - DD58171D2D299FC50041FB98 /* BluetoothDeviceDelegate.swift */, - DD58171B2D299F8D0041FB98 /* BluetoothDevice.swift */, - DD5817162D2710E50041FB98 /* BLEDeviceSelectionView.swift */, - DDFF3D7E2D1414A200BF9D9E /* BLEDevice.swift */, - DD5334AF2D1447C500CDD6EA /* BLEManager.swift */, - ); - path = BT; - sourceTree = ""; - }; - FC16A97624995FEE003D6245 /* Application */ = { - isa = PBXGroup; - children = ( - FC9788172485969B00A7906C /* AppDelegate.swift */, - AA1B2C3D4E5F6A7B8C9D0E1F /* LoopFollowApp.swift */, - BB2C3D4E5F6A7B8C9D0E1F2A /* MainTabView.swift */, - FC9788272485969C00A7906C /* LaunchScreen.storyboard */, - ); - path = Application; - sourceTree = ""; - }; - FC16A9782499657E003D6245 /* Controllers */ = { - isa = PBXGroup; - children = ( - DD7E19802ACDA0EA00DBD158 /* Nightscout */, - FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */, - FC16A97924996673003D6245 /* NightScout.swift */, - FC16A97C24996747003D6245 /* SpeakBG.swift */, - FC16A97E249969E2003D6245 /* Graphs.swift */, - FC1BDD2A24A22650001B652C /* Stats.swift */, - FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */, - A1B2C3D4E5F6A7B8C9D0E1F2 /* StatsDisplayModel.swift */, - A1B2C3D4E5F6A7B8C9D0E1F4 /* StatsDisplayView.swift */, - FCA2DDE52501095000254A8C /* Timers.swift */, - DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */, - 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - FC7CE4A8248ABE0D001F83B8 /* Alarm Sounds */ = { - isa = PBXGroup; - children = ( - FC7CE58C248ABEF1001F83B8 /* alarm-notification.m4a */, - FC7CE58B248ABEF1001F83B8 /* alarm.mp3 */, - FC7CE4CE248ABE2F001F83B8 /* 1-millisecond-of-silence.mp3 */, - FC7CE4C9248ABE2F001F83B8 /* 20ms-of-silence.caf */, - FC7CE4EC248ABE33001F83B8 /* 500ms-of-silence.mp3 */, - FC7CE507248ABE35001F83B8 /* Alarm_Buzzer.caf */, - FC7CE4CB248ABE2F001F83B8 /* Alarm_Clock.caf */, - FC7CE50C248ABE36001F83B8 /* Alert_Tone_Busy.caf */, - FC7CE4EB248ABE33001F83B8 /* Alert_Tone_Ringtone_1.caf */, - FC7CE4B8248ABE2C001F83B8 /* Alert_Tone_Ringtone_2.caf */, - FC7CE4D3248ABE30001F83B8 /* Alien_Siren.caf */, - FC7CE4FF248ABE35001F83B8 /* Ambulance.caf */, - FC7CE4DE248ABE32001F83B8 /* Analog_Watch_Alarm.caf */, - FC7CE4ED248ABE33001F83B8 /* Big_Clock_Ticking.caf */, - FC7CE4B1248ABE2C001F83B8 /* Burglar_Alarm_Siren_1.caf */, - FC7CE4F1248ABE33001F83B8 /* Burglar_Alarm_Siren_2.caf */, - FC7CE4C6248ABE2E001F83B8 /* Cartoon_Ascend_Climb_Sneaky.caf */, - FC7CE4FC248ABE34001F83B8 /* Cartoon_Ascend_Then_Descend.caf */, - FC7CE4B9248ABE2D001F83B8 /* Cartoon_Bounce_To_Ceiling.caf */, - FC7CE4CC248ABE2F001F83B8 /* Cartoon_Dreamy_Glissando_Harp.caf */, - FC7CE508248ABE35001F83B8 /* Cartoon_Fail_Strings_Trumpet.caf */, - FC7CE4E0248ABE32001F83B8 /* Cartoon_Machine_Clumsy_Loop.caf */, - FC7CE4FB248ABE34001F83B8 /* Cartoon_Siren.caf */, - FC7CE4B0248ABE2B001F83B8 /* Cartoon_Tip_Toe_Sneaky_Walk.caf */, - FC7CE4D7248ABE31001F83B8 /* Cartoon_Uh_Oh.caf */, - FC7CE4BF248ABE2D001F83B8 /* Cartoon_Villain_Horns.caf */, - FC7CE4EF248ABE33001F83B8 /* Cell_Phone_Ring_Tone.caf */, - FC7CE505248ABE35001F83B8 /* Chimes_Glassy.caf */, - FC7CE4AF248ABE2B001F83B8 /* Computer_Magic.caf */, - FC7CE4F4248ABE34001F83B8 /* CSFX-2_Alarm.caf */, - FC7CE4BD248ABE2D001F83B8 /* Cuckoo_Clock.caf */, - FC7CE4C1248ABE2E001F83B8 /* Dhol_Shuffleloop.caf */, - FC7CE4D2248ABE30001F83B8 /* Discreet.caf */, - FC7CE4CD248ABE2F001F83B8 /* Early_Sunrise.caf */, - FC7CE4B2248ABE2C001F83B8 /* Emergency_Alarm_Carbon_Monoxide.caf */, - FC7CE4DD248ABE32001F83B8 /* Emergency_Alarm_Siren.caf */, - FC7CE50B248ABE36001F83B8 /* Emergency_Alarm.caf */, - FC7CE4C4248ABE2E001F83B8 /* Ending_Reached.caf */, - FC7CE4C7248ABE2E001F83B8 /* Fly.caf */, - FC7CE500248ABE35001F83B8 /* Ghost_Hover.caf */, - FC7CE50A248ABE36001F83B8 /* Good_Morning.caf */, - FC7CE4DF248ABE32001F83B8 /* Hell_Yeah_Somewhat_Calmer.caf */, - FC7CE4F7248ABE34001F83B8 /* In_A_Hurry.caf */, - FC7CE4D6248ABE31001F83B8 /* Indeed.caf */, - FC7CE501248ABE35001F83B8 /* Insistently.caf */, - FC7CE4AB248ABE2B001F83B8 /* Jingle_All_The_Way.caf */, - FC7CE4BA248ABE2D001F83B8 /* Laser_Shoot.caf */, - FC7CE4BC248ABE2D001F83B8 /* Machine_Charge.caf */, - FC7CE4DA248ABE31001F83B8 /* Magical_Twinkle.caf */, - FC7CE504248ABE35001F83B8 /* Marching_Heavy_Footed_Fat_Elephants.caf */, - FC7CE4E2248ABE32001F83B8 /* Marimba_Descend.caf */, - FC7CE4B5248ABE2C001F83B8 /* Marimba_Flutter_or_Shake.caf */, - FC7CE517248ABE37001F83B8 /* Martian_Gun.caf */, - FC7CE4EA248ABE33001F83B8 /* Martian_Scanner.caf */, - FC7CE4EE248ABE33001F83B8 /* Metallic.caf */, - FC7CE4BB248ABE2D001F83B8 /* Nightguard.caf */, - FC7CE4AE248ABE2B001F83B8 /* Not_Kiddin.caf */, - FC7CE4C8248ABE2E001F83B8 /* Open_Your_Eyes_And_See.caf */, - FC7CE4F8248ABE34001F83B8 /* Orchestral_Horns.caf */, - FC7CE515248ABE37001F83B8 /* Oringz.caf */, - FC7CE4D4248ABE30001F83B8 /* Pager_Beeps.caf */, - FC7CE4CA248ABE2F001F83B8 /* Remembers_Me_Of_Asia.caf */, - FC7CE4AA248ABE2B001F83B8 /* Rise_And_Shine.caf */, - FC7CE4E9248ABE33001F83B8 /* Rush.caf */, - FC7CE4D8248ABE31001F83B8 /* Sci-Fi_Air_Raid_Alarm.caf */, - FC7CE4D5248ABE31001F83B8 /* Sci-Fi_Alarm_Loop_1.caf */, - FC7CE514248ABE36001F83B8 /* Sci-Fi_Alarm_Loop_2.caf */, - FC7CE4B7248ABE2C001F83B8 /* Sci-Fi_Alarm_Loop_3.caf */, - FC7CE510248ABE36001F83B8 /* Sci-Fi_Alarm_Loop_4.caf */, - FC7CE511248ABE36001F83B8 /* Sci-Fi_Alarm.caf */, - FC7CE4C2248ABE2E001F83B8 /* Sci-Fi_Computer_Console_Alarm.caf */, - FC7CE4E7248ABE33001F83B8 /* Sci-Fi_Console_Alarm.caf */, - FC7CE4E4248ABE32001F83B8 /* Sci-Fi_Eerie_Alarm.caf */, - FC7CE4F9248ABE34001F83B8 /* Sci-Fi_Engine_Shut_Down.caf */, - FC7CE4E6248ABE32001F83B8 /* Sci-Fi_Incoming_Message_Alert.caf */, - FC7CE4F6248ABE34001F83B8 /* Sci-Fi_Spaceship_Message.caf */, - FC7CE4AD248ABE2B001F83B8 /* Sci-Fi_Spaceship_Warm_Up.caf */, - FC7CE4B6248ABE2C001F83B8 /* Sci-Fi_Warning.caf */, - FC7CE4B3248ABE2C001F83B8 /* Signature_Corporate.caf */, - FC7CE4FA248ABE34001F83B8 /* silence-1sec.mp3 */, - FC7CE4A9248ABE2B001F83B8 /* Siri_Alert_Calibration_Needed.caf */, - FC7CE4DC248ABE32001F83B8 /* Siri_Alert_Device_Muted.caf */, - FC7CE4D0248ABE30001F83B8 /* Siri_Alert_Glucose_Dropping_Fast.caf */, - FC7CE4E5248ABE32001F83B8 /* Siri_Alert_Glucose_Rising_Fast.caf */, - FC7CE4E1248ABE32001F83B8 /* Siri_Alert_High_Glucose.caf */, - FC7CE4FE248ABE35001F83B8 /* Siri_Alert_Low_Glucose.caf */, - FC7CE509248ABE36001F83B8 /* Siri_Alert_Missed_Readings.caf */, - FC7CE4F0248ABE33001F83B8 /* Siri_Alert_Transmitter_Battery_Low.caf */, - FC7CE4DB248ABE31001F83B8 /* Siri_Alert_Urgent_High_Glucose.caf */, - FC7CE4E3248ABE32001F83B8 /* Siri_Alert_Urgent_Low_Glucose.caf */, - FC7CE503248ABE35001F83B8 /* Siri_Calibration_Needed.caf */, - FC7CE506248ABE35001F83B8 /* Siri_Device_Muted.caf */, - FC7CE516248ABE37001F83B8 /* Siri_Glucose_Dropping_Fast.caf */, - FC7CE4C0248ABE2D001F83B8 /* Siri_Glucose_Rising_Fast.caf */, - FC7CE4C3248ABE2E001F83B8 /* Siri_High_Glucose.caf */, - FC7CE4F5248ABE34001F83B8 /* Siri_Low_Glucose.caf */, - FC7CE4C5248ABE2E001F83B8 /* Siri_Missed_Readings.caf */, - FC7CE513248ABE36001F83B8 /* Siri_Transmitter_Battery_Low.caf */, - FC7CE4BE248ABE2D001F83B8 /* Siri_Urgent_High_Glucose.caf */, - FC7CE512248ABE36001F83B8 /* Siri_Urgent_Low_Glucose.caf */, - FC7CE4E8248ABE33001F83B8 /* Soft_Marimba_Pad_Positive.caf */, - FC7CE4CF248ABE30001F83B8 /* Soft_Warm_Airy_Optimistic.caf */, - FC7CE4F3248ABE34001F83B8 /* Soft_Warm_Airy_Reassuring.caf */, - FC7CE4D9248ABE31001F83B8 /* Store_Door_Chime.caf */, - FC7CE502248ABE35001F83B8 /* Sunny.caf */, - FC7CE4AC248ABE2B001F83B8 /* Thunder_Sound_FX.caf */, - FC7CE4FD248ABE35001F83B8 /* Time_Has_Come.caf */, - FC7CE50D248ABE36001F83B8 /* Tornado_Siren.caf */, - FC7CE4D1248ABE30001F83B8 /* Two_Turtle_Doves.caf */, - FC7CE50F248ABE36001F83B8 /* Unpaved.caf */, - FC7CE4B4248ABE2C001F83B8 /* Wake_Up_Will_You.caf */, - FC7CE50E248ABE36001F83B8 /* Win_Gain.caf */, - FC7CE4F2248ABE34001F83B8 /* Wrong_Answer.caf */, - ); - path = "Alarm Sounds"; - sourceTree = ""; - }; - FC7CE59A248D334B001F83B8 /* Resources */ = { - isa = PBXGroup; - children = ( - FCC6885D24896A6C00A0279D /* silence.mp3 */, - FCC6885B2489559400A0279D /* blank.wav */, - FC7CE4A8248ABE0D001F83B8 /* Alarm Sounds */, - FC9788252485969C00A7906C /* Assets.xcassets */, - FC7CE59B248D33A9001F83B8 /* dragbar.png */, - ); - path = Resources; - sourceTree = ""; - }; - FC8DEEE32485D1680075863F /* LoopFollow */ = { - isa = PBXGroup; - children = ( - 379BECAF2F65DA4B0069DC62 /* LiveActivitySettingsView.swift */, - 376310762F5CD65100656488 /* LiveActivity */, - 6589CC612E9E7D1600BB18FE /* Settings */, - 65AC26702ED245DF00421360 /* Treatments */, - 65AC25F52ECFD5E800421360 /* Stats */, - DDCF9A7E2D85FCE6004DF4DD /* Alarm */, - FC16A97624995FEE003D6245 /* Application */, - DDFF3D792D140F1800BF9D9E /* BackgroundRefresh */, - DD50C74D2D0828250057AE6F /* Contact */, - FC16A9782499657E003D6245 /* Controllers */, - DD98F54224BCEF190007425A /* Extensions */, - FCC688542489367300A0279D /* Helpers */, - FC8DEEE62485D1ED0075863F /* Info.plist */, - DD5334252C61667700062F9D /* InfoDisplaySettings */, - DD13BC732C3FD60E0062313B /* InfoTable */, - DD9ED0C62D355225000D2A63 /* Log */, - FCC688702489A57C00A0279D /* Loop Follow.entitlements */, - DD0C0C692C4852A100DBADDF /* Metric */, - DD2C2E4D2D3B8ACF006413A5 /* Nightscout */, - DD0C0C6E2C4AFFB800DBADDF /* Remote */, - FC7CE59A248D334B001F83B8 /* Resources */, - DDC7E5142DBCE1B900EB1127 /* Snoozer */, - DDC7E5CD2DC6637800EB1127 /* Storage */, - DDEF503D2D32753A00999A5D /* Task */, - FCC68871248A736700A0279D /* ViewControllers */, - ); - path = LoopFollow; - sourceTree = ""; - }; FC97880B2485969B00A7906C = { isa = PBXGroup; children = ( - 379BECA92F6588300069DC62 /* RestartLiveActivityIntent.swift */, 374DAACA2F5B924B00BB663B /* LoopFollowLAExtensionExtension.entitlements */, DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */, DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */, DDB0AF4F2BB1A81F00AFA48B /* Scripts */, DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */, - FC3AE7B3249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld */, FC5A5C3C2497B229009C550E /* Config.xcconfig */, - FC8DEEE32485D1680075863F /* LoopFollow */, DDCC3AD72DDE1790006F1C10 /* Tests */, 37A4BDDE2F5B6B4A00EEB289 /* LoopFollowLAExtension */, FC9788152485969B00A7906C /* Products */, 8E32230C453C93FDCE59C2B9 /* Pods */, 6A5880E0B811AF443B05AB02 /* Frameworks */, + 9555FE45F7FAEFA6142D04CA /* LoopFollow */, + BC0FBFD3754955D028EF6631 /* Shared */, ); sourceTree = ""; }; @@ -1695,77 +209,6 @@ name = Products; sourceTree = ""; }; - FCC688512489363F00A0279D /* Framework */ = { - isa = PBXGroup; - children = ( - FCC6886624898F8000A0279D /* UserDefaultsValue.swift */, - FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */, - DDD10EFE2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift */, - DDD10F042C529DA200D76A8E /* ObservableValue.swift */, - DD4878022C7B297E0048F05C /* StorageValue.swift */, - DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */, - ); - path = Framework; - sourceTree = ""; - }; - FCC688542489367300A0279D /* Helpers */ = { - isa = PBXGroup; - children = ( - DD026E5A2EA2C9C300A39CB5 /* InsulinFormatter.swift */, - DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */, - 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */, - DD4A407D2E6AFEE6007B318B /* AuthService.swift */, - DD1D52BF2E4C100000000001 /* AppearanceMode.swift */, - DD1D52C12E4C100000000002 /* PredictionDisplayType.swift */, - DD1D52B82E1EB5DC00432050 /* TabPosition.swift */, - DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */, - DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */, - DD7B0D432D730A320063DCB6 /* CycleHelper.swift */, - DDF6999C2C5AAA4C0058A8D9 /* Views */, - FCC6886E2489A53800A0279D /* AppConstants.swift */, - BDFBE69CEF18416D84959974 /* Telemetry.swift */, - FCC6886A24898FD800A0279D /* ObservationToken.swift */, - FCC6886C2489909D00A0279D /* AnyConvertible.swift */, - FCC688592489554800A0279D /* BackgroundTaskAudio.swift */, - A1A1A10001000000A0CFEED2 /* APNsCredentialValidator.swift */, - A8CA8BE0B3D247408FE088B4 /* BackgroundRefreshManager.swift */, - FCFEEC9F2488157B00402A7F /* Chart.swift */, - FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */, - FC16A98024996C07003D6245 /* DateTime.swift */, - 654132E92E19F24800BDBE08 /* TOTPGenerator.swift */, - FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */, - FCEF87AA24A1417900AE6FA0 /* Localizer.swift */, - FC1BDD2E24A232A3001B652C /* DataStructs.swift */, - FCD2A27C24C9D044009F7B7B /* Globals.swift */, - FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */, - DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */, - DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */, - DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */, - DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */, - DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */, - DDF699952C5582290058A8D9 /* TextFieldWithToolBar.swift */, - DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */, - DDDC01DC2E244B3100D9975C /* JWTManager.swift */, - A1A1A10002000000A0CFEED2 /* LogRedactor.swift */, - 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */, - ); - path = Helpers; - sourceTree = ""; - }; - FCC68871248A736700A0279D /* ViewControllers */ = { - isa = PBXGroup; - children = ( - DD7A3B5C2F1E8D9A00B4C6E1 /* BGDisplayView.swift */, - DD7A3B5E2F1E8DA000B4C6E1 /* LineChartWrapper.swift */, - DD7A3B602F1E8DA600B4C6E1 /* MainHomeView.swift */, - CC3D4E5F6A7B8C9D0E1F2A3B /* MoreMenuView.swift */, - EE5F6A7B8C9D0E1F2A3B4C5D /* NightscoutContentView.swift */, - FC97881B2485969B00A7906C /* MainViewController.swift */, - FC97881D2485969B00A7906C /* NightScoutViewController.swift */, - ); - path = ViewControllers; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1783,10 +226,9 @@ ); fileSystemSynchronizedGroups = ( 37A4BDDE2F5B6B4A00EEB289 /* LoopFollowLAExtension */, + BC0FBFD3754955D028EF6631 /* Shared */, ); name = LoopFollowLAExtensionExtension; - packageProductDependencies = ( - ); productName = LoopFollowLAExtensionExtension; productReference = 37A4BDD92F5B6B4A00EEB289 /* LoopFollowLAExtensionExtension.appex */; productType = "com.apple.product-type.app-extension"; @@ -1808,8 +250,6 @@ DDCC3AD72DDE1790006F1C10 /* Tests */, ); name = Tests; - packageProductDependencies = ( - ); productName = Tests; productReference = DDCC3AD62DDE1790006F1C10 /* Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -1833,8 +273,8 @@ 37A4BDE72F5B6B4C00EEB289 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( - 65AC25F52ECFD5E800421360 /* Stats */, - 65AC26702ED245DF00421360 /* Treatments */, + 9555FE45F7FAEFA6142D04CA /* LoopFollow */, + BC0FBFD3754955D028EF6631 /* Shared */, ); name = LoopFollow; packageProductDependencies = ( @@ -1908,128 +348,10 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - FC7CE51C248ABE37001F83B8 /* Sci-Fi_Spaceship_Warm_Up.caf in Resources */, - FC7CE526248ABE37001F83B8 /* Sci-Fi_Alarm_Loop_3.caf in Resources */, - FC7CE54C248ABE37001F83B8 /* Emergency_Alarm_Siren.caf in Resources */, - FC7CE559248ABE37001F83B8 /* Martian_Scanner.caf in Resources */, - FC7CE524248ABE37001F83B8 /* Marimba_Flutter_or_Shake.caf in Resources */, - FC7CE52D248ABE37001F83B8 /* Siri_Urgent_High_Glucose.caf in Resources */, - FC7CE573248ABE37001F83B8 /* Marching_Heavy_Footed_Fat_Elephants.caf in Resources */, - FC7CE51F248ABE37001F83B8 /* Cartoon_Tip_Toe_Sneaky_Walk.caf in Resources */, - FC7CE53B248ABE37001F83B8 /* Cartoon_Dreamy_Glissando_Harp.caf in Resources */, - FC7CE547248ABE37001F83B8 /* Sci-Fi_Air_Raid_Alarm.caf in Resources */, - FC7CE57F248ABE37001F83B8 /* Sci-Fi_Alarm_Loop_4.caf in Resources */, - FC7CE576248ABE37001F83B8 /* Alarm_Buzzer.caf in Resources */, - FC7CE51D248ABE37001F83B8 /* Not_Kiddin.caf in Resources */, - FCC6885E24896A6C00A0279D /* silence.mp3 in Resources */, - FC7CE570248ABE37001F83B8 /* Insistently.caf in Resources */, - FC7CE521248ABE37001F83B8 /* Emergency_Alarm_Carbon_Monoxide.caf in Resources */, - FC7CE52C248ABE37001F83B8 /* Cuckoo_Clock.caf in Resources */, - FC7CE53D248ABE37001F83B8 /* 1-millisecond-of-silence.mp3 in Resources */, - FC7CE585248ABE37001F83B8 /* Siri_Glucose_Dropping_Fast.caf in Resources */, - FC7CE563248ABE37001F83B8 /* CSFX-2_Alarm.caf in Resources */, - FC7CE58D248ABEF2001F83B8 /* alarm.mp3 in Resources */, - FC7CE534248ABE37001F83B8 /* Siri_Missed_Readings.caf in Resources */, - FC7CE548248ABE37001F83B8 /* Store_Door_Chime.caf in Resources */, - FC7CE584248ABE37001F83B8 /* Oringz.caf in Resources */, - FC7CE56D248ABE37001F83B8 /* Siri_Alert_Low_Glucose.caf in Resources */, - FC7CE53F248ABE37001F83B8 /* Siri_Alert_Glucose_Dropping_Fast.caf in Resources */, - FC7CE561248ABE37001F83B8 /* Wrong_Answer.caf in Resources */, - FCC6885C2489559400A0279D /* blank.wav in Resources */, DDB0AF552BB1B24A00AFA48B /* BuildDetails.plist in Resources */, - FC7CE528248ABE37001F83B8 /* Cartoon_Bounce_To_Ceiling.caf in Resources */, - FC7CE57B248ABE37001F83B8 /* Alert_Tone_Busy.caf in Resources */, - FC7CE51B248ABE37001F83B8 /* Thunder_Sound_FX.caf in Resources */, - FC7CE57D248ABE37001F83B8 /* Win_Gain.caf in Resources */, - FC7CE55F248ABE37001F83B8 /* Siri_Alert_Transmitter_Battery_Low.caf in Resources */, - FC9788292485969C00A7906C /* LaunchScreen.storyboard in Resources */, - FC7CE583248ABE37001F83B8 /* Sci-Fi_Alarm_Loop_2.caf in Resources */, - FC7CE549248ABE37001F83B8 /* Magical_Twinkle.caf in Resources */, - FC7CE540248ABE37001F83B8 /* Two_Turtle_Doves.caf in Resources */, - FC7CE545248ABE37001F83B8 /* Indeed.caf in Resources */, - FC7CE564248ABE37001F83B8 /* Siri_Low_Glucose.caf in Resources */, - FC7CE55D248ABE37001F83B8 /* Metallic.caf in Resources */, - FC7CE568248ABE37001F83B8 /* Sci-Fi_Engine_Shut_Down.caf in Resources */, - FC7CE580248ABE37001F83B8 /* Sci-Fi_Alarm.caf in Resources */, DDF2C0142BEFD468007A20E6 /* blacklisted-versions.json in Resources */, - FC7CE533248ABE37001F83B8 /* Ending_Reached.caf in Resources */, - FC7CE558248ABE37001F83B8 /* Rush.caf in Resources */, - FC7CE52A248ABE37001F83B8 /* Nightguard.caf in Resources */, - FC7CE569248ABE37001F83B8 /* silence-1sec.mp3 in Resources */, - FC7CE519248ABE37001F83B8 /* Rise_And_Shine.caf in Resources */, - FC7CE565248ABE37001F83B8 /* Sci-Fi_Spaceship_Message.caf in Resources */, - FC7CE52B248ABE37001F83B8 /* Machine_Charge.caf in Resources */, - FC7CE536248ABE37001F83B8 /* Fly.caf in Resources */, - FC7CE567248ABE37001F83B8 /* Orchestral_Horns.caf in Resources */, - FC7CE544248ABE37001F83B8 /* Sci-Fi_Alarm_Loop_1.caf in Resources */, - FC7CE551248ABE37001F83B8 /* Marimba_Descend.caf in Resources */, - FC7CE520248ABE37001F83B8 /* Burglar_Alarm_Siren_1.caf in Resources */, - FC7CE54D248ABE37001F83B8 /* Analog_Watch_Alarm.caf in Resources */, - FC7CE530248ABE37001F83B8 /* Dhol_Shuffleloop.caf in Resources */, - FC7CE59C248D33A9001F83B8 /* dragbar.png in Resources */, - FC7CE56C248ABE37001F83B8 /* Time_Has_Come.caf in Resources */, - FC7CE574248ABE37001F83B8 /* Chimes_Glassy.caf in Resources */, - FC7CE55A248ABE37001F83B8 /* Alert_Tone_Ringtone_1.caf in Resources */, - FC7CE543248ABE37001F83B8 /* Pager_Beeps.caf in Resources */, - FC7CE56F248ABE37001F83B8 /* Ghost_Hover.caf in Resources */, - FC7CE537248ABE37001F83B8 /* Open_Your_Eyes_And_See.caf in Resources */, - FC7CE56E248ABE37001F83B8 /* Ambulance.caf in Resources */, - FC7CE571248ABE37001F83B8 /* Sunny.caf in Resources */, - FC7CE546248ABE37001F83B8 /* Cartoon_Uh_Oh.caf in Resources */, - FC9788262485969C00A7906C /* Assets.xcassets in Resources */, - FC7CE557248ABE37001F83B8 /* Soft_Marimba_Pad_Positive.caf in Resources */, - FC7CE54F248ABE37001F83B8 /* Cartoon_Machine_Clumsy_Loop.caf in Resources */, - FC7CE562248ABE37001F83B8 /* Soft_Warm_Airy_Reassuring.caf in Resources */, - FC7CE54B248ABE37001F83B8 /* Siri_Alert_Device_Muted.caf in Resources */, - FC7CE566248ABE37001F83B8 /* In_A_Hurry.caf in Resources */, - FC7CE531248ABE37001F83B8 /* Sci-Fi_Computer_Console_Alarm.caf in Resources */, - FC7CE532248ABE37001F83B8 /* Siri_High_Glucose.caf in Resources */, - FC7CE555248ABE37001F83B8 /* Sci-Fi_Incoming_Message_Alert.caf in Resources */, - FC7CE518248ABE37001F83B8 /* Siri_Alert_Calibration_Needed.caf in Resources */, FC5A5C3D2497B229009C550E /* Config.xcconfig in Resources */, - FC7CE550248ABE37001F83B8 /* Siri_Alert_High_Glucose.caf in Resources */, DDCFCAF22B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig in Resources */, - FC7CE525248ABE37001F83B8 /* Sci-Fi_Warning.caf in Resources */, - FC7CE556248ABE37001F83B8 /* Sci-Fi_Console_Alarm.caf in Resources */, - FC7CE55C248ABE37001F83B8 /* Big_Clock_Ticking.caf in Resources */, - FC7CE57C248ABE37001F83B8 /* Tornado_Siren.caf in Resources */, - FC7CE57E248ABE37001F83B8 /* Unpaved.caf in Resources */, - FC7CE539248ABE37001F83B8 /* Remembers_Me_Of_Asia.caf in Resources */, - FC7CE542248ABE37001F83B8 /* Alien_Siren.caf in Resources */, - FC7CE51E248ABE37001F83B8 /* Computer_Magic.caf in Resources */, - FC7CE541248ABE37001F83B8 /* Discreet.caf in Resources */, - FC7CE535248ABE37001F83B8 /* Cartoon_Ascend_Climb_Sneaky.caf in Resources */, - FC7CE529248ABE37001F83B8 /* Laser_Shoot.caf in Resources */, - FC7CE54A248ABE37001F83B8 /* Siri_Alert_Urgent_High_Glucose.caf in Resources */, - FC7CE579248ABE37001F83B8 /* Good_Morning.caf in Resources */, - FC7CE538248ABE37001F83B8 /* 20ms-of-silence.caf in Resources */, - FC7CE56B248ABE37001F83B8 /* Cartoon_Ascend_Then_Descend.caf in Resources */, - FC7CE54E248ABE37001F83B8 /* Hell_Yeah_Somewhat_Calmer.caf in Resources */, - FC7CE586248ABE37001F83B8 /* Martian_Gun.caf in Resources */, - FC7CE523248ABE37001F83B8 /* Wake_Up_Will_You.caf in Resources */, - FC7CE552248ABE37001F83B8 /* Siri_Alert_Urgent_Low_Glucose.caf in Resources */, - FC7CE577248ABE37001F83B8 /* Cartoon_Fail_Strings_Trumpet.caf in Resources */, - FC7CE582248ABE37001F83B8 /* Siri_Transmitter_Battery_Low.caf in Resources */, - FC7CE57A248ABE37001F83B8 /* Emergency_Alarm.caf in Resources */, - FC7CE51A248ABE37001F83B8 /* Jingle_All_The_Way.caf in Resources */, - FC7CE53C248ABE37001F83B8 /* Early_Sunrise.caf in Resources */, - FC7CE560248ABE37001F83B8 /* Burglar_Alarm_Siren_2.caf in Resources */, - FC7CE522248ABE37001F83B8 /* Signature_Corporate.caf in Resources */, - FC7CE572248ABE37001F83B8 /* Siri_Calibration_Needed.caf in Resources */, - FC7CE53E248ABE37001F83B8 /* Soft_Warm_Airy_Optimistic.caf in Resources */, - FC7CE58E248ABEF2001F83B8 /* alarm-notification.m4a in Resources */, - FC7CE52F248ABE37001F83B8 /* Siri_Glucose_Rising_Fast.caf in Resources */, - FC7CE55E248ABE37001F83B8 /* Cell_Phone_Ring_Tone.caf in Resources */, - FC7CE554248ABE37001F83B8 /* Siri_Alert_Glucose_Rising_Fast.caf in Resources */, - FC7CE575248ABE37001F83B8 /* Siri_Device_Muted.caf in Resources */, - FC7CE52E248ABE37001F83B8 /* Cartoon_Villain_Horns.caf in Resources */, - FC7CE527248ABE37001F83B8 /* Alert_Tone_Ringtone_2.caf in Resources */, - FC7CE53A248ABE37001F83B8 /* Alarm_Clock.caf in Resources */, - FC7CE578248ABE37001F83B8 /* Siri_Alert_Missed_Readings.caf in Resources */, - FC7CE55B248ABE37001F83B8 /* 500ms-of-silence.mp3 in Resources */, - FC7CE581248ABE37001F83B8 /* Siri_Urgent_Low_Glucose.caf in Resources */, - FC7CE553248ABE37001F83B8 /* Sci-Fi_Eerie_Alarm.caf in Resources */, - FC7CE56A248ABE37001F83B8 /* Cartoon_Siren.caf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2123,13 +445,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DD91E4DE2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */, - 374A77AA2F5BE17000E96858 /* AppGroupID.swift in Sources */, - 37E4DD0E2F7E097D000511C8 /* LALivenessStore.swift in Sources */, - 374A77AB2F5BE17000E96858 /* GlucoseSnapshot.swift in Sources */, - 37E4DD112F7E0D35000511C8 /* LALivenessMarker.swift in Sources */, - 374A77AC2F5BE17000E96858 /* LAAppGroupSettings.swift in Sources */, - 374A77AD2F5BE17000E96858 /* GlucoseLiveActivityAttributes.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2144,305 +459,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DD5334292C6166A500062F9D /* InfoDisplaySettingsView.swift in Sources */, - DD48781E2C7DAF2F0048F05C /* PushNotificationManager.swift in Sources */, - DD0B9D582DE1F3B20090C337 /* AlarmType+canAcknowledge.swift in Sources */, - DD7F4BC52DD3CE0700D449E9 /* AlarmBGLimitSection.swift in Sources */, - DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */, - DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */, - DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */, - DD9ACA082D32F68B00415D8A /* BGTask.swift in Sources */, - DD9ACA102D34129200415D8A /* Task.swift in Sources */, - DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */, - DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */, - DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */, - 374A77B42F5BE1AC00E96858 /* StorageCurrentGlucoseStateProvider.swift in Sources */, - 374A77B52F5BE1AC00E96858 /* GlucoseSnapshotBuilder.swift in Sources */, - 374A77B72F5BE1AC00E96858 /* LiveActivityManager.swift in Sources */, - 374A77B82F5BE1AC00E96858 /* GlucoseSnapshotStore.swift in Sources */, - 374A77B92F5BE1AC00E96858 /* PreferredGlucoseUnit.swift in Sources */, - DDE75D232DE5E505007C1FC1 /* Glyph.swift in Sources */, - DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */, - DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */, - DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */, - DD7F4C0F2DD51EC200D449E9 /* TempTargetStartCondition.swift in Sources */, - DDBD19962DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift in Sources */, - 656F8C102E49F36F0008DC1D /* QRCodeDisplayView.swift in Sources */, - DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */, - DD9ACA0E2D340BFF00415D8A /* AlarmTask.swift in Sources */, - DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */, - DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */, - 656F8C122E49F3780008DC1D /* QRCodeGenerator.swift in Sources */, - 6541341A2E1DC27900BDBE08 /* OverridePresetData.swift in Sources */, - FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */, - DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */, - DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */, - DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */, - DD5334212C60EBEE00062F9D /* InsulinCartridgeChange.swift in Sources */, - DDDB86F12DF7223C00AADDAC /* DeleteAlarmSection.swift in Sources */, - DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */, - FC97881E2485969B00A7906C /* NightScoutViewController.swift in Sources */, - DD608A0A2C23593900F91132 /* SMB.swift in Sources */, - FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */, - DD7F4C252DD7B20700D449E9 /* AlarmType+timeUnit.swift in Sources */, - DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */, - DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */, - DDCC3A502DDED000006F1C10 /* PumpBatteryCondition.swift in Sources */, - 656F8C142E49F3D20008DC1D /* RemoteCommandSettings.swift in Sources */, - DD12D4872E1705E6004E0112 /* AlarmsContainerView.swift in Sources */, - DD83164A2DE4C504004467AA /* SettingsStepperRow.swift in Sources */, - DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */, - DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */, - FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */, - 9C7FB98C98BE4FF98F4815EE /* Telemetry.swift in Sources */, - DDE75D2D2DE71401007C1FC1 /* TogglableSecureInput.swift in Sources */, - DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */, - DD0B9D562DE1EC8A0090C337 /* AlarmType+Snooze.swift in Sources */, - DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */, - FC16A97A24996673003D6245 /* NightScout.swift in Sources */, - DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */, - DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */, - DDCC3A4F2DDC5B54006F1C10 /* BatteryDropCondition.swift in Sources */, - FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */, - DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */, - DDB9FC7B2DDB573F00EFAA76 /* IOBCondition.swift in Sources */, - DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */, - DDCC3A5A2DDC988F006F1C10 /* CarbSample.swift in Sources */, - F19449721F3B792730A0F4FD /* PendingFutureCarb.swift in Sources */, - 2D8068C66833EEAED7B4BEB8 /* FutureCarbsCondition.swift in Sources */, - ACE7F6DE0D065BEB52CDC0DB /* FutureCarbsAlarmEditor.swift in Sources */, - DD0650F12DCE9A9E004D3B41 /* MissedReadingCondition.swift in Sources */, - DDC6CA4B2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift in Sources */, - 6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */, - DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */, - B500000000000000000000C2 /* QuickPickSectionHeader.swift in Sources */, - DDFF3D7F2D1414A200BF9D9E /* BLEDevice.swift in Sources */, - DD9ACA042D32821400415D8A /* DeviceStatusTask.swift in Sources */, - FC16A97D24996747003D6245 /* SpeakBG.swift in Sources */, - DDFD5C532CB167DA00D3FD68 /* TRCCommandType.swift in Sources */, - DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */, - DD0650F92DCFE7BE004D3B41 /* FastDropAlarmEditor.swift in Sources */, - DD5817172D2710E90041FB98 /* BLEDeviceSelectionView.swift in Sources */, - FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */, - DDCC3A562DDC9617006F1C10 /* MissedBolusCondition.swift in Sources */, - DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */, - DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */, - DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */, - DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */, - DDE75D272DE5E539007C1FC1 /* ActionRow.swift in Sources */, - DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */, - DD58171E2D299FCA0041FB98 /* BluetoothDeviceDelegate.swift in Sources */, - DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */, - DDD10F032C518A6500D76A8E /* TreatmentResponse.swift in Sources */, - DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */, - DD7F4C1F2DD6648B00D449E9 /* FastRiseCondition.swift in Sources */, - DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */, - DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */, - DD0650F32DCE9B3D004D3B41 /* MissedReadingEditor.swift in Sources */, - 6584B1012E4A263900135D4D /* TOTPService.swift in Sources */, - DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */, - DDEF503A2D31615000999A5D /* LogManager.swift in Sources */, - B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */, - B500000000000000000000A4 /* QuickPickBolusesManager.swift in Sources */, - B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */, - B500000000000000000000B4 /* QuickPickMealsManager.swift in Sources */, - DD4878172C7B75350048F05C /* BolusView.swift in Sources */, - DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */, - 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */, - DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */, - DD50C10A2F60B00000000002 /* NightscoutSocketManager.swift in Sources */, - DD50C10A2F60B00000000004 /* NightscoutSocketDataHandler.swift in Sources */, - DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */, - DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */, - 65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */, - DD0650F72DCFDA26004D3B41 /* InfoBanner.swift in Sources */, - DDE75D292DE5E56C007C1FC1 /* LinkRow.swift in Sources */, - DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */, - DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */, - DDDC31CC2E13A7DF009EA0F3 /* AddAlarmSheet.swift in Sources */, - DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */, - DD9ACA0C2D33BB8600415D8A /* CalendarTask.swift in Sources */, - DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */, - DD13BC7B2C3FE64A0062313B /* InfoTableView.swift in Sources */, - DD4E5F6A7B8C9D0E2F2A3B4C /* RemoteContentView.swift in Sources */, - DD48780A2C7B30D40048F05C /* RemoteSettingsViewModel.swift in Sources */, - AB1CD0012C7B30D40048F05C /* RemoteDiagnostics.swift in Sources */, - FCFEECA02488157B00402A7F /* Chart.swift in Sources */, - DDDC31CE2E13A811009EA0F3 /* AlarmTile.swift in Sources */, - DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */, - DDC7E5422DBD8A1600EB1127 /* AlarmGeneralSection.swift in Sources */, - DD7F4BC72DD473A600D449E9 /* FastDropCondition.swift in Sources */, - DD7F4C0D2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift in Sources */, - DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */, - DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */, - DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */, - DDC7E5472DBD8A1600EB1127 /* AlarmEditor.swift in Sources */, - DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */, - DDA9ACA62D6A66D000E6F1A9 /* ContactColorMode.swift in Sources */, - DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */, - DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */, - FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */, - FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */, - DDF6999E2C5AAA640058A8D9 /* ErrorMessageView.swift in Sources */, - CC3D4E5F6A7B8C9D0E2F2A3B /* MoreMenuView.swift in Sources */, - DD7A3B5D2F1E8D9A00B4C6E1 /* BGDisplayView.swift in Sources */, - DD7A3B5F2F1E8DA000B4C6E1 /* LineChartWrapper.swift in Sources */, - DD7A3B612F1E8DA600B4C6E1 /* MainHomeView.swift in Sources */, - EE5F6A7B8C9D0E2F2A3B4C5D /* NightscoutContentView.swift in Sources */, - 654132E72E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift in Sources */, - DD4878152C7B75230048F05C /* MealView.swift in Sources */, - FC16A97F249969E2003D6245 /* Graphs.swift in Sources */, - FC8589BF252B54F500C8FC73 /* Mobileprovision.swift in Sources */, - DD4878052C7B2C970048F05C /* Storage.swift in Sources */, - DD493AE12ACF22FE009A6922 /* Profile.swift in Sources */, - 6589CC622E9E7D1600BB18FE /* ImportExportSettingsView.swift in Sources */, - 6589CC632E9E7D1600BB18FE /* GeneralSettingsView.swift in Sources */, - 5544D8C363FB5D3B9BF8CE4A /* APNSettingsView.swift in Sources */, - 6589CC642E9E7D1600BB18FE /* ContactSettingsView.swift in Sources */, - 37E4DD0D2F7E0967000511C8 /* LALivenessStore.swift in Sources */, - 6589CC652E9E7D1600BB18FE /* DexcomSettingsViewModel.swift in Sources */, - 6589CC662E9E7D1600BB18FE /* AdvancedSettingsView.swift in Sources */, - 6589CC672E9E7D1600BB18FE /* ImportExportSettingsViewModel.swift in Sources */, - 6589CC682E9E7D1600BB18FE /* ExportableSettings.swift in Sources */, - 6589CC692E9E7D1600BB18FE /* ContactSettingsViewModel.swift in Sources */, - 6589CC6A2E9E7D1600BB18FE /* DexcomSettingsView.swift in Sources */, - 6589CC6B2E9E7D1600BB18FE /* TabCustomizationModal.swift in Sources */, - 6589CC6C2E9E7D1600BB18FE /* GraphSettingsView.swift in Sources */, - 6589CC6D2E9E7D1600BB18FE /* CalendarSettingsView.swift in Sources */, - 6589CC6E2E9E7D1600BB18FE /* SettingsMenuView.swift in Sources */, - 65A100012F5AA00000AA1001 /* UnitsSettingsView.swift in Sources */, - 65A100032F5AA00000AA1002 /* UnitsConfigurationView.swift in Sources */, - 657F98182F043D8100F732BD /* HomeContentView.swift in Sources */, - 6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */, - 6589CC712E9E7D1600BB18FE /* ShareLogNoticeView.swift in Sources */, - DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */, - DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */, - DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */, - DDC6CA452DD8D8E60060EE25 /* PumpChangeCondition.swift in Sources */, - DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */, - DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */, - DDCC3A542DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift in Sources */, - DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */, - DD13BC752C3FD6210062313B /* InfoType.swift in Sources */, - DDCC3A4D2DDBB77C006F1C10 /* PhoneBatteryAlarmEditor.swift in Sources */, - DDCC3A5B2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift in Sources */, - DDC6CA492DD8E47A0060EE25 /* PumpVolumeCondition.swift in Sources */, - DD9ACA0A2D33095600415D8A /* MinAgoTask.swift in Sources */, - DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */, - DDD10EFF2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift in Sources */, - DDB9FC7D2DDB575300EFAA76 /* IOBAlarmEditor.swift in Sources */, - DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */, - DD7F4C0B2DD51C5500D449E9 /* OverrideEndCondition.swift in Sources */, - DDFF3D872D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift in Sources */, - 6589CC712E9E814F00BB18FE /* AlarmSelectionView.swift in Sources */, - DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */, - DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */, - FC97881C2485969B00A7906C /* MainViewController.swift in Sources */, - DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */, - DD493AD52ACF2109009A6922 /* ResumePump.swift in Sources */, - DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */, - DD8316442DE47CA9004467AA /* BGPicker.swift in Sources */, - FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */, - DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */, - DDB9FC7F2DDB584500EFAA76 /* BolusEntry.swift in Sources */, - FC9788182485969B00A7906C /* AppDelegate.swift in Sources */, - DD4A407E2E6AFEE6007B318B /* AuthService.swift in Sources */, - 654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */, - DDDC01DD2E244B3100D9975C /* JWTManager.swift in Sources */, - A1A1A10002000000A0CFEED1 /* LogRedactor.swift in Sources */, - DDD10F072C529DE800D76A8E /* Observable.swift in Sources */, - DDFF3D852D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift in Sources */, - DDCF9A882D85FD33004DF4DD /* AlarmData.swift in Sources */, - DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */, - 66E3D12E66AA4534A144A54B /* BackgroundRefreshManager.swift in Sources */, - 379BECB02F65DA4B0069DC62 /* LiveActivitySettingsView.swift in Sources */, - DD4878082C7B30BF0048F05C /* RemoteSettingsView.swift in Sources */, - DDE75D2B2DE5E613007C1FC1 /* NavigationRow.swift in Sources */, - DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */, - DDEF503C2D31BE2D00999A5D /* TaskScheduler.swift in Sources */, - DD0650EB2DCE8385004D3B41 /* LowBGCondition.swift in Sources */, - AA1B2C3D4E5F6A7B8C9D0E2F /* LoopFollowApp.swift in Sources */, - BB2C3D4E5F6A7B8C9D0E2F2A /* MainTabView.swift in Sources */, - DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */, - DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */, - DD493AE92ACF2445009A6922 /* BGData.swift in Sources */, - DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */, - FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */, - DD4AFB6B2DB6BF2A00BB593F /* Binding+Optional.swift in Sources */, - DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */, - DD5DA27C2DC930D6003D44FC /* GlucoseValue.swift in Sources */, - DD9ACA062D32AF7900415D8A /* TreatmentsTask.swift in Sources */, - DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */, - 657F99E92F0BC81500F732BD /* OTPSecureMessenger.swift in Sources */, - DD9ED0CE2D35587A000D2A63 /* LogEntry.swift in Sources */, - DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */, - DD83164C2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift in Sources */, - DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */, - DDF699962C5582290058A8D9 /* TextFieldWithToolBar.swift in Sources */, - DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */, - DD0650EF2DCE96FF004D3B41 /* HighBGCondition.swift in Sources */, - DDC6CA472DD8D9010060EE25 /* PumpChangeAlarmEditor.swift in Sources */, - DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */, - 65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */, - DD0C0C682C48529400DBADDF /* Metric.swift in Sources */, - FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */, - DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */, - DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */, - DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */, - 374A77A52F5BE17000E96858 /* AppGroupID.swift in Sources */, - 374A77A62F5BE17000E96858 /* GlucoseSnapshot.swift in Sources */, - 374A77A72F5BE17000E96858 /* LAAppGroupSettings.swift in Sources */, - 374A77A82F5BE17000E96858 /* GlucoseLiveActivityAttributes.swift in Sources */, - DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */, - DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */, - DD026E5B2EA2C9C300A39CB5 /* InsulinFormatter.swift in Sources */, - DD5334B02D1447C500CDD6EA /* BLEManager.swift in Sources */, - DD4878032C7B297E0048F05C /* StorageValue.swift in Sources */, - FC1BDD2B24A22650001B652C /* Stats.swift in Sources */, - DDA9ACAC2D6B317100E6F1A9 /* ContactType.swift in Sources */, - DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */, - FC1BDD2D24A23204001B652C /* MainViewController+updateStats.swift in Sources */, - A1B2C3D4E5F6A7B8C9D0E1F3 /* StatsDisplayModel.swift in Sources */, - A1B2C3D4E5F6A7B8C9D0E1F5 /* StatsDisplayView.swift in Sources */, - DD4878102C7B74BF0048F05C /* TrioRemoteControlView.swift in Sources */, - DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */, - FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */, - DD5334232C60ED3600062F9D /* IAge.swift in Sources */, - FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */, - DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */, - DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */, - DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */, - DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */, - DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */, - DD1D52C02E4C100000000001 /* AppearanceMode.swift in Sources */, - DD1D52C22E4C100000000002 /* PredictionDisplayType.swift in Sources */, - DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */, - 654132EA2E19F24800BDBE08 /* TOTPGenerator.swift in Sources */, - 6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */, - DD0650F52DCF303F004D3B41 /* AlarmStepperSection.swift in Sources */, - 379BECAA2F6588300069DC62 /* RestartLiveActivityIntent.swift in Sources */, - DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */, - DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */, - DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */, - DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */, - DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */, - DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */, - DD8316482DE49EE5004467AA /* Storage+Migrate.swift in Sources */, - FCA2DDE62501095000254A8C /* Timers.swift in Sources */, - DD2C2E512D3B8B0C006413A5 /* NightscoutSettingsViewModel.swift in Sources */, - DD9ED0C82D355244000D2A63 /* LogViewModel.swift in Sources */, - DDFF3D892D1429AB00BF9D9E /* BackgroundRefreshType.swift in Sources */, - DD493AE32ACF2358009A6922 /* CAge.swift in Sources */, - DD493ADD2ACF21E0009A6922 /* Basals.swift in Sources */, - FC16A98124996C07003D6245 /* DateTime.swift in Sources */, - FC3CAB022493B6220068A152 /* BackgroundTaskAudio.swift in Sources */, - A1A1A10001000000A0CFEED1 /* APNsCredentialValidator.swift in Sources */, - DDCC3A582DDC9655006F1C10 /* MissedBolusAlarmEditor.swift in Sources */, - DDEF50402D479B8A00884336 /* LoopAPNSService.swift in Sources */, - DD485F142E454B2600CE8CBF /* SecureMessenger.swift in Sources */, - DDEF50422D479BAA00884336 /* LoopAPNSCarbsView.swift in Sources */, - DDEF50432D479BBA00884336 /* LoopAPNSBolusView.swift in Sources */, - DDEF50452D479BDA00884336 /* LoopAPNSRemoteView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2462,17 +478,6 @@ }; /* End PBXTargetDependency section */ -/* Begin PBXVariantGroup section */ - FC9788272485969C00A7906C /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - FC9788282485969C00A7906C /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ 37A4BDEB2F5B6B4C00EEB289 /* Debug */ = { isa = XCBuildConfiguration; @@ -2494,7 +499,7 @@ INFOPLIST_FILE = LoopFollowLAExtension/ExtensionInfo.plist; INFOPLIST_KEY_CFBundleDisplayName = LoopFollowLAExtension; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Jon Fawcett. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2546,7 +551,7 @@ INFOPLIST_FILE = LoopFollowLAExtension/ExtensionInfo.plist; INFOPLIST_KEY_CFBundleDisplayName = LoopFollowLAExtension; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Jon Fawcett. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2590,9 +595,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(BUILT_PRODUCTS_DIR)/Charts", "$(BUILT_PRODUCTS_DIR)/ShareClient", - "$(BUILT_PRODUCTS_DIR)/SwiftAlgorithms", ); GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -2623,9 +626,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(BUILT_PRODUCTS_DIR)/Charts", "$(BUILT_PRODUCTS_DIR)/ShareClient", - "$(BUILT_PRODUCTS_DIR)/SwiftAlgorithms", ); GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -2773,7 +774,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = LoopFollow/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2798,7 +799,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = LoopFollow/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2853,20 +854,6 @@ }; /* End XCConfigurationList section */ -/* Begin XCVersionGroup section */ - FC3AE7B3249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - FC3AE7B4249E8E0E00AAE1E0 /* LoopFollow.xcdatamodel */, - ); - currentVersion = FC3AE7B4249E8E0E00AAE1E0 /* LoopFollow.xcdatamodel */; - name = LoopFollow.xcdatamodeld; - path = LoopFollow/LoopFollow.xcdatamodeld; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; - }; -/* End XCVersionGroup section */ - /* Begin XCRemoteSwiftPackageReference section */ DD50C10A2F60A00000000002 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */ = { isa = XCRemoteSwiftPackageReference; diff --git a/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift b/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift index 17d5b73c1..b603f6152 100644 --- a/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift +++ b/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift @@ -7,22 +7,33 @@ struct AddAlarmSheet: View { let onSelect: (AlarmType) -> Void @Environment(\.dismiss) private var dismiss + @State private var searchText = "" + private let columns = [ GridItem(.adaptive(minimum: 110), spacing: 16), ] + private func matches(_ type: AlarmType) -> Bool { + let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !query.isEmpty else { return true } + return type.rawValue.localizedCaseInsensitiveContains(query) + || type.blurb.localizedCaseInsensitiveContains(query) + || type.group.rawValue.localizedCaseInsensitiveContains(query) + } + var body: some View { NavigationStack { ScrollView { LazyVGrid(columns: columns, spacing: 16) { ForEach(AlarmType.Group.allCases, id: \.self) { group in - if AlarmType.allCases.contains(where: { $0.group == group }) { + let types = AlarmType.allCases.filter { $0.group == group && matches($0) } + if !types.isEmpty { Section(header: Text(group.rawValue) .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 4) ) { - ForEach(AlarmType.allCases.filter { $0.group == group }, id: \.self) { type in + ForEach(types, id: \.self) { type in AlarmTile(type: type) { onSelect(type) } @@ -32,8 +43,25 @@ struct AddAlarmSheet: View { } } .padding() + + if !AlarmType.allCases.contains(where: matches) { + VStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .font(.largeTitle) + .foregroundColor(.secondary) + Text("No Results") + .font(.headline) + Text("No alarms match “\(searchText)”.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .padding(.top, 40) + .padding(.horizontal) + } } .navigationTitle("Add Alarm") + .searchable(text: $searchText, prompt: "Search alarms") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 2de36fb0f..f429aa04a 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -76,6 +76,17 @@ struct Alarm: Identifiable, Codable, Equatable { /// Number of minutes that must satisfy the alarm criteria var persistentMinutes: Int? + /// When set, the low alarm stays silent while BG is rising (positive delta), + /// only firing when the latest reading is flat or still falling. + var suppressIfRising: Bool = false + + /// When set, the high alarm stays silent while BG is falling (negative delta), + /// only firing when the latest reading is flat or still rising. + var suppressIfFalling: Bool = false + + /// When set, the phone-battery alarm stays silent while the phone is charging. + var suppressIfCharging: Bool = false + /// Size of window to observe values, for example battery drop of x within this number of minutes, var monitoringWindow: Int? @@ -101,7 +112,8 @@ struct Alarm: Identifiable, Codable, Equatable { enum CodingKeys: String, CodingKey { case id, type, name, isEnabled, snoozedUntil case aboveBG, belowBG, threshold, predictiveMinutes, delta - case persistentMinutes, monitoringWindow, soundFile + case persistentMinutes, suppressIfRising, suppressIfFalling, suppressIfCharging + case monitoringWindow, soundFile case snoozeDuration, playSoundOption, repeatSoundOption case soundDelay, activeOption case missedBolusPrebolusWindow, missedBolusIgnoreSmallBolusUnits @@ -124,6 +136,9 @@ struct Alarm: Identifiable, Codable, Equatable { predictiveMinutes = try container.decodeIfPresent(Int.self, forKey: .predictiveMinutes) delta = try container.decodeIfPresent(Double.self, forKey: .delta) persistentMinutes = try container.decodeIfPresent(Int.self, forKey: .persistentMinutes) + suppressIfRising = try container.decodeIfPresent(Bool.self, forKey: .suppressIfRising) ?? false + suppressIfFalling = try container.decodeIfPresent(Bool.self, forKey: .suppressIfFalling) ?? false + suppressIfCharging = try container.decodeIfPresent(Bool.self, forKey: .suppressIfCharging) ?? false monitoringWindow = try container.decodeIfPresent(Int.self, forKey: .monitoringWindow) soundFile = try container.decode(SoundFile.self, forKey: .soundFile) snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 5 @@ -154,6 +169,9 @@ struct Alarm: Identifiable, Codable, Equatable { try container.encodeIfPresent(predictiveMinutes, forKey: .predictiveMinutes) try container.encodeIfPresent(delta, forKey: .delta) try container.encodeIfPresent(persistentMinutes, forKey: .persistentMinutes) + try container.encode(suppressIfRising, forKey: .suppressIfRising) + try container.encode(suppressIfFalling, forKey: .suppressIfFalling) + try container.encode(suppressIfCharging, forKey: .suppressIfCharging) try container.encodeIfPresent(monitoringWindow, forKey: .monitoringWindow) try container.encode(soundFile, forKey: .soundFile) try container.encode(snoozeDuration, forKey: .snoozeDuration) @@ -262,7 +280,7 @@ struct Alarm: Identifiable, Codable, Equatable { AlarmManager.shared.sendNotification(title: type.rawValue, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze") if playSound { - AlarmSound.setSoundFile(str: soundFile.rawValue) + AlarmSound.setSoundFile(soundFile) // Only use delay if repeating is enabled, otherwise delay doesn't make sense let delay = shouldRepeat ? soundDelay : 0 AlarmSound.play(repeating: shouldRepeat, delay: delay) @@ -300,8 +318,8 @@ struct Alarm: Identifiable, Codable, Equatable { persistentMinutes = 0 case .fastDrop: soundFile = .bigClockTicking - delta = 18 - monitoringWindow = 2 + delta = 15 + monitoringWindow = 3 case .fastRise: soundFile = .cartoonFailStringsTrumpet delta = 10 @@ -311,7 +329,7 @@ struct Alarm: Identifiable, Codable, Equatable { threshold = 16 case .notLooping: soundFile = .sciFiEngineShutDown - threshold = 31 + threshold = 16 case .missedBolus: soundFile = .dholShuffleloop monitoringWindow = 15 @@ -364,6 +382,15 @@ struct Alarm: Identifiable, Codable, Equatable { snoozeDuration = 0 aboveBG = 180 belowBG = 70 + case .dbSize: + /// Nightscout's own dbsize plugin warns at 60% and calls 75% urgent + soundFile = .wrongAnswer + threshold = 75 + snoozeDuration = 1 + /// The database fills over weeks, so there is never a reason to wake someone for it + activeOption = .day + playSoundOption = .day + repeatSoundOption = .day } } } @@ -383,7 +410,7 @@ extension AlarmType { case .iob, .cob, .missedBolus, .futureCarbs, .recBolus: return .insulin case .battery, .batteryDrop, .pump, .pumpBattery, .pumpChange, - .sensorChange, .notLooping, .buildExpire: + .sensorChange, .notLooping, .buildExpire, .dbSize: return .device case .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd: return .other @@ -410,6 +437,7 @@ extension AlarmType { case .sensorChange: return "sensor.tag.radiowaves.forward" case .notLooping: return "circle.slash" case .buildExpire: return "calendar.badge.exclamationmark" + case .dbSize: return "externaldrive.badge.exclamationmark" case .overrideStart: return "play.circle" case .overrideEnd: return "stop.circle" case .tempTargetStart: return "flag" @@ -438,6 +466,7 @@ extension AlarmType { case .sensorChange: return "Sensor change due." case .notLooping: return "Loop hasn’t completed." case .buildExpire: return "Looping-app build expiring." + case .dbSize: return "Nightscout database filling up." case .overrideStart: return "Override just started." case .overrideEnd: return "Override ended." case .tempTargetStart: return "Temp target started." diff --git a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift index eb1011443..8e60bcb9d 100644 --- a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift @@ -8,9 +8,15 @@ protocol AlarmCondition { init() /// pure, per-alarm logic against `AlarmData` func evaluate(alarm: Alarm, data: AlarmData, now: Date) -> Bool + /// `true` when `belowBG`/`aboveBG` are this condition's own trigger + /// threshold, checked in `evaluate`; `false` (the default) treats them + /// as activation limits enforced by `passesBGLimits`. + var checksOwnBGLimits: Bool { get } } extension AlarmCondition { + var checksOwnBGLimits: Bool { false } + /// Returns `true` when the alarm is allowed to continue evaluating /// after BG-limit checks; `false` blocks it immediately. func passesBGLimits(alarm: Alarm, data: AlarmData) -> Bool { @@ -24,7 +30,14 @@ extension AlarmCondition { if alarm.type.isBGBased && !haveBG { return false } // ──────────────────────────────────── - // 2. No limits? we’re done. + // 2. Conditions that check their own threshold in `evaluate` + // skip the limit gate (the data requirement above applies + // regardless). + // ──────────────────────────────────── + if checksOwnBGLimits { return true } + + // ──────────────────────────────────── + // 3. No limits? we’re done. // ──────────────────────────────────── if alarm.belowBG == nil && alarm.aboveBG == nil { return true } diff --git a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift index 1fcd80acf..f5618832e 100644 --- a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift @@ -11,6 +11,11 @@ struct BatteryCondition: AlarmCondition { guard let limit = alarm.threshold, limit > 0 else { return false } guard let level = data.latestBattery else { return false } + // Optional: stay silent while the phone is charging back up. + if alarm.suppressIfCharging, data.latestBatteryIsCharging == true { + return false + } + return level <= limit } } diff --git a/LoopFollow/Alarm/AlarmCondition/DBSizeCondition.swift b/LoopFollow/Alarm/AlarmCondition/DBSizeCondition.swift new file mode 100644 index 000000000..8ad00d9c3 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/DBSizeCondition.swift @@ -0,0 +1,18 @@ +// LoopFollow +// DBSizeCondition.swift + +import Foundation + +/// Fires when the Nightscout database has filled **≥ threshold percent** +/// of the size limit configured on the site (`DBSIZE_MAX`). +struct DBSizeCondition: AlarmCondition { + static let type: AlarmType = .dbSize + init() {} + + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { + guard let limitPercentage = alarm.threshold, limitPercentage > 0 else { return false } + guard let usedPercentage = data.dbSizePercentage else { return false } + + return usedPercentage >= limitPercentage + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift index 26ea46083..6ba8b9b47 100644 --- a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift @@ -35,6 +35,16 @@ struct HighBGCondition: AlarmCondition { } } + // Optional: stay silent while the latest reading is below the previous one; + // only alarm when BG is flat or still rising. + if alarm.suppressIfFalling, data.bgReadings.count >= 2 { + let last = data.bgReadings[data.bgReadings.count - 1] + let previous = data.bgReadings[data.bgReadings.count - 2] + if last.sgv > 0, previous.sgv > 0, last.sgv < previous.sgv { + return false + } + } + return persistentOK } } diff --git a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift index 77abc55a1..cb4610f74 100644 --- a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift @@ -4,17 +4,23 @@ import Foundation /// Fires when: -/// • every BG in `persistentMinutes` (if set) **and** the latest BG are ≤ `threshold`; **or** -/// • any predicted BG within `predictiveMinutes` is ≤ `threshold`. +/// • every BG in `persistentMinutes` (if set) **and** the latest BG are ≤ `belowBG`; **or** +/// • any predicted BG within `predictiveMinutes` is ≤ `belowBG`. struct LowBGCondition: AlarmCondition { static let type: AlarmType = .low init() {} + /// `belowBG` is this alarm's trigger threshold, not an activation limit: + /// the predictive look-ahead must be able to run while the latest BG is + /// still above it, so `evaluate` checks the threshold itself. + var checksOwnBGLimits: Bool { true } + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { // ──────────────────────────────── // 0. sanity checks // ──────────────────────────────── guard let belowBG = alarm.belowBG else { return false } + guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } func isLow(_ g: GlucoseValue) -> Bool { g.sgv > 0 && Double(g.sgv) <= belowBG @@ -58,8 +64,21 @@ struct LowBGCondition: AlarmCondition { } // ──────────────────────────────── - // 3. final decision + // 3. rising BG suppression (optional) + // Stay silent while the latest reading is above the previous one; + // only alarm when BG is flat or still falling. + // ──────────────────────────────── + if alarm.suppressIfRising, data.bgReadings.count >= 2 { + let last = data.bgReadings[data.bgReadings.count - 1] + let previous = data.bgReadings[data.bgReadings.count - 2] + if last.sgv > 0, previous.sgv > 0, last.sgv > previous.sgv { + return false + } + } + + // ──────────────────────────────── + // 4. final decision // ──────────────────────────────── - return persistentOK || predictiveTrigger + return (isLow(latest) && persistentOK) || predictiveTrigger } } diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index b960a9223..588c37c5f 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -20,7 +20,9 @@ struct AlarmData: Codable { let IOB: Double? let recentBoluses: [BolusEntry] let latestBattery: Double? + let latestBatteryIsCharging: Bool? let latestPumpBattery: Double? let batteryHistory: [DataStructs.batteryStruct] let recentCarbs: [CarbSample] + let dbSizePercentage: Double? } diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index e8ff4aff5..14901c5a1 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -83,6 +83,7 @@ struct AlarmEditor: View { case .batteryDrop: BatteryDropAlarmEditor(alarm: $alarm) case .missedBolus: MissedBolusAlarmEditor(alarm: $alarm) case .futureCarbs: FutureCarbsAlarmEditor(alarm: $alarm) + case .dbSize: DBSizeAlarmEditor(alarm: $alarm) } } } diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift index 5d5beb1bd..e78de27a2 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift @@ -2,6 +2,7 @@ // AlarmAudioSection.swift import SwiftUI +import UniformTypeIdentifiers struct AlarmAudioSection: View { @Binding var alarm: Alarm @@ -111,27 +112,37 @@ private struct TonePickerSheet: View { @Binding var selected: SoundFile @Environment(\.dismiss) private var dismiss + @State private var customSounds: [CustomSound] = [] + @State private var showingImporter = false + @State private var importError: String? + var body: some View { NavigationView { ScrollViewReader { proxy in List { - ForEach(SoundFile.allCases) { tone in + Section { + ForEach(customSounds) { sound in + toneRow(tone: .custom(sound.id), label: sound.displayName) + .id(SoundFile.custom(sound.id)) + } + .onDelete(perform: deleteCustomSounds) + Button { - selected = tone - AlarmSound.setSoundFile(str: tone.rawValue) - AlarmSound.stop() - AlarmSound.playTest() + showingImporter = true } label: { - HStack { - Text(tone.displayName) - if tone == selected { - Spacer() - Image(systemName: "checkmark") - .foregroundColor(.accentColor) - } - } + Label("Import Sound…", systemImage: "plus.circle") + } + } header: { + Text("Custom") + } footer: { + Text("Custom sounds stay on this device and aren't included in settings export.") + } + + Section(header: Text("Built-in")) { + ForEach(SoundFile.allBuiltins) { tone in + toneRow(tone: tone, label: tone.displayName) + .id(tone) } - .id(tone) } } .navigationTitle("Choose Tone") @@ -144,9 +155,81 @@ private struct TonePickerSheet: View { } } .onAppear { + reloadCustomSounds() proxy.scrollTo(selected, anchor: .center) } + .fileImporter( + isPresented: $showingImporter, + allowedContentTypes: [.audio], + allowsMultipleSelection: false + ) { result in + handleImport(result) + } + .alert( + "Import Failed", + isPresented: Binding( + get: { importError != nil }, + set: { if !$0 { importError = nil } } + ), + actions: { Button("OK", role: .cancel) { importError = nil } }, + message: { Text(importError ?? "") } + ) + } + } + } + + @ViewBuilder + private func toneRow(tone: SoundFile, label: String) -> some View { + Button { + selected = tone + AlarmSound.setSoundFile(tone) + AlarmSound.stop() + AlarmSound.playTest() + } label: { + HStack { + Text(label) + if tone == selected { + Spacer() + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + } + + private func reloadCustomSounds() { + customSounds = CustomSoundStore.shared.list() + } + + private func deleteCustomSounds(at offsets: IndexSet) { + let deleted = offsets.map { customSounds[$0] } + for sound in deleted { + CustomSoundStore.shared.delete(sound.id) + } + // If the alarm was pointing at a sound we just deleted, fall back to a valid + // built-in so the picker and the alarm don't keep a dangling reference. + if deleted.contains(where: { SoundFile.custom($0.id) == selected }) { + selected = .fallback + } + reloadCustomSounds() + } + + private func handleImport(_ result: Result<[URL], Error>) { + switch result { + case let .success(urls): + guard let url = urls.first else { return } + do { + let imported = try CustomSoundStore.shared.importFile(at: url) + reloadCustomSounds() + selected = .custom(imported.id) + AlarmSound.setSoundFile(.custom(imported.id)) + AlarmSound.stop() + AlarmSound.playTest() + } catch { + importError = error.localizedDescription } + case let .failure(error): + importError = error.localizedDescription } } } diff --git a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift index d67bbb711..4d0f8e968 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift @@ -3,122 +3,239 @@ import Foundation -enum SoundFile: String, CaseIterable, Identifiable, Codable { - case alarmBuzzer = "Alarm_Buzzer" - case alarmClock = "Alarm_Clock" - case alertToneBusy = "Alert_Tone_Busy" - case alertToneRingtone1 = "Alert_Tone_Ringtone_1" - case alertToneRingtone2 = "Alert_Tone_Ringtone_2" - case alienSiren = "Alien_Siren" - case ambulance = "Ambulance" - case analogWatchAlarm = "Analog_Watch_Alarm" - case bigClockTicking = "Big_Clock_Ticking" - case burglarAlarmSiren1 = "Burglar_Alarm_Siren_1" - case burglarAlarmSiren2 = "Burglar_Alarm_Siren_2" - case cartoonAscendClimbSneaky = "Cartoon_Ascend_Climb_Sneaky" - case cartoonAscendThenDescend = "Cartoon_Ascend_Then_Descend" - case cartoonBounceToCeiling = "Cartoon_Bounce_To_Ceiling" - case cartoonDreamyGlissandoHarp = "Cartoon_Dreamy_Glissando_Harp" - case cartoonFailStringsTrumpet = "Cartoon_Fail_Strings_Trumpet" - case cartoonMachineClumsyLoop = "Cartoon_Machine_Clumsy_Loop" - case cartoonSiren = "Cartoon_Siren" - case cartoonTipToeSneakyWalk = "Cartoon_Tip_Toe_Sneaky_Walk" - case cartoonUhOh = "Cartoon_Uh_Oh" - case cartoonVillainHorns = "Cartoon_Villain_Horns" - case cellPhoneRingTone = "Cell_Phone_Ring_Tone" - case chimesGlassy = "Chimes_Glassy" - case computerMagic = "Computer_Magic" - case csfx2Alarm = "CSFX-2_Alarm" - case cuckooClock = "Cuckoo_Clock" - case dholShuffleloop = "Dhol_Shuffleloop" - case discreet = "Discreet" - case earlySunrise = "Early_Sunrise" - case emergencyAlarmCarbonMonoxide = "Emergency_Alarm_Carbon_Monoxide" - case emergencyAlarmSiren = "Emergency_Alarm_Siren" - case emergencyAlarm = "Emergency_Alarm" - case endingReached = "Ending_Reached" - case fly = "Fly" - case ghostHover = "Ghost_Hover" - case goodMorning = "Good_Morning" - case hellYeahSomewhatCalmer = "Hell_Yeah_Somewhat_Calmer" - case inAHurry = "In_A_Hurry" - case indeed = "Indeed" - case insistently = "Insistently" - case jingleAllTheWay = "Jingle_All_The_Way" - case laserShoot = "Laser_Shoot" - case machineCharge = "Machine_Charge" - case magicalTwinkle = "Magical_Twinkle" - case marchingHeavyFootedFatElephants = "Marching_Heavy_Footed_Fat_Elephants" - case marimbaDescend = "Marimba_Descend" - case marimbaFlutterOrShake = "Marimba_Flutter_or_Shake" - case martianGun = "Martian_Gun" - case martianScanner = "Martian_Scanner" - case metallic = "Metallic" - case nightguard = "Nightguard" - case notKiddin = "Not_Kiddin" - case openYourEyesAndSee = "Open_Your_Eyes_And_See" - case orchestralHorns = "Orchestral_Horns" - case oringz = "Oringz" - case pagerBeeps = "Pager_Beeps" - case remembersMeOfAsia = "Remembers_Me_Of_Asia" - case riseAndShine = "Rise_And_Shine" - case rush = "Rush" - case sciFiAirRaidAlarm = "Sci-Fi_Air_Raid_Alarm" - case sciFiAlarmLoop1 = "Sci-Fi_Alarm_Loop_1" - case sciFiAlarmLoop2 = "Sci-Fi_Alarm_Loop_2" - case sciFiAlarmLoop3 = "Sci-Fi_Alarm_Loop_3" - case sciFiAlarmLoop4 = "Sci-Fi_Alarm_Loop_4" - case sciFiAlarm = "Sci-Fi_Alarm" - case sciFiComputerConsoleAlarm = "Sci-Fi_Computer_Console_Alarm" - case sciFiConsoleAlarm = "Sci-Fi_Console_Alarm" - case sciFiEerieAlarm = "Sci-Fi_Eerie_Alarm" - case sciFiEngineShutDown = "Sci-Fi_Engine_Shut_Down" - case sciFiIncomingMessageAlert = "Sci-Fi_Incoming_Message_Alert" - case sciFiSpaceshipMessage = "Sci-Fi_Spaceship_Message" - case sciFiSpaceshipWarmUp = "Sci-Fi_Spaceship_Warm_Up" - case sciFiWarning = "Sci-Fi_Warning" - case signatureCorporate = "Signature_Corporate" - case siriAlertCalibrationNeeded = "Siri_Alert_Calibration_Needed" - case siriAlertDeviceMuted = "Siri_Alert_Device_Muted" - case siriAlertGlucoseDroppingFast = "Siri_Alert_Glucose_Dropping_Fast" - case siriAlertGlucoseRisingFast = "Siri_Alert_Glucose_Rising_Fast" - case siriAlertHighGlucose = "Siri_Alert_High_Glucose" - case siriAlertLowGlucose = "Siri_Alert_Low_Glucose" - case siriAlertMissedReadings = "Siri_Alert_Missed_Readings" - case siriAlertTransmitterBatteryLow = "Siri_Alert_Transmitter_Battery_Low" - case siriAlertUrgentHighGlucose = "Siri_Alert_Urgent_High_Glucose" - case siriAlertUrgentLowGlucose = "Siri_Alert_Urgent_Low_Glucose" - case siriCalibrationNeeded = "Siri_Calibration_Needed" - case siriDeviceMuted = "Siri_Device_Muted" - case siriGlucoseDroppingFast = "Siri_Glucose_Dropping_Fast" - case siriGlucoseRisingFast = "Siri_Glucose_Rising_Fast" - case siriHighGlucose = "Siri_High_Glucose" - case siriLowGlucose = "Siri_Low_Glucose" - case siriMissedReadings = "Siri_Missed_Readings" - case siriTransmitterBatteryLow = "Siri_Transmitter_Battery_Low" - case siriUrgentHighGlucose = "Siri_Urgent_High_Glucose" - case siriUrgentLowGlucose = "Siri_Urgent_Low_Glucose" - case softMarimbaPadPositive = "Soft_Marimba_Pad_Positive" - case softWarmAiryOptimistic = "Soft_Warm_Airy_Optimistic" - case softWarmAiryReassuring = "Soft_Warm_Airy_Reassuring" - case storeDoorChime = "Store_Door_Chime" - case sunny = "Sunny" - case thunderSoundFX = "Thunder_Sound_FX" - case timeHasCome = "Time_Has_Come" - case tornadoSiren = "Tornado_Siren" - case twoTurtleDoves = "Two_Turtle_Doves" - case unpaved = "Unpaved" - case wakeUpWillYou = "Wake_Up_Will_You" - case winGain = "Win_Gain" - case wrongAnswer = "Wrong_Answer" +/// A sound chosen for an alarm. +/// +/// `.builtin` wraps the filename (no extension) of a `.caf` resource bundled with the app. +/// `.custom` references an audio file imported by the user into `Documents/CustomSounds/`, +/// keyed by UUID (see `CustomSoundStore`). +/// +/// Codable note: built-in values encode/decode as a bare string (e.g. `"Indeed"`) so stored +/// alarm data written by older app versions continues to load. Custom values encode as a +/// keyed object and are new with this version. +enum SoundFile: Hashable, Identifiable { + case builtin(String) + case custom(UUID) - // Identifiable conformance - var id: SoundFile { self } + var id: String { + switch self { + case let .builtin(name): return "builtin:\(name)" + case let .custom(uuid): return "custom:\(uuid.uuidString)" + } + } - /// Human-friendly name (spaces instead of underscores) + /// Human-friendly name for display in pickers. var displayName: String { - rawValue - .replacingOccurrences(of: "_", with: " ") - .replacingOccurrences(of: " ", with: " ") + switch self { + case let .builtin(name): + return name + .replacingOccurrences(of: "_", with: " ") + .replacingOccurrences(of: " ", with: " ") + case let .custom(uuid): + return CustomSoundStore.shared.displayName(for: uuid) ?? "Custom Sound" + } + } + + /// Convenience for call sites that still think in terms of a bundle filename. + var builtinName: String? { + if case let .builtin(name) = self { return name } + return nil + } + + /// Back-compat shim for migration code that reads legacy string values out of UserDefaults. + /// The string is treated as a built-in filename; playback will fall back gracefully if the + /// file isn't actually in the bundle. + init?(rawValue: String) { + guard !rawValue.isEmpty else { return nil } + self = .builtin(rawValue) } } + +extension SoundFile: Codable { + private enum CodingKeys: String, CodingKey { + case kind, name, id + } + + init(from decoder: Decoder) throws { + // Legacy form: bare string == built-in filename. + if let container = try? decoder.singleValueContainer(), + let name = try? container.decode(String.self) + { + self = .builtin(name) + return + } + let c = try decoder.container(keyedBy: CodingKeys.self) + let kind = try c.decode(String.self, forKey: .kind) + switch kind { + case "builtin": + let name = try c.decode(String.self, forKey: .name) + self = .builtin(name) + case "custom": + let uuid = try c.decode(UUID.self, forKey: .id) + self = .custom(uuid) + default: + throw DecodingError.dataCorruptedError( + forKey: .kind, + in: c, + debugDescription: "Unknown SoundFile kind: \(kind)" + ) + } + } + + func encode(to encoder: Encoder) throws { + switch self { + case let .builtin(name): + // Preserve legacy wire format so older app versions can still read it. + var c = encoder.singleValueContainer() + try c.encode(name) + case let .custom(uuid): + var c = encoder.container(keyedBy: CodingKeys.self) + try c.encode("custom", forKey: .kind) + try c.encode(uuid, forKey: .id) + } + } +} + +// MARK: - Built-in catalog + +extension SoundFile { + /// Fallback used when a referenced sound file can't be located (e.g. a deleted custom sound). + static let fallback: SoundFile = .indeed + + /// Every built-in sound shipped with the app, in the order shown in the picker. + static let allBuiltins: [SoundFile] = [ + .alarmBuzzer, .alarmClock, .alertToneBusy, .alertToneRingtone1, .alertToneRingtone2, + .alienSiren, .ambulance, .analogWatchAlarm, .bigClockTicking, .burglarAlarmSiren1, + .burglarAlarmSiren2, .cartoonAscendClimbSneaky, .cartoonAscendThenDescend, + .cartoonBounceToCeiling, .cartoonDreamyGlissandoHarp, .cartoonFailStringsTrumpet, + .cartoonMachineClumsyLoop, .cartoonSiren, .cartoonTipToeSneakyWalk, .cartoonUhOh, + .cartoonVillainHorns, .cellPhoneRingTone, .chimesGlassy, .computerMagic, .csfx2Alarm, + .cuckooClock, .dholShuffleloop, .discreet, .earlySunrise, .emergencyAlarmCarbonMonoxide, + .emergencyAlarmSiren, .emergencyAlarm, .endingReached, .fly, .ghostHover, .goodMorning, + .hellYeahSomewhatCalmer, .inAHurry, .indeed, .insistently, .jingleAllTheWay, .laserShoot, + .machineCharge, .magicalTwinkle, .marchingHeavyFootedFatElephants, .marimbaDescend, + .marimbaFlutterOrShake, .martianGun, .martianScanner, .metallic, .nightguard, .notKiddin, + .openYourEyesAndSee, .orchestralHorns, .oringz, .pagerBeeps, .remembersMeOfAsia, + .riseAndShine, .rush, .sciFiAirRaidAlarm, .sciFiAlarmLoop1, .sciFiAlarmLoop2, + .sciFiAlarmLoop3, .sciFiAlarmLoop4, .sciFiAlarm, .sciFiComputerConsoleAlarm, + .sciFiConsoleAlarm, .sciFiEerieAlarm, .sciFiEngineShutDown, .sciFiIncomingMessageAlert, + .sciFiSpaceshipMessage, .sciFiSpaceshipWarmUp, .sciFiWarning, .signatureCorporate, + .siriAlertCalibrationNeeded, .siriAlertDeviceMuted, .siriAlertGlucoseDroppingFast, + .siriAlertGlucoseRisingFast, .siriAlertHighGlucose, .siriAlertLowGlucose, + .siriAlertMissedReadings, .siriAlertTransmitterBatteryLow, .siriAlertUrgentHighGlucose, + .siriAlertUrgentLowGlucose, .siriCalibrationNeeded, .siriDeviceMuted, + .siriGlucoseDroppingFast, .siriGlucoseRisingFast, .siriHighGlucose, .siriLowGlucose, + .siriMissedReadings, .siriTransmitterBatteryLow, .siriUrgentHighGlucose, + .siriUrgentLowGlucose, .softMarimbaPadPositive, .softWarmAiryOptimistic, + .softWarmAiryReassuring, .storeDoorChime, .sunny, .thunderSoundFX, .timeHasCome, + .tornadoSiren, .twoTurtleDoves, .unpaved, .wakeUpWillYou, .winGain, .wrongAnswer, + ] + + // Static aliases so existing call sites like `soundFile = .indeed` keep compiling. + static let alarmBuzzer: SoundFile = .builtin("Alarm_Buzzer") + static let alarmClock: SoundFile = .builtin("Alarm_Clock") + static let alertToneBusy: SoundFile = .builtin("Alert_Tone_Busy") + static let alertToneRingtone1: SoundFile = .builtin("Alert_Tone_Ringtone_1") + static let alertToneRingtone2: SoundFile = .builtin("Alert_Tone_Ringtone_2") + static let alienSiren: SoundFile = .builtin("Alien_Siren") + static let ambulance: SoundFile = .builtin("Ambulance") + static let analogWatchAlarm: SoundFile = .builtin("Analog_Watch_Alarm") + static let bigClockTicking: SoundFile = .builtin("Big_Clock_Ticking") + static let burglarAlarmSiren1: SoundFile = .builtin("Burglar_Alarm_Siren_1") + static let burglarAlarmSiren2: SoundFile = .builtin("Burglar_Alarm_Siren_2") + static let cartoonAscendClimbSneaky: SoundFile = .builtin("Cartoon_Ascend_Climb_Sneaky") + static let cartoonAscendThenDescend: SoundFile = .builtin("Cartoon_Ascend_Then_Descend") + static let cartoonBounceToCeiling: SoundFile = .builtin("Cartoon_Bounce_To_Ceiling") + static let cartoonDreamyGlissandoHarp: SoundFile = .builtin("Cartoon_Dreamy_Glissando_Harp") + static let cartoonFailStringsTrumpet: SoundFile = .builtin("Cartoon_Fail_Strings_Trumpet") + static let cartoonMachineClumsyLoop: SoundFile = .builtin("Cartoon_Machine_Clumsy_Loop") + static let cartoonSiren: SoundFile = .builtin("Cartoon_Siren") + static let cartoonTipToeSneakyWalk: SoundFile = .builtin("Cartoon_Tip_Toe_Sneaky_Walk") + static let cartoonUhOh: SoundFile = .builtin("Cartoon_Uh_Oh") + static let cartoonVillainHorns: SoundFile = .builtin("Cartoon_Villain_Horns") + static let cellPhoneRingTone: SoundFile = .builtin("Cell_Phone_Ring_Tone") + static let chimesGlassy: SoundFile = .builtin("Chimes_Glassy") + static let computerMagic: SoundFile = .builtin("Computer_Magic") + static let csfx2Alarm: SoundFile = .builtin("CSFX-2_Alarm") + static let cuckooClock: SoundFile = .builtin("Cuckoo_Clock") + static let dholShuffleloop: SoundFile = .builtin("Dhol_Shuffleloop") + static let discreet: SoundFile = .builtin("Discreet") + static let earlySunrise: SoundFile = .builtin("Early_Sunrise") + static let emergencyAlarmCarbonMonoxide: SoundFile = .builtin("Emergency_Alarm_Carbon_Monoxide") + static let emergencyAlarmSiren: SoundFile = .builtin("Emergency_Alarm_Siren") + static let emergencyAlarm: SoundFile = .builtin("Emergency_Alarm") + static let endingReached: SoundFile = .builtin("Ending_Reached") + static let fly: SoundFile = .builtin("Fly") + static let ghostHover: SoundFile = .builtin("Ghost_Hover") + static let goodMorning: SoundFile = .builtin("Good_Morning") + static let hellYeahSomewhatCalmer: SoundFile = .builtin("Hell_Yeah_Somewhat_Calmer") + static let inAHurry: SoundFile = .builtin("In_A_Hurry") + static let indeed: SoundFile = .builtin("Indeed") + static let insistently: SoundFile = .builtin("Insistently") + static let jingleAllTheWay: SoundFile = .builtin("Jingle_All_The_Way") + static let laserShoot: SoundFile = .builtin("Laser_Shoot") + static let machineCharge: SoundFile = .builtin("Machine_Charge") + static let magicalTwinkle: SoundFile = .builtin("Magical_Twinkle") + static let marchingHeavyFootedFatElephants: SoundFile = .builtin("Marching_Heavy_Footed_Fat_Elephants") + static let marimbaDescend: SoundFile = .builtin("Marimba_Descend") + static let marimbaFlutterOrShake: SoundFile = .builtin("Marimba_Flutter_or_Shake") + static let martianGun: SoundFile = .builtin("Martian_Gun") + static let martianScanner: SoundFile = .builtin("Martian_Scanner") + static let metallic: SoundFile = .builtin("Metallic") + static let nightguard: SoundFile = .builtin("Nightguard") + static let notKiddin: SoundFile = .builtin("Not_Kiddin") + static let openYourEyesAndSee: SoundFile = .builtin("Open_Your_Eyes_And_See") + static let orchestralHorns: SoundFile = .builtin("Orchestral_Horns") + static let oringz: SoundFile = .builtin("Oringz") + static let pagerBeeps: SoundFile = .builtin("Pager_Beeps") + static let remembersMeOfAsia: SoundFile = .builtin("Remembers_Me_Of_Asia") + static let riseAndShine: SoundFile = .builtin("Rise_And_Shine") + static let rush: SoundFile = .builtin("Rush") + static let sciFiAirRaidAlarm: SoundFile = .builtin("Sci-Fi_Air_Raid_Alarm") + static let sciFiAlarmLoop1: SoundFile = .builtin("Sci-Fi_Alarm_Loop_1") + static let sciFiAlarmLoop2: SoundFile = .builtin("Sci-Fi_Alarm_Loop_2") + static let sciFiAlarmLoop3: SoundFile = .builtin("Sci-Fi_Alarm_Loop_3") + static let sciFiAlarmLoop4: SoundFile = .builtin("Sci-Fi_Alarm_Loop_4") + static let sciFiAlarm: SoundFile = .builtin("Sci-Fi_Alarm") + static let sciFiComputerConsoleAlarm: SoundFile = .builtin("Sci-Fi_Computer_Console_Alarm") + static let sciFiConsoleAlarm: SoundFile = .builtin("Sci-Fi_Console_Alarm") + static let sciFiEerieAlarm: SoundFile = .builtin("Sci-Fi_Eerie_Alarm") + static let sciFiEngineShutDown: SoundFile = .builtin("Sci-Fi_Engine_Shut_Down") + static let sciFiIncomingMessageAlert: SoundFile = .builtin("Sci-Fi_Incoming_Message_Alert") + static let sciFiSpaceshipMessage: SoundFile = .builtin("Sci-Fi_Spaceship_Message") + static let sciFiSpaceshipWarmUp: SoundFile = .builtin("Sci-Fi_Spaceship_Warm_Up") + static let sciFiWarning: SoundFile = .builtin("Sci-Fi_Warning") + static let signatureCorporate: SoundFile = .builtin("Signature_Corporate") + static let siriAlertCalibrationNeeded: SoundFile = .builtin("Siri_Alert_Calibration_Needed") + static let siriAlertDeviceMuted: SoundFile = .builtin("Siri_Alert_Device_Muted") + static let siriAlertGlucoseDroppingFast: SoundFile = .builtin("Siri_Alert_Glucose_Dropping_Fast") + static let siriAlertGlucoseRisingFast: SoundFile = .builtin("Siri_Alert_Glucose_Rising_Fast") + static let siriAlertHighGlucose: SoundFile = .builtin("Siri_Alert_High_Glucose") + static let siriAlertLowGlucose: SoundFile = .builtin("Siri_Alert_Low_Glucose") + static let siriAlertMissedReadings: SoundFile = .builtin("Siri_Alert_Missed_Readings") + static let siriAlertTransmitterBatteryLow: SoundFile = .builtin("Siri_Alert_Transmitter_Battery_Low") + static let siriAlertUrgentHighGlucose: SoundFile = .builtin("Siri_Alert_Urgent_High_Glucose") + static let siriAlertUrgentLowGlucose: SoundFile = .builtin("Siri_Alert_Urgent_Low_Glucose") + static let siriCalibrationNeeded: SoundFile = .builtin("Siri_Calibration_Needed") + static let siriDeviceMuted: SoundFile = .builtin("Siri_Device_Muted") + static let siriGlucoseDroppingFast: SoundFile = .builtin("Siri_Glucose_Dropping_Fast") + static let siriGlucoseRisingFast: SoundFile = .builtin("Siri_Glucose_Rising_Fast") + static let siriHighGlucose: SoundFile = .builtin("Siri_High_Glucose") + static let siriLowGlucose: SoundFile = .builtin("Siri_Low_Glucose") + static let siriMissedReadings: SoundFile = .builtin("Siri_Missed_Readings") + static let siriTransmitterBatteryLow: SoundFile = .builtin("Siri_Transmitter_Battery_Low") + static let siriUrgentHighGlucose: SoundFile = .builtin("Siri_Urgent_High_Glucose") + static let siriUrgentLowGlucose: SoundFile = .builtin("Siri_Urgent_Low_Glucose") + static let softMarimbaPadPositive: SoundFile = .builtin("Soft_Marimba_Pad_Positive") + static let softWarmAiryOptimistic: SoundFile = .builtin("Soft_Warm_Airy_Optimistic") + static let softWarmAiryReassuring: SoundFile = .builtin("Soft_Warm_Airy_Reassuring") + static let storeDoorChime: SoundFile = .builtin("Store_Door_Chime") + static let sunny: SoundFile = .builtin("Sunny") + static let thunderSoundFX: SoundFile = .builtin("Thunder_Sound_FX") + static let timeHasCome: SoundFile = .builtin("Time_Has_Come") + static let tornadoSiren: SoundFile = .builtin("Tornado_Siren") + static let twoTurtleDoves: SoundFile = .builtin("Two_Turtle_Doves") + static let unpaved: SoundFile = .builtin("Unpaved") + static let wakeUpWillYou: SoundFile = .builtin("Wake_Up_Will_You") + static let winGain: SoundFile = .builtin("Win_Gain") + static let wrongAnswer: SoundFile = .builtin("Wrong_Answer") +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/DBSizeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/DBSizeAlarmEditor.swift new file mode 100644 index 000000000..8852cf4c7 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/DBSizeAlarmEditor.swift @@ -0,0 +1,33 @@ +// LoopFollow +// DBSizeAlarmEditor.swift + +import SwiftUI + +struct DBSizeAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + text: "This warns you when the Nightscout database has filled the percentage you choose. Nightscout measures the used space against its own DBSIZE_MAX setting, which is 496 MiB unless the site owner changed it, so the percentage is only meaningful when that setting matches the real hosting limit.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Database Size", + footer: "This alerts you when the database reaches or exceeds this percentage of the limit configured on the Nightscout site.", + title: "At or Above", + range: 0 ... 100, + step: 5, + unitLabel: "%", + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 38243c22e..1adf217aa 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -34,6 +34,14 @@ struct HighBgAlarmEditor: View { value: $alarm.persistentMinutes ) + Section( + header: Text("FALLING BG"), + footer: Text("Stay silent while BG is falling. The alert only sounds " + + "when the latest reading is flat or still rising.") + ) { + Toggle("Skip if BG is falling", isOn: $alarm.suppressIfFalling) + } + AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 555db90cd..a21db2239 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -8,13 +8,13 @@ struct LowBgAlarmEditor: View { var body: some View { Group { - InfoBanner(text: "This warns you if the glucose is too low now or might be soon, based on predictions. Note: predictions is currently not available for Trio.") + InfoBanner(text: "This warns you if the glucose is too low now or might be soon, based on the forecast.") AlarmGeneralSection(alarm: $alarm) AlarmBGSection( header: "Low Limit", - footer: "Alert when any reading or prediction is at or below this value.", + footer: "Alert when any reading or forecast is at or below this value.", title: "BG", range: 40 ... 150, value: $alarm.belowBG @@ -33,7 +33,7 @@ struct LowBgAlarmEditor: View { AlarmStepperSection( header: "PREDICTION", - footer: "Look ahead this many minutes in Loop’s prediction; " + footer: "Look ahead this many minutes in the forecast; " + "if any future value is at or below the threshold, " + "you’ll be warned early. Set 0 to disable.", title: "Predictive", @@ -43,6 +43,14 @@ struct LowBgAlarmEditor: View { value: $alarm.predictiveMinutes ) + Section( + header: Text("RISING BG"), + footer: Text("Stay silent while BG is rising. The alert only sounds " + + "when the latest reading is flat or still falling.") + ) { + Toggle("Skip if BG is rising", isOn: $alarm.suppressIfRising) + } + AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift index 67c0bd9c9..d08ee68ed 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift @@ -25,6 +25,14 @@ struct PhoneBatteryAlarmEditor: View { value: $alarm.threshold ) + Section( + header: Text("CHARGING"), + footer: Text("Stay silent while the phone is charging. Requires the " + + "uploader to report charging status; if it doesn't, the alert still sounds.") + ) { + Toggle("Skip while charging", isOn: $alarm.suppressIfCharging) + } + AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 3240208db..9748f99c3 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -22,21 +22,41 @@ struct AlarmListView: View { @State private var sheetInfo: SheetInfo? @State private var deleteAfterDismiss: UUID? @State private var selectedAlarm: Alarm? + @State private var searchText = "" + + // MARK: - Search + + private func matches(_ alarm: Alarm) -> Bool { + let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !query.isEmpty else { return true } + return alarm.name.localizedCaseInsensitiveContains(query) + || alarm.type.rawValue.localizedCaseInsensitiveContains(query) + } + + private var hasResults: Bool { + !snoozedAlarms.isEmpty || !activeAlarms.isEmpty || !inactiveAlarms.isEmpty + } + + // Snapshot of "now" used to categorize snoozed vs. active alarms. SwiftUI does + // not re-render when the wall clock passes a snooze's expiry, so we refresh this + // whenever the screen appears or the app returns to the foreground. + @State private var now = Date() + @Environment(\.scenePhase) private var scenePhase // MARK: - Categorized Alarms private var snoozedAlarms: [Alarm] { - store.value.filter { $0.snoozedUntil ?? .distantPast > Date() && $0.isEnabled } + store.value.filter { $0.snoozedUntil ?? .distantPast > now && $0.isEnabled && matches($0) } .sorted(by: Alarm.byPriorityThenSpec) } private var activeAlarms: [Alarm] { - store.value.filter { $0.isEnabled && ($0.snoozedUntil ?? .distantPast <= Date()) } + store.value.filter { $0.isEnabled && ($0.snoozedUntil ?? .distantPast <= now) && matches($0) } .sorted(by: Alarm.byPriorityThenSpec) } private var inactiveAlarms: [Alarm] { - store.value.filter { !$0.isEnabled } + store.value.filter { !$0.isEnabled && matches($0) } .sorted(by: Alarm.byPriorityThenSpec) } @@ -81,6 +101,23 @@ struct AlarmListView: View { } } } + .overlay { + if !hasResults, !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + VStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .font(.largeTitle) + .foregroundColor(.secondary) + Text("No Results") + .font(.headline) + Text("No alarms match “\(searchText)”.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .padding(.horizontal) + } + } + .searchable(text: $searchText, prompt: "Search alarms") .sheet(item: $sheetInfo, onDismiss: handleSheetDismiss) { info in sheetContent(for: info) } @@ -90,6 +127,10 @@ struct AlarmListView: View { Button { sheetInfo = .picker } label: { Image(systemName: "plus") } } } + .onAppear { now = Date() } + .onChange(of: scenePhase) { phase in + if phase == .active { now = Date() } + } .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) } @@ -111,7 +152,7 @@ struct AlarmListView: View { Text(alarm.name) .foregroundColor(.primary) - if let until = alarm.snoozedUntil, until > Date() { + if let until = alarm.snoozedUntil, until > now { HStack(spacing: 4) { Image(systemName: "zzz") .font(.caption2) @@ -154,6 +195,9 @@ struct AlarmListView: View { AddAlarmSheet { type in let new = Alarm(type: type) store.value.append(new) + // First alarm the user adds is the moment notifications become + // useful — request authorization here rather than at app launch. + NotificationAuthorization.requestIfNeeded() sheetInfo = .editor(id: new.id, isNew: true) } diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 3f5aa84ec..29c6128d9 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -34,6 +34,7 @@ class AlarmManager { BatteryCondition.self, BatteryDropCondition.self, FutureCarbsCondition.self, + DBSizeCondition.self, ] ) { var dict = [AlarmType: AlarmCondition]() @@ -45,6 +46,15 @@ class AlarmManager { let now = Date() var alarmTriggered = false + // No alarm activity while onboarding is on screen — existing or + // QR-imported alarms shouldn't sound mid-setup. + if Observable.shared.isOnboardingActive.value { + if Observable.shared.currentAlarm.value != nil { + stopAlarm() + } + return + } + let config = Storage.shared.alarmConfiguration.value // Honor the "Snooze All" setting. If active, stop any current alarm and exit. diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift index 134e1fb5b..6fa92d50e 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift @@ -7,7 +7,7 @@ extension AlarmType { /// What “unit” we use for snoozeDuration for this alarmType. var snoozeTimeUnit: TimeUnit { switch self { - case .buildExpire: + case .buildExpire, .dbSize: return .day case .low, .high, .fastDrop, .fastRise, .missedReading, .notLooping, .missedBolus, diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift index a60c664ad..68ea50f71 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift @@ -37,7 +37,7 @@ extension AlarmType { return (direction: .descending, key: { $0.threshold }) - case .sensorChange: + case .sensorChange, .dbSize: return (direction: .ascending, key: { $0.threshold }) diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift index 9f5f3b5d1..a2c9096db 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift @@ -12,7 +12,7 @@ extension AlarmType { return true // These are alarms without memory, if they only are acknowledged - they would alarm again immediately case - .batteryDrop, .missedReading, .notLooping, .battery, .pumpBattery, .buildExpire, .iob, .sensorChange, .pumpChange, .pump: + .batteryDrop, .missedReading, .notLooping, .battery, .pumpBattery, .buildExpire, .iob, .sensorChange, .pumpChange, .pump, .dbSize: return false } } diff --git a/LoopFollow/Alarm/AlarmType/AlarmType.swift b/LoopFollow/Alarm/AlarmType/AlarmType.swift index 11a51885e..45c2aa9af 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType.swift @@ -29,6 +29,7 @@ enum AlarmType: String, CaseIterable, Codable { case tempTargetStart = "Temp Target Started" case tempTargetEnd = "Temp Target Ended" case buildExpire = "Looping app expiration" + case dbSize = "Nightscout Database Size" } extension AlarmType { diff --git a/LoopFollow/Alarm/CustomSoundStore.swift b/LoopFollow/Alarm/CustomSoundStore.swift new file mode 100644 index 000000000..1d20e7aad --- /dev/null +++ b/LoopFollow/Alarm/CustomSoundStore.swift @@ -0,0 +1,237 @@ +// LoopFollow +// CustomSoundStore.swift + +import AVFoundation +import Foundation + +/// A user-imported alarm sound stored inside the app's Documents directory. +struct CustomSound: Identifiable, Hashable { + let id: UUID + let displayName: String + let url: URL +} + +/// Manages the pool of user-imported alarm sounds. +/// +/// Files live in `Documents/CustomSounds/.`. A tiny sidecar `index.json` +/// maps each UUID to its original filename so the picker can show something meaningful. +/// Files dropped into `Documents/` via the Files app are picked up on next `list()` and +/// moved into `CustomSounds/` with a fresh UUID. +final class CustomSoundStore { + static let shared = CustomSoundStore() + + /// Hard cap on imported audio file size. Alarm sounds should be short; this prevents + /// users from accidentally importing a full podcast episode. + static let maxFileBytes: Int = 2 * 1024 * 1024 // 2 MB + /// Hard cap on audio duration. Alarms loop or repeat via their own delay, so long clips + /// provide no benefit and bloat storage. + static let maxDurationSeconds: TimeInterval = 30 + + enum ImportError: LocalizedError { + case unreadable + case tooLarge(Int) + case tooLong(TimeInterval) + case notAudio + + var errorDescription: String? { + switch self { + case .unreadable: return "Couldn't read the selected file." + case let .tooLarge(bytes): + let mb = Double(bytes) / (1024 * 1024) + return String(format: "File is too large (%.1f MB). Max %d MB.", + mb, CustomSoundStore.maxFileBytes / (1024 * 1024)) + case let .tooLong(seconds): + return String(format: "Audio is too long (%.1fs). Max %.0fs.", + seconds, CustomSoundStore.maxDurationSeconds) + case .notAudio: return "That file isn't a supported audio format." + } + } + } + + private let fileManager = FileManager.default + private let directory: URL + private let indexURL: URL + private let queue = DispatchQueue(label: "CustomSoundStore", qos: .userInitiated) + private var index: [UUID: String] = [:] + + private init() { + let documents = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! + directory = documents.appendingPathComponent("CustomSounds", isDirectory: true) + indexURL = directory.appendingPathComponent("index.json") + try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true) + loadIndex() + } + + // MARK: - Public API + + /// All custom sounds available, sorted by display name. + func list() -> [CustomSound] { + return queue.sync { + absorbDroppedFiles() + pruneMissing() + return index.compactMap { id, name in + guard let url = fileURL(for: id) else { return nil } + return CustomSound(id: id, displayName: name, url: url) + } + .sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending } + } + } + + /// Display name for a given custom sound, or nil if it's been deleted. + func displayName(for id: UUID) -> String? { + queue.sync { index[id] } + } + + /// Resolve a custom sound to its on-disk URL, or nil if missing. + func url(for id: UUID) -> URL? { + queue.sync { fileURL(for: id) } + } + + /// Import the audio at `sourceURL` into the store, validate it, and return a reference. + func importFile(at sourceURL: URL) throws -> CustomSound { + try queue.sync { + let needsScope = sourceURL.startAccessingSecurityScopedResource() + defer { if needsScope { sourceURL.stopAccessingSecurityScopedResource() } } + + // Validate in place before touching the file so a rejected import never + // deletes the user's original. + let size = (try? sourceURL.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0 + if size > Self.maxFileBytes { + throw ImportError.tooLarge(size) + } + if !validateAudio(at: sourceURL) { + throw ImportError.notAudio + } + if let duration = audioDuration(at: sourceURL), duration > Self.maxDurationSeconds { + throw ImportError.tooLong(duration) + } + + let newID = UUID() + let ext = sourceURL.pathExtension.isEmpty ? "audio" : sourceURL.pathExtension + let destURL = directory.appendingPathComponent("\(newID.uuidString).\(ext)") + + // If the picked file already lives in our shared Documents folder, move it + // so absorbDroppedFiles() won't re-ingest the leftover original as a + // duplicate. Otherwise copy, leaving the source untouched. + let documentsRoot = directory.deletingLastPathComponent().resolvingSymlinksInPath().path + let sourceInDocuments = sourceURL.resolvingSymlinksInPath().path.hasPrefix(documentsRoot + "/") + do { + if sourceInDocuments { + try fileManager.moveItem(at: sourceURL, to: destURL) + } else { + try fileManager.copyItem(at: sourceURL, to: destURL) + } + } catch { + throw ImportError.unreadable + } + + let displayName = sourceURL.deletingPathExtension().lastPathComponent + index[newID] = displayName.isEmpty ? "Custom Sound" : displayName + saveIndex() + return CustomSound(id: newID, displayName: index[newID]!, url: destURL) + } + } + + /// Delete a custom sound. Alarms referencing it will fall back to the default built-in. + func delete(_ id: UUID) { + queue.sync { + if let url = fileURL(for: id) { + try? fileManager.removeItem(at: url) + } + index.removeValue(forKey: id) + saveIndex() + } + } + + // MARK: - Internals + + private func fileURL(for id: UUID) -> URL? { + guard let contents = try? fileManager.contentsOfDirectory(atPath: directory.path) else { + return nil + } + let prefix = id.uuidString + "." + guard let match = contents.first(where: { $0.hasPrefix(prefix) }) else { return nil } + return directory.appendingPathComponent(match) + } + + private func loadIndex() { + guard + let data = try? Data(contentsOf: indexURL), + let decoded = try? JSONDecoder().decode([String: String].self, from: data) + else { + index = [:] + return + } + var result: [UUID: String] = [:] + for (key, value) in decoded { + if let uuid = UUID(uuidString: key) { + result[uuid] = value + } + } + index = result + } + + private func saveIndex() { + let encodable = Dictionary(uniqueKeysWithValues: index.map { ($0.key.uuidString, $0.value) }) + if let data = try? JSONEncoder().encode(encodable) { + try? data.write(to: indexURL, options: .atomic) + } + } + + /// Drop entries from the index that point to files that no longer exist on disk + /// (e.g. the user deleted them via the Files app). + private func pruneMissing() { + var didChange = false + for id in Array(index.keys) where fileURL(for: id) == nil { + index.removeValue(forKey: id) + didChange = true + } + if didChange { saveIndex() } + } + + /// Pick up audio files that were dropped into the Documents directory via the + /// Files app and move them into `CustomSounds/` with a fresh UUID. + /// + /// Each candidate is validated *in place* against the same size/duration/format + /// rules as `importFile(at:)` before being moved. A file that fails any check is + /// left untouched in Documents — we never move (and therefore never delete) a file + /// the user put there. + private func absorbDroppedFiles() { + let documents = directory.deletingLastPathComponent() + guard let entries = try? fileManager.contentsOfDirectory(at: documents, includingPropertiesForKeys: [.fileSizeKey]) else { + return + } + let audioExtensions: Set = ["mp3", "wav", "m4a", "aac", "aif", "aiff", "caf"] + var didChange = false + for entry in entries { + var isDir: ObjCBool = false + guard fileManager.fileExists(atPath: entry.path, isDirectory: &isDir), !isDir.boolValue else { continue } + guard audioExtensions.contains(entry.pathExtension.lowercased()) else { continue } + + let size = (try? entry.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0 + guard size <= Self.maxFileBytes, validateAudio(at: entry) else { continue } + if let duration = audioDuration(at: entry), duration > Self.maxDurationSeconds { continue } + + let newID = UUID() + let dest = directory.appendingPathComponent("\(newID.uuidString).\(entry.pathExtension)") + do { + try fileManager.moveItem(at: entry, to: dest) + } catch { + continue + } + let baseName = entry.deletingPathExtension().lastPathComponent + index[newID] = baseName.isEmpty ? "Custom Sound" : baseName + didChange = true + } + if didChange { saveIndex() } + } + + private func validateAudio(at url: URL) -> Bool { + return (try? AVAudioPlayer(contentsOf: url)) != nil + } + + private func audioDuration(at url: URL) -> TimeInterval? { + guard let player = try? AVAudioPlayer(contentsOf: url) else { return nil } + return player.duration + } +} diff --git a/LoopFollow/Alarm/SnoozeState.swift b/LoopFollow/Alarm/SnoozeState.swift deleted file mode 100644 index 900838dfc..000000000 --- a/LoopFollow/Alarm/SnoozeState.swift +++ /dev/null @@ -1,9 +0,0 @@ -// LoopFollow -// SnoozeState.swift - -import Foundation - -struct SnoozeState: Codable { - var isSnoozed: Bool = false - var snoozeUntil: Date? -} diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index 6c9d8e884..3c364fc2c 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -2,7 +2,6 @@ // AppDelegate.swift import AVFoundation -import EventKit import UIKit import UserNotifications @@ -13,19 +12,40 @@ class AppDelegate: UIResponder, UIApplicationDelegate { LogManager.shared.log(category: .general, message: "App started") LogManager.shared.cleanupOldLogs() - let options: UNAuthorizationOptions = [.alert, .sound, .badge] - notificationCenter.requestAuthorization(options: options) { - didAllow, _ in - if !didAllow { - LogManager.shared.log(category: .general, message: "User has declined notifications") - } - } - - let store = EKEventStore() - store.requestCalendarAccess { granted, error in - if !granted { - LogManager.shared.log(category: .calendar, message: "Failed to get calendar access: \(String(describing: error))") - return + // Notification and calendar permissions are no longer requested here. + // They're deferred to the moment the user opts into the feature that + // needs them (alarms request notifications via NotificationAuthorization; + // the Calendar settings screen requests calendar access), so a fresh + // install isn't fronted with permission prompts before onboarding. + + // Before-First-Unlock detection. isProtectedDataAvailable is false on ANY + // locked launch, so it alone isn't a BFU signal — post-first-unlock + // UserDefaults (class C) reads fine while locked. Only true BFU makes a key + // that should exist read as absent. Suspect only when ALL presence probes + // are absent, so an existing user who updated but hasn't foregrounded this + // build isn't misread on an ordinary locked launch. Probes cover every user + // shape (marker / migrated / consented / NS-configured); presence, not value. + _ = Storage.shared // ensure every StorageValue is registered before recovery + let storageConfirmedReadable = StorageReadiness.markerExists + || Storage.shared.migrationStep.exists + || Storage.shared.telemetryConsentDecisionMade.exists + || Storage.shared.url.exists + let suspectBFU = !UIApplication.shared.isProtectedDataAvailable && !storageConfirmedReadable + StorageReadiness.configure(suspectBFU: suspectBFU) + LogManager.shared.log(category: .general, message: "BFU check: isProtectedDataAvailable=\(UIApplication.shared.isProtectedDataAvailable), storageConfirmedReadable=\(storageConfirmedReadable), suspectBFU=\(suspectBFU)") + + if suspectBFU { + // Driven here, not MainViewController: on a BG-only launch (BGAppRefreshTask, + // BLE wake) the home VC may not exist yet. protectedDataDidBecomeAvailable is + // authoritative; willEnterForeground is a fallback. + let nc = NotificationCenter.default + nc.addObserver(self, selector: #selector(protectedDataDidBecomeAvailable), name: UIApplication.protectedDataDidBecomeAvailableNotification, object: nil) + nc.addObserver(self, selector: #selector(handleWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) + + // Race guard: protected data may have become available between the check + // above and the observer registration just now. + if UIApplication.shared.isProtectedDataAvailable { + completeStorageRecovery() } } @@ -35,7 +55,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UNUserNotificationCenter.current().delegate = self - _ = BLEManager.shared + // Only spin up Bluetooth if the user has chosen a BLE-based background + // refresh. Initializing BLEManager creates a CBCentralManager, which + // triggers the Bluetooth permission prompt — deferring it keeps that + // prompt off fresh installs until the feature is actually enabled. + if Storage.shared.backgroundRefreshType.value.isBluetooth { + _ = BLEManager.shared + } // Ensure VolumeButtonHandler is initialized so it can receive alarm notifications _ = VolumeButtonHandler.shared @@ -46,42 +72,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { BackgroundRefreshManager.shared.register() - // Telemetry: record this cold launch (used by the rolling - // coldLaunches7d signal). If the running build's SHA differs from - // the one we last sent for, fire an immediate ping — the scheduler - // alone can't notice an app update. Otherwise let the 24h scheduler - // handle cadence: its first run is lastSentAt + 24h, so a relaunch - // a few hours after the previous send simply waits out the - // remainder. See Helpers/Telemetry.swift. - TelemetryClient.shared.recordColdLaunch() - Task.detached { - if TelemetryClient.shared.buildShaChangedSinceLastSend() { - await TelemetryClient.shared.maybeSend() + // Telemetry mutates rolling history (coldLaunches7d), so defer past a BFU + // window — poisoned defaults would discard real history. Runs synchronously + // on a normal launch. + StorageReadiness.whenReady { + // SHA change fires an immediate ping (the scheduler can't notice an app + // update); otherwise the 24h scheduler handles cadence. See Telemetry.swift. + TelemetryClient.shared.recordColdLaunch() + Task.detached { + if TelemetryClient.shared.buildShaChangedSinceLastSend() { + await TelemetryClient.shared.maybeSend() + } + TelemetryClient.shared.scheduleRecurring() } - TelemetryClient.shared.scheduleRecurring() - } - - // Detect Before-First-Unlock launch. If protected data is unavailable here, - // StorageValues were cached from encrypted UserDefaults and need a reload - // once the device is unlocked. - let bfu = !UIApplication.shared.isProtectedDataAvailable - Storage.shared.needsBFUReload = bfu - LogManager.shared.log(category: .general, message: "BFU check: isProtectedDataAvailable=\(!bfu), needsBFUReload=\(bfu)") - - // Recovery is driven from AppDelegate (not MainViewController) because under - // the SwiftUI App lifecycle the home tab's UIHostingController is materialized - // lazily — on a BG-only launch (BGAppRefreshTask, BLE wake) MainViewController - // may not exist when the device is unlocked, and would miss willEnterForeground. - // protectedDataDidBecomeAvailable fires the moment file protection lifts and - // is the authoritative signal; willEnterForeground is a fallback. - let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(protectedDataDidBecomeAvailable), name: UIApplication.protectedDataDidBecomeAvailableNotification, object: nil) - nc.addObserver(self, selector: #selector(handleWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) - - // Race guard: protected data may have become available between the check - // above and the observer registration just now. - if Storage.shared.needsBFUReload, UIApplication.shared.isProtectedDataAvailable { - performBFUReloadIfNeeded() } return true @@ -90,19 +93,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - BFU recovery @objc private func protectedDataDidBecomeAvailable() { - performBFUReloadIfNeeded() + completeStorageRecovery() } @objc private func handleWillEnterForeground() { - performBFUReloadIfNeeded() + completeStorageRecovery() } - private func performBFUReloadIfNeeded() { - guard Storage.shared.needsBFUReload else { return } - Storage.shared.needsBFUReload = false - LogManager.shared.log(category: .general, message: "BFU reload triggered — reloading all StorageValues") - Storage.shared.reloadAll() - LogManager.shared.log(category: .general, message: "BFU reload complete: url='\(Storage.shared.url.value)'") + private func completeStorageRecovery() { + // recover() hydrates every value and opens the gate; true only on the one + // transition that did the work, so the notification fires exactly once. + guard StorageReadiness.recover() else { return } + LogManager.shared.log(category: .general, message: "BFU recovery complete: url='\(Storage.shared.url.value)'") NotificationCenter.default.post(name: .bfuReloadCompleted, object: nil) } @@ -207,7 +209,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { extension Notification.Name { /// Posted by AppDelegate after a Before-First-Unlock recovery completes - /// (Storage.reloadAll has run with the now-decrypted UserDefaults). + /// (StorageReadiness.recover has hydrated every value from the now-decrypted + /// UserDefaults). static let bfuReloadCompleted = Notification.Name("LoopFollow.bfuReloadCompleted") } diff --git a/LoopFollow/Application/LoopFollowApp.swift b/LoopFollow/Application/LoopFollowApp.swift index ee5e2f0fc..4725a12bf 100644 --- a/LoopFollow/Application/LoopFollowApp.swift +++ b/LoopFollow/Application/LoopFollowApp.swift @@ -6,18 +6,27 @@ import SwiftUI @main struct LoopFollowApp: App { @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate + @ObservedObject private var storageReady = StorageReadiness.ready var body: some Scene { WindowGroup { - MainTabView() - .onOpenURL { url in - guard url.scheme == AppGroupID.urlScheme, url.host == "la-tap" else { return } - #if !targetEnvironment(macCatalyst) - DispatchQueue.main.async { - NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil) - } - #endif - } + // Gate the UI on storage readiness so nothing (bootstrap, telemetry + // consent, onboarding) is built against a poisoned cache. True + // synchronously on a normal launch; only false briefly while a BFU + // background launch is foregrounded mid-hydration. + if storageReady.value { + MainTabView() + .onOpenURL { url in + guard url.scheme == AppGroupID.urlScheme, url.host == "la-tap" else { return } + #if !targetEnvironment(macCatalyst) + DispatchQueue.main.async { + NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil) + } + #endif + } + } else { + StorageLoadingView() + } } } } diff --git a/LoopFollow/Application/MainTabView.swift b/LoopFollow/Application/MainTabView.swift index 140204358..cd7bb7a40 100644 --- a/LoopFollow/Application/MainTabView.swift +++ b/LoopFollow/Application/MainTabView.swift @@ -13,14 +13,31 @@ struct MainTabView: View { @ObservedObject private var snoozerPosition = Storage.shared.snoozerPosition @ObservedObject private var statisticsPosition = Storage.shared.statisticsPosition @ObservedObject private var treatmentsPosition = Storage.shared.treatmentsPosition + @ObservedObject private var activeBanner = Observable.shared.activeBanner @State private var showTelemetryConsent = false + @State private var showOnboarding = false private var orderedItems: [TabItem] { Storage.shared.orderedTabBarItems() } var body: some View { + // The banner sits in a VStack above the TabView (rather than a + // .safeAreaInset) so the UIKit-hosted Home content is physically + // pushed down too — safe-area changes don't propagate into + // UIViewControllerRepresentable. + VStack(spacing: 0) { + if let message = activeBanner.value { + AppBannerView(message: message) + .transition(.move(edge: .top).combined(with: .opacity)) + } + tabView + } + .animation(.easeInOut(duration: 0.25), value: activeBanner.value) + } + + private var tabView: some View { TabView(selection: $selectedTab.value) { ForEach(Array(orderedItems.prefix(4).enumerated()), id: \.element) { index, item in tabContent(for: item) @@ -47,21 +64,61 @@ struct MainTabView: View { // onAppear (not app launch) keeps it off the BG-only refresh path. MainViewController.bootstrap() - // One-time consent prompt. Previously presented by SceneDelegate, - // which was removed in the storyboard→SwiftUI migration; without - // this, fresh installs stay permanently undecided and telemetry - // never sends. The storage flag keeps it to a single appearance. - if !Storage.shared.telemetryConsentDecisionMade.value { - showTelemetryConsent = true + // Show the first-run onboarding once for everyone. Returning users + // get a prominent Skip on the welcome screen. The telemetry consent + // prompt is deferred until onboarding is dismissed so the two never + // appear on top of one another. + if !Storage.shared.hasCompletedOnboarding.value { + Observable.shared.isOnboardingActive.value = true + showOnboarding = true + } else { + runPostOnboardingPrompts() } } - .sheet(isPresented: $showTelemetryConsent) { + .fullScreenCover(isPresented: $showOnboarding, onDismiss: { + Observable.shared.isOnboardingActive.value = false + + // Covers both finishing and skipping onboarding — the telemetry and + // notification steps live inside the flow, so anyone who skips still + // needs these handled here. + runPostOnboardingPrompts() + }) { + OnboardingContainerView(onClose: { showOnboarding = false }) + } + .sheet(isPresented: $showTelemetryConsent, onDismiss: { + // Ask for notifications only once telemetry is resolved, so the system + // prompt never stacks on top of the consent sheet. + requestNotificationsIfAlarmsEnabled() + }) { // User must explicitly choose — no swipe-to-dismiss. TelemetryConsentView() .interactiveDismissDisabled(true) } } + /// Runs after onboarding closes, whether it was completed or skipped. Telemetry + /// consent and notification permission both live inside the onboarding flow, so + /// a skip would otherwise bypass them. Telemetry consent goes first (as a + /// sheet); the notification request follows on its dismissal so the two never + /// appear at once. When the user completed the flow these are already decided, + /// so both calls are no-ops. + private func runPostOnboardingPrompts() { + if !Storage.shared.telemetryConsentDecisionMade.value { + showTelemetryConsent = true // notifications requested on its dismiss + } else { + requestNotificationsIfAlarmsEnabled() + } + } + + /// Deferred-permission policy: only ask for notifications once there's an + /// enabled alarm that needs them. Safe to call repeatedly — it's a no-op once + /// the status is determined. + private func requestNotificationsIfAlarmsEnabled() { + if Storage.shared.alarms.value.contains(where: { $0.isEnabled }) { + NotificationAuthorization.requestIfNeeded() + } + } + @ViewBuilder private func tabContent(for item: TabItem) -> some View { switch item { diff --git a/LoopFollow/Application/StorageLoadingView.swift b/LoopFollow/Application/StorageLoadingView.swift new file mode 100644 index 000000000..38d48d864 --- /dev/null +++ b/LoopFollow/Application/StorageLoadingView.swift @@ -0,0 +1,19 @@ +// LoopFollow +// StorageLoadingView.swift + +import SwiftUI + +/// Shown at the root until `StorageReadiness.ready` — only while a BFU background +/// launch is foregrounded mid-hydration; a normal launch opens straight to +/// `MainTabView`. Must not read `Storage` (values are poisoned here) — system +/// colors only. +struct StorageLoadingView: View { + var body: some View { + ZStack { + Color(.systemBackground) + .ignoresSafeArea() + ProgressView() + .controlSize(.large) + } + } +} diff --git a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift index c4a39874c..e16b344de 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift @@ -8,25 +8,63 @@ import Foundation class BLEManager: NSObject, ObservableObject { static let shared = BLEManager() + /// Whether the shared instance has been created (and therefore a + /// CBCentralManager exists / the Bluetooth prompt has been triggered). + /// Reading this does not instantiate `shared`, so callers can avoid forcing + /// Bluetooth initialization — and its permission prompt — when not needed. + private(set) static var isInitialized = false + @Published private(set) var devices: [BLEDevice] = [] private var centralManager: CBCentralManager! private var activeDevice: BluetoothDevice? + private var readinessCancellable: AnyCancellable? + + /// Arrival times of recent heartbeat dropouts. Main-queue-confined + /// (centralManager delivers on .main) and deliberately not persisted: + /// stale history must not resurrect the banner after a relaunch. + private var heartbeatDropoutTimes: [Date] = [] + + /// A beat this much later than expected counts as one dropout event. + /// Stricter than the 15% logging margin - normal jitter must not count. + private let dropoutFactor = 1.5 + + /// Sliding window over which dropout events are counted. + private let dropoutWindow: TimeInterval = 60 * 60 + + /// Dropout events within the window needed to raise the banner. + private let dropoutTriggerCount = 5 override private init() { super.init() + BLEManager.isInitialized = true centralManager = CBCentralManager( delegate: self, queue: .main ) - if let device = Storage.shared.selectedBLEDevice.value { - devices.append(device) - findAndUpdateDevice(with: device.id.uuidString) { device in - device.rssi = 0 + connectSelectedDeviceIfNeeded() + + // After BFU, selectedBLEDevice reads nil until hydration — reconnect when + // storage becomes ready. dropFirst skips the current value (init handled the + // already-ready case), so this fires only on the false→true recovery. + readinessCancellable = StorageReadiness.ready.$value + .dropFirst() + .filter { $0 } + .sink { [weak self] _ in + self?.connectSelectedDeviceIfNeeded() } - connect(device: device) + } + + private func connectSelectedDeviceIfNeeded() { + guard activeDevice == nil, let device = Storage.shared.selectedBLEDevice.value else { return } + if !devices.contains(where: { $0.id == device.id }) { + devices.append(device) + } + findAndUpdateDevice(with: device.id.uuidString) { device in + device.rssi = 0 } + connect(device: device) } func getSelectedDevice() -> BLEDevice? { @@ -49,6 +87,8 @@ class BLEManager: NSObject, ObservableObject { activeDevice = nil device.lastHeartbeatTime = nil } + heartbeatDropoutTimes.removeAll() + BannerManager.shared.clear(.heartbeat) Storage.shared.selectedBLEDevice.value = nil } @@ -208,6 +248,7 @@ extension BLEManager: BluetoothDeviceDelegate { let delay = elapsedTime - expectedInterval LogManager.shared.log(category: .bluetooth, message: "Heartbeat triggered (Delayed by \(String(format: "%.1f", delay)) seconds)") } + recordHeartbeatOutcome(elapsed: elapsedTime, expectedInterval: expectedInterval, now: now) } else { LogManager.shared.log(category: .bluetooth, message: "Heartbeat triggered (First heartbeat)") } @@ -216,6 +257,55 @@ extension BLEManager: BluetoothDeviceDelegate { TaskScheduler.shared.checkTasksNow() } + + /// Counts late heartbeats over a sliding window and raises a banner when + /// dropouts become frequent - the typical symptom of a dying transmitter + /// battery. Detection is arrival-based on purpose: a struggling battery + /// still delivers (late) beats, whereas total silence usually means + /// out-of-range or a dead device and is already surfaced by the + /// connection status in Background Refresh settings and by BG alarms. + private func recordHeartbeatOutcome(elapsed: TimeInterval, expectedInterval: TimeInterval, now: Date) { + heartbeatDropoutTimes.removeAll { now.timeIntervalSince($0) > dropoutWindow } + + // A gap this large means out-of-range, Bluetooth off or a suspended + // app - not a struggling battery. Discard the history collected + // before the blind spot instead of counting it. + let resetGap = max(6 * expectedInterval, 30 * 60) + if elapsed > resetGap { + if !heartbeatDropoutTimes.isEmpty { + heartbeatDropoutTimes.removeAll() + LogManager.shared.log(category: .bluetooth, message: "Heartbeat gap of \(Int(elapsed)) seconds, resetting dropout history") + } + } else if elapsed > expectedInterval * dropoutFactor { + heartbeatDropoutTimes.append(now) + LogManager.shared.log(category: .bluetooth, message: "Heartbeat dropout recorded (\(heartbeatDropoutTimes.count) in the last hour)") + } + + // Hysteresis: raise at the trigger count, clear only once the window + // is completely clean, so the banner doesn't flap at the boundary. + if heartbeatDropoutTimes.count >= dropoutTriggerCount { + BannerManager.shared.report(source: .heartbeat, severity: .warning, text: heartbeatDropoutBannerText()) + } else if heartbeatDropoutTimes.isEmpty { + BannerManager.shared.clear(.heartbeat) + } + } + + /// The text must stay stable while the condition persists (no counts or + /// durations) so BannerManager's dedupe keeps the banner from + /// re-animating on every beat and user dismissal keeps working. + private func heartbeatDropoutBannerText() -> String { + let name = activeDevice?.deviceName ?? "heartbeat device" + let cause: String + switch Storage.shared.backgroundRefreshType.value { + case .rileyLink: + cause = "a low RileyLink battery" + case .omnipodDash: + cause = "a low pod battery" + default: + cause = "a low transmitter battery" + } + return "Heartbeat: repeated Bluetooth dropouts from \(name). This can be a sign of \(cause) or a weak Bluetooth connection." + } } extension BLEManager { diff --git a/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift index f4db899f0..55ef8f683 100644 --- a/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift @@ -4,7 +4,6 @@ import CoreBluetooth import Foundation import os -import UIKit class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { weak var bluetoothDeviceDelegate: BluetoothDeviceDelegate? diff --git a/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift b/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift deleted file mode 100644 index 7559f478e..000000000 --- a/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift +++ /dev/null @@ -1,31 +0,0 @@ -// LoopFollow -// DexcomG7HeartBeat.swift - -// Denna behövs - -import Foundation - -/// A simple class to represent the Dexcom G7 Heartbeat. -/// It wraps around a `BLEPeripheral` to store relevant information. -public class DexcomG7HeartBeat { - // MARK: - Properties - - /// The BLEPeripheral instance associated with this heartbeat. - public let blePeripheral: BLEPeripheral - - // MARK: - Initialization - - /// Initializes a new DexcomG7HeartBeat instance. - /// - Parameters: - /// - address: The unique address of the BLE device. - /// - name: The name of the BLE device. - /// - alias: An optional alias for the device. - public init(address: String, name: String, alias: String? = nil) { - blePeripheral = BLEPeripheral( - address: address, - name: name, - alias: alias, - peripheralType: .DexcomG7HeartBeatType - ) - } -} diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift index 21cbdf43f..1bf23fe0c 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift @@ -33,6 +33,12 @@ class BackgroundRefreshSettingsViewModel: ObservableObject { private func handleBackgroundRefreshTypeChange(_ newValue: BackgroundRefreshType) { LogManager.shared.log(category: .general, message: "Background refresh type changed to: \(newValue.rawValue)") - BLEManager.shared.disconnect() + // Touch BLEManager only when switching to a Bluetooth mode (the user is + // opting in, so the permission prompt belongs here) or when it's already + // running and needs to be torn down. Switching between non-BLE modes must + // not initialize Bluetooth — that would prompt without cause. + if newValue.isBluetooth || BLEManager.isInitialized { + BLEManager.shared.disconnect() + } } } diff --git a/LoopFollow/Banner/AppBannerView.swift b/LoopFollow/Banner/AppBannerView.swift new file mode 100644 index 000000000..56d196909 --- /dev/null +++ b/LoopFollow/Banner/AppBannerView.swift @@ -0,0 +1,75 @@ +// LoopFollow +// AppBannerView.swift + +import SwiftUI + +/// Dismissable banner shown at the top of the app, above the tab content. +struct AppBannerView: View { + let message: BannerMessage + + var body: some View { + HStack(alignment: .top, spacing: 12) { + Image(systemName: iconName) + .font(.title3) + .foregroundColor(iconColor) + + Text(message.text) + .font(.callout) + .lineLimit(3) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + + Button { + BannerManager.shared.dismissCurrent() + } label: { + Image(systemName: "xmark.circle.fill") + .font(.title3) + .foregroundColor(.secondary) + } + .accessibilityLabel("Dismiss") + } + .padding() + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(tint) + ) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(border, lineWidth: 1) + ) + .padding(.horizontal, 10) + .padding(.vertical, 4) + } + + private var iconName: String { + switch message.severity { + case .info: return "info.circle.fill" + case .warning: return "exclamationmark.triangle.fill" + case .error: return "xmark.octagon.fill" + } + } + + private var iconColor: Color { + switch message.severity { + case .info: return .blue + case .warning: return .orange + case .error: return .red + } + } + + private var tint: Color { + switch message.severity { + case .info: return Color.blue.opacity(0.20) + case .warning: return Color.orange.opacity(0.20) + case .error: return Color.red.opacity(0.20) + } + } + + private var border: Color { + switch message.severity { + case .info: return Color.blue.opacity(0.40) + case .warning: return Color.orange.opacity(0.40) + case .error: return Color.red.opacity(0.40) + } + } +} diff --git a/LoopFollow/Banner/BannerManager.swift b/LoopFollow/Banner/BannerManager.swift new file mode 100644 index 000000000..bfefb0e2c --- /dev/null +++ b/LoopFollow/Banner/BannerManager.swift @@ -0,0 +1,172 @@ +// LoopFollow +// BannerManager.swift + +import Foundation +import ShareClient + +/// Identifies who produced a banner message. Each source owns at most one +/// message at a time; reporting a new message for a source replaces its old one. +enum BannerSource: Hashable { + case nightscout + case dexcom + case heartbeat + case custom(String) +} + +enum BannerSeverity: Int, Comparable { + case info + case warning + case error + + static func < (lhs: BannerSeverity, rhs: BannerSeverity) -> Bool { + lhs.rawValue < rhs.rawValue + } +} + +struct BannerMessage: Equatable, Identifiable { + let id: UUID + let source: BannerSource + let severity: BannerSeverity + let text: String + let timestamp: Date +} + +/// App-wide banner state. Producers call `report`/`clear`; the view layer +/// observes `Observable.shared.activeBanner` and calls `dismissCurrent()`. +final class BannerManager { + static let shared = BannerManager() + + /// How long a user dismissal suppresses a still-occurring error before it re-appears. + static let dismissCooldown: TimeInterval = 30 * 60 + + /// Minimum time between Nightscout diagnostic probes (status.json calls). + private static let diagnosticInterval: TimeInterval = 5 * 60 + + // All state is read and mutated on the main queue only. + private var messages: [BannerSource: BannerMessage] = [:] + private var dismissals: [BannerSource: (until: Date, text: String)] = [:] + private var lastNightscoutDiagnostic: Date? + + private init() {} + + func report(source: BannerSource, severity: BannerSeverity = .error, text: String) { + DispatchQueue.main.async { + if let existing = self.messages[source], existing.text == text, existing.severity == severity { + // Same problem re-reported (fetches retry every 10-60 s): keep the + // message untouched so the banner doesn't re-animate, but publish in + // case a dismissal cooldown has expired since the last report. + } else { + // A different problem: show it even if the previous one was dismissed. + self.dismissals[source] = nil + self.messages[source] = BannerMessage( + id: UUID(), + source: source, + severity: severity, + text: text, + timestamp: Date() + ) + } + self.publish() + } + } + + func clear(_ source: BannerSource) { + DispatchQueue.main.async { + guard self.messages[source] != nil || self.dismissals[source] != nil else { return } + self.messages[source] = nil + self.dismissals[source] = nil + self.publish() + } + } + + func dismissCurrent() { + DispatchQueue.main.async { + guard let current = Observable.shared.activeBanner.value else { return } + self.dismissals[current.source] = (Date().addingTimeInterval(Self.dismissCooldown), current.text) + self.publish() + } + } + + /// Classifies a failed Nightscout fetch by probing status.json, so the banner + /// can say *why* it failed (invalid token, site not found, no network, ...) + /// instead of showing a generic decode/transport error. + func reportNightscoutFetchFailure(_ underlying: Error) { + DispatchQueue.main.async { + if let last = self.lastNightscoutDiagnostic, Date().timeIntervalSince(last) < Self.diagnosticInterval { + return + } + self.lastNightscoutDiagnostic = Date() + + NightscoutUtils.verifyURLAndToken { error, _, _, _ in + if let error = error { + if case .emptyAddress = error { + // URL was removed while a fetch was in flight — not an error state. + self.clear(.nightscout) + return + } + self.report( + source: .nightscout, + severity: .error, + text: "Nightscout: \(error.localizedDescription)" + ) + } else { + // Server reachable and token accepted, yet the data fetch failed. + LogManager.shared.log( + category: .nightscout, + message: "Nightscout diagnostic OK but data fetch failed: \(underlying)", + limitIdentifier: "Nightscout diagnostic OK but data fetch failed" + ) + self.report( + source: .nightscout, + severity: .warning, + text: "Nightscout: data download failed, but the server is reachable. Retrying automatically." + ) + } + } + } + } + + func reportDexcomFailure(_ error: ShareError, nightscoutFallback: Bool) { + var text: String + switch error { + case let .loginError(errorCode): + // Dexcom has returned both legacy "SSO_Authenticate…" codes and newer + // ones like "AccountPasswordInvalid" — match on the common substrings. + if errorCode.contains("AccountNotFound") { + text = "Dexcom Share: account not found — check your username." + } else if errorCode.contains("PasswordInvalid") { + text = "Dexcom Share: incorrect username or password." + } else if errorCode.contains("MaxAttempts") { + text = "Dexcom Share: too many failed login attempts — temporarily locked out." + } else { + text = "Dexcom Share: login failed (\(errorCode))." + } + case .httpError: + text = "Dexcom Share: network error while downloading." + case .fetchError, .dataError, .dateError: + text = "Dexcom Share: could not download readings." + } + + if nightscoutFallback { + text += " Using Nightscout as backup." + } + report(source: .dexcom, severity: nightscoutFallback ? .warning : .error, text: text) + } + + /// Pushes the highest-priority non-dismissed message to the UI. + private func publish() { + let now = Date() + let candidate = messages.values + .filter { message in + guard let dismissal = dismissals[message.source] else { return true } + return now >= dismissal.until || dismissal.text != message.text + } + .max { lhs, rhs in + (lhs.severity, lhs.timestamp) < (rhs.severity, rhs.timestamp) + } + + if Observable.shared.activeBanner.value != candidate { + Observable.shared.activeBanner.set(candidate) + } + } +} diff --git a/LoopFollow/Charts/BGChartModel.swift b/LoopFollow/Charts/BGChartModel.swift new file mode 100644 index 000000000..a08bab90f --- /dev/null +++ b/LoopFollow/Charts/BGChartModel.swift @@ -0,0 +1,599 @@ +// LoopFollow +// BGChartModel.swift + +import Foundation +import SwiftUI + +/// Interaction state shared between the main chart (which owns the gestures) +/// and the small overview chart (which shows a viewport box and navigates on +/// tap). Kept separate from BGChartModel so high-frequency pan/zoom writes +/// don't invalidate views that only observe the data. +final class BGChartInteraction: ObservableObject { + /// Date at the leading (left) edge of the main chart's visible window. + @Published var scrollPosition: Date + /// Length of the visible x-axis window in seconds. + @Published var visibleSeconds: TimeInterval + /// True while the main chart should keep auto-scrolling to "now"; cleared + /// when the user pans back into history, re-armed when they return to the edge. + @Published var followLatest: Bool = true + + init() { + let seconds = Self.visibleSeconds(forScale: Storage.shared.chartScaleX.value) + visibleSeconds = seconds + scrollPosition = Date().addingTimeInterval(-seconds * 0.7) + } + + /// Maps the persisted zoom scale (24 h divided by the scale factor) to a + /// visible-window length. + static func visibleSeconds(forScale scale: Double) -> TimeInterval { + guard scale > 0 else { return 6 * 3600 } + return min(max(24 * 3600 / scale, 15 * 60), 24 * 3600) + } + + /// Persists the current zoom back into the stored scale representation. + func persistZoom() { + Storage.shared.chartScaleX.value = 24 * 3600 / visibleSeconds + } +} + +final class BGChartModel: ObservableObject { + struct BGPoint: Identifiable { + let date: Date + let value: Double + let color: Color + var id: Double { date.timeIntervalSince1970 } + } + + struct TreatmentPoint: Identifiable { + let date: Date + let value: Double + let sgv: Double + let label: String + let pillText: String + /// Where the symbol is drawn. Equals `date` unless `spread` nudged it + /// left to keep a crowded run of treatments from stacking up. + var drawnDate: Date + var id: Double { date.timeIntervalSince1970 } + + init(date: Date, value: Double, sgv: Double, label: String, pillText: String) { + self.date = date + self.value = value + self.sgv = sgv + self.label = label + self.pillText = pillText + drawnDate = date + } + } + + struct BasalStep: Identifiable { + let start: Date + let end: Date + let rate: Double + var id: TimeInterval { start.timeIntervalSince1970 } + } + + struct ScheduledBasalPoint: Identifiable { + let date: Date + let rate: Double + var id: TimeInterval { date.timeIntervalSince1970 } + } + + struct BandRect: Identifiable { + let start: Date + let end: Date + let yBottom: Double + let yTop: Double + let label: String + let pillText: String + var id: String { "\(start.timeIntervalSince1970)-\(end.timeIntervalSince1970)" } + } + + struct ConePoint: Identifiable { + let date: Date + let yMin: Double + let yMax: Double + var id: Double { date.timeIntervalSince1970 } + } + + /// A maximal stretch of consecutive BG readings sharing one range color. + /// Each run renders as a single LineMark series; runs share their boundary + /// point so the line stays visually continuous across color changes. + /// (One series per run — tens per day — instead of one per segment, which + /// was a Swift Charts layout hotspot at hundreds of series.) + struct BGRun: Identifiable { + let id: Int + let color: Color + let points: [BGPoint] + } + + @Published var bg: [BGPoint] = [] + @Published var bgRuns: [BGRun] = [] + @Published var yesterday: [BGPoint] = [] + @Published var prediction: [BGPoint] = [] + @Published var ztPrediction: [BGPoint] = [] + @Published var iobPrediction: [BGPoint] = [] + @Published var cobPrediction: [BGPoint] = [] + @Published var uamPrediction: [BGPoint] = [] + + /// Prediction cone band (min/max envelope). Set by updateOpenAPSPredictionDisplay; + /// preserved across rebuild() since it has no source array on the view controller. + /// The didSet keeps the canvas generation in sync for call sites that assign the + /// cone directly without triggering a rebuild. + @Published var cone: [ConePoint] = [] { + didSet { generation &+= 1 } + } + + @Published var basal: [BasalStep] = [] + @Published var basalScheduled: [ScheduledBasalPoint] = [] + + @Published var boluses: [TreatmentPoint] = [] + @Published var carbs: [TreatmentPoint] = [] + @Published var smbs: [TreatmentPoint] = [] + @Published var bgChecks: [TreatmentPoint] = [] + @Published var suspends: [TreatmentPoint] = [] + @Published var resumes: [TreatmentPoint] = [] + @Published var sensorStarts: [TreatmentPoint] = [] + @Published var notes: [TreatmentPoint] = [] + + @Published var overrides: [BandRect] = [] + @Published var tempTargets: [BandRect] = [] + + // Backend-aware band colors: Loop draws overrides green / temp targets purple, + // Trio (and other OpenAPS backends) use the inverse. Mirrors TreatmentGraphColors. + @Published var overrideColor: Color = .green + @Published var tempTargetColor: Color = .purple + + @Published var maxBG: Double = 250 + @Published var maxBasal: Double = 5 + @Published var lowLine: Double = 70 + @Published var highLine: Double = 180 + @Published var domainStart: Date = .init(timeIntervalSince1970: 0) + @Published var domainEnd: Date = .init(timeIntervalSince1970: 0) + + @Published var now: Date = .init() + @Published var diaMarkers: [Date] = [] + @Published var midnightMarkers: [Date] = [] + @Published var thirtyMinMark: Date? = nil + @Published var ninetyMinMark: Date? = nil + + /// Shared pan/zoom/follow state (see BGChartInteraction). A separate object so + /// per-frame gesture writes don't invalidate views that only observe the data. + let interaction = BGChartInteraction() + + /// Monotonic data version. Bumped whenever chart data changes; the chart + /// canvases use it (instead of comparing arrays) to decide whether a + /// re-layout is needed, so panning — which changes none of the data — can + /// provably skip their bodies. + private(set) var generation: Int = 0 + + private var rebuildScheduled = false + + @Published var showLines: Bool = true + @Published var showDots: Bool = true + @Published var showDIA: Bool = true + @Published var show30Min: Bool = false + @Published var show90Min: Bool = false + @Published var showMidnight: Bool = false + @Published var smallGraphTreatments: Bool = true + + private static let doseFormatter: NumberFormatter = { + let nf = NumberFormatter() + nf.locale = .current + nf.numberStyle = .decimal + nf.usesGroupingSeparator = false + nf.minimumIntegerDigits = 0 + nf.minimumFractionDigits = 0 + nf.maximumFractionDigits = 2 + return nf + }() + + private func formatDose(_ value: Double) -> String { + Self.doseFormatter.string(from: NSNumber(value: value)) ?? String(value) + } + + /// Formatter for the time line at the bottom of every selection pill. + /// Recreated on each rebuild so 12/24-hour and graph-time-zone settings apply. + private var pillTimeFormatter = BGChartModel.makePillTimeFormatter() + + private static func makePillTimeFormatter() -> DateFormatter { + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate(dateTimeUtils.is24Hour() ? "HH:mm" : "hh:mm") + if Storage.shared.graphTimeZoneEnabled.value, + let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) + { + df.timeZone = tz + } + return df + } + + /// The pill's time line for a given point in time. + func pillTimeString(for date: Date) -> String { + pillTimeFormatter.string(from: date) + } + + /// Nightscout remote-command error notes embed a JSON payload after + /// the human-readable message ("Error text {\"bolus-entry\": 1.5, ...}"). + /// Returns the message plus a compact summary of the payload, or nil when + /// the note contains no JSON. + private static func extractMessage(from note: String) -> String? { + guard let jsonStartIndex = note.range(of: "{\"")?.lowerBound else { + return nil + } + + let errorMessage = String(note[.. Color { + if Double(sgv) >= thresholds.high { + return .yellow + } else if Double(sgv) <= thresholds.low { + return .red + } else { + return .green + } + } + + /// Groups consecutive same-colored readings into line runs (see BGRun). + /// The segment between two points takes the color of the earlier point. + private static func makeRuns(_ points: [BGPoint]) -> [BGRun] { + guard let first = points.first else { return [] } + var runs: [BGRun] = [] + var runColor = first.color + var runPoints: [BGPoint] = [first] + for pt in points.dropFirst() { + runPoints.append(pt) + if pt.color != runColor { + runs.append(BGRun(id: runs.count, color: runColor, points: runPoints)) + runPoints = [pt] + runColor = pt.color + } + } + if runPoints.count > 1 { + runs.append(BGRun(id: runs.count, color: runColor, points: runPoints)) + } + return runs + } + + /// Minimum drawn spacing between two treatments of the same population, and + /// the furthest a treatment may be moved from its true time to reach it. + /// Boluses and SMBs share a y-anchor and symbol footprint, so they are + /// decluttered as one population; carbs carry a wider "30 3h" label, so + /// they need — and are allowed — more room. + private enum Spread { + static let bolusGap: TimeInterval = 240 + static let bolusShift: TimeInterval = 240 + static let carbGap: TimeInterval = 250 + static let carbShift: TimeInterval = 250 + } + + /// Nudges crowded treatments left so their symbols don't stack. The newest + /// point in a run keeps its true time and earlier ones give way, never by + /// more than `maxShift` — so a treatment is at most `maxShift` from where it + /// really happened, and an isolated one never moves at all. + private static func spread(_ points: [TreatmentPoint], minGap: TimeInterval, maxShift: TimeInterval) -> [TreatmentPoint] { + var out = points.sorted { $0.date < $1.date } + spreadSorted(&out, minGap: minGap, maxShift: maxShift) + return out + } + + /// Spreads two treatment kinds as a single population — a bolus dot and an + /// SMB triangle drawn at the same minute overlap just like two dots would — + /// then hands each kind back its own points. + private static func spreadTogether(_ first: [TreatmentPoint], _ second: [TreatmentPoint], minGap: TimeInterval, maxShift: TimeInterval) -> ([TreatmentPoint], [TreatmentPoint]) { + let tagged = (first.map { (isFirst: true, point: $0) } + second.map { (isFirst: false, point: $0) }) + .sorted { $0.point.date < $1.point.date } + var points = tagged.map(\.point) + spreadSorted(&points, minGap: minGap, maxShift: maxShift) + var outFirst: [TreatmentPoint] = [] + var outSecond: [TreatmentPoint] = [] + for (tag, point) in zip(tagged, points) { + if tag.isFirst { + outFirst.append(point) + } else { + outSecond.append(point) + } + } + return (outFirst, outSecond) + } + + /// `points` must be sorted ascending by `date`. + private static func spreadSorted(_ out: inout [TreatmentPoint], minGap: TimeInterval, maxShift: TimeInterval) { + guard out.count > 1 else { return } + + // Walking left from the newest point, each point yields to its right + // neighbor until it hits its own left bound (`date - maxShift`). + var clamped = [Bool](repeating: false, count: out.count) + for i in stride(from: out.count - 2, through: 0, by: -1) { + let wanted = out[i + 1].drawnDate.addingTimeInterval(-minGap) + guard out[i].drawnDate > wanted else { continue } + let leftBound = out[i].date.addingTimeInterval(-maxShift) + if wanted <= leftBound { + out[i].drawnDate = leftBound + clamped[i] = true + } else { + out[i].drawnDate = wanted + } + } + + // A chain of clamped points was squeezed against its left bound and may + // have piled up there (several same-time treatments all land at + // date - maxShift). Re-space each chain evenly between that bound and + // the first point to its right that still had room. + var i = 0 + while i < out.count - 1 { + guard clamped[i] else { + i += 1 + continue + } + var last = i + while clamped[last + 1] { + last += 1 + } + let anchorIndex = last + 1 + let anchor = out[anchorIndex].drawnDate + let leftBound = out[i].date.addingTimeInterval(-maxShift) + let spacing = anchor.timeIntervalSince(leftBound) / Double(anchorIndex - i) + for k in i ... last { + let ideal = anchor.addingTimeInterval(-spacing * Double(anchorIndex - k)) + let boundK = out[k].date.addingTimeInterval(-maxShift) + out[k].drawnDate = min(max(ideal, boundK), out[k].date) + } + i = anchorIndex + 1 + } + } + + /// Schedules a rebuild, coalescing bursts: a refresh cycle calls a dozen + /// update*Graph() entry points back-to-back, and rebuilding once per + /// runloop turn is enough. + func rebuild() { + guard !rebuildScheduled else { + return + } + rebuildScheduled = true + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.rebuildScheduled = false + self.performRebuild() + } + } + + private func performRebuild() { + guard let vc = MainViewController.shared else { return } + + pillTimeFormatter = Self.makePillTimeFormatter() + + let maxBGValue = Double(vc.calculateMaxBgGraphValue()) + maxBG = max(maxBGValue, Storage.shared.minBGScale.value) + + // Same thresholds the stats and main header use: fixed 70–180 / 70–140 + // for the TIR/TITR range modes, the user's lines for custom mode. + let thresholds = UnitSettingsStore.shared.effectiveThresholds() + lowLine = thresholds.low + highLine = thresholds.high + + showLines = Storage.shared.showLines.value + showDots = Storage.shared.showDots.value + showDIA = Storage.shared.showDIALines.value + show30Min = Storage.shared.show30MinLine.value + show90Min = Storage.shared.show90MinLine.value + showMidnight = Storage.shared.showMidnightLines.value + smallGraphTreatments = Storage.shared.smallGraphTreatments.value + + let isLoop = Storage.shared.device.value == "Loop" + overrideColor = isLoop ? .green : .purple + tempTargetColor = isLoop ? .purple : .green + + // Clamp plotted BG to the display range (see #600); color is still + // keyed off the true reading. + let minDisplay = globalVariables.minDisplayGlucose + let maxDisplay = globalVariables.maxDisplayGlucose + func clampSgv(_ sgv: Int) -> Double { Double(min(max(sgv, minDisplay), maxDisplay)) } + + bg = vc.bgData.map { BGPoint(date: Date(timeIntervalSince1970: $0.date), value: clampSgv($0.sgv), color: colorFor($0.sgv, thresholds: thresholds)) } + bgRuns = Self.makeRuns(bg) + + // Yesterday comparison overlay (#665): already +24h shifted, dimmed gray, no dots. + if Storage.shared.showYesterdayLine.value { + yesterday = vc.yesterdayBGData.map { + BGPoint(date: Date(timeIntervalSince1970: $0.date), value: clampSgv($0.sgv), color: Color(.systemGray).opacity(0.4)) + } + } else { + yesterday = [] + } + + prediction = vc.predictionData.map { BGPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), color: .purple) } + ztPrediction = vc.ztPredictionData.map { BGPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), color: .purple) } + iobPrediction = vc.iobPredictionData.map { BGPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), color: .purple) } + cobPrediction = vc.cobPredictionData.map { BGPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), color: .purple) } + uamPrediction = vc.uamPredictionData.map { BGPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), color: .purple) } + + let bolusPoints = vc.bolusData.map { + let dose = self.formatDose($0.value) + return TreatmentPoint( + date: Date(timeIntervalSince1970: $0.date), + value: $0.value, + sgv: Double($0.sgv), + label: dose, + pillText: "Bolus\n\(dose)U\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + ) + } + carbs = Self.spread(vc.carbData.map { + let grams = Int($0.value) + var label = "\(grams)" + if $0.absorptionTime > 0, Storage.shared.showAbsorption.value { + label += " \($0.absorptionTime / 60)h" + } + return TreatmentPoint( + date: Date(timeIntervalSince1970: $0.date), + value: $0.value, + sgv: Double($0.sgv), + label: label, + pillText: "Carbs\n\(grams)g\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + ) + }, minGap: Spread.carbGap, maxShift: Spread.carbShift) + let smbPoints = vc.smbData.map { + let dose = self.formatDose($0.value) + return TreatmentPoint( + date: Date(timeIntervalSince1970: $0.date), + value: $0.value, + sgv: Double($0.sgv), + label: dose, + pillText: "SMB\n\(dose)U\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + ) + } + (boluses, smbs) = Self.spreadTogether(bolusPoints, smbPoints, minGap: Spread.bolusGap, maxShift: Spread.bolusShift) + bgChecks = vc.bgCheckData.map { + TreatmentPoint( + date: Date(timeIntervalSince1970: $0.date), + value: Double($0.sgv), + sgv: Double($0.sgv), + label: "", + pillText: "BG Check\n\(Localizer.toDisplayUnits(String($0.sgv)))\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + ) + } + suspends = vc.suspendGraphData.map { + TreatmentPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), sgv: Double($0.sgv), label: "", pillText: "Suspend\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))") + } + resumes = vc.resumeGraphData.map { + TreatmentPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), sgv: Double($0.sgv), label: "", pillText: "Resume\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))") + } + sensorStarts = vc.sensorStartGraphData.map { + TreatmentPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), sgv: Double($0.sgv), label: "", pillText: "Sensor Start\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))") + } + notes = vc.noteGraphData.map { + TreatmentPoint( + date: Date(timeIntervalSince1970: $0.date), + value: Double($0.sgv), + sgv: Double($0.sgv), + label: $0.note, + pillText: "\(Self.extractMessage(from: $0.note) ?? $0.note)\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + ) + } + + basalScheduled = vc.basalScheduleData.map { + ScheduledBasalPoint(date: Date(timeIntervalSince1970: $0.date), rate: $0.basalRate) + } + + var steps: [BasalStep] = [] + let sortedBasal = vc.basalData.sorted { $0.date < $1.date } + for i in 0 ..< sortedBasal.count { + let start = sortedBasal[i].date + let end = i + 1 < sortedBasal.count + ? sortedBasal[i + 1].date + : dateTimeUtils.getNowTimeIntervalUTC() + guard end > start else { continue } + steps.append(BasalStep( + start: Date(timeIntervalSince1970: start), + end: Date(timeIntervalSince1970: end), + rate: sortedBasal[i].basalRate + )) + } + basal = steps + let computedMaxBasal = steps.map(\.rate).max() ?? 0 + maxBasal = max(computedMaxBasal, Storage.shared.minBasalScale.value) + + let yTop = maxBG - 5 + let yBottom = maxBG - 25 + overrides = vc.overrideGraphData.map { + let overrideName = $0.reason.trimmingCharacters(in: .whitespacesAndNewlines) + let displayName = overrideName.isEmpty ? "Override" : overrideName + return BandRect( + start: Date(timeIntervalSince1970: $0.date), + end: Date(timeIntervalSince1970: $0.endDate), + yBottom: yBottom, + yTop: yTop, + label: displayName, + pillText: "Override\n\(displayName)\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + ) + } + tempTargets = vc.tempTargetGraphData.map { + let target = $0.correctionRange.first.map { String($0) } ?? "" + // Temp targets render at the BG level they target (±5 mg/dL); + // only overrides live in the top strip. + let yCenter = Double($0.correctionRange.first ?? 0) + return BandRect( + start: Date(timeIntervalSince1970: $0.date), + end: Date(timeIntervalSince1970: $0.endDate), + yBottom: yCenter - 5, + yTop: yCenter + 5, + label: "Temp Target", + pillText: "Temp Target\n\(Localizer.toDisplayUnits(target))\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + ) + } + + let currentNow = Date(timeIntervalSince1970: dateTimeUtils.getNowTimeIntervalUTC()) + now = currentNow + let hoursBack = TimeInterval(Storage.shared.downloadDays.value * 24 * 3600) + domainStart = currentNow.addingTimeInterval(-hoursBack) + // Everything drawn in the future (predictions, cone, override/temp-target + // bands) is bounded by the "Hours of Prediction" setting, so the scale + // domain ends there too. The 15-minute floor keeps room for follow + // mode's right-side padding when predictions are set to 0 (and matches + // the floor Overrides.swift uses for future bands). + let hoursForward = max(Storage.shared.predictionToLoad.value, 0.25) * 3600 + domainEnd = currentNow.addingTimeInterval(hoursForward) + + // 30/90 min lookback markers + thirtyMinMark = currentNow.addingTimeInterval(-1800) + ninetyMinMark = currentNow.addingTimeInterval(-5400) + + // DIA markers: every hour going back for 6 hours + var dia: [Date] = [] + for i in 1 ... 6 { + dia.append(currentNow.addingTimeInterval(TimeInterval(-i * 3600))) + } + diaMarkers = dia + + // Midnight markers: every local midnight within domain + var midnights: [Date] = [] + let calendar: Calendar = { + var cal = Calendar(identifier: .gregorian) + if Storage.shared.graphTimeZoneEnabled.value, + let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) + { + cal.timeZone = tz + } + return cal + }() + var cursor = calendar.startOfDay(for: domainStart) + while cursor <= domainEnd { + if cursor >= domainStart { + midnights.append(cursor) + } + guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break } + cursor = next + } + midnightMarkers = midnights + + generation &+= 1 + } +} diff --git a/LoopFollow/Charts/BGChartStubs.swift b/LoopFollow/Charts/BGChartStubs.swift new file mode 100644 index 000000000..ba8714a82 --- /dev/null +++ b/LoopFollow/Charts/BGChartStubs.swift @@ -0,0 +1,194 @@ +// LoopFollow +// BGChartStubs.swift + +import Foundation +import SwiftUI +import UIKit + +/// The `update*Graph()` / `createGraph()` entry points the rest of the app +/// calls to refresh the chart. Rendering happens in BGChartModel + +/// BGChartView (Apple Swift Charts); these update lightweight state +/// (topBG / topPredictionBG / prediction arrays) and trigger a (coalesced) +/// model rebuild. +extension MainViewController { + func createGraph() { + chartModel.rebuild() + // The chart's follow/auto-return logic runs off model.now, so arm the + // once-a-minute "now" tick. + startGraphNowTimer() + } + + func createSmallBGGraph() {} + + func updateBGGraphSettings() { + chartModel.rebuild() + } + + private func recomputeTopBG() { + let maxBGOffset: Double = 50 + topBG = Storage.shared.minBGScale.value + for entry in bgData { + if Double(entry.sgv) > topBG - maxBGOffset { + topBG = Double(entry.sgv) + maxBGOffset + } + } + } + + private func recomputeTopPredictionBG() { + let maxBGOffset: Double = 20 + topPredictionBG = Storage.shared.minBGScale.value + let allPredictions = predictionData + ztPredictionData + iobPredictionData + cobPredictionData + uamPredictionData + for entry in allPredictions { + let v = Double(entry.sgv) + if v > topPredictionBG - maxBGOffset { + topPredictionBG = v + maxBGOffset + } + } + } + + func updateBGGraph() { + recomputeTopBG() + // Profile processing defers building the scheduled-basal segments until + // the first BG render has happened (Profile.swift checks firstGraphLoad). + // Clear the flag here or the scheduled basal line never gets data. + if !bgData.isEmpty { + firstGraphLoad = false + } + chartModel.rebuild() + } + + func updatePredictionGraph(color _: UIColor? = nil) { + recomputeTopPredictionBG() + chartModel.rebuild() + } + + func updatePredictionGraphGeneric( + dataIndex: Int, + predictionData: [ShareGlucoseData], + chartLabel _: String, + color _: UIColor + ) { + switch dataIndex { + case 12: ztPredictionData = predictionData + case 13: iobPredictionData = predictionData + case 14: cobPredictionData = predictionData + case 15: uamPredictionData = predictionData + default: break + } + recomputeTopPredictionBG() + chartModel.rebuild() + } + + // Removes the Loop forecast line. Used when the active system switches away from Loop. + func clearLoopPredictionGraph() { + guard !predictionData.isEmpty else { return } + predictionData.removeAll() + updatePredictionGraph() + } + + // Removes the Trio/OpenAPS forecast (ZT/IOB/COB/UAM lines and the cone). Used when the active system switches away from Trio/OpenAPS. + // Also drops the stored predBGs: the device observer calls updateOpenAPSPredictionDisplay after this runs, + // and would otherwise redraw the cleared forecast from them. + func clearOpenAPSPredictionGraph() { + let hasLineData = !ztPredictionData.isEmpty || !iobPredictionData.isEmpty || !cobPredictionData.isEmpty || !uamPredictionData.isEmpty + guard hasLineData || !chartModel.cone.isEmpty || openAPSPredBGs != nil else { return } + openAPSPredBGs = nil + openAPSPredUpdatedTime = nil + ztPredictionData = [] + iobPredictionData = [] + cobPredictionData = [] + uamPredictionData = [] + chartModel.cone = [] + recomputeTopPredictionBG() + chartModel.rebuild() + } + + // Routes OpenAPS/Trio predictions either into a cone band or individual + // prediction lines. Cone is only for OpenAPS-based backends; Loop always + // uses lines. + func updateOpenAPSPredictionDisplay() { + guard let predBGs = openAPSPredBGs else { + chartModel.cone = [] + return + } + + let displayType: PredictionDisplayType = Storage.shared.device.value == "Loop" + ? .lines + : Storage.shared.predictionDisplayType.value + let toLoad = Int(Storage.shared.predictionToLoad.value * 12) + let predictionStart = openAPSPredUpdatedTime ?? Date().timeIntervalSince1970 + let types = ["ZT", "IOB", "COB", "UAM"] + let minDisplay = globalVariables.minDisplayGlucose + let maxDisplay = globalVariables.maxDisplayGlucose + + topPredictionBG = Storage.shared.minBGScale.value + + if displayType == .cone { + // Clear individual prediction lines. + ztPredictionData = [] + iobPredictionData = [] + cobPredictionData = [] + uamPredictionData = [] + + let allArrays = types.compactMap { predBGs[$0] }.filter { !$0.isEmpty } + var coneData: [BGChartModel.ConePoint] = [] + if !allArrays.isEmpty { + // Cap at the shortest predBG array length so every cone point uses + // the same set of contributing arrays. Matches Trio's ForecastSetup. + let coneLength = min(allArrays.map { $0.count }.min()!, toLoad + 1) + var t = predictionStart + for i in 0 ..< coneLength { + let valuesAtIndex = allArrays.compactMap { i < $0.count ? $0[i] : nil } + if !valuesAtIndex.isEmpty { + var yMin = max(valuesAtIndex.min()!, Double(minDisplay)) + var yMax = min(valuesAtIndex.max()!, Double(maxDisplay)) + // Keep a minimum ±1 mg/dL band so the cone stays visible when predictions agree. + if yMin == yMax { + yMin -= 1 + yMax += 1 + } + coneData.append(BGChartModel.ConePoint( + date: Date(timeIntervalSince1970: t), + yMin: yMin, + yMax: yMax + )) + if yMax > topPredictionBG - 20 { topPredictionBG = yMax + 20 } + } + t += 300 + } + } + chartModel.cone = coneData + } else { + chartModel.cone = [] + + // dataIndex mapping matches updatePredictionGraphGeneric: 12=ZT 13=IOB 14=COB 15=UAM + for (offset, type) in types.enumerated() { + var predictionData: [ShareGlucoseData] = [] + if let graphData = predBGs[type] { + var t = predictionStart + for i in 0 ... toLoad where i < graphData.count { + let clamped = min(max(Int(round(graphData[i])), minDisplay), maxDisplay) + predictionData.append(ShareGlucoseData(sgv: clamped, date: t, direction: "flat")) + t += 300 + } + } + updatePredictionGraphGeneric(dataIndex: 12 + offset, predictionData: predictionData, chartLabel: type, color: .clear) + } + } + + chartModel.rebuild() + } + + func updateBasalGraph() { chartModel.rebuild() } + func updateBolusGraph() { chartModel.rebuild() } + func updateCarbGraph() { chartModel.rebuild() } + func updateBasalScheduledGraph() { chartModel.rebuild() } + func updateOverrideGraph() { chartModel.rebuild() } + func updateBGCheckGraph() { chartModel.rebuild() } + func updateSuspendGraph() { chartModel.rebuild() } + func updateResumeGraph() { chartModel.rebuild() } + func updateSensorStart() { chartModel.rebuild() } + func updateNotes() { chartModel.rebuild() } + func updateSmbGraph() { chartModel.rebuild() } + func updateTempTargetGraph() { chartModel.rebuild() } +} diff --git a/LoopFollow/Charts/BGChartView.swift b/LoopFollow/Charts/BGChartView.swift new file mode 100644 index 000000000..e7aa43b3c --- /dev/null +++ b/LoopFollow/Charts/BGChartView.swift @@ -0,0 +1,1623 @@ +// LoopFollow +// BGChartView.swift + +import Charts +import SwiftUI + +/// Shared generator for the light tick emitted when a scrub lands on a new +/// anchor. File-scoped so it stays prepared across ticks instead of being +/// re-created per body evaluation. +private let scrubHaptic = UISelectionFeedbackGenerator() + +private enum BGChartConfig { + /// Tightest pinch-in zoom. + static let minVisibleSeconds: TimeInterval = 15 * 60 + /// Widest pinch-out zoom. + static let maxVisibleSeconds: TimeInterval = 24 * 3600 + /// Double-tap cycles the visible window through these presets. + static let zoomPresets: [TimeInterval] = [3600, 3 * 3600, 6 * 3600, 12 * 3600, 24 * 3600] + /// Anchor of the geometric grid pinch commits quantize to. + static let zoomGridBaseSeconds: TimeInterval = 3600 + /// Geometric grid for pinch commits (~4 % per step). Every committed zoom + /// step re-lays the canvas, so this bounds a halving of the visible window + /// to roughly 18 re-layouts instead of hundreds. + static let zoomStepRatio: Double = 1.04 + /// Live pinch previews as a transform; once the stretch drifts past this + /// ratio a crisp re-layout is committed mid-gesture, bounding distortion. + static let pinchCommitScaleDrift: Double = 1.25 + /// Render window extends this many visible-windows beyond each visible edge. + static let renderWindowPadFactor = 1.5 + /// Re-anchor when a visible edge gets within this fraction of a + /// visible-window of the render window's edge. + static let renderWindowMarginFactor = 0.5 + /// How far (pt) a one-finger touch may travel and still count as a + /// stationary press-to-inspect; beyond this the touch becomes a pan. + static let inspectMovementTolerance: CGFloat = 10 + /// How long (s) a one-finger touch must rest before inspect latches. + static let inspectHoldDelay: TimeInterval = 0.2 + /// How close the visible window's trailing edge must sit to "now" before + /// we treat the user as "at the latest data" and resume auto-following. + static let followEdgeTolerance: TimeInterval = 90 + /// While following, "now" sits at this fraction of the visible window, + /// leaving room on the right for predictions/cone. + static let followNowFraction = 0.7 + /// How long after the last navigation in history before a data tick pulls + /// the chart back to "now". + static let autoFollowPause: TimeInterval = 5 * 60 + /// Max distance between the scrub date and an anchor for it to be selected. + static let selectionTolerance: TimeInterval = 20 * 60 + /// Half-width (pt) of the scrub capture band: treatments whose symbol is + /// within this screen distance of the finger join the pill alongside the + /// (ever-present) nearest BG reading. + static let scrubCaptureRadius: CGFloat = 22 + /// Time cap on the capture band, so wide zooms — where a finger-width + /// covers hours — don't sweep far-away treatments into the pill. + static let scrubCaptureMaxSeconds: TimeInterval = 5 * 60 + /// Screen-space radius (pt) within which a tap selects a mark. + static let tapHitRadius: CGFloat = 30 +} + +/// Small y-domain headroom keeps the top axis label readable instead of +/// pinning it to the chart edge. +private func chartYDomainUpperBound(_ maxBG: Double) -> Double { + let clampedMax = max(maxBG, 1) + let topPadding = max(clampedMax * 0.02, 1) + return clampedMax + topPadding +} + +struct BGChartView: View { + enum Config { + case small + case main + } + + let model: BGChartModel + let config: Config + + var body: some View { + if config == .small { + SmallBGChart(model: model, interaction: model.interaction) + } else { + MainBGChart(model: model, interaction: model.interaction) + } + } +} + +// MARK: - Main chart shell (gestures, transforms, overlays) + +/// The interactive BG chart. +/// +/// Rendering/interaction strategy: the chart content is +/// laid out ONCE per (data, zoom) change onto a wide fixed-width canvas +/// covering a render window around the visible viewport. Panning translates +/// that canvas with a pure `.offset` transform — the canvas is an Equatable +/// child whose stored properties exclude the pan position, so SwiftUI skips +/// its body during pans and momentum. Zoom is a pure-SwiftUI MagnifyGesture +/// whose live preview is a `.scaleEffect(x:)` stretch anchored under the +/// pinch centroid, committed on a geometric zoom grid. A one-finger press +/// held stationary latches into inspect mode and scrubs a selection that is +/// rendered by a shell overlay (never re-laying the canvas). Double-tap +/// cycles zoom presets. No `.chartScrollableAxes`, no UIKit gesture hacks. +private struct MainBGChart: View { + @ObservedObject var model: BGChartModel + @ObservedObject var interaction: BGChartInteraction + + @State private var didInitialize = false + + /// Rendered slice of the domain. The canvas covers only this window + /// (visible ± `renderWindowPadFactor` viewports), bounding canvas width + /// and per-layout cost no matter how long the data domain grows. + @State private var renderWindowStart = Date() + @State private var renderWindowEnd = Date().addingTimeInterval(3600) + + /// Plot area of the static axis overlay, in shell coordinates. The + /// selection overlay uses it for its value-to-pixel maps. + @State private var plotFrame: CGRect = .zero + + /// Measured size of the visible selection pill (see PillSizePreferenceKey). + @State private var pillSize: CGSize = .zero + + // Pinch state: live preview stretch plus the anchor captured at pinch + // start so the zoom stays anchored under the pinch centroid. + @State private var pinchScale: CGFloat = 1 + @State private var pinchAnchor: ( + visibleAtStart: TimeInterval, + anchorDate: Date, + anchorFraction: CGFloat + )? + + /// Leading edge captured when a one-finger drag transitions into panning. + @State private var panBaseline: Date? + + /// Date under the user's finger while inspecting, else nil. + @State private var selection: Date? + + /// Anchor selected by tapping a mark; sticky until the user taps empty + /// space, taps another mark, or starts a pan/zoom/inspect. + @State private var tapped: SelectionAnchor? + + /// True once a held press has engaged inspect; from then on finger + /// movement scrubs the selection instead of panning, until the finger lifts. + @State private var isInspectLatched = false + + /// Most recent finger location, so the hold timer can place the selection + /// even if the finger produced no further events after touch-down. + @State private var lastTouchLocation: CGPoint? + + /// Armed at touch-down; fires after `inspectHoldDelay` and latches inspect + /// if the touch is still down and stationary. A timer is required because + /// DragGesture only reports *changes* — a perfectly still finger generates + /// no events after touch-down. + @State private var inspectHoldTask: Task? + @State private var touchDownTime: Date? + + /// Drives post-flick deceleration; cancelled by any new touch. + @State private var momentumTask: Task? + + /// Anchor date of the last haptic tick, so scrub jitter doesn't re-fire it. + @State private var lastHapticAnchorDate: Date? + + /// While the user is (or recently was) navigating history, auto-return to + /// "now" is paused until this instant. Refreshed by every scroll movement + /// away from the live edge, cleared on return to it. + @State private var autoFollowPausedUntil: Date? + + private var timeZoneForAxis: TimeZone { + if Storage.shared.graphTimeZoneEnabled.value, + let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) + { + return tz + } + return .current + } + + var body: some View { + GeometryReader { geo in + chart(viewport: geo.size) + } + .background(Color(.systemBackground)) + } + + private func chart(viewport: CGSize) -> some View { + let viewportWidth = max(viewport.width, 1) + let windowSeconds = max(renderWindowEnd.timeIntervalSince(renderWindowStart), 1) + let canvasWidth = viewportWidth * CGFloat(windowSeconds / interaction.visibleSeconds) + // Pixel offset of the canvas for the current leading-edge date. + // Derived, not stored: re-anchoring the window recomputes it consistently. + let canvasOffsetX = CGFloat( + interaction.scrollPosition.timeIntervalSince(renderWindowStart) / windowSeconds + ) * canvasWidth + + return ZStack(alignment: .topLeading) { + BGChartCanvas( + model: model, + generation: model.generation, + isSmall: false, + windowStart: renderWindowStart, + windowEnd: renderWindowEnd, + canvasWidth: canvasWidth, + height: viewport.height, + visibleSeconds: interaction.visibleSeconds, + timeZone: timeZoneForAxis + ) + .equatable() + .offset(x: -canvasOffsetX) + .scaleEffect( + x: pinchScale, + y: 1, + anchor: pinchScaleAnchor(viewportWidth: viewportWidth, canvasWidth: canvasWidth) + ) + + // Pinned y-axis labels (BG trailing, basal leading) over the + // scrolling canvas. The canvas's own y-axis is hidden — its + // trailing edge sits far off-screen on the wide canvas. + StaticYAxisOverlay(generation: model.generation, maxBG: model.maxBG, maxBasal: model.maxBasal) + .equatable() + .frame(width: viewportWidth, height: viewport.height) + .allowsHitTesting(false) + + selectionOverlay(viewportWidth: viewportWidth) + .allowsHitTesting(false) + + overrideBandLabelsOverlay(viewportWidth: viewportWidth) + .allowsHitTesting(false) + + if !interaction.followLatest { + jumpToNowButton + } + } + .frame(width: viewportWidth, height: viewport.height, alignment: .topLeading) + .clipped() + .contentShape(Rectangle()) + .simultaneousGesture(panAndInspectGesture(viewportWidth: viewportWidth)) + .simultaneousGesture(magnifyGesture(viewportWidth: viewportWidth)) + // Double-tap zooms; a single tap (only recognized once the double-tap + // window lapses) selects the mark under the finger, or clears the pill. + .simultaneousGesture( + TapGesture(count: 2) + .exclusively(before: SpatialTapGesture()) + .onEnded { value in + switch value { + case .first: + cycleZoomPreset() + case let .second(tap): + handleTap(at: tap.location, viewportWidth: viewportWidth) + } + } + ) + .onPreferenceChange(PlotFramePreferenceKey.self) { plotFrame = $0 } + .onPreferenceChange(PillSizePreferenceKey.self) { pillSize = $0 } + .onChange(of: interaction.scrollPosition) { _, _ in + updateRenderWindow() + updateFollowState() + } + .onChange(of: interaction.visibleSeconds) { _, _ in + updateRenderWindow(force: true) + } + .onChange(of: model.now) { _, _ in + if interaction.followLatest { + scrollToNow(animated: true) + } else if let pausedUntil = autoFollowPausedUntil, Date() >= pausedUntil { + // Idle in history past the pause: the next data tick pulls the + // chart back to live. + scrollToNow(animated: true) + } + updateRenderWindow() + } + .onAppear { + if !didInitialize { + didInitialize = true + scrollToNow(animated: false) + updateRenderWindow(force: true) + } + } + .onDisappear { + momentumTask?.cancel() + inspectHoldTask?.cancel() + } + } + + private var jumpToNowButton: some View { + Button { + scrollToNow(animated: true) + } label: { + Image(systemName: "arrow.right.to.line") + .font(.footnote.bold()) + .foregroundColor(.primary) + .padding(8) + .background(Circle().fill(Color(.secondarySystemBackground))) + .overlay(Circle().stroke(Color.primary.opacity(0.25), lineWidth: 0.5)) + } + .padding(.trailing, 44) + .padding(.bottom, 28) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) + } + + // MARK: Render window / follow state + + /// Re-anchors the render window when the visible window nears its edge. + /// Between re-anchors, panning stays a pure offset transform. + private func updateRenderWindow(force: Bool = false) { + let visibleSeconds = interaction.visibleSeconds + let pad = BGChartConfig.renderWindowPadFactor * visibleSeconds + let margin = BGChartConfig.renderWindowMarginFactor * visibleSeconds + let domainStart = model.domainStart + let domainEnd = max(model.domainEnd, domainStart.addingTimeInterval(1)) + let visibleStart = max(interaction.scrollPosition, domainStart) + // Before the first rebuild the scroll position can sit entirely outside + // the (not yet populated) domain; never let the window invert. + let visibleEnd = max( + min(interaction.scrollPosition.addingTimeInterval(visibleSeconds), domainEnd), + visibleStart + ) + + let nearLeft = visibleStart.timeIntervalSince(renderWindowStart) < margin + && renderWindowStart > domainStart + let nearRight = renderWindowEnd.timeIntervalSince(visibleEnd) < margin + && renderWindowEnd < domainEnd + let uncovered = visibleStart < renderWindowStart || visibleEnd > renderWindowEnd + guard force || nearLeft || nearRight || uncovered else { return } + + let newStart = max(visibleStart.addingTimeInterval(-pad), domainStart) + let newEnd = max( + min(visibleEnd.addingTimeInterval(pad), domainEnd), + newStart.addingTimeInterval(1) + ) + guard newStart != renderWindowStart || newEnd != renderWindowEnd else { return } + renderWindowStart = newStart + renderWindowEnd = newEnd + } + + /// Re-arms following only when the visible window's trailing edge has + /// caught back up to "now"; otherwise the user is in history, and every + /// movement there refreshes the auto-return pause. + private func updateFollowState() { + let trailingEdge = interaction.scrollPosition.addingTimeInterval(interaction.visibleSeconds) + let follow = trailingEdge >= model.now.addingTimeInterval(-BGChartConfig.followEdgeTolerance) + autoFollowPausedUntil = follow ? nil : Date().addingTimeInterval(BGChartConfig.autoFollowPause) + if interaction.followLatest != follow { + interaction.followLatest = follow + } + } + + /// Clamps a proposed leading edge so the visible window never leaves the domain. + private func clampedLeadingEdge(_ proposed: Date) -> Date { + // Before the first rebuild the domain is an epoch-0 placeholder; + // clamping against it would pin the chart to "the beginning of time" + // and (via the follow observer) disarm auto-following for good. + guard model.domainEnd > model.domainStart.addingTimeInterval(1) else { return proposed } + let earliest = model.domainStart + let latest = model.domainEnd.addingTimeInterval(-interaction.visibleSeconds) + return min(max(proposed, earliest), max(earliest, latest)) + } + + /// Positions "now" at `followNowFraction` of the visible window and keeps + /// following armed. Never yanks the chart out from under an active gesture + /// or momentum; the next data tick re-anchors as usual. + private func scrollToNow(animated: Bool) { + guard pinchAnchor == nil, panBaseline == nil, !isInspectLatched, momentumTask == nil else { return } + interaction.followLatest = true + let target = clampedLeadingEdge( + model.now.addingTimeInterval(-interaction.visibleSeconds * BGChartConfig.followNowFraction) + ) + // Animate only moves the render window already covers (its padding + // guarantees a window around the visible edges): the offset transform + // then interpolates over unchanged canvas content. A longer move + // re-anchors the window, which would swap the content out from under + // the in-flight animation — so deep returns snap instead. + let distance = abs(target.timeIntervalSince(interaction.scrollPosition)) + if animated, distance <= interaction.visibleSeconds { + withAnimation(.easeInOut(duration: 0.3)) { + interaction.scrollPosition = target + } + } else { + interaction.scrollPosition = target + } + } + + // MARK: Gestures + + private var isPinching: Bool { pinchAnchor != nil } + + /// Converts a horizontal translation (pt) into a time delta within the visible window. + private func timeDelta(forTranslation dx: CGFloat, viewportWidth: CGFloat) -> TimeInterval { + TimeInterval(dx / viewportWidth) * interaction.visibleSeconds + } + + /// One-finger gesture: movement pans (with momentum on release); a press + /// held in place latches into inspect, after which dragging scrubs the + /// selection until the finger lifts. Panning only mutates scrollPosition + /// (a transform) and scrubbing only the shell overlay, so neither path + /// re-lays the canvas. + private func panAndInspectGesture(viewportWidth: CGFloat) -> some Gesture { + DragGesture(minimumDistance: 0) + .onChanged { value in + momentumTask?.cancel() + momentumTask = nil + guard !isPinching else { + inspectHoldTask?.cancel() + if selection != nil { selection = nil } + panBaseline = nil + touchDownTime = nil + isInspectLatched = false + return + } + lastTouchLocation = value.location + if touchDownTime == nil { + touchDownTime = value.time + scheduleInspectHold(viewportWidth: viewportWidth) + } + + if isInspectLatched { + updateSelection(atViewportX: value.location.x, viewportWidth: viewportWidth) + return + } + + let distance = hypot(value.translation.width, value.translation.height) + if panBaseline == nil, distance < BGChartConfig.inspectMovementTolerance { + // Finger is stationary: the hold timer armed at touch-down + // will latch inspect if it stays that way. + } else { + // Finger is travelling: pan. The touch can no longer become an inspect. + inspectHoldTask?.cancel() + if selection != nil { selection = nil } + if tapped != nil { tapped = nil } + if panBaseline == nil { + // Compensate for the distance already travelled inside the + // tolerance so the pan engages without a positional jump. + panBaseline = interaction.scrollPosition.addingTimeInterval( + timeDelta(forTranslation: value.translation.width, viewportWidth: viewportWidth) + ) + } + if let baseline = panBaseline { + interaction.scrollPosition = clampedLeadingEdge( + baseline.addingTimeInterval( + -timeDelta(forTranslation: value.translation.width, viewportWidth: viewportWidth) + ) + ) + } + } + } + .onEnded { value in + inspectHoldTask?.cancel() + if selection != nil { selection = nil } + touchDownTime = nil + isInspectLatched = false + lastTouchLocation = nil + lastHapticAnchorDate = nil + let wasPanning = panBaseline != nil + panBaseline = nil + guard wasPanning, !isPinching else { return } + // Momentum: initial velocity in seconds of chart time per second. + let velocity = -timeDelta(forTranslation: value.velocity.width, viewportWidth: viewportWidth) + startMomentum(velocitySecondsPerSecond: velocity) + } + } + + /// Arms the inspect hold: after `inspectHoldDelay`, if the touch is still + /// down and has neither become a pan nor a pinch, latch into inspect mode + /// at the finger's last known position — with a haptic so the mode change is felt. + private func scheduleInspectHold(viewportWidth: CGFloat) { + inspectHoldTask?.cancel() + inspectHoldTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: UInt64(BGChartConfig.inspectHoldDelay * 1_000_000_000)) + guard !Task.isCancelled, + touchDownTime != nil, + panBaseline == nil, + !isPinching, + !isInspectLatched + else { return } + + isInspectLatched = true + tapped = nil + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + scrubHaptic.prepare() + if let location = lastTouchLocation { + updateSelection(atViewportX: location.x, viewportWidth: viewportWidth) + } + } + } + + private func updateSelection(atViewportX x: CGFloat, viewportWidth: CGFloat) { + let fraction = min(max(x / viewportWidth, 0), 1) + let date = interaction.scrollPosition.addingTimeInterval( + interaction.visibleSeconds * TimeInterval(fraction) + ) + selection = date + // A featherlight tick whenever the indicator snaps to a different item. + let captureWindow = scrubCaptureWindow(viewportWidth: viewportWidth) + if let anchor = selectionAnchor(for: date, captureWindow: captureWindow), anchor.date != lastHapticAnchorDate { + lastHapticAnchorDate = anchor.date + scrubHaptic.selectionChanged() + scrubHaptic.prepare() + } + } + + /// Deceleration after a flick. Mutates only scrollPosition (a transform), + /// so each frame costs a GPU translation — same profile as live panning. + private func startMomentum(velocitySecondsPerSecond initialVelocity: TimeInterval) { + let visibleSeconds = interaction.visibleSeconds + // Ignore tiny flicks. + guard abs(initialVelocity) > visibleSeconds * 0.05 else { return } + momentumTask?.cancel() + momentumTask = Task { @MainActor in + var velocity = initialVelocity + var expected = interaction.scrollPosition + let frameDuration: TimeInterval = 1.0 / 60.0 + while !Task.isCancelled, abs(velocity) > visibleSeconds * 0.02 { + try? await Task.sleep(nanoseconds: UInt64(frameDuration * 1_000_000_000)) + guard !Task.isCancelled else { break } + // Someone else (small-chart tap, data snap) moved the chart: yield. + guard interaction.scrollPosition == expected else { break } + let next = clampedLeadingEdge(expected.addingTimeInterval(velocity * frameDuration)) + guard next != expected else { break } // hit a domain edge + interaction.scrollPosition = next + expected = next + velocity *= 0.97 + } + // Every reassignment of momentumTask cancels the old task first, so + // an uncancelled task is still the current one and may clear the slot; + // a cancelled one must not (it could clobber its successor). + if !Task.isCancelled { + momentumTask = nil + } + } + } + + /// Two-finger pinch drives the zoom continuously, anchored under the pinch + /// centroid. Zoom changes re-lay the canvas, so commits are quantized to a + /// geometric grid to bound the number of re-layouts per pinch. + private func magnifyGesture(viewportWidth _: CGFloat) -> some Gesture { + MagnifyGesture(minimumScaleDelta: 0.01) + .onChanged { value in + momentumTask?.cancel() + momentumTask = nil + if pinchAnchor == nil { + let fraction = min(max(value.startAnchor.x, 0), 1) + pinchAnchor = ( + visibleAtStart: interaction.visibleSeconds, + anchorDate: interaction.scrollPosition.addingTimeInterval( + interaction.visibleSeconds * TimeInterval(fraction) + ), + anchorFraction: fraction + ) + inspectHoldTask?.cancel() + if selection != nil { selection = nil } + if tapped != nil { tapped = nil } + isInspectLatched = false + } + guard let pinch = pinchAnchor, value.magnification > 0 else { return } + + // Live pinch previews as a transform: stretch the already-laid-out + // canvas about the centroid. Pinch out (magnification > 1) narrows + // the visible window, i.e. zooms in. Once the stretch drifts past + // the commit threshold, a crisp re-layout is committed mid-gesture + // and the transform continues from that new baseline. + let proposed = min( + max( + pinch.visibleAtStart / TimeInterval(value.magnification), + BGChartConfig.minVisibleSeconds + ), + BGChartConfig.maxVisibleSeconds + ) + pinchScale = CGFloat(interaction.visibleSeconds / proposed) + + let drift = BGChartConfig.pinchCommitScaleDrift + if pinchScale > drift || pinchScale < 1 / drift { + commitPinchZoom(proposed) + } + } + .onEnded { _ in + guard pinchAnchor != nil else { return } + commitPinchZoom(interaction.visibleSeconds / TimeInterval(pinchScale)) + // The commit no-ops when the zoom quantizes back to the current + // value; the preview must still un-stretch. + pinchScale = 1 + pinchAnchor = nil + interaction.persistZoom() + } + } + + /// Quantizes to the geometric zoom grid and re-lays the canvas exactly + /// once: window re-anchor happens in the same transaction. + private func commitPinchZoom(_ proposed: TimeInterval) { + guard let pinch = pinchAnchor else { return } + let ratio = BGChartConfig.zoomStepRatio + let base = BGChartConfig.zoomGridBaseSeconds + let step = (log(proposed / base) / log(ratio)).rounded() + var quantized = base * pow(ratio, step) + quantized = min( + max(quantized, BGChartConfig.minVisibleSeconds), + BGChartConfig.maxVisibleSeconds + ) + guard quantized != interaction.visibleSeconds else { return } + + interaction.visibleSeconds = quantized + interaction.scrollPosition = clampedLeadingEdge( + pinch.anchorDate.addingTimeInterval(-quantized * TimeInterval(pinch.anchorFraction)) + ) + updateRenderWindow(force: true) + pinchScale = 1 + } + + /// Anchor for the live-pinch stretch: the centroid's layout position on + /// the canvas, so the content under the fingers stays put on screen. + private func pinchScaleAnchor(viewportWidth: CGFloat, canvasWidth: CGFloat) -> UnitPoint { + guard let pinch = pinchAnchor, canvasWidth > 0 else { return .center } + return UnitPoint(x: pinch.anchorFraction * viewportWidth / canvasWidth, y: 0.5) + } + + /// Double-tap cycles the zoom presets. Snap, no animation: animating the + /// zoom would animate canvasWidth, re-laying the canvas every frame. + private func cycleZoomPreset() { + let presets = BGChartConfig.zoomPresets + let current = interaction.visibleSeconds + let next = presets.first(where: { $0 > current + 1 }) ?? presets[0] + momentumTask?.cancel() + momentumTask = nil + tapped = nil + let trailing = interaction.scrollPosition.addingTimeInterval(current) + interaction.visibleSeconds = next + if interaction.followLatest { + interaction.scrollPosition = clampedLeadingEdge( + model.now.addingTimeInterval(-next * BGChartConfig.followNowFraction) + ) + } else { + interaction.scrollPosition = clampedLeadingEdge(trailing.addingTimeInterval(-next)) + } + updateRenderWindow(force: true) + interaction.persistZoom() + } + + // MARK: Selection + + private struct SelectionAnchor { + /// Where the pill/indicator is drawn — a treatment's `drawnDate`, so the + /// indicator lands on the symbol even when decluttering moved it. + let date: Date + let value: Double + /// One pill entry per item under the selector (see PillLabel). + let texts: [String] + } + + /// Feeds every treatment mark to `body` as (drawnDate, value, pillText). + /// Single source for both the scrub lookup and the tap hit test. + private func forEachTreatmentAnchor(_ body: (Date, Double, String) -> Void) { + for group in [model.boluses, model.carbs, model.smbs, model.bgChecks, + model.notes, model.suspends, model.resumes, model.sensorStarts] + { + for t in group { + body(t.drawnDate, t.sgv, t.pillText) + } + } + } + + /// Pill entry for a BG reading. Shared by the scrub lookup and the tap hit test. + private func bgPillText(for point: BGChartModel.BGPoint) -> String { + "BG\n\(Localizer.toDisplayUnits(String(Int(point.value))))\n\(model.pillTimeString(for: point.date))" + } + + private func bandPillTexts(at date: Date) -> [String] { + var texts: [String] = [] + if let band = model.overrides + .filter({ date >= $0.start && date <= $0.end }) + .max(by: { $0.start < $1.start }) + { + texts.append(band.pillText) + } + if let band = model.tempTargets + .filter({ date >= $0.start && date <= $0.end }) + .max(by: { $0.start < $1.start }) + { + texts.append(band.pillText) + } + return texts + } + + /// Band (override / temp target) under the given date+value, if any. + private func bandAnchor(at date: Date, value: Double) -> SelectionAnchor? { + for band in model.overrides where date >= band.start && date <= band.end { + if value >= band.yBottom, value <= band.yTop { + let midY = (band.yTop + band.yBottom) / 2 + return SelectionAnchor(date: date, value: midY, texts: [band.pillText]) + } + } + for band in model.tempTargets where date >= band.start && date <= band.end { + if value >= band.yBottom, value <= band.yTop { + let midY = (band.yTop + band.yBottom) / 2 + return SelectionAnchor(date: date, value: midY, texts: [band.pillText]) + } + } + return nil + } + + /// Seconds of chart time covered by `scrubCaptureRadius` at the current + /// zoom, bounded by `scrubCaptureMaxSeconds`. + private func scrubCaptureWindow(viewportWidth: CGFloat) -> TimeInterval { + min( + BGChartConfig.scrubCaptureMaxSeconds, + TimeInterval(BGChartConfig.scrubCaptureRadius / viewportWidth) * interaction.visibleSeconds + ) + } + + /// Scrub lookup (time-only). Collects everything under the finger instead + /// of picking a single winner: every treatment inside the capture window + /// joins the pill, and the nearest BG reading always does — so treatments + /// and glucose readings can never hide one another. The indicator snaps + /// to the nearest collected item; the pill stacks them all (treatments in + /// drawn order, BG last). + private func selectionAnchor(for selected: Date, captureWindow: TimeInterval) -> SelectionAnchor? { + struct Item { + let date: Date + let value: Double + let text: String + let distance: TimeInterval + } + + var captured: [Item] = [] + var nearestTreatment: Item? + forEachTreatmentAnchor { date, value, text in + let item = Item(date: date, value: value, text: text, distance: abs(date.timeIntervalSince(selected))) + if item.distance <= captureWindow { + captured.append(item) + } + if item.distance < (nearestTreatment?.distance ?? .greatestFiniteMagnitude) { + nearestTreatment = item + } + } + captured.sort { $0.date < $1.date } + + var nearestBG: Item? + for p in model.bg { + let d = abs(p.date.timeIntervalSince(selected)) + if d < (nearestBG?.distance ?? .greatestFiniteMagnitude) { + nearestBG = Item(date: p.date, value: p.value, text: bgPillText(for: p), distance: d) + } + } + + var items = captured + if let nearestBG, nearestBG.distance <= BGChartConfig.selectionTolerance { + items.append(nearestBG) + } + if let primary = items.min(by: { $0.distance < $1.distance }) { + let texts = items.map(\.text) + bandPillTexts(at: selected) + return SelectionAnchor(date: primary.date, value: primary.value, texts: texts) + } + + // Nothing under the finger. Reach for the nearest treatment (data gaps + // leave treatments without BG neighbors), then for a band (any height) + // at the scrub time. + if let nearestTreatment, nearestTreatment.distance <= BGChartConfig.selectionTolerance { + let texts = [nearestTreatment.text] + bandPillTexts(at: selected) + return SelectionAnchor(date: nearestTreatment.date, value: nearestTreatment.value, texts: texts) + } + for band in model.overrides where selected >= band.start && selected <= band.end { + let midY = (band.yTop + band.yBottom) / 2 + return SelectionAnchor(date: selected, value: midY, texts: [band.pillText]) + } + for band in model.tempTargets where selected >= band.start && selected <= band.end { + let midY = (band.yTop + band.yBottom) / 2 + return SelectionAnchor(date: selected, value: midY, texts: [band.pillText]) + } + + return nil + } + + /// Tap hit test (screen-space, 2D). Treatments take priority, then BG + /// points, then the override/temp-target bands under the finger. + /// Returns nil when the tap lands on nothing — which clears the pill. + private func tappedAnchor(at location: CGPoint, viewportWidth: CGFloat) -> SelectionAnchor? { + let radius = BGChartConfig.tapHitRadius + var best: SelectionAnchor? + var bestDistance2 = radius * radius + + func consider(_ date: Date, _ value: Double, _ text: String) { + let dx = xPosition(for: date, viewportWidth: viewportWidth) - location.x + let dy = yPosition(forValue: value) - location.y + let d2 = dx * dx + dy * dy + if d2 <= bestDistance2 { + bestDistance2 = d2 + best = SelectionAnchor(date: date, value: value, texts: [text]) + } + } + + forEachTreatmentAnchor(consider) + if best == nil { + for p in model.bg { + consider(p.date, p.value, bgPillText(for: p)) + } + } + if best == nil { + let date = interaction.scrollPosition.addingTimeInterval( + interaction.visibleSeconds * TimeInterval(location.x / viewportWidth) + ) + return bandAnchor(at: date, value: value(atY: location.y)) + } + if let best { + let texts = best.texts + bandPillTexts(at: best.date) + return SelectionAnchor(date: best.date, value: best.value, texts: texts) + } + return nil + } + + private func handleTap(at location: CGPoint, viewportWidth: CGFloat) { + guard plotFrame.height > 0 else { return } + tapped = tappedAnchor(at: location, viewportWidth: viewportWidth) + } + + /// The anchor the overlay should show: a live scrub wins over a sticky tap. + private func activeAnchor(viewportWidth: CGFloat) -> SelectionAnchor? { + if isInspectLatched, let selected = selection { + return selectionAnchor(for: selected, captureWindow: scrubCaptureWindow(viewportWidth: viewportWidth)) + } + return tapped + } + + // MARK: Value-to-pixel maps (shared by the selection overlay and tap hit test) + + private func xPosition(for date: Date, viewportWidth: CGFloat) -> CGFloat { + CGFloat( + date.timeIntervalSince(interaction.scrollPosition) / interaction.visibleSeconds + ) * viewportWidth + } + + private func yPosition(forValue value: Double) -> CGFloat { + let yMax = chartYDomainUpperBound(model.maxBG) + let clamped = min(max(value, 0), yMax) + return plotFrame.minY + CGFloat(1 - clamped / yMax) * plotFrame.height + } + + private func value(atY y: CGFloat) -> Double { + guard plotFrame.height > 0 else { return 0 } + let yMax = chartYDomainUpperBound(model.maxBG) + let fraction = 1 - (y - plotFrame.minY) / plotFrame.height + return Double(min(max(fraction, 0), 1)) * yMax + } + + @ViewBuilder + private func overrideBandLabelsOverlay(viewportWidth: CGFloat) -> some View { + if plotFrame.height > 0 { + let visibleStart = interaction.scrollPosition + let visibleEnd = interaction.scrollPosition.addingTimeInterval(interaction.visibleSeconds) + + ForEach(model.overrides.filter { $0.end >= visibleStart && $0.start <= visibleEnd }) { band in + let visibleBandStart = max(band.start, visibleStart) + let visibleBandEnd = min(band.end, visibleEnd) + let xStart = xPosition(for: visibleBandStart, viewportWidth: viewportWidth) + let xEnd = xPosition(for: visibleBandEnd, viewportWidth: viewportWidth) + let bandWidth = max(0, xEnd - xStart) + let y = yPosition(forValue: (band.yBottom + band.yTop) / 2) + + if bandWidth > 24 { + Text(band.label) + .font(.caption2) + .foregroundStyle(.white) + .lineLimit(1) + .truncationMode(.tail) + .padding(.trailing, 10) + .frame(width: max(0, bandWidth - 6), alignment: .trailing) + .clipped() + .position(x: xStart + bandWidth / 2, y: y) + } + } + } + } + + /// Vertical indicator + pill for the current selection, rendered in the + /// shell with the same linear maps the canvas uses — neither scrubbing + /// nor a tapped pill ever re-lays the canvas. + /// + /// The pill wraps long texts (notes) at its max width; placement uses the + /// measured pill size so the pill always sits fully on screen, below the + /// anchor when there is room and above it otherwise. Wrapping relies on + /// SwiftUI's word wrapping, which respects the actual font metrics, so + /// there is no manual line splitting. + @ViewBuilder + private func selectionOverlay(viewportWidth: CGFloat) -> some View { + if plotFrame.height > 0, let anchor = activeAnchor(viewportWidth: viewportWidth) { + let x = xPosition(for: anchor.date, viewportWidth: viewportWidth) + if x >= 0, x <= viewportWidth { + let y = yPosition(forValue: anchor.value) + + Rectangle() + .fill(Color.primary.opacity(0.5)) + .frame(width: 1, height: plotFrame.height) + .position(x: x, y: plotFrame.midY) + + // Measured size lags the text by one frame; fall back to a + // small nominal size until the first measurement lands. + let pillW = max(pillSize.width, 60) + let pillH = max(pillSize.height, 28) + let labelX = min(max(x, pillW / 2 + 4), viewportWidth - pillW / 2 - 4) + let below = y + 14 + pillH / 2 + let above = y - 14 - pillH / 2 + let fitsBelow = below + pillH / 2 <= plotFrame.maxY - 4 + let labelY = fitsBelow ? below : max(above, plotFrame.minY + pillH / 2 + 4) + PillLabel(texts: anchor.texts, maxWidth: min(300, viewportWidth - 16)) + .position(x: labelX, y: labelY) + } + } + } +} + +// MARK: - Small chart (overview + tap/drag-to-navigate) + +private struct SmallBGChart: View { + @ObservedObject var model: BGChartModel + @ObservedObject var interaction: BGChartInteraction + + /// True once the touch has travelled far enough to count as a drag; from + /// then on the main chart tracks the finger live instead of waiting for release. + @State private var isScrubbing = false + + var body: some View { + GeometryReader { geo in + let width = max(geo.size.width, 1) + let domainSeconds = max(model.domainEnd.timeIntervalSince(model.domainStart), 1) + + ZStack(alignment: .topLeading) { + BGChartCanvas( + model: model, + generation: model.generation, + isSmall: true, + windowStart: model.domainStart, + windowEnd: model.domainStart.addingTimeInterval(domainSeconds), + canvasWidth: width, + height: geo.size.height, + visibleSeconds: domainSeconds, + timeZone: .current + ) + .equatable() + + viewportBox(width: width, height: geo.size.height, domainSeconds: domainSeconds) + .allowsHitTesting(false) + } + .contentShape(Rectangle()) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { value in + let distance = hypot(value.translation.width, value.translation.height) + if isScrubbing || distance >= BGChartConfig.inspectMovementTolerance { + isScrubbing = true + navigate(toLocalX: value.location.x, width: width, domainSeconds: domainSeconds, animated: false) + } + } + .onEnded { value in + navigate(toLocalX: value.location.x, width: width, domainSeconds: domainSeconds, animated: !isScrubbing) + isScrubbing = false + } + ) + } + .background(Color(.systemBackground)) + } + + /// Box showing where the main chart's visible window currently sits. + private func viewportBox(width: CGFloat, height: CGFloat, domainSeconds: TimeInterval) -> some View { + let startFraction = interaction.scrollPosition.timeIntervalSince(model.domainStart) / domainSeconds + let boxWidth = min(max(2, CGFloat(interaction.visibleSeconds / domainSeconds) * width), width) + let boxLeft = min(max(CGFloat(startFraction) * width, 0), width - boxWidth) + return Rectangle() + .fill(Color.primary.opacity(0.08)) + .overlay(Rectangle().stroke(Color.primary.opacity(0.35), lineWidth: 1)) + .frame(width: boxWidth, height: height) + .offset(x: boxLeft) + } + + /// Maps a touch x to a time and recenters the main chart on it (clamped to + /// the data window). Following re-arms automatically via the main chart's + /// scroll observer if the user lands back at the latest data. + private func navigate(toLocalX x: CGFloat, width: CGFloat, domainSeconds: TimeInterval, animated: Bool) { + let fraction = min(max(x / width, 0), 1) + let date = model.domainStart.addingTimeInterval(domainSeconds * TimeInterval(fraction)) + let length = interaction.visibleSeconds + var target = date.addingTimeInterval(-length / 2) + let minStart = model.domainStart + let maxStart = model.domainEnd.addingTimeInterval(-length) + if maxStart > minStart { + target = min(max(target, minStart), maxStart) + } + // Same rule as the main chart's scrollToNow: only nearby targets (still + // inside the render window) animate cleanly; long jumps snap. Live + // drag-tracking never animates — each move must land within the frame. + if animated, abs(target.timeIntervalSince(interaction.scrollPosition)) <= interaction.visibleSeconds { + withAnimation(.easeInOut(duration: 0.3)) { + interaction.scrollPosition = target + } + } else { + interaction.scrollPosition = target + } + } +} + +// MARK: - Canvas (laid out once per data / zoom change; translated while panning) + +/// The chart content, laid out over the render window at fixed width. Stored +/// properties deliberately exclude the pan position, and the explicit +/// Equatable conformance below compares only cheap metadata (`generation` +/// stands in for the data arrays) — so SwiftUI provably skips this body +/// during panning and momentum. +private struct BGChartCanvas: View, Equatable { + let model: BGChartModel // plain reference on purpose: updates arrive via `generation` + let generation: Int + let isSmall: Bool + let windowStart: Date + let windowEnd: Date + let canvasWidth: CGFloat + let height: CGFloat + let visibleSeconds: TimeInterval + let timeZone: TimeZone + + static func == (lhs: BGChartCanvas, rhs: BGChartCanvas) -> Bool { + lhs.generation == rhs.generation && + lhs.isSmall == rhs.isSmall && + lhs.windowStart == rhs.windowStart && + lhs.windowEnd == rhs.windowEnd && + lhs.canvasWidth == rhs.canvasWidth && + lhs.height == rhs.height && + lhs.visibleSeconds == rhs.visibleSeconds && + lhs.timeZone == rhs.timeZone + } + + private var basalScale: Double { + guard model.maxBasal > 0 else { return 0 } + return model.maxBG / model.maxBasal + } + + var body: some View { + let showTreatments = !isSmall || model.smallGraphTreatments + let chart = Chart { + if showTreatments { + bgBandMarks + basalMarks + scheduledBasalMarks + } + coneMarks + if !isSmall { + yesterdayMarks + } + bgLineMarks + bgPointsMark + predictionLineMark + predictionVariantMarks + if showTreatments { + treatmentMarks + } + if !isSmall { + ruleMarks + } else if model.showMidnight { + midnightRuleMarks + } + } + .chartXScale(domain: windowStart ... windowEnd) + .chartYScale(domain: 0 ... chartYDomainUpperBound(model.maxBG)) + .chartLegend(.hidden) + .chartYAxis(.hidden) + + return Group { + if isSmall { + chart.chartXAxis(.hidden) + } else { + chart.chartXAxis { + AxisMarks(values: xAxisMarkDates()) { value in + AxisValueLabel { + if let date = value.as(Date.self) { + xAxisLabel(for: date) + } + } + } + } + } + } + .frame(width: canvasWidth, height: height) + } + + // MARK: Windowing + + // The mark series sliced to the render window: marks outside it clip + // invisibly but still cost layout, so an unfiltered re-layout does several + // times the work for nothing. + + private func windowed(_ items: [T], date: (T) -> Date) -> [T] { + items.filter { item in + let d = date(item) + return d >= windowStart && d <= windowEnd + } + } + + /// For line/area series: like `windowed`, but keeps one point beyond each + /// edge so segments crossing (or spanning) the window survive. Assumes + /// ascending dates. + private func windowedLine(_ items: [T], date: (T) -> Date) -> [T] { + guard items.count > 1 else { return items } + let firstInside = items.firstIndex { date($0) >= windowStart } ?? items.count + let firstBeyond = items.firstIndex { date($0) > windowEnd } ?? items.count - 1 + let lo = max(firstInside - 1, 0) + let hi = min(firstBeyond, items.count - 1) + guard lo <= hi else { return [] } + return Array(items[lo ... hi]) + } + + // MARK: X axis + + /// Grid/label stride ladder for the current zoom level. + private var xAxisStrideSeconds: TimeInterval { + let visibleHours = visibleSeconds / 3600 + if visibleHours <= 2 { return 1800 } + if visibleHours <= 6 { return 3600 } + if visibleHours <= 12 { return 2 * 3600 } + return 4 * 3600 + } + + /// Axis mark dates anchored to local midnight (in the configured graph + /// time zone), unlike `.stride(by: .hour)`, which anchors to the domain start. + private func xAxisMarkDates() -> [Date] { + var cal = Calendar(identifier: .gregorian) + cal.timeZone = timeZone + let stride = xAxisStrideSeconds + var mark = cal.startOfDay(for: windowStart) + var marks: [Date] = [] + while mark <= windowEnd { + if mark >= windowStart { + marks.append(mark) + } + mark = mark.addingTimeInterval(stride) + } + return marks + } + + /// Midnight ticks carry the day ("Tue 7") so panned-back history stays + /// unambiguous; other ticks show the hour (plus minutes at sub-hour strides). + private func xAxisLabel(for date: Date) -> some View { + var cal = Calendar(identifier: .gregorian) + cal.timeZone = timeZone + let comps = cal.dateComponents([.hour, .minute], from: date) + return Group { + if comps.hour == 0, comps.minute == 0 { + Text(date, format: .dateTime.weekday(.abbreviated).day()) + } else if xAxisStrideSeconds < 3600 { + Text(date, format: .dateTime.hour().minute()) + } else { + Text(date, format: .dateTime.hour()) + } + } + .font(.footnote) + .environment(\.timeZone, timeZone) + } + + // MARK: Marks + + @ChartContentBuilder + private var bgBandMarks: some ChartContent { + ForEach(model.overrides.filter { $0.end >= windowStart && $0.start <= windowEnd }) { band in + RectangleMark( + xStart: .value("start", band.start), + xEnd: .value("end", band.end), + yStart: .value("yBottom", band.yBottom), + yEnd: .value("yTop", band.yTop) + ) + .foregroundStyle(model.overrideColor.opacity(0.6)) + } + + ForEach(model.tempTargets.filter { $0.end >= windowStart && $0.start <= windowEnd }) { band in + RectangleMark( + xStart: .value("start", band.start), + xEnd: .value("end", band.end), + yStart: .value("yBottom", band.yBottom), + yEnd: .value("yTop", band.yTop) + ) + .foregroundStyle(model.tempTargetColor.opacity(0.5)) + } + } + + @ChartContentBuilder + private var basalMarks: some ChartContent { + ForEach(model.basal.filter { $0.end >= windowStart && $0.start <= windowEnd }) { step in + RectangleMark( + xStart: .value("start", step.start), + xEnd: .value("end", step.end), + yStart: .value("yBottom", 0.0), + yEnd: .value("rate", step.rate * basalScale) + ) + .foregroundStyle(.blue.opacity(0.35)) + } + } + + @ChartContentBuilder + private var scheduledBasalMarks: some ChartContent { + ForEach(windowedLine(model.basalScheduled) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("rate", pt.rate * basalScale), + series: .value("series", "basalScheduled") + ) + .lineStyle(StrokeStyle(lineWidth: 2, dash: [10, 5])) + .foregroundStyle(Color.blue.opacity(0.8)) + } + } + + @ChartContentBuilder + private var coneMarks: some ChartContent { + ForEach(windowedLine(model.cone) { $0.date }) { pt in + AreaMark( + x: .value("time", pt.date), + yStart: .value("yMin", pt.yMin), + yEnd: .value("yMax", pt.yMax) + ) + // Same fill as Trio's cone. + .foregroundStyle(Color(.systemBlue).opacity(0.4)) + .interpolationMethod(.monotone) + } + } + + @ChartContentBuilder + private var yesterdayMarks: some ChartContent { + ForEach(windowedLine(model.yesterday) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("bg", pt.value), + series: .value("series", "yesterday") + ) + .foregroundStyle(Color(.systemGray).opacity(0.4)) + .lineStyle(StrokeStyle(lineWidth: 1.5)) + .interpolationMethod(.linear) + } + } + + @ChartContentBuilder + private var bgLineMarks: some ChartContent { + if model.showLines { + ForEach(model.bgRuns) { run in + if let first = run.points.first, let last = run.points.last, + last.date >= windowStart, first.date <= windowEnd + { + ForEach(windowedLine(run.points) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("bg", pt.value), + series: .value("series", "bg-\(run.id)") + ) + .foregroundStyle(run.color) + .lineStyle(StrokeStyle(lineWidth: 2.5)) + .interpolationMethod(.linear) + } + } + } + } + } + + @ChartContentBuilder + private var bgPointsMark: some ChartContent { + if model.showDots { + ForEach(windowed(model.bg) { $0.date }) { pt in + PointMark( + x: .value("time", pt.date), + y: .value("bg", pt.value) + ) + .symbolSize(isSmall ? 14 : 30) + .foregroundStyle(pt.color) + } + } + } + + @ChartContentBuilder + private var predictionLineMark: some ChartContent { + ForEach(windowedLine(model.prediction) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("bg", pt.value), + series: .value("series", "prediction") + ) + .foregroundStyle(.purple) + .lineStyle(StrokeStyle(lineWidth: 2)) + } + } + + @ChartContentBuilder + private var predictionVariantMarks: some ChartContent { + ForEach(windowedLine(model.ztPrediction) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("bg", pt.value), + series: .value("series", "zt") + ) + .foregroundStyle(Color("ZT")) + .lineStyle(StrokeStyle(lineWidth: 2)) + } + + ForEach(windowedLine(model.iobPrediction) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("bg", pt.value), + series: .value("series", "iob") + ) + .foregroundStyle(Color("Insulin")) + .lineStyle(StrokeStyle(lineWidth: 2)) + } + + ForEach(windowedLine(model.cobPrediction) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("bg", pt.value), + series: .value("series", "cob") + ) + .foregroundStyle(Color(.systemOrange)) + .lineStyle(StrokeStyle(lineWidth: 2)) + } + + ForEach(windowedLine(model.uamPrediction) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("bg", pt.value), + series: .value("series", "uam") + ) + .foregroundStyle(Color("UAM")) + .lineStyle(StrokeStyle(lineWidth: 2)) + } + } + + @ChartContentBuilder + private var treatmentMarks: some ChartContent { + ForEach(windowed(model.boluses) { $0.drawnDate }) { pt in + PointMark( + x: .value("time", pt.drawnDate), + y: .value("sgv", pt.sgv) + ) + .symbolSize(isSmall ? 24 : 64) + .foregroundStyle(Color.blue.opacity(0.75)) + .annotation(position: .top, alignment: .center) { + if !isSmall, Storage.shared.showValues.value { + Text(pt.label).font(.caption2).foregroundColor(.primary) + } + } + } + + ForEach(windowed(model.carbs) { $0.drawnDate }) { pt in + PointMark( + x: .value("time", pt.drawnDate), + y: .value("sgv", pt.sgv) + ) + .symbolSize(isSmall ? 24 : 64) + .foregroundStyle(Color.orange.opacity(0.75)) + .annotation(position: .top, alignment: .center) { + if !isSmall, Storage.shared.showValues.value { + Text(pt.label).font(.caption2).foregroundColor(.primary) + } + } + } + + ForEach(windowed(model.smbs) { $0.drawnDate }) { pt in + PointMark( + x: .value("time", pt.drawnDate), + y: .value("sgv", pt.sgv) + ) + .symbol(DownwardTriangle()) + .symbolSize(isSmall ? 28 : 64) + .foregroundStyle(Color.blue.opacity(0.75)) + .annotation(position: .top, alignment: .center) { + if !isSmall, Storage.shared.showValues.value { + Text(pt.label).font(.caption2).foregroundColor(.primary) + } + } + } + + ForEach(windowed(model.bgChecks) { $0.drawnDate }) { pt in + PointMark( + x: .value("time", pt.drawnDate), + y: .value("sgv", pt.sgv) + ) + .symbolSize(isSmall ? 22 : 54) + .foregroundStyle(Color.red.opacity(0.75)) + } + + ForEach(windowed(model.suspends) { $0.drawnDate }) { pt in + PointMark( + x: .value("time", pt.drawnDate), + y: .value("sgv", pt.sgv) + ) + .symbol(.square) + .symbolSize(isSmall ? 22 : 54) + .foregroundStyle(Color.teal.opacity(0.75)) + } + + ForEach(windowed(model.resumes) { $0.drawnDate }) { pt in + PointMark( + x: .value("time", pt.drawnDate), + y: .value("sgv", pt.sgv) + ) + .symbol(.square) + .symbolSize(isSmall ? 22 : 54) + .foregroundStyle(Color.teal.opacity(0.5)) + } + + ForEach(windowed(model.sensorStarts) { $0.drawnDate }) { pt in + PointMark( + x: .value("time", pt.drawnDate), + y: .value("sgv", pt.sgv) + ) + .symbol(.cross) + .symbolSize(isSmall ? 22 : 54) + .foregroundStyle(Color.indigo.opacity(0.75)) + } + + ForEach(windowed(model.notes) { $0.drawnDate }) { pt in + PointMark( + x: .value("time", pt.drawnDate), + y: .value("sgv", pt.sgv) + ) + .symbolSize(isSmall ? 22 : 54) + .foregroundStyle(Color.gray.opacity(0.75)) + } + } + + @ChartContentBuilder + private var ruleMarks: some ChartContent { + RuleMark(y: .value("low", model.lowLine)) + .lineStyle(StrokeStyle(lineWidth: 2)) + .foregroundStyle(Color.red.opacity(0.5)) + + RuleMark(y: .value("high", model.highLine)) + .lineStyle(StrokeStyle(lineWidth: 2)) + .foregroundStyle(Color.yellow.opacity(0.5)) + + if model.now >= windowStart, model.now <= windowEnd { + RuleMark(x: .value("now", model.now)) + .lineStyle(StrokeStyle(lineWidth: 1)) + .foregroundStyle(Color.gray.opacity(0.6)) + } + + if model.show30Min, let d = model.thirtyMinMark, d >= windowStart, d <= windowEnd { + RuleMark(x: .value("30m", d)) + .lineStyle(StrokeStyle(lineWidth: 0.5)) + .foregroundStyle(Color.blue.opacity(0.7)) + } + if model.show90Min, let d = model.ninetyMinMark, d >= windowStart, d <= windowEnd { + RuleMark(x: .value("90m", d)) + .lineStyle(StrokeStyle(lineWidth: 0.5)) + .foregroundStyle(Color.orange.opacity(0.7)) + } + if model.showDIA { + ForEach(model.diaMarkers.filter { $0 >= windowStart && $0 <= windowEnd }, id: \.self) { d in + RuleMark(x: .value("dia", d)) + .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [2, 2])) + .foregroundStyle(Color.gray.opacity(0.5)) + } + } + if model.showMidnight { + midnightRuleMarks + } + } + + @ChartContentBuilder + private var midnightRuleMarks: some ChartContent { + ForEach(model.midnightMarkers.filter { $0 >= windowStart && $0 <= windowEnd }, id: \.self) { d in + RuleMark(x: .value("midnight", d)) + .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [5, 3])) + .foregroundStyle(Color.teal.opacity(0.6)) + } + } +} + +// MARK: - Pinned y-axis overlay + +/// Renders only the y-axes — BG values trailing, basal rates leading — at a +/// fixed position over the scrolling canvas, plus a phantom x-axis label that +/// reserves exactly the height of the canvas's hour labels so both plots end +/// at the same y. Also reports its plot frame so the shell's selection +/// overlay shares the same value-to-pixel mapping. +private struct StaticYAxisOverlay: View, Equatable { + let generation: Int + let maxBG: Double + let maxBasal: Double + + private var basalScale: Double { + guard maxBasal > 0 else { return 0 } + return maxBG / maxBasal + } + + private var leadingBasalTicks: [Double] { + guard basalScale > 0 else { return [] } + let ticks = stride(from: 0.0, through: maxBasal, by: max(0.5, maxBasal / 4)) + .map { $0 * basalScale } + return Array(ticks) + } + + var body: some View { + Chart { + // Invisible content at the domain corners so the scales resolve + // and the plot (and with it the axes) materializes. + PointMark(x: .value("edge", 0.0), y: .value("min", 0.0)) + .opacity(0) + PointMark(x: .value("edge", 1.0), y: .value("max", maxBG)) + .opacity(0) + } + .chartXScale(domain: 0 ... 1) + .chartYScale(domain: 0 ... chartYDomainUpperBound(maxBG)) + .chartLegend(.hidden) + .chartXAxis { + AxisMarks(values: [0.5]) { _ in + AxisValueLabel { + Text("00").font(.footnote).foregroundStyle(.clear) + } + } + } + .chartYAxis { + AxisMarks(position: .trailing) { value in + AxisTick() + AxisValueLabel { + if let v = value.as(Double.self) { + Text(Localizer.toDisplayUnits(String(v))) + } + } + } + AxisMarks(position: .leading, values: leadingBasalTicks) { value in + AxisTick() + AxisValueLabel { + if let v = value.as(Double.self) { + let rate = basalScale > 0 ? v / basalScale : 0 + Text(String(format: "%.1fU", rate)) + } + } + } + } + .chartOverlay { proxy in + GeometryReader { geo in + Color.clear.preference( + key: PlotFramePreferenceKey.self, + value: proxy.plotFrame.map { geo[$0] } ?? .zero + ) + } + } + } +} + +private struct PlotFramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() + } +} + +/// Rendered size of the visible selection pill, reported so the shell can +/// clamp its position using the real (wrapped) dimensions. +private struct PillSizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() + } +} + +// MARK: - Shared pieces + +private struct DownwardTriangle: ChartSymbolShape { + var perceptualUnitRect: CGRect { CGRect(x: 0, y: 0, width: 1, height: 1) } + + func path(in rect: CGRect) -> Path { + var p = Path() + p.move(to: CGPoint(x: rect.minX, y: rect.minY)) + p.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) + p.addLine(to: CGPoint(x: rect.midX, y: rect.maxY)) + p.closeSubpath() + return p + } +} + +private struct PillLabel: View { + /// One entry per selected item. A lone entry keeps its multi-line layout; + /// several stack as compact one-line-per-item rows so the pill stays + /// readable over a busy cluster. + let texts: [String] + let maxWidth: CGFloat + + var body: some View { + content + .padding(.horizontal, 6) + .padding(.vertical, 3) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(Color(.secondarySystemBackground)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(Color.primary, lineWidth: 0.5) + ) + ) + .background( + GeometryReader { geo in + Color.clear.preference(key: PillSizePreferenceKey.self, value: geo.size) + } + ) + // Transparent flexible container: it caps the width the text can + // wrap to, while the visible pill above still hugs its content. + .frame(maxWidth: maxWidth) + } + + @ViewBuilder + private var content: some View { + if texts.count == 1 { + // Bound pathological texts; a note this long is better read in + // Nightscout than on a chart pill. + entry(texts[0], lineLimit: 10) + } else { + VStack(spacing: 3) { + ForEach(texts.indices, id: \.self) { index in + if index > 0 { + // Fixed-width hairline: a Divider would greedily + // stretch the hugging pill to its max width. + Rectangle() + .fill(Color.primary.opacity(0.25)) + .frame(width: 46, height: 0.5) + } + // Stacked items collapse to "Bolus 2.5U 14:32" rows. + entry(texts[index].replacingOccurrences(of: "\n", with: " "), lineLimit: 4) + } + } + } + } + + private func entry(_ text: String, lineLimit: Int) -> some View { + Text(text) + .font(.caption2) + .foregroundColor(.primary) + .multilineTextAlignment(.center) + .lineLimit(lineLimit) + } +} diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index 7f32fd181..fafd2bf6f 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -58,7 +58,30 @@ class AlarmSound { } static func setSoundFile(str: String) { - soundURL = Bundle.main.url(forResource: str, withExtension: "caf")! + setSoundFile(.builtin(str)) + } + + static func setSoundFile(_ file: SoundFile) { + if let resolved = resolveURL(for: file) { + soundURL = resolved + return + } + LogManager.shared.log( + category: .alarm, + message: "AlarmSound - missing sound for \(file.id); falling back to \(SoundFile.fallback.id)" + ) + if let fallback = resolveURL(for: .fallback) { + soundURL = fallback + } + } + + private static func resolveURL(for file: SoundFile) -> URL? { + switch file { + case let .builtin(name): + return Bundle.main.url(forResource: name, withExtension: "caf") + case let .custom(uuid): + return CustomSoundStore.shared.url(for: uuid) + } } /* diff --git a/LoopFollow/Controllers/BackgroundAlertManager.swift b/LoopFollow/Controllers/BackgroundAlertManager.swift index 0ba3664b1..8b844c983 100644 --- a/LoopFollow/Controllers/BackgroundAlertManager.swift +++ b/LoopFollow/Controllers/BackgroundAlertManager.swift @@ -76,7 +76,10 @@ class BackgroundAlertManager { removeDeliveredNotifications() let isBluetoothActive = Storage.shared.backgroundRefreshType.value.isBluetooth - let expectedHeartbeat = BLEManager.shared.expectedHeartbeatInterval() + // Only query BLEManager for a Bluetooth mode — touching it otherwise would + // initialize CoreBluetooth and trigger the permission prompt for users + // (e.g. Silent Tune) who never opted into Bluetooth. + let expectedHeartbeat = isBluetoothActive ? BLEManager.shared.expectedHeartbeatInterval() : nil // Define alerts let alerts: [BackgroundAlert] = [ diff --git a/LoopFollow/Controllers/Graphs.swift b/LoopFollow/Controllers/Graphs.swift deleted file mode 100644 index 184b67b8f..000000000 --- a/LoopFollow/Controllers/Graphs.swift +++ /dev/null @@ -1,2182 +0,0 @@ -// LoopFollow -// Graphs.swift - -import Charts -import Foundation -import UIKit - -import Charts - -/// Fill colors for the override and temp-target bars on the BG graph. -/// -/// Loop draws overrides green and temp targets purple, while Trio (and other -/// OpenAPS-based algorithms) use the inverse — overrides purple, temp targets -/// green. We follow the active backend's convention so the colors match the -/// looping app the user is running. -enum TreatmentGraphColors { - static var override: NSUIColor { - Storage.shared.device.value == "Loop" ? .systemGreen : .systemPurple - } - - static var tempTarget: NSUIColor { - Storage.shared.device.value == "Loop" ? .systemPurple : .systemGreen - } -} - -enum GraphDataIndex: Int { - case bg = 0 - case prediction = 1 - case basal = 2 - case bolus = 3 - case carbs = 4 - case basalScheduled = 5 - case override = 6 - case bgCheck = 7 - case suspend = 8 - case resumePump = 9 - case sensorStart = 10 - case note = 11 - case ztPrediction = 12 - case iobPrediction = 13 - case cobPrediction = 14 - case uamPrediction = 15 - case smb = 16 - case tempTarget = 17 - case predictionCone = 18 - case yesterday = 19 -} - -extension GraphDataIndex { - var description: String { - switch self { - case .bg: return "BG" - case .prediction: return "Prediction" - case .basal: return "Basal" - case .bolus: return "Bolus" - case .carbs: return "Carbs" - case .basalScheduled: return "Basal Scheduled" - case .override: return "Override" - case .bgCheck: return "BG Check" - case .suspend: return "Suspend" - case .resumePump: return "Resume Pump" - case .sensorStart: return "Sensor Start" - case .note: return "Note" - case .ztPrediction: return "ZT Prediction" - case .iobPrediction: return "IOB Prediction" - case .cobPrediction: return "COB Prediction" - case .uamPrediction: return "UAM Prediction" - case .smb: return "SMB" - case .tempTarget: return "Temp Target" - case .predictionCone: return "Prediction Cone" - case .yesterday: return "Yesterday" - } - } -} - -class CompositeRenderer: LineChartRenderer { - let tempTargetRenderer: TempTargetRenderer - let triangleRenderer: TriangleRenderer - let coneRenderer: ConeOfUncertaintyRenderer - - init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, tempTargetDataSetIndex: Int, smbDataSetIndex: Int, coneDataSetIndex: Int) { - tempTargetRenderer = TempTargetRenderer( - dataProvider: dataProvider, - animator: animator, - viewPortHandler: viewPortHandler, - tempTargetDataSetIndex: tempTargetDataSetIndex - ) - triangleRenderer = TriangleRenderer( - dataProvider: dataProvider, - animator: animator, - viewPortHandler: viewPortHandler, - smbDataSetIndex: smbDataSetIndex - ) - coneRenderer = ConeOfUncertaintyRenderer( - dataProvider: dataProvider, - animator: animator, - viewPortHandler: viewPortHandler, - coneDataSetIndex: coneDataSetIndex - ) - super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!) - } - - override func drawExtras(context: CGContext) { - super.drawExtras(context: context) - coneRenderer.drawExtras(context: context) - tempTargetRenderer.drawExtras(context: context) - triangleRenderer.drawExtras(context: context) - } -} - -class TriangleRenderer: LineChartRenderer { - let smbDataSetIndex: Int - - init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, smbDataSetIndex: Int) { - self.smbDataSetIndex = smbDataSetIndex - super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!) - } - - override func drawExtras(context: CGContext) { - super.drawExtras(context: context) - - guard let dataProvider = dataProvider else { return } - - if dataProvider.lineData?.dataSets.count ?? 0 > smbDataSetIndex, let lineDataSet = dataProvider.lineData?.dataSets[smbDataSetIndex] as? LineChartDataSet { - let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency) - let phaseY = animator.phaseY - - for j in 0 ..< lineDataSet.entryCount { - guard let e = lineDataSet.entryForIndex(j) else { continue } - - let pt = trans.pixelForValues(x: e.x, y: e.y * phaseY) - - context.saveGState() - context.beginPath() - context.move(to: CGPoint(x: pt.x, y: pt.y + 9)) - context.addLine(to: CGPoint(x: pt.x - 5, y: pt.y - 1)) - context.addLine(to: CGPoint(x: pt.x + 5, y: pt.y - 1)) - context.closePath() - - context.setFillColor(lineDataSet.circleColors.first!.cgColor) - context.fillPath() - - context.restoreGState() - } - } - } -} - -class TempTargetChartDataEntry: ChartDataEntry { - var xStart: Double = 0.0 - var xEnd: Double = 0.0 - var yTop: Double = 0.0 - var yBottom: Double = 0.0 - - required init() { - super.init() - } - - init(xStart: Double, xEnd: Double, yTop: Double, yBottom: Double, data: Any?) { - self.xStart = xStart - self.xEnd = xEnd - self.yTop = yTop - self.yBottom = yBottom - - super.init(x: xStart, y: yTop) - self.data = data - } - - override func copy(with _: NSZone? = nil) -> Any { - let copy = TempTargetChartDataEntry( - xStart: xStart, - xEnd: xEnd, - yTop: yTop, - yBottom: yBottom, - data: data - ) - return copy - } -} - -class TempTargetRenderer: LineChartRenderer { - let tempTargetDataSetIndex: Int - - init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, tempTargetDataSetIndex: Int) { - self.tempTargetDataSetIndex = tempTargetDataSetIndex - super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!) - } - - override func drawExtras(context: CGContext) { - super.drawExtras(context: context) - - guard let dataProvider = dataProvider else { return } - - if dataProvider.lineData?.dataSets.count ?? 0 > tempTargetDataSetIndex, - let lineDataSet = dataProvider.lineData?.dataSets[tempTargetDataSetIndex] as? LineChartDataSet - { - let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency) - let phaseY = animator.phaseY - - for i in 0 ..< lineDataSet.entryCount { - guard let entry = lineDataSet.entryForIndex(i) as? TempTargetChartDataEntry else { continue } - - let xStart = entry.xStart - let xEnd = entry.xEnd - let yTop = entry.yTop * phaseY - let yBottom = entry.yBottom * phaseY - - let leftTop = trans.pixelForValues(x: xStart, y: yTop) - let rightBottom = trans.pixelForValues(x: xEnd, y: yBottom) - - var rect = CGRect(x: leftTop.x, y: leftTop.y, width: rightBottom.x - leftTop.x, height: rightBottom.y - leftTop.y) - if rect.width < 0 { - rect.origin.x += rect.width - rect.size.width = abs(rect.width) - } - if rect.height < 0 { - rect.origin.y += rect.height - rect.size.height = abs(rect.height) - } - - context.saveGState() - context.setFillColor(TreatmentGraphColors.tempTarget.withAlphaComponent(0.5).cgColor) - context.fill(rect) - context.restoreGState() - } - } - } -} - -let ScaleXMax: Double = 150.0 -extension MainViewController { - private func graphRangeThresholds() -> (low: Double, high: Double) { - UnitSettingsStore.shared.effectiveThresholds() - } - - func updateChartRenderers() { - let tempTargetDataIndex = GraphDataIndex.tempTarget.rawValue - let smbDataIndex = GraphDataIndex.smb.rawValue - let coneDataIndex = GraphDataIndex.predictionCone.rawValue - - let compositeRenderer = CompositeRenderer( - dataProvider: BGChart, - animator: BGChart.chartAnimator, - viewPortHandler: BGChart.viewPortHandler, - tempTargetDataSetIndex: tempTargetDataIndex, - smbDataSetIndex: smbDataIndex, - coneDataSetIndex: coneDataIndex - ) - BGChart.renderer = compositeRenderer - - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - } - - func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight _: Highlight) { - if chartView == BGChartFull { - BGChart.moveViewToX(entry.x) - } - if entry.data as? String == "hide" { - BGChart.highlightValue(nil, callDelegate: false) - } - } - - func chartScaled(_: ChartViewBase, scaleX _: CGFloat, scaleY _: CGFloat) { - // dont store huge values - var scale = Double(BGChart.scaleX) - if scale > ScaleXMax { - scale = ScaleXMax - } - Storage.shared.chartScaleX.value = scale - } - - func createGraph() { - // Create the BG Graph Data - let bgChartEntry = [ChartDataEntry]() - let maxBG = Storage.shared.minBGScale.value - - // Setup BG line details - let lineBG = LineChartDataSet(entries: bgChartEntry, label: "") - lineBG.circleRadius = CGFloat(globalVariables.dotBG) - lineBG.circleColors = [NSUIColor.systemGreen] - lineBG.drawCircleHoleEnabled = false - lineBG.axisDependency = YAxis.AxisDependency.right - lineBG.highlightEnabled = true - lineBG.drawValuesEnabled = false - - if Storage.shared.showLines.value { - lineBG.lineWidth = 2 - } else { - lineBG.lineWidth = 0 - } - if Storage.shared.showDots.value { - lineBG.drawCirclesEnabled = true - } else { - lineBG.drawCirclesEnabled = false - } - lineBG.setDrawHighlightIndicators(false) - lineBG.valueFont.withSize(50) - - // Setup Prediction line details - let predictionChartEntry = [ChartDataEntry]() - let linePrediction = LineChartDataSet(entries: predictionChartEntry, label: "") - linePrediction.circleRadius = CGFloat(globalVariables.dotBG) - linePrediction.circleColors = [NSUIColor.systemPurple] - linePrediction.colors = [NSUIColor.systemPurple] - linePrediction.drawCircleHoleEnabled = false - linePrediction.axisDependency = YAxis.AxisDependency.right - linePrediction.highlightEnabled = true - linePrediction.drawValuesEnabled = false - - if Storage.shared.showLines.value { - linePrediction.lineWidth = 2 - } else { - linePrediction.lineWidth = 0 - } - if Storage.shared.showDots.value { - linePrediction.drawCirclesEnabled = true - } else { - linePrediction.drawCirclesEnabled = false - } - linePrediction.setDrawHighlightIndicators(false) - linePrediction.valueFont.withSize(50) - - // create Basal graph data - let chartEntry = [ChartDataEntry]() - let maxBasal = Storage.shared.minBasalScale.value - let lineBasal = LineChartDataSet(entries: chartEntry, label: "") - lineBasal.setDrawHighlightIndicators(false) - lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5) - lineBasal.lineWidth = 0 - lineBasal.drawFilledEnabled = true - lineBasal.fillColor = NSUIColor.systemBlue - lineBasal.fillAlpha = 0.5 - lineBasal.drawCirclesEnabled = false - lineBasal.axisDependency = YAxis.AxisDependency.left - lineBasal.highlightEnabled = true - lineBasal.drawValuesEnabled = false - lineBasal.fillFormatter = basalFillFormatter() - - // Boluses - let chartEntryBolus = [ChartDataEntry]() - let lineBolus = LineChartDataSet(entries: chartEntryBolus, label: "") - lineBolus.circleRadius = CGFloat(globalVariables.dotBolus) - lineBolus.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)] - lineBolus.drawCircleHoleEnabled = false - lineBolus.setDrawHighlightIndicators(false) - lineBolus.setColor(NSUIColor.systemBlue, alpha: 1.0) - lineBolus.lineWidth = 0 - lineBolus.axisDependency = YAxis.AxisDependency.right - lineBolus.valueFormatter = ChartYDataValueFormatter() - lineBolus.valueTextColor = NSUIColor.label - lineBolus.fillColor = NSUIColor.systemBlue - lineBolus.fillAlpha = 0.6 - - lineBolus.drawCirclesEnabled = true - lineBolus.drawFilledEnabled = false - - lineBolus.drawValuesEnabled = Storage.shared.showValues.value - lineBolus.highlightEnabled = true - - // Carbs - let chartEntryCarbs = [ChartDataEntry]() - let lineCarbs = LineChartDataSet(entries: chartEntryCarbs, label: "") - lineCarbs.circleRadius = CGFloat(globalVariables.dotCarb) - lineCarbs.circleColors = [NSUIColor.systemOrange.withAlphaComponent(0.75)] - lineCarbs.drawCircleHoleEnabled = false - lineCarbs.setDrawHighlightIndicators(false) - lineCarbs.setColor(NSUIColor.systemBlue, alpha: 1.0) - lineCarbs.lineWidth = 0 - lineCarbs.axisDependency = YAxis.AxisDependency.right - lineCarbs.valueFormatter = ChartYDataValueFormatter() - lineCarbs.valueTextColor = NSUIColor.label - lineCarbs.fillColor = NSUIColor.systemOrange - lineCarbs.fillAlpha = 0.6 - - lineCarbs.drawCirclesEnabled = true - lineCarbs.drawFilledEnabled = false - - lineCarbs.drawValuesEnabled = Storage.shared.showValues.value - lineCarbs.highlightEnabled = true - - // create Scheduled Basal graph data - let chartBasalScheduledEntry = [ChartDataEntry]() - let lineBasalScheduled = LineChartDataSet(entries: chartBasalScheduledEntry, label: "") - lineBasalScheduled.setDrawHighlightIndicators(false) - lineBasalScheduled.setColor(NSUIColor.systemBlue, alpha: 0.8) - lineBasalScheduled.lineWidth = 2 - lineBasalScheduled.drawFilledEnabled = false - lineBasalScheduled.drawCirclesEnabled = false - lineBasalScheduled.axisDependency = YAxis.AxisDependency.left - lineBasalScheduled.highlightEnabled = false - lineBasalScheduled.drawValuesEnabled = false - lineBasalScheduled.lineDashLengths = [10.0, 5.0] - - // create Override graph data - let chartOverrideEntry = [ChartDataEntry]() - let lineOverride = LineChartDataSet(entries: chartOverrideEntry, label: "") - lineOverride.setDrawHighlightIndicators(false) - lineOverride.lineWidth = 0 - lineOverride.drawFilledEnabled = true - lineOverride.fillFormatter = OverrideFillFormatter() - lineOverride.fillColor = TreatmentGraphColors.override - lineOverride.fillAlpha = 0.6 - lineOverride.drawCirclesEnabled = false - lineOverride.axisDependency = YAxis.AxisDependency.right - lineOverride.highlightEnabled = true - lineOverride.drawValuesEnabled = false - - // BG Check - let chartEntryBGCheck = [ChartDataEntry]() - let lineBGCheck = LineChartDataSet(entries: chartEntryBGCheck, label: "") - lineBGCheck.circleRadius = CGFloat(globalVariables.dotOther) - lineBGCheck.circleColors = [NSUIColor.systemRed.withAlphaComponent(0.75)] - lineBGCheck.drawCircleHoleEnabled = false - lineBGCheck.setDrawHighlightIndicators(false) - lineBGCheck.setColor(NSUIColor.systemRed, alpha: 1.0) - lineBGCheck.drawCirclesEnabled = true - lineBGCheck.lineWidth = 0 - lineBGCheck.highlightEnabled = true - lineBGCheck.axisDependency = YAxis.AxisDependency.right - lineBGCheck.valueFormatter = ChartYDataValueFormatter() - lineBGCheck.drawValuesEnabled = false - - // Suspend Pump - let chartEntrySuspend = [ChartDataEntry]() - let lineSuspend = LineChartDataSet(entries: chartEntrySuspend, label: "") - lineSuspend.circleRadius = CGFloat(globalVariables.dotOther) - lineSuspend.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] - lineSuspend.drawCircleHoleEnabled = false - lineSuspend.setDrawHighlightIndicators(false) - lineSuspend.setColor(NSUIColor.systemGray2, alpha: 1.0) - lineSuspend.drawCirclesEnabled = true - lineSuspend.lineWidth = 0 - lineSuspend.highlightEnabled = true - lineSuspend.axisDependency = YAxis.AxisDependency.right - lineSuspend.valueFormatter = ChartYDataValueFormatter() - lineSuspend.drawValuesEnabled = false - - // Resume Pump - let chartEntryResume = [ChartDataEntry]() - let lineResume = LineChartDataSet(entries: chartEntryResume, label: "") - lineResume.circleRadius = CGFloat(globalVariables.dotOther) - lineResume.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] - lineResume.drawCircleHoleEnabled = false - lineResume.setDrawHighlightIndicators(false) - lineResume.setColor(NSUIColor.systemGray4, alpha: 1.0) - lineResume.drawCirclesEnabled = true - lineResume.lineWidth = 0 - lineResume.highlightEnabled = true - lineResume.axisDependency = YAxis.AxisDependency.right - lineResume.valueFormatter = ChartYDataValueFormatter() - lineResume.drawValuesEnabled = false - - // Sensor Start - let chartEntrySensor = [ChartDataEntry]() - let lineSensor = LineChartDataSet(entries: chartEntrySensor, label: "") - lineSensor.circleRadius = CGFloat(globalVariables.dotOther) - lineSensor.circleColors = [NSUIColor.systemIndigo.withAlphaComponent(0.75)] - lineSensor.drawCircleHoleEnabled = false - lineSensor.setDrawHighlightIndicators(false) - lineSensor.setColor(NSUIColor.systemGray3, alpha: 1.0) - lineSensor.drawCirclesEnabled = true - lineSensor.lineWidth = 0 - lineSensor.highlightEnabled = true - lineSensor.axisDependency = YAxis.AxisDependency.right - lineSensor.valueFormatter = ChartYDataValueFormatter() - lineSensor.drawValuesEnabled = false - - // Notes - let chartEntryNote = [ChartDataEntry]() - let lineNote = LineChartDataSet(entries: chartEntryNote, label: "") - lineNote.circleRadius = CGFloat(globalVariables.dotOther) - lineNote.circleColors = [NSUIColor.systemGray.withAlphaComponent(0.75)] - lineNote.drawCircleHoleEnabled = false - lineNote.setDrawHighlightIndicators(false) - lineNote.setColor(NSUIColor.systemGray3, alpha: 1.0) - lineNote.drawCirclesEnabled = true - lineNote.lineWidth = 0 - lineNote.highlightEnabled = true - lineNote.axisDependency = YAxis.AxisDependency.right - lineNote.valueFormatter = ChartYDataValueFormatter() - lineNote.drawValuesEnabled = false - - // Setup COB Prediction line details - let COBpredictionChartEntry = [ChartDataEntry]() - let COBlinePrediction = LineChartDataSet(entries: COBpredictionChartEntry, label: "") - COBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) - COBlinePrediction.circleColors = [NSUIColor.systemPurple] - COBlinePrediction.colors = [NSUIColor.systemPurple] - COBlinePrediction.drawCircleHoleEnabled = false - COBlinePrediction.axisDependency = YAxis.AxisDependency.right - COBlinePrediction.highlightEnabled = true - COBlinePrediction.drawValuesEnabled = false - - if Storage.shared.showLines.value { - COBlinePrediction.lineWidth = 2 - } else { - COBlinePrediction.lineWidth = 0 - } - if Storage.shared.showDots.value { - COBlinePrediction.drawCirclesEnabled = true - } else { - COBlinePrediction.drawCirclesEnabled = false - } - COBlinePrediction.setDrawHighlightIndicators(false) - COBlinePrediction.valueFont.withSize(50) - - // Setup IOB Prediction line details - let IOBpredictionChartEntry = [ChartDataEntry]() - let IOBlinePrediction = LineChartDataSet(entries: IOBpredictionChartEntry, label: "") - IOBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) - IOBlinePrediction.circleColors = [NSUIColor.systemPurple] - IOBlinePrediction.colors = [NSUIColor.systemPurple] - IOBlinePrediction.drawCircleHoleEnabled = false - IOBlinePrediction.axisDependency = YAxis.AxisDependency.right - IOBlinePrediction.highlightEnabled = true - IOBlinePrediction.drawValuesEnabled = false - - if Storage.shared.showLines.value { - IOBlinePrediction.lineWidth = 2 - } else { - IOBlinePrediction.lineWidth = 0 - } - if Storage.shared.showDots.value { - IOBlinePrediction.drawCirclesEnabled = true - } else { - IOBlinePrediction.drawCirclesEnabled = false - } - IOBlinePrediction.setDrawHighlightIndicators(false) - IOBlinePrediction.valueFont.withSize(50) - - // Setup UAM Prediction line details - let UAMpredictionChartEntry = [ChartDataEntry]() - let UAMlinePrediction = LineChartDataSet(entries: UAMpredictionChartEntry, label: "") - UAMlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) - UAMlinePrediction.circleColors = [NSUIColor.systemPurple] - UAMlinePrediction.colors = [NSUIColor.systemPurple] - UAMlinePrediction.drawCircleHoleEnabled = false - UAMlinePrediction.axisDependency = YAxis.AxisDependency.right - UAMlinePrediction.highlightEnabled = true - UAMlinePrediction.drawValuesEnabled = false - - if Storage.shared.showLines.value { - UAMlinePrediction.lineWidth = 2 - } else { - UAMlinePrediction.lineWidth = 0 - } - if Storage.shared.showDots.value { - UAMlinePrediction.drawCirclesEnabled = true - } else { - UAMlinePrediction.drawCirclesEnabled = false - } - linePrediction.setDrawHighlightIndicators(false) - linePrediction.valueFont.withSize(50) - - // Setup ZT Prediction line details - let ZTpredictionChartEntry = [ChartDataEntry]() - let ZTlinePrediction = LineChartDataSet(entries: ZTpredictionChartEntry, label: "") - ZTlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) - ZTlinePrediction.circleColors = [NSUIColor.systemPurple] - ZTlinePrediction.colors = [NSUIColor.systemPurple] - ZTlinePrediction.drawCircleHoleEnabled = false - ZTlinePrediction.axisDependency = YAxis.AxisDependency.right - ZTlinePrediction.highlightEnabled = true - ZTlinePrediction.drawValuesEnabled = false - - if Storage.shared.showLines.value { - ZTlinePrediction.lineWidth = 2 - } else { - ZTlinePrediction.lineWidth = 0 - } - if Storage.shared.showDots.value { - ZTlinePrediction.drawCirclesEnabled = true - } else { - ZTlinePrediction.drawCirclesEnabled = false - } - ZTlinePrediction.setDrawHighlightIndicators(false) - ZTlinePrediction.valueFont.withSize(50) - - // SMB - let chartEntrySmb = [ChartDataEntry]() - let lineSmb = LineChartDataSet(entries: chartEntrySmb, label: "") - lineSmb.circleRadius = CGFloat(globalVariables.dotBolus) - lineSmb.circleColors = [NSUIColor.systemBlue.withAlphaComponent(1.0)] - lineSmb.drawCircleHoleEnabled = false - lineSmb.setDrawHighlightIndicators(false) - lineSmb.setColor(NSUIColor.red, alpha: 1.0) - lineSmb.lineWidth = 0 - lineSmb.axisDependency = YAxis.AxisDependency.right - lineSmb.valueFormatter = ChartYDataValueFormatter() - lineSmb.valueTextColor = NSUIColor.label - - lineSmb.drawCirclesEnabled = false - lineSmb.drawFilledEnabled = false - - lineSmb.drawValuesEnabled = Storage.shared.showValues.value - lineSmb.highlightEnabled = true - - // TempTarget graph data - let chartTempTargetEntry = [ChartDataEntry]() - let lineTempTarget = LineChartDataSet(entries: chartTempTargetEntry, label: "") - lineTempTarget.setDrawHighlightIndicators(false) - lineTempTarget.lineWidth = 0 - lineTempTarget.drawFilledEnabled = false - lineTempTarget.fillColor = NSUIColor.systemPurple - lineTempTarget.fillAlpha = 0.6 - lineTempTarget.drawCirclesEnabled = false - lineTempTarget.axisDependency = YAxis.AxisDependency.right - lineTempTarget.highlightEnabled = true - lineTempTarget.drawValuesEnabled = false - - // Setup the chart data of all lines - let data = LineChartData() - - data.append(lineBG) // Dataset 0 - data.append(linePrediction) // Dataset 1 - data.append(lineBasal) // Dataset 2 - data.append(lineBolus) // Dataset 3 - data.append(lineCarbs) // Dataset 4 - data.append(lineBasalScheduled) // Dataset 5 - data.append(lineOverride) // Dataset 6 - data.append(lineBGCheck) // Dataset 7 - data.append(lineSuspend) // Dataset 8 - data.append(lineResume) // Dataset 9 - data.append(lineSensor) // Dataset 10 - data.append(lineNote) // Dataset 11 - data.append(ZTlinePrediction) // Dataset 12 - data.append(IOBlinePrediction) // Dataset 13 - data.append(COBlinePrediction) // Dataset 14 - data.append(UAMlinePrediction) // Dataset 15 - data.append(lineSmb) // Dataset 16 - data.append(lineTempTarget) // Dataset 17 - - // Dataset 18: Prediction Cone (rendered via ConeOfUncertaintyRenderer) - let lineCone = LineChartDataSet(entries: [ChartDataEntry](), label: "") - lineCone.lineWidth = 0 - lineCone.drawCirclesEnabled = false - lineCone.drawValuesEnabled = false - lineCone.highlightEnabled = false - lineCone.axisDependency = YAxis.AxisDependency.right - data.append(lineCone) - - // Dataset 19: Yesterday's BG comparison overlay (thin dimmed gray line, no dots) - let lineYesterday = LineChartDataSet(entries: [ChartDataEntry](), label: "") - lineYesterday.lineWidth = 1.5 - lineYesterday.setColor(NSUIColor.systemGray, alpha: 0.4) - lineYesterday.drawCirclesEnabled = false - lineYesterday.drawValuesEnabled = false - lineYesterday.highlightEnabled = false - lineYesterday.axisDependency = YAxis.AxisDependency.right - data.append(lineYesterday) - - data.setValueFont(UIFont.systemFont(ofSize: 12)) - - // Add marker popups for bolus and carbs - let marker = PillMarker(color: .secondarySystemBackground, font: UIFont.boldSystemFont(ofSize: 14), textColor: .label) - BGChart.marker = marker - - // Clear limit lines so they don't add multiples when changing the settings - BGChart.rightAxis.removeAllLimitLines() - - let thresholds = graphRangeThresholds() - - // Add lower red line - let ll = ChartLimitLine() - ll.limit = thresholds.low - ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5) - BGChart.rightAxis.addLimitLine(ll) - - // Add upper yellow line - let ul = ChartLimitLine() - ul.limit = thresholds.high - ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5) - BGChart.rightAxis.addLimitLine(ul) - - // Add vertical lines as configured - createVerticalLines() - startGraphNowTimer() - - // Setup the main graph overall details - BGChart.xAxis.valueFormatter = ChartXValueFormatter() - BGChart.xAxis.granularity = 1800 - BGChart.xAxis.labelTextColor = NSUIColor.label - BGChart.xAxis.labelPosition = XAxis.LabelPosition.bottom - BGChart.xAxis.drawGridLinesEnabled = false - - BGChart.leftAxis.enabled = true - BGChart.leftAxis.labelPosition = YAxis.LabelPosition.insideChart - BGChart.leftAxis.axisMaximum = maxBasal - BGChart.leftAxis.axisMinimum = 0 - BGChart.leftAxis.drawGridLinesEnabled = false - BGChart.leftAxis.granularityEnabled = true - BGChart.leftAxis.granularity = 0.5 - - BGChart.rightAxis.labelTextColor = NSUIColor.label - BGChart.rightAxis.labelPosition = YAxis.LabelPosition.insideChart - BGChart.rightAxis.axisMinimum = 0.0 - BGChart.rightAxis.axisMaximum = Double(maxBG) - BGChart.rightAxis.gridLineDashLengths = [5.0, 5.0] - BGChart.rightAxis.drawGridLinesEnabled = false - BGChart.rightAxis.valueFormatter = ChartYMMOLValueFormatter() - BGChart.rightAxis.granularityEnabled = true - BGChart.rightAxis.granularity = 50 - - BGChart.maxHighlightDistance = 15.0 - BGChart.legend.enabled = false - BGChart.scaleYEnabled = false - BGChart.drawGridBackgroundEnabled = true - BGChart.gridBackgroundColor = NSUIColor.secondarySystemBackground - - BGChart.highlightValue(nil, callDelegate: false) - - BGChart.data = data - BGChart.setExtraOffsets(left: 5, top: 10, right: 5, bottom: 10) - } - - func createVerticalLines() { - BGChart.xAxis.removeAllLimitLines() - BGChartFull.xAxis.removeAllLimitLines() - createNowAndDIALines() - createMidnightLines() - } - - func createNowAndDIALines() { - let ul = ChartLimitLine() - ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC()) - ul.lineColor = NSUIColor.systemGray.withAlphaComponent(0.5) - ul.lineWidth = 1 - BGChart.xAxis.addLimitLine(ul) - - if Storage.shared.show30MinLine.value { - let ul2 = ChartLimitLine() - ul2.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -30 * 60)) - ul2.lineColor = NSUIColor.systemBlue.withAlphaComponent(0.5) - ul2.lineWidth = 1 - BGChart.xAxis.addLimitLine(ul2) - } - - if Storage.shared.showDIALines.value { - for i in 1 ..< 7 { - let ul = ChartLimitLine() - ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC() - Double(i * 60 * 60)) - ul.lineColor = NSUIColor.systemGray.withAlphaComponent(0.3) - let dash = 10.0 - Double(i) - let space = 5.0 + Double(i) - ul.lineDashLengths = [CGFloat(dash), CGFloat(space)] - ul.lineWidth = 1 - BGChart.xAxis.addLimitLine(ul) - } - } - - if Storage.shared.show90MinLine.value { - let ul3 = ChartLimitLine() - ul3.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -90 * 60)) - ul3.lineColor = NSUIColor.systemOrange.withAlphaComponent(0.5) - ul3.lineWidth = 1 - BGChart.xAxis.addLimitLine(ul3) - } - } - - func createMidnightLines() { - // Draw a line at midnight: useful when showing multiple days of data - if Storage.shared.showMidnightLines.value { - var midnightTimeInterval: TimeInterval - if Storage.shared.graphTimeZoneEnabled.value, - let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) - { - var cal = Calendar.current - cal.timeZone = tz - midnightTimeInterval = cal.startOfDay(for: Date()).timeIntervalSince1970 - } else { - midnightTimeInterval = dateTimeUtils.getTimeIntervalMidnightToday() - } - let graphHours = 24 * Storage.shared.downloadDays.value - let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) - while midnightTimeInterval > graphStart { - // Large chart - let ul = ChartLimitLine() - ul.limit = Double(midnightTimeInterval) - ul.lineColor = NSUIColor.systemTeal.withAlphaComponent(0.5) - ul.lineDashLengths = [CGFloat(2), CGFloat(5)] - ul.lineWidth = 1 - BGChart.xAxis.addLimitLine(ul) - - // Small chart - let sl = ChartLimitLine() - sl.limit = Double(midnightTimeInterval) - sl.lineColor = NSUIColor.systemTeal - sl.lineDashLengths = [CGFloat(2), CGFloat(2)] - sl.lineWidth = 1 - BGChartFull.xAxis.addLimitLine(sl) - - midnightTimeInterval = midnightTimeInterval.advanced(by: -24 * 60 * 60) - } - } - } - - func updateBGGraphSettings() { - let dataIndex = 0 - let dataIndexPrediction = 1 - let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - let linePrediction = BGChart.lineData!.dataSets[dataIndexPrediction] as! LineChartDataSet - if Storage.shared.showLines.value { - lineBG.lineWidth = 2 - linePrediction.lineWidth = 2 - } else { - lineBG.lineWidth = 0 - linePrediction.lineWidth = 0 - } - if Storage.shared.showDots.value { - lineBG.drawCirclesEnabled = true - linePrediction.drawCirclesEnabled = true - } else { - lineBG.drawCirclesEnabled = false - linePrediction.drawCirclesEnabled = false - } - - BGChart.rightAxis.axisMinimum = 0 - - // Clear limit lines so they don't add multiples when changing the settings - BGChart.rightAxis.removeAllLimitLines() - - let thresholds = graphRangeThresholds() - - // Add lower red line - let ll = ChartLimitLine() - ll.limit = thresholds.low - ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5) - BGChart.rightAxis.addLimitLine(ll) - - // Add upper yellow line - let ul = ChartLimitLine() - ul.limit = thresholds.high - ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5) - BGChart.rightAxis.addLimitLine(ul) - - // Re-create vertical markers in case their settings changed - createVerticalLines() - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - - // Reflect the yesterday overlay toggle immediately, and reload the BG window - // so the extra day of history is fetched (or dropped) when the toggle changed. - updateYesterdayBGGraph() - TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date()) - - // Re-render prediction display in case display type changed - updateOpenAPSPredictionDisplay() - } - - func updateBGGraph() { - let dataIndex = 0 - let entries = bgData - guard !entries.isEmpty else { - return - } - let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - mainChart.removeAll(keepingCapacity: false) - smallChart.removeAll(keepingCapacity: false) - let maxBGOffset: Double = 50 - - var colors = [NSUIColor]() - - topBG = Storage.shared.minBGScale.value - let thresholds = graphRangeThresholds() - for i in 0 ..< entries.count { - // Clamp the plotted y-value to the same bounds the header text uses - // (HIGH/LOW), so the graph stays consistent with the main display. - // The pill tooltip still shows the raw reading. - let plottedSgv = Double(min(max(entries[i].sgv, globalVariables.minDisplayGlucose), globalVariables.maxDisplayGlucose)) - if plottedSgv > topBG - maxBGOffset { - topBG = plottedSgv + maxBGOffset - } - let value = ChartDataEntry(x: Double(entries[i].date), y: plottedSgv, data: formatPillText(line1: Localizer.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date)) - mainChart.append(value) - smallChart.append(value) - - if Double(entries[i].sgv) >= thresholds.high { - colors.append(NSUIColor.systemYellow) - } else if Double(entries[i].sgv) <= thresholds.low { - colors.append(NSUIColor.systemRed) - } else { - colors.append(NSUIColor.systemGreen) - } - } - - // Set Colors - let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - - let lineBGSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - lineBG.colors.removeAll() - lineBG.circleColors.removeAll() - lineBGSmall.colors.removeAll() - lineBGSmall.circleColors.removeAll() - - if colors.count > 0 { - for i in 0 ..< colors.count { - mainChart.addColor(colors[i]) - mainChart.circleColors.append(colors[i]) - smallChart.addColor(colors[i]) - smallChart.circleColors.append(colors[i]) - } - } - - BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) - BGChart.setVisibleXRangeMinimum(600) - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - BGChartFull.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - - updateYesterdayBGGraph() - - // The initial zoom is a one-shot, relative to the chart's current - // viewport. Skip it until the chart actually has a width — otherwise a - // refresh that lands while the view is loaded but off-screen (e.g. Home - // lives in the Menu, so MainViewController is force-loaded headless by - // bootstrap()) would consume firstGraphLoad against a zero-size viewport - // and leave the main graph blank. viewDidAppear re-runs updateBGGraph - // once a real frame exists, applying the zoom correctly. - if firstGraphLoad, BGChart.bounds.width > 0 { - var scaleX = CGFloat(Storage.shared.chartScaleX.value) - if scaleX > CGFloat(ScaleXMax) { - scaleX = CGFloat(ScaleXMax) - Storage.shared.chartScaleX.value = ScaleXMax - } - BGChart.zoom(scaleX: scaleX, scaleY: 1, x: 1, y: 1) - firstGraphLoad = false - } - - // Move to current reading everytime new readings load - // Check if auto-scrolling should be performed - if autoScrollPauseUntil == nil || Date() > autoScrollPauseUntil! { - BGChart.moveViewToAnimated(xValue: dateTimeUtils.getNowTimeIntervalUTC() - (BGChart.visibleXRange * 0.7), yValue: 0.0, axis: .right, duration: 1, easingOption: .easeInBack) - } - } - - // Populates (or clears) the dimmed "yesterday" comparison overlay on the main graph. - // Points in yesterdayBGData are already shifted +24h so they align with today's clock time. - func updateYesterdayBGGraph() { - let dataIndex = GraphDataIndex.yesterday.rawValue - guard let lineData = BGChart.lineData, - dataIndex < lineData.dataSets.count, - let dataSet = lineData.dataSets[dataIndex] as? LineChartDataSet - else { - return - } - - dataSet.removeAll(keepingCapacity: false) - - if Storage.shared.showYesterdayLine.value { - for entry in yesterdayBGData { - // Clamp the plotted y-value to the same bounds the main BG line uses. - let plottedSgv = Double(min(max(entry.sgv, globalVariables.minDisplayGlucose), globalVariables.maxDisplayGlucose)) - dataSet.append(ChartDataEntry(x: entry.date, y: plottedSgv)) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - } - - func updatePredictionGraph(color: UIColor? = nil) { - let dataIndex = 1 - var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - mainChart.clear() - smallChart.clear() - - var colors = [NSUIColor]() - let maxBGOffset: Double = 20 - - topPredictionBG = Storage.shared.minBGScale.value - for i in 0 ..< predictionData.count { - var predictionVal = Double(predictionData[i].sgv) - if Double(predictionVal) > topPredictionBG - maxBGOffset { - topPredictionBG = predictionVal + maxBGOffset - } - - if i == 0 { - if Storage.shared.showDots.value { - colors.append((color ?? NSUIColor.systemPurple).withAlphaComponent(0.0)) - } else { - colors.append((color ?? NSUIColor.systemPurple).withAlphaComponent(1.0)) - } - } else if predictionVal > 400 { - colors.append(color ?? NSUIColor.systemYellow) - } else if predictionVal < 0 { - colors.append(color ?? NSUIColor.systemRed) - } else { - colors.append(color ?? NSUIColor.systemPurple) - } - - let value = ChartDataEntry(x: predictionData[i].date, y: predictionVal, data: formatPillText(line1: Localizer.toDisplayUnits(String(predictionData[i].sgv)), time: predictionData[i].date)) - mainChart.addEntry(value) - smallChart.addEntry(value) - } - - smallChart.circleColors.removeAll() - smallChart.colors.removeAll() - mainChart.colors.removeAll() - mainChart.circleColors.removeAll() - if colors.count > 0 { - for i in 0 ..< colors.count { - mainChart.addColor(colors[i]) - mainChart.circleColors.append(colors[i]) - smallChart.addColor(colors[i]) - smallChart.circleColors.append(colors[i]) - } - } - BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - - func updateBasalGraph() { - var dataIndex = 2 - BGChart.lineData?.dataSets[dataIndex].clear() - BGChartFull.lineData?.dataSets[dataIndex].clear() - var maxBasal = Storage.shared.minBasalScale.value - var maxBasalSmall = 0.0 - for i in 0 ..< basalData.count { - let value = ChartDataEntry(x: Double(basalData[i].date), y: Double(basalData[i].basalRate), data: formatPillText(line1: String(basalData[i].basalRate), time: basalData[i].date)) - BGChart.data?.dataSets[dataIndex].addEntry(value) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(value) - } - if basalData[i].basalRate > maxBasal { - maxBasal = basalData[i].basalRate - } - if basalData[i].basalRate > maxBasalSmall { - maxBasalSmall = basalData[i].basalRate - } - } - - BGChart.leftAxis.axisMaximum = maxBasal - BGChartFull.leftAxis.axisMaximum = maxBasalSmall - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func updateBasalScheduledGraph() { - var dataIndex = 5 - BGChart.lineData?.dataSets[dataIndex].clear() - BGChartFull.lineData?.dataSets[dataIndex].clear() - for i in 0 ..< basalScheduleData.count { - let value = ChartDataEntry(x: Double(basalScheduleData[i].date), y: Double(basalScheduleData[i].basalRate)) - BGChart.data?.dataSets[dataIndex].addEntry(value) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(value) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func updateBolusGraph() { - var dataIndex = 3 - var yTop: Double = 370 - var yBottom: Double = 345 - var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - mainChart.clear() - smallChart.clear() - - var colors = [NSUIColor]() - for i in 0 ..< bolusData.count { - let formatter = NumberFormatter() - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 2 - formatter.minimumIntegerDigits = 0 - - // Check overlapping carbs to shift left if needed - let bolusShift = findNextBolusTime(timeWithin: 240, needle: bolusData[i].date, haystack: bolusData, startingIndex: i) - var dateTimeStamp = bolusData[i].date - - colors.append(NSUIColor.systemBlue.withAlphaComponent(1.0)) - - if bolusShift { - // Move it half the distance between BG readings - dateTimeStamp = dateTimeStamp - 150 - } - - // skip if outside of visible area - let graphHours = 24 * Storage.shared.downloadDays.value - if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - - let valueString = formatter.string(from: NSNumber(value: bolusData[i].value)) ?? "" - let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(bolusData[i].sgv), data: valueString + "\r\r" + formatPillText(line1: valueString + " U", time: bolusData[i].date)) - mainChart.addEntry(dot) - if Storage.shared.smallGraphTreatments.value { - smallChart.addEntry(dot) - } - } - - // Set Colors - let lineBolus = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - let lineBolusSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - lineBolus.colors.removeAll() - lineBolus.circleColors.removeAll() - lineBolusSmall.colors.removeAll() - lineBolusSmall.circleColors.removeAll() - - if colors.count > 0 { - for i in 0 ..< colors.count { - mainChart.addColor(colors[i]) - mainChart.circleColors.append(colors[i]) - smallChart.addColor(colors[i]) - smallChart.circleColors.append(colors[i]) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func updateSmbGraph() { - var dataIndex = 16 - var yTop: Double = 370 - var yBottom: Double = 345 - var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - mainChart.clear() - smallChart.clear() - let lightBlue = NSUIColor(red: 135 / 255, green: 206 / 255, blue: 235 / 255, alpha: 1.0) // Light Sky Blue - - var colors = [NSUIColor]() - for i in 0 ..< smbData.count { - let formatter = NumberFormatter() - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 2 - formatter.minimumIntegerDigits = 0 - - let bolusShift = findNextBolusTime(timeWithin: 240, needle: smbData[i].date, haystack: smbData, startingIndex: i) - var dateTimeStamp = smbData[i].date - - let nowTime = dateTimeUtils.getNowTimeIntervalUTC() - let diffTimeHours = (nowTime - dateTimeStamp) / 60 / 60 - if diffTimeHours <= 1 { - colors.append(lightBlue.withAlphaComponent(1.0)) - } else if diffTimeHours > 6 { - colors.append(lightBlue.withAlphaComponent(0.25)) - } else { - let thisAlpha = 1.0 - (0.15 * diffTimeHours) - colors.append(lightBlue.withAlphaComponent(CGFloat(thisAlpha))) - } - - if bolusShift { - dateTimeStamp = dateTimeStamp - 150 - } - - let graphHours = 24 * Storage.shared.downloadDays.value - if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - - let valueString = formatter.string(from: NSNumber(value: smbData[i].value)) ?? "" - let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(smbData[i].sgv), data: valueString + "\r\r" + formatPillText(line1: valueString + " U", time: smbData[i].date)) - mainChart.addEntry(dot) - if Storage.shared.smallGraphTreatments.value { - smallChart.addEntry(dot) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func updateCarbGraph() { - var dataIndex = 4 - var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - mainChart.removeAll(keepingCapacity: true) - smallChart.removeAll(keepingCapacity: true) - - var colors = [NSUIColor]() - for i in 0 ..< carbData.count { - let formatter = NumberFormatter() - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 2 - formatter.minimumIntegerDigits = 1 - - let carbAmountString = formatter.string(from: NSNumber(value: carbData[i].value))! - var valueString = carbAmountString - var markerLine1 = carbAmountString + "g" - - var hours = 3 - if carbData[i].absorptionTime > 0, Storage.shared.showAbsorption.value { - hours = carbData[i].absorptionTime / 60 - valueString += " " + String(hours) + "h" - markerLine1 += " " + String(hours) + "h" - } - - // Check overlapping carbs to shift left if needed - let carbShift = findNextCarbTime(timeWithin: 250, needle: carbData[i].date, haystack: carbData, startingIndex: i) - var dateTimeStamp = carbData[i].date - - colors.append(NSUIColor.systemOrange.withAlphaComponent(1.0)) - - // skip if outside of visible area - let graphHours = 24 * Storage.shared.downloadDays.value - if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - - if carbShift { - dateTimeStamp = dateTimeStamp - 250 - } - - let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(carbData[i].sgv), data: valueString + "\r\r" + formatPillText(line1: markerLine1, time: carbData[i].date)) - BGChart.data?.dataSets[dataIndex].addEntry(dot) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(dot) - } - } - - // Set Colors - let lineCarbs = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - let lineCarbsSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - lineCarbs.colors.removeAll() - lineCarbs.circleColors.removeAll() - lineCarbsSmall.colors.removeAll() - lineCarbsSmall.circleColors.removeAll() - - if colors.count > 0 { - for i in 0 ..< colors.count { - mainChart.addColor(colors[i]) - mainChart.circleColors.append(colors[i]) - smallChart.addColor(colors[i]) - smallChart.circleColors.append(colors[i]) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func updateBGCheckGraph() { - var dataIndex = 7 - BGChart.lineData?.dataSets[dataIndex].clear() - BGChartFull.lineData?.dataSets[dataIndex].clear() - - for i in 0 ..< bgCheckData.count { - let formatter = NumberFormatter() - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 2 - formatter.minimumIntegerDigits = 1 - - // skip if outside of visible area - let graphHours = 24 * Storage.shared.downloadDays.value - if bgCheckData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - - let value = ChartDataEntry(x: Double(bgCheckData[i].date), y: Double(bgCheckData[i].sgv), data: formatPillText(line1: Localizer.toDisplayUnits(String(bgCheckData[i].sgv)), time: bgCheckData[i].date)) - BGChart.data?.dataSets[dataIndex].addEntry(value) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(value) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func updateSuspendGraph() { - var dataIndex = 8 - BGChart.lineData?.dataSets[dataIndex].clear() - BGChartFull.lineData?.dataSets[dataIndex].clear() - let thisData = suspendGraphData - for i in 0 ..< thisData.count { - // skip if outside of visible area - let graphHours = 24 * Storage.shared.downloadDays.value - if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - - let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Suspend Pump", time: thisData[i].date)) - BGChart.data?.dataSets[dataIndex].addEntry(value) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(value) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func updateResumeGraph() { - var dataIndex = 9 - BGChart.lineData?.dataSets[dataIndex].clear() - BGChartFull.lineData?.dataSets[dataIndex].clear() - let thisData = resumeGraphData - for i in 0 ..< thisData.count { - // skip if outside of visible area - let graphHours = 24 * Storage.shared.downloadDays.value - if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - - let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Resume Pump", time: thisData[i].date)) - BGChart.data?.dataSets[dataIndex].addEntry(value) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(value) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func updateSensorStart() { - var dataIndex = 10 - BGChart.lineData?.dataSets[dataIndex].clear() - BGChartFull.lineData?.dataSets[dataIndex].clear() - let thisData = sensorStartGraphData - for i in 0 ..< thisData.count { - // skip if outside of visible area - let graphHours = 24 * Storage.shared.downloadDays.value - if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - - let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Start Sensor", time: thisData[i].date)) - BGChart.data?.dataSets[dataIndex].addEntry(value) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(value) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func updateNotes() { - var dataIndex = 11 - BGChart.lineData?.dataSets[dataIndex].clear() - BGChartFull.lineData?.dataSets[dataIndex].clear() - let thisData = noteGraphData - for i in 0 ..< thisData.count { - // skip if outside of visible area - let graphHours = 24 * Storage.shared.downloadDays.value - if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - - let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: thisData[i].note, time: thisData[i].date)) - BGChart.data?.dataSets[dataIndex].addEntry(value) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(value) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func createSmallBGGraph() { - let entries = bgData - var bgChartEntry = [ChartDataEntry]() - var colors = [NSUIColor]() - var maxBG = Storage.shared.minBGScale.value - - let lineBG = LineChartDataSet(entries: bgChartEntry, label: "") - - lineBG.drawCirclesEnabled = false - // line2.setDrawHighlightIndicators(false) - lineBG.highlightEnabled = true - lineBG.drawHorizontalHighlightIndicatorEnabled = false - lineBG.drawVerticalHighlightIndicatorEnabled = false - lineBG.highlightColor = NSUIColor.label - lineBG.drawValuesEnabled = false - lineBG.lineWidth = 1.5 - lineBG.axisDependency = YAxis.AxisDependency.right - - // Setup Prediction line details - var predictionChartEntry = [ChartDataEntry]() - let linePrediction = LineChartDataSet(entries: predictionChartEntry, label: "") - linePrediction.drawCirclesEnabled = false - // line2.setDrawHighlightIndicators(false) - linePrediction.setColor(NSUIColor.systemPurple) - linePrediction.highlightEnabled = true - linePrediction.drawHorizontalHighlightIndicatorEnabled = false - linePrediction.drawVerticalHighlightIndicatorEnabled = false - linePrediction.highlightColor = NSUIColor.label - linePrediction.drawValuesEnabled = false - linePrediction.lineWidth = 1.5 - linePrediction.axisDependency = YAxis.AxisDependency.right - - // create Basal graph data - var chartEntry = [ChartDataEntry]() - var maxBasal = Storage.shared.minBasalScale.value - let lineBasal = LineChartDataSet(entries: chartEntry, label: "") - lineBasal.setDrawHighlightIndicators(false) - lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5) - lineBasal.lineWidth = 0 - lineBasal.drawFilledEnabled = true - lineBasal.fillColor = NSUIColor.systemBlue - lineBasal.fillAlpha = 0.35 - lineBasal.drawCirclesEnabled = false - lineBasal.axisDependency = YAxis.AxisDependency.left - lineBasal.highlightEnabled = false - lineBasal.drawValuesEnabled = false - lineBasal.fillFormatter = basalFillFormatter() - - // Boluses - var chartEntryBolus = [ChartDataEntry]() - let lineBolus = LineChartDataSet(entries: chartEntryBolus, label: "") - lineBolus.circleRadius = 2 - lineBolus.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)] - lineBolus.drawCircleHoleEnabled = false - lineBolus.setDrawHighlightIndicators(false) - lineBolus.setColor(NSUIColor.systemBlue, alpha: 1.0) - lineBolus.lineWidth = 0 - lineBolus.axisDependency = YAxis.AxisDependency.right - lineBolus.valueFormatter = ChartYDataValueFormatter() - lineBolus.valueTextColor = NSUIColor.label - lineBolus.fillColor = NSUIColor.systemBlue - lineBolus.fillAlpha = 0.6 - lineBolus.drawCirclesEnabled = true - lineBolus.drawFilledEnabled = false - lineBolus.drawValuesEnabled = false - lineBolus.highlightEnabled = false - - // Carbs - var chartEntryCarbs = [ChartDataEntry]() - let lineCarbs = LineChartDataSet(entries: chartEntryCarbs, label: "") - lineCarbs.circleRadius = 2 - lineCarbs.circleColors = [NSUIColor.systemOrange.withAlphaComponent(0.75)] - lineCarbs.drawCircleHoleEnabled = false - lineCarbs.setDrawHighlightIndicators(false) - lineCarbs.setColor(NSUIColor.systemBlue, alpha: 1.0) - lineCarbs.lineWidth = 0 - lineCarbs.axisDependency = YAxis.AxisDependency.right - lineCarbs.valueFormatter = ChartYDataValueFormatter() - lineCarbs.valueTextColor = NSUIColor.label - lineCarbs.fillColor = NSUIColor.systemOrange - lineCarbs.fillAlpha = 0.6 - lineCarbs.drawCirclesEnabled = true - lineCarbs.drawFilledEnabled = false - lineCarbs.drawValuesEnabled = false - lineCarbs.highlightEnabled = false - - // create Scheduled Basal graph data - var chartBasalScheduledEntry = [ChartDataEntry]() - let lineBasalScheduled = LineChartDataSet(entries: chartBasalScheduledEntry, label: "") - lineBasalScheduled.setDrawHighlightIndicators(false) - lineBasalScheduled.setColor(NSUIColor.systemBlue, alpha: 0.8) - lineBasalScheduled.lineWidth = 0.5 - lineBasalScheduled.drawFilledEnabled = false - lineBasalScheduled.drawCirclesEnabled = false - lineBasalScheduled.axisDependency = YAxis.AxisDependency.left - lineBasalScheduled.highlightEnabled = false - lineBasalScheduled.drawValuesEnabled = false - lineBasalScheduled.lineDashLengths = [2, 1] - - // create Override graph data - var chartOverrideEntry = [ChartDataEntry]() - let lineOverride = LineChartDataSet(entries: chartOverrideEntry, label: "") - lineOverride.setDrawHighlightIndicators(false) - lineOverride.lineWidth = 0 - lineOverride.drawFilledEnabled = true - lineOverride.fillFormatter = OverrideFillFormatter() - lineOverride.fillColor = TreatmentGraphColors.override - lineOverride.fillAlpha = 0.6 - lineOverride.drawCirclesEnabled = false - lineOverride.axisDependency = YAxis.AxisDependency.right - lineOverride.highlightEnabled = true - lineOverride.drawValuesEnabled = false - - // BG Check - var chartEntryBGCheck = [ChartDataEntry]() - let lineBGCheck = LineChartDataSet(entries: chartEntryBGCheck, label: "") - lineBGCheck.circleRadius = 2 - lineBGCheck.circleColors = [NSUIColor.systemRed.withAlphaComponent(0.75)] - lineBGCheck.drawCircleHoleEnabled = false - lineBGCheck.setDrawHighlightIndicators(false) - lineBGCheck.setColor(NSUIColor.systemRed, alpha: 1.0) - lineBGCheck.drawCirclesEnabled = true - lineBGCheck.lineWidth = 0 - lineBGCheck.highlightEnabled = false - lineBGCheck.axisDependency = YAxis.AxisDependency.right - lineBGCheck.valueFormatter = ChartYDataValueFormatter() - lineBGCheck.drawValuesEnabled = false - - // Suspend Pump - var chartEntrySuspend = [ChartDataEntry]() - let lineSuspend = LineChartDataSet(entries: chartEntrySuspend, label: "") - lineSuspend.circleRadius = 2 - lineSuspend.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] - lineSuspend.drawCircleHoleEnabled = false - lineSuspend.setDrawHighlightIndicators(false) - lineSuspend.setColor(NSUIColor.systemGray2, alpha: 1.0) - lineSuspend.drawCirclesEnabled = true - lineSuspend.lineWidth = 0 - lineSuspend.highlightEnabled = false - lineSuspend.axisDependency = YAxis.AxisDependency.right - lineSuspend.valueFormatter = ChartYDataValueFormatter() - lineSuspend.drawValuesEnabled = false - - // Resume Pump - var chartEntryResume = [ChartDataEntry]() - let lineResume = LineChartDataSet(entries: chartEntryResume, label: "") - lineResume.circleRadius = 2 - lineResume.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] - lineResume.drawCircleHoleEnabled = false - lineResume.setDrawHighlightIndicators(false) - lineResume.setColor(NSUIColor.systemGray4, alpha: 1.0) - lineResume.drawCirclesEnabled = true - lineResume.lineWidth = 0 - lineResume.highlightEnabled = false - lineResume.axisDependency = YAxis.AxisDependency.right - lineResume.valueFormatter = ChartYDataValueFormatter() - lineResume.drawValuesEnabled = false - - // Sensor Start - var chartEntrySensor = [ChartDataEntry]() - let lineSensor = LineChartDataSet(entries: chartEntrySensor, label: "") - lineSensor.circleRadius = 2 - lineSensor.circleColors = [NSUIColor.systemIndigo.withAlphaComponent(0.75)] - lineSensor.drawCircleHoleEnabled = false - lineSensor.setDrawHighlightIndicators(false) - lineSensor.setColor(NSUIColor.systemGray3, alpha: 1.0) - lineSensor.drawCirclesEnabled = true - lineSensor.lineWidth = 0 - lineSensor.highlightEnabled = false - lineSensor.axisDependency = YAxis.AxisDependency.right - lineSensor.valueFormatter = ChartYDataValueFormatter() - lineSensor.drawValuesEnabled = false - - // Notes - var chartEntryNote = [ChartDataEntry]() - let lineNote = LineChartDataSet(entries: chartEntryNote, label: "") - lineNote.circleRadius = 2 - lineNote.circleColors = [NSUIColor.systemGray.withAlphaComponent(0.75)] - lineNote.drawCircleHoleEnabled = false - lineNote.setDrawHighlightIndicators(false) - lineNote.setColor(NSUIColor.systemGray3, alpha: 1.0) - lineNote.drawCirclesEnabled = true - lineNote.lineWidth = 0 - lineNote.highlightEnabled = false - lineNote.axisDependency = YAxis.AxisDependency.right - lineNote.valueFormatter = ChartYDataValueFormatter() - lineNote.drawValuesEnabled = false - - // Setup COB Prediction line details - var COBpredictionChartEntry = [ChartDataEntry]() - let COBlinePrediction = LineChartDataSet(entries: COBpredictionChartEntry, label: "") - COBlinePrediction.drawCirclesEnabled = false - COBlinePrediction.setColor(NSUIColor.systemPurple) - COBlinePrediction.highlightEnabled = true - COBlinePrediction.drawHorizontalHighlightIndicatorEnabled = false - COBlinePrediction.drawVerticalHighlightIndicatorEnabled = false - COBlinePrediction.highlightColor = NSUIColor.label - COBlinePrediction.drawValuesEnabled = false - COBlinePrediction.lineWidth = 1.5 - COBlinePrediction.axisDependency = YAxis.AxisDependency.right - - // Setup IOB Prediction line details - var IOBpredictionChartEntry = [ChartDataEntry]() - let IOBlinePrediction = LineChartDataSet(entries: IOBpredictionChartEntry, label: "") - IOBlinePrediction.drawCirclesEnabled = false - IOBlinePrediction.setColor(NSUIColor.systemPurple) - IOBlinePrediction.highlightEnabled = true - IOBlinePrediction.drawHorizontalHighlightIndicatorEnabled = false - IOBlinePrediction.drawVerticalHighlightIndicatorEnabled = false - IOBlinePrediction.highlightColor = NSUIColor.label - IOBlinePrediction.drawValuesEnabled = false - IOBlinePrediction.lineWidth = 1.5 - IOBlinePrediction.axisDependency = YAxis.AxisDependency.right - - // Setup UAM Prediction line details - var UAMpredictionChartEntry = [ChartDataEntry]() - let UAMlinePrediction = LineChartDataSet(entries: UAMpredictionChartEntry, label: "") - UAMlinePrediction.drawCirclesEnabled = false - UAMlinePrediction.setColor(NSUIColor.systemPurple) - UAMlinePrediction.highlightEnabled = true - UAMlinePrediction.drawHorizontalHighlightIndicatorEnabled = false - UAMlinePrediction.drawVerticalHighlightIndicatorEnabled = false - UAMlinePrediction.highlightColor = NSUIColor.label - UAMlinePrediction.drawValuesEnabled = false - UAMlinePrediction.lineWidth = 1.5 - UAMlinePrediction.axisDependency = YAxis.AxisDependency.right - - // Setup ZT Prediction line details - var ZTpredictionChartEntry = [ChartDataEntry]() - let ZTlinePrediction = LineChartDataSet(entries: ZTpredictionChartEntry, label: "") - ZTlinePrediction.drawCirclesEnabled = false - ZTlinePrediction.setColor(NSUIColor.systemPurple) - ZTlinePrediction.highlightEnabled = true - ZTlinePrediction.drawHorizontalHighlightIndicatorEnabled = false - ZTlinePrediction.drawVerticalHighlightIndicatorEnabled = false - ZTlinePrediction.highlightColor = NSUIColor.label - ZTlinePrediction.drawValuesEnabled = false - ZTlinePrediction.lineWidth = 1.5 - ZTlinePrediction.axisDependency = YAxis.AxisDependency.right - - // SMB - var chartEntrySmb = [ChartDataEntry]() - let lineSmb = LineChartDataSet(entries: chartEntrySmb, label: "") - lineSmb.circleRadius = 2 - lineSmb.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)] - lineSmb.drawCircleHoleEnabled = false - lineSmb.setDrawHighlightIndicators(false) - lineSmb.setColor(NSUIColor.systemBlue, alpha: 1.0) - lineSmb.lineWidth = 0 - lineSmb.axisDependency = YAxis.AxisDependency.right - lineSmb.valueFormatter = ChartYDataValueFormatter() - lineSmb.valueTextColor = NSUIColor.label - lineSmb.fillColor = NSUIColor.systemBlue - lineSmb.fillAlpha = 0.6 - lineSmb.drawCirclesEnabled = true - lineSmb.drawFilledEnabled = false - lineSmb.drawValuesEnabled = false - lineSmb.highlightEnabled = false - - // Temp Target graph data - let chartTempTargetEntry = [ChartDataEntry]() - let lineTempTarget = LineChartDataSet(entries: chartTempTargetEntry, label: "") - lineTempTarget.setDrawHighlightIndicators(false) - lineTempTarget.lineWidth = 0 - lineTempTarget.drawFilledEnabled = false - lineTempTarget.fillColor = NSUIColor.systemPurple - lineTempTarget.fillAlpha = 0.6 - lineTempTarget.drawCirclesEnabled = false - lineTempTarget.axisDependency = YAxis.AxisDependency.right - lineTempTarget.highlightEnabled = true - lineTempTarget.drawValuesEnabled = false - - // Setup the chart data of all lines - let data = LineChartData() - data.append(lineBG) // Dataset 0 - data.append(linePrediction) // Dataset 1 - data.append(lineBasal) // Dataset 2 - data.append(lineBolus) // Dataset 3 - data.append(lineCarbs) // Dataset 4 - data.append(lineBasalScheduled) // Dataset 5 - data.append(lineOverride) // Dataset 6 - data.append(lineBGCheck) // Dataset 7 - data.append(lineSuspend) // Dataset 8 - data.append(lineResume) // Dataset 9 - data.append(lineSensor) // Dataset 10 - data.append(lineNote) // Dataset 11 - data.append(ZTlinePrediction) // Dataset 12 - data.append(IOBlinePrediction) // Dataset 13 - data.append(COBlinePrediction) // Dataset 14 - data.append(UAMlinePrediction) // Dataset 15 - data.append(lineSmb) // Dataset 16 - data.append(lineTempTarget) // Dataset 17 - - // Dataset 18: Prediction Cone placeholder (not rendered on small chart) - let lineConeSmall = LineChartDataSet(entries: [ChartDataEntry](), label: "") - lineConeSmall.lineWidth = 0 - lineConeSmall.drawCirclesEnabled = false - lineConeSmall.drawValuesEnabled = false - lineConeSmall.highlightEnabled = false - lineConeSmall.axisDependency = YAxis.AxisDependency.right - data.append(lineConeSmall) - - BGChartFull.highlightPerDragEnabled = true - BGChartFull.leftAxis.enabled = false - BGChartFull.leftAxis.axisMaximum = maxBasal - BGChartFull.leftAxis.axisMinimum = 0 - - BGChartFull.rightAxis.enabled = false - BGChartFull.rightAxis.axisMinimum = 0.0 - BGChartFull.rightAxis.axisMaximum = Double(maxBG) - - BGChartFull.xAxis.drawLabelsEnabled = false - BGChartFull.xAxis.drawGridLinesEnabled = false - BGChartFull.xAxis.drawAxisLineEnabled = false - BGChartFull.legend.enabled = false - BGChartFull.scaleYEnabled = false - BGChartFull.scaleXEnabled = false - BGChartFull.drawGridBackgroundEnabled = false - BGChartFull.data = data - } - - func updateOverrideGraph() { - var dataIndex = 6 - var yTop = Double(calculateMaxBgGraphValue() - 5) - var yBottom = Double(calculateMaxBgGraphValue() - 25) - var chart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - chart.clear() - smallChart.clear() - // Refresh the fill color in case the backend (Loop vs Trio) changed. - chart.fillColor = TreatmentGraphColors.override - smallChart.fillColor = TreatmentGraphColors.override - let thisData = overrideGraphData - - var colors = [NSUIColor]() - for i in 0 ..< thisData.count { - let thisItem = thisData[i] - let multiplier = thisItem.insulNeedsScaleFactor as! Double * 100.0 - var labelText = thisItem.reason + "\r\n" - labelText += String(Int(thisItem.insulNeedsScaleFactor * 100)) + "% " - if thisItem.correctionRange.count == 2 { - labelText += String(thisItem.correctionRange[0]) + "-" + String(thisItem.correctionRange[1]) - } - if thisItem.enteredBy.count > 0 { - labelText += "\r\nEntered By: " + thisItem.enteredBy - } - - // Start Dot - // Shift dots 30 seconds to create an empty 0 space between consecutive temps - let preStartDot = ChartDataEntry(x: Double(thisItem.date), y: yBottom, data: labelText) - BGChart.data?.dataSets[dataIndex].addEntry(preStartDot) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(preStartDot) - } - - let startDot = ChartDataEntry(x: Double(thisItem.date + 1), y: yTop, data: labelText) - BGChart.data?.dataSets[dataIndex].addEntry(startDot) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(startDot) - } - - // End Dot - let endDot = ChartDataEntry(x: Double(thisItem.endDate - 2), y: yTop, data: labelText) - BGChart.data?.dataSets[dataIndex].addEntry(endDot) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(endDot) - } - - // Post end dot - let postEndDot = ChartDataEntry(x: Double(thisItem.endDate - 1), y: yBottom, data: labelText) - BGChart.data?.dataSets[dataIndex].addEntry(postEndDot) - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].addEntry(postEndDot) - } - } - - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - if Storage.shared.smallGraphTreatments.value { - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func getChartDataSets(for index: GraphDataIndex) -> (chart: LineChartDataSet?, smallChart: LineChartDataSet?) { - guard let chart = BGChart.lineData, - index.rawValue < chart.dataSets.count, - let smallChartData = BGChartFull.lineData, - index.rawValue < smallChartData.dataSets.count - else { - // print("Warning: Invalid GraphDataIndex \(index.description) or lineData is nil.") - return (nil, nil) - } - - let chartDataSet = chart.dataSets[index.rawValue] as? LineChartDataSet - let smallChartDataSet = smallChartData.dataSets[index.rawValue] as? LineChartDataSet - - return (chartDataSet, smallChartDataSet) - } - - func addEntryToCharts(entry: ChartDataEntry, chart: LineChartDataSet, smallChart: LineChartDataSet?) { - chart.addEntry(entry) - if Storage.shared.smallGraphTreatments.value, let smallChart = smallChart { - smallChart.addEntry(entry) - } - } - - func updateTempTargetGraph() { - let dataIndex = GraphDataIndex.tempTarget.rawValue - guard let chartData = BGChart.lineData, - chartData.dataSets.count > dataIndex, - let mainChartDataSet = chartData.dataSets[dataIndex] as? LineChartDataSet - else { - print("Error: Could not retrieve temp target datasets.") - return - } - - mainChartDataSet.clear() - - var smallChartDataSet: LineChartDataSet? - if Storage.shared.smallGraphTreatments.value, - let smallChartData = BGChartFull.lineData, - smallChartData.dataSets.count > dataIndex, - let smallDataSet = smallChartData.dataSets[dataIndex] as? LineChartDataSet - { - smallChartDataSet = smallDataSet - smallChartDataSet?.clear() - } - - let thisData = tempTargetGraphData - - for tempTarget in thisData { - let xStart = tempTarget.date - let xEnd = tempTarget.endDate - let yCenter = Double(tempTarget.correctionRange[0]) - let yTop = yCenter + 5.0 - let yBottom = yCenter - 5.0 - - let entry = TempTargetChartDataEntry( - xStart: xStart, - xEnd: xEnd, - yTop: yTop, - yBottom: yBottom, - data: nil - ) - mainChartDataSet.addEntry(entry) - - if let smallDataSet = smallChartDataSet { - smallDataSet.addEntry(entry) - } - } - - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - - if let smallDataSet = smallChartDataSet { - let tempTargetRendererSmall = TempTargetRenderer( - dataProvider: BGChartFull, - animator: BGChartFull.chartAnimator, - viewPortHandler: BGChartFull.viewPortHandler, - tempTargetDataSetIndex: dataIndex - ) - BGChartFull.renderer = tempTargetRendererSmall - - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } - } - - func extractMessage(from logEntry: String) -> String? { - // Check if this is a JSON-containing log entry - guard let jsonStartIndex = logEntry.range(of: "{\"")?.lowerBound else { - return nil - } - - // Extract the error message part (before JSON) - let errorMessage = String(logEntry[.. String { - let messageToWrap = extractMessage(from: text) ?? text - - guard maxLineLength > 0 else { - return messageToWrap - } - - var result: [String] = [] - let lines = messageToWrap.components(separatedBy: .newlines) - - for line in lines { - var currentLine = "" - let words = line.components(separatedBy: .whitespaces) - - for word in words { - if word.count > maxLineLength { - if !currentLine.isEmpty { - result.append(currentLine) - currentLine = "" - } - var wordToSplit = word - while !wordToSplit.isEmpty { - let splitIndex = wordToSplit.index(wordToSplit.startIndex, offsetBy: min(maxLineLength, wordToSplit.count)) - result.append(String(wordToSplit[.. String { - let dateFormatter = DateFormatter() - if dateTimeUtils.is24Hour() { - dateFormatter.setLocalizedDateFormatFromTemplate("HH:mm") - } else { - dateFormatter.setLocalizedDateFormatFromTemplate("hh:mm") - } - - if Storage.shared.graphTimeZoneEnabled.value, - let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) - { - dateFormatter.timeZone = tz - } - - let wrappedLine1 = wrapText(line1, maxLineLength: 40) - - let date = Date(timeIntervalSince1970: time) - let formattedDate = dateFormatter.string(from: date) - - if let line2 = line2 { - return wrappedLine1 + "\r\n" + line2 + "\r\n" + formattedDate - } else { - return wrappedLine1 + "\r\n" + formattedDate - } - } - - func updateConeGraph(coneData: [ConeChartDataEntry]) { - let dataIndex = GraphDataIndex.predictionCone.rawValue - let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - mainChart.clear() - for entry in coneData { - mainChart.addEntry(entry) - } - BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) - updateChartRenderers() - } - - func clearConeGraph() { - let dataIndex = GraphDataIndex.predictionCone.rawValue - guard let lineData = BGChart.lineData, lineData.dataSets.count > dataIndex else { return } - lineData.dataSets[dataIndex].clear() - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - } - - func updateOpenAPSPredictionDisplay() { - guard let predBGs = openAPSPredBGs else { return } - - // Cone is only for OpenAPS-based systems; Loop always uses lines - let displayType: PredictionDisplayType = Storage.shared.device.value == "Loop" ? .lines : Storage.shared.predictionDisplayType.value - let toLoad = Int(Storage.shared.predictionToLoad.value * 12) - let predictionStart = openAPSPredUpdatedTime ?? Date().timeIntervalSince1970 - - let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [ - ("ZT", "ZT", GraphDataIndex.ztPrediction.rawValue), - ("IOB", "Insulin", GraphDataIndex.iobPrediction.rawValue), - ("COB", "LoopYellow", GraphDataIndex.cobPrediction.rawValue), - ("UAM", "UAM", GraphDataIndex.uamPrediction.rawValue), - ] - - topPredictionBG = Storage.shared.minBGScale.value - - if displayType == .cone { - var allArrays = [[Double]]() - for (type, _, _) in predictionTypes { - if let arr = predBGs[type], !arr.isEmpty { - allArrays.append(arr) - } - } - - var coneData = [ConeChartDataEntry]() - if !allArrays.isEmpty { - // Cap at the shortest predBG array length so every cone point uses - // the same set of contributing arrays. Matches Trio's ForecastSetup. - let coneLength = min(allArrays.map { $0.count }.min()!, toLoad + 1) - var t = predictionStart - for i in 0 ..< coneLength { - var valuesAtIndex = [Double]() - for arr in allArrays where i < arr.count { - valuesAtIndex.append(arr[i]) - } - if !valuesAtIndex.isEmpty { - var yMin = max(valuesAtIndex.min()!, Double(globalVariables.minDisplayGlucose)) - var yMax = min(valuesAtIndex.max()!, Double(globalVariables.maxDisplayGlucose)) - // Ensure minimum ±1 mg/dL range so the cone is visible when predictions agree - if yMin == yMax { - yMin -= 1 - yMax += 1 - } - coneData.append(ConeChartDataEntry(x: t, yMin: yMin, yMax: yMax)) - if yMax > topPredictionBG - 20 { topPredictionBG = yMax + 20 } - } - t += 300 - } - } - - updateConeGraph(coneData: coneData) - - // Clear individual prediction lines - for (_, _, dataIndex) in predictionTypes { - updatePredictionGraphGeneric(dataIndex: dataIndex, predictionData: [], chartLabel: "", color: .clear) - } - - } else { - clearConeGraph() - - for (type, colorName, dataIndex) in predictionTypes { - var predictionData = [ShareGlucoseData]() - if let graphdata = predBGs[type] { - var t = predictionStart - for i in 0 ... toLoad { - if i < graphdata.count { - let v = graphdata[i] - let clamped = min(max(Int(round(v)), globalVariables.minDisplayGlucose), globalVariables.maxDisplayGlucose) - predictionData.append(ShareGlucoseData(sgv: clamped, date: t, direction: "flat")) - t += 300 - } - } - } - let color = UIColor(named: colorName) ?? UIColor.systemPurple - updatePredictionGraphGeneric(dataIndex: dataIndex, predictionData: predictionData, chartLabel: type, color: color) - } - } - } - - func updatePredictionGraphGeneric( - dataIndex: Int, - predictionData: [ShareGlucoseData], - chartLabel: String, - color: UIColor - ) { - let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - mainChart.clear() - smallChart.clear() - - var colors = [NSUIColor]() - let maxBGOffset: Double = 20 - - for i in 0 ..< predictionData.count { - let predictionVal = Double(predictionData[i].sgv) - if predictionVal > topPredictionBG - maxBGOffset { - topPredictionBG = predictionVal + maxBGOffset - } - - if i == 0 { - if Storage.shared.showDots.value { - colors.append(color.withAlphaComponent(0.0)) - } else { - colors.append(color.withAlphaComponent(1.0)) - } - } else { - colors.append(color) - } - - let value = ChartDataEntry( - x: predictionData[i].date, - y: predictionVal, - data: formatPillText( - line1: chartLabel, - time: predictionData[i].date, - line2: Localizer.toDisplayUnits(String(predictionVal)) - ) - ) - mainChart.addEntry(value) - smallChart.addEntry(value) - } - - smallChart.circleColors.removeAll() - smallChart.colors.removeAll() - mainChart.colors.removeAll() - mainChart.circleColors.removeAll() - if colors.count > 0 { - for color in colors { - mainChart.addColor(color) - mainChart.circleColors.append(color) - smallChart.addColor(color) - smallChart.circleColors.append(color) - } - } - - BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) - BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChart.data?.notifyDataChanged() - BGChart.notifyDataSetChanged() - BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() - BGChartFull.data?.notifyDataChanged() - BGChartFull.notifyDataSetChanged() - } -} diff --git a/LoopFollow/Controllers/NightScout.swift b/LoopFollow/Controllers/NightScout.swift index f87f407cc..90e3c1e9c 100644 --- a/LoopFollow/Controllers/NightScout.swift +++ b/LoopFollow/Controllers/NightScout.swift @@ -2,7 +2,6 @@ // NightScout.swift import Foundation -import UIKit extension MainViewController { // NS Cage Struct @@ -59,7 +58,6 @@ extension MainViewController { func clearOldSmb() { smbData.removeAll() updateSmbGraph() - updateChartRenderers() } func clearOldCarb() { @@ -74,13 +72,16 @@ extension MainViewController { func clearOldOverride() { overrideGraphData.removeAll() + Observable.shared.override.value = nil + Observable.shared.overrideEndAt.value = nil updateOverrideGraph() } func clearOldTempTarget() { tempTargetGraphData.removeAll() + Observable.shared.tempTarget.value = nil + Observable.shared.tempTargetEndAt.value = nil updateTempTargetGraph() - updateChartRenderers() } func clearOldSuspend() { diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index 2d6c73a77..a568746c7 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -2,7 +2,6 @@ // BGData.swift import Foundation -import UIKit extension MainViewController { /// Number of days of BG history to request from the source. One extra day is @@ -22,6 +21,7 @@ extension MainViewController { dexShare?.fetchData(count) { err, result in if let error = err { LogManager.shared.log(category: .dexcom, message: "Error fetching Dexcom data: \(error.localizedDescription)", limitIdentifier: "Error fetching Dexcom data") + BannerManager.shared.reportDexcomFailure(error, nightscoutFallback: IsNightscoutEnabled()) self.webLoadNSBGData() return } @@ -32,6 +32,8 @@ extension MainViewController { return } + BannerManager.shared.clear(.dexcom) + // If Dex data is old, load from NS instead let latestDate = data[0].date let now = dateTimeUtils.getNowTimeIntervalUTC() @@ -60,6 +62,7 @@ extension MainViewController { func webLoadNSBGData(dexData: [ShareGlucoseData] = []) { // This kicks it out in the instance where dexcom fails but they aren't using NS && if !IsNightscoutEnabled() { + BannerManager.shared.clear(.nightscout) Storage.shared.lastBGChecked.value = Date() return } @@ -75,6 +78,7 @@ extension MainViewController { NightscoutUtils.executeRequest(eventType: .sgv, parameters: parameters) { (result: Result<[ShareGlucoseData], Error>) in switch result { case let .success(entriesResponse): + BannerManager.shared.clear(.nightscout) var nsData = entriesResponse DispatchQueue.main.async { // transform NS data to look like Dex data @@ -103,6 +107,7 @@ extension MainViewController { } case let .failure(error): LogManager.shared.log(category: .nightscout, message: "Failed to fetch bg data: \(error)", limitIdentifier: "Failed to fetch bg data") + BannerManager.shared.reportNightscoutFetchFailure(error) DispatchQueue.main.async { TaskScheduler.shared.rescheduleTask( id: .fetchBG, diff --git a/LoopFollow/Controllers/Nightscout/CAge.swift b/LoopFollow/Controllers/Nightscout/CAge.swift index ed996fb1a..7e20bfade 100644 --- a/LoopFollow/Controllers/Nightscout/CAge.swift +++ b/LoopFollow/Controllers/Nightscout/CAge.swift @@ -51,7 +51,7 @@ extension MainViewController { formatter.zeroFormattingBehavior = [.pad] // Pad with zeroes where appropriate for the locale if let formattedDuration = formatter.string(from: secondsAgo) { - infoManager.updateInfoData(type: .cage, value: formattedDuration) + infoManager.updateInfoData(type: .cage, value: formattedDuration, numericValue: secondsAgo / 86400) } } } diff --git a/LoopFollow/Controllers/Nightscout/DBSize.swift b/LoopFollow/Controllers/Nightscout/DBSize.swift new file mode 100644 index 000000000..056971a4d --- /dev/null +++ b/LoopFollow/Controllers/Nightscout/DBSize.swift @@ -0,0 +1,62 @@ +// LoopFollow +// DBSize.swift + +import Foundation + +/// Response shape of `/api/v2/properties/dbsize`. +/// +/// Nightscout's `dbsize` plugin reports the Mongo `dataSize + indexSize` sum +/// against `DBSIZE_MAX` (defaults to 496 MiB when the site admin has not set it). +/// The key is absent when the plugin has not produced a property yet. +struct DBSizeProperties: Codable { + let dbsize: DBSizeData? +} + +struct DBSizeData: Codable { + /// Used size, in MiB. + let totalDataSize: Double? + /// Used size as a percentage of `details.maxSize`, floored by the server. + let dataPercentage: Int? + let details: Details? + + struct Details: Codable { + /// The configured limit in MiB (`DBSIZE_MAX`). + let maxSize: Double? + let dataSize: Double? + } +} + +extension MainViewController { + // NS Database Size Web Call + func webLoadNSDBSize() { + NightscoutUtils.executeRequest(eventType: .dbSize, parameters: [:]) { (result: Result) in + switch result { + case let .success(properties): + self.updateDBSize(data: properties.dbsize) + case let .failure(error): + LogManager.shared.log(category: .nightscout, message: "webLoadNSDBSize, error: \(error.localizedDescription)") + } + } + } + + // NS Database Size Response Processor + func updateDBSize(data: DBSizeData?) { + // Nightscout still reports the property when `db.stats()` failed, but with a zero + // size. Its own pill hides on that, so treat it as "no reading" rather than 0%. + guard let data = data, let usedMiB = data.totalDataSize, usedMiB > 0 else { + infoManager.clearInfoData(type: .dbSize) + Observable.shared.dbSizePercentage.set(nil) + return + } + + Observable.shared.dbSizePercentage.set(data.dataPercentage.map(Double.init)) + + let used = Localizer.formatToLocalizedString(usedMiB, maxFractionDigits: 0, minFractionDigits: 0) + + if let percentage = data.dataPercentage { + infoManager.updateInfoData(type: .dbSize, value: "\(used) MiB (\(percentage)%)") + } else { + infoManager.updateInfoData(type: .dbSize, value: "\(used) MiB") + } + } +} diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index d2d6920d8..89126e6b1 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -66,6 +66,7 @@ extension MainViewController { // NS Device Status Response Processor func updateDeviceStatusDisplay(jsonDeviceStatus: [[String: AnyObject]]) { let previousIOBText = Observable.shared.iobText.value + let previousDeviceWasLoop = Storage.shared.device.value == "Loop" infoManager.clearInfoData(types: [.iob, .cob, .battery, .pump, .pumpBattery, .target, .isf, .carbRatio, .updated, .recBolus, .tdd]) // For Loop, clear the current override here - For Trio, it is handled using treatments @@ -113,11 +114,13 @@ extension MainViewController { if let reservoirData = lastPumpRecord["reservoir"] as? Double { latestPumpVolume = reservoirData - infoManager.updateInfoData(type: .pump, value: String(format: "%.0f", reservoirData) + "U") + infoManager.updateInfoData(type: .pump, value: String(format: "%.0f", reservoirData) + "U", numericValue: reservoirData) Storage.shared.lastPumpReservoirU.value = reservoirData } else { + // Pumps that only report "50+" get treated as exactly 50, both + // for the volume alarm and for the info row's coloring. latestPumpVolume = 50.0 - infoManager.updateInfoData(type: .pump, value: "50+U") + infoManager.updateInfoData(type: .pump, value: "50+U", numericValue: 50.0) Storage.shared.lastPumpReservoirU.value = nil } } @@ -126,21 +129,23 @@ extension MainViewController { if let pumpBatteryRecord = lastPumpRecord["battery"] as? [String: AnyObject], let pumpBatteryPercent = pumpBatteryRecord["percent"] as? Double { - infoManager.updateInfoData(type: .pumpBattery, value: String(format: "%.0f", pumpBatteryPercent) + "%") + infoManager.updateInfoData(type: .pumpBattery, value: String(format: "%.0f", pumpBatteryPercent) + "%", numericValue: pumpBatteryPercent) Observable.shared.pumpBatteryLevel.value = pumpBatteryPercent } if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject], let upbat = uploader["battery"] as? Double { + let isCharging = uploader["isCharging"] as? Bool let batteryText: String - if let isCharging = uploader["isCharging"] as? Bool, isCharging { + if isCharging == true { batteryText = "⚡️ " + String(format: "%.0f", upbat) + "%" } else { batteryText = String(format: "%.0f", upbat) + "%" } - infoManager.updateInfoData(type: .battery, value: batteryText) + infoManager.updateInfoData(type: .battery, value: batteryText, numericValue: upbat) Observable.shared.deviceBatteryLevel.value = upbat + Observable.shared.deviceBatteryIsCharging.value = isCharging let timestamp = uploader["timestamp"] as? Date ?? Date() let currentBattery = DataStructs.batteryStruct(batteryLevel: upbat, timestamp: timestamp) @@ -155,6 +160,17 @@ extension MainViewController { // Loop - handle new data if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String: AnyObject]? { + // Some pumps report no `pump.clock`; without it alertLastLoopTime stays 0 + // and the forecast anchors to epoch 0. Fall back to the loop cycle timestamp. + if (lastDeviceStatus?["pump"] as? [String: AnyObject])?["clock"] == nil, + let loopTimestampString = lastLoopRecord["timestamp"] as? String, + let loopTimestamp = formatter.date(from: loopTimestampString)?.timeIntervalSince1970, + loopTimestamp > (Observable.shared.alertLastLoopTime.value ?? 0) + { + Observable.shared.alertLastLoopTime.value = loopTimestamp + Storage.shared.lastLoopTime.value = loopTimestamp + } + DeviceStatusLoop(formatter: formatter, lastLoopRecord: lastLoopRecord) var oText = "" @@ -188,6 +204,17 @@ extension MainViewController { DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord) } + // If the active looping system flipped (Loop ⇄ Trio/OpenAPS), drop the previous + // system's forecast so it doesn't linger next to the one just drawn above. + let currentDeviceIsLoop = Storage.shared.device.value == "Loop" + if currentDeviceIsLoop != previousDeviceWasLoop { + if currentDeviceIsLoop { + clearOpenAPSPredictionGraph() + } else { + clearLoopPredictionGraph() + } + } + // Start the timer based on the timestamp let now = dateTimeUtils.getNowTimeIntervalUTC() let secondsAgo = now - (Observable.shared.alertLastLoopTime.value ?? 0) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index 771fdc68d..bd5699726 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -68,7 +68,7 @@ extension MainViewController { let prediction = predictdata["values"] as! [Double] Observable.shared.predictionText.value = Localizer.toDisplayUnits(String(Int(round(prediction.last!)))) Observable.shared.predictionColor.value = .purple - if Storage.shared.downloadPrediction.value, previousLastLoopTime < lastLoopTime { + if Storage.shared.downloadPrediction.value, previousLastLoopTime < lastLoopTime || predictionData.isEmpty { predictionData.removeAll() var predictionTime = lastLoopTime let toLoad = Int(Storage.shared.predictionToLoad.value * 12) @@ -101,9 +101,11 @@ extension MainViewController { updatePredictionGraph() } if let recBolus = lastLoopRecord["recommendedBolus"] as? Double { - let formattedRecBolus = String(format: "%.2fU", recBolus) - infoManager.updateInfoData(type: .recBolus, value: formattedRecBolus) + infoManager.updateInfoData(type: .recBolus, value: InsulinFormatter.shared.string(recBolus), numericValue: recBolus) Observable.shared.deviceRecBolus.value = recBolus + } else { + infoManager.clearInfoData(type: .recBolus) + Observable.shared.deviceRecBolus.value = nil } if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String: AnyObject] { if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 { diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index ca8338a47..7dfdb4cdb 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -3,7 +3,6 @@ import Foundation import HealthKit -import UIKit extension MainViewController { func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) { @@ -20,7 +19,7 @@ extension MainViewController { var updatedTime: TimeInterval? - if let timestamp = enactedOrSuggested["timestamp"] as? String, + if let timestamp = enactedOrSuggested["deliverAt"] as? String ?? enactedOrSuggested["timestamp"] as? String, let parsedTime = formatter.date(from: timestamp)?.timeIntervalSince1970 { updatedTime = parsedTime @@ -32,7 +31,9 @@ extension MainViewController { // ISF let profileISF = profileManager.currentISF() var enactedISF: HKQuantity? - if let enactedISFValue = enactedOrSuggested["ISF"] as? Double { + // Some uploaders (e.g. Trio Swift oref with dynamic ISF) report a + // placeholder 0 in the structured ISF field; treat that as missing. + if let enactedISFValue = enactedOrSuggested["ISF"] as? Double, enactedISFValue != 0 { enactedISF = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: enactedISFValue) } if let profileISF = profileISF, let enactedISF = enactedISF, profileISF != enactedISF { @@ -44,9 +45,14 @@ extension MainViewController { } // Carb Ratio (CR) + // Prefer the structured CR field; fall back to parsing it out of the + // reason string for uploaders that don't expose it (a CR of 0 is never + // valid, so it's treated as missing). let profileCR = profileManager.currentCarbRatio() var enactedCR: Double? - if let reasonString = enactedOrSuggested["reason"] as? String { + if let structuredCR = enactedOrSuggested["CR"] as? Double, structuredCR != 0 { + enactedCR = structuredCR + } else if let reasonString = enactedOrSuggested["reason"] as? String { let pattern = "CR: (\\d+(?:\\.\\d+)?)" if let regex = try? NSRegularExpression(pattern: pattern) { let nsString = reasonString as NSString @@ -107,10 +113,11 @@ extension MainViewController { } // Recommended Bolus - if let rec = InsulinMetric(from: lastLoopRecord, key: "recommendedBolus") { - infoManager.updateInfoData(type: .recBolus, value: rec) - Observable.shared.deviceRecBolus.value = rec.value + if let rec = lastLoopRecord["recommendedBolus"] as? Double { + infoManager.updateInfoData(type: .recBolus, value: InsulinFormatter.shared.string(rec)) + Observable.shared.deviceRecBolus.value = rec } else { + infoManager.clearInfoData(type: .recBolus) Observable.shared.deviceRecBolus.value = nil } @@ -155,7 +162,9 @@ extension MainViewController { } // TDD - if let tddMetric = InsulinMetric(from: enactedOrSuggested, key: "TDD") { + if let tddMetric = InsulinMetric(from: enactedOrSuggested, key: "TDD") + ?? InsulinMetric(from: lastLoopRecord["enacted"], key: "TDD") + { infoManager.updateInfoData(type: .tdd, value: tddMetric) Storage.shared.lastTdd.value = tddMetric.value } diff --git a/LoopFollow/Controllers/Nightscout/IAge.swift b/LoopFollow/Controllers/Nightscout/IAge.swift index 50e9bd592..71909cc49 100644 --- a/LoopFollow/Controllers/Nightscout/IAge.swift +++ b/LoopFollow/Controllers/Nightscout/IAge.swift @@ -55,7 +55,7 @@ extension MainViewController { formatter.zeroFormattingBehavior = [.pad] if let formattedDuration = formatter.string(from: secondsAgo) { - infoManager.updateInfoData(type: .iage, value: formattedDuration) + infoManager.updateInfoData(type: .iage, value: formattedDuration, numericValue: secondsAgo / 86400) } } } diff --git a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift index 2508c94e2..6db744aa9 100644 --- a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift +++ b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift @@ -40,8 +40,10 @@ class NightscoutSocketManager { return } - let url = Storage.shared.url.value - let token = Storage.shared.token.value + // Sanitize defensively: values saved before this fix may still hold a stray + // whitespace/control char that crashes Socket.IO's URL builder on iOS 26. + let url = NightscoutUtils.sanitizeConnectionInput(Storage.shared.url.value) + let token = NightscoutUtils.sanitizeConnectionInput(Storage.shared.token.value) guard !url.isEmpty else { disconnect() diff --git a/LoopFollow/Controllers/Nightscout/SAge.swift b/LoopFollow/Controllers/Nightscout/SAge.swift index f5de50a78..580d39f1a 100644 --- a/LoopFollow/Controllers/Nightscout/SAge.swift +++ b/LoopFollow/Controllers/Nightscout/SAge.swift @@ -72,7 +72,7 @@ extension MainViewController { formatter.zeroFormattingBehavior = [.pad] // Pad with zeroes where appropriate for the locale if let formattedDuration = formatter.string(from: secondsAgo) { - infoManager.updateInfoData(type: .sage, value: formattedDuration) + infoManager.updateInfoData(type: .sage, value: formattedDuration, numericValue: secondsAgo / 86400) } } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift index 15cc1a6b6..533bdd67d 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift @@ -2,7 +2,6 @@ // BGCheck.swift import Foundation -import UIKit extension MainViewController { // NS BG Check Response Processor diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift index 5d75adb2d..23e2e0427 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift @@ -10,6 +10,7 @@ extension MainViewController { carbData.removeAll() var lastFoundIndex = 0 var lastFoundBolus = 0 + var lastFoundSmb = 0 for currentEntry in entries.reversed() { var carbDate: String @@ -35,7 +36,10 @@ extension MainViewController { let bolusTime = findNearestBolusbyTime(timeWithin: 300, needle: dateTimeStamp, haystack: bolusData, startingIndex: lastFoundBolus) lastFoundBolus = bolusTime.foundIndex - offset = bolusTime.offset ? 70 : 20 + let smbTime = findNearestBolusbyTime(timeWithin: 300, needle: dateTimeStamp, haystack: smbData, startingIndex: lastFoundSmb) + lastFoundSmb = smbTime.foundIndex + + offset = (bolusTime.offset || smbTime.offset) ? 70 : 20 } if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (3600 * Storage.shared.predictionToLoad.value)) { @@ -81,7 +85,7 @@ extension MainViewController { } let resultString = String(format: "%.0f", totalCarbs) - infoManager.updateInfoData(type: .carbsToday, value: resultString) + infoManager.updateInfoData(type: .carbsToday, value: resultString, numericValue: totalCarbs) Storage.shared.lastCarbsToday.value = totalCarbs } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift index d160bd1c9..2bff1c691 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift @@ -9,7 +9,7 @@ extension MainViewController { updateIage(data: entries) } else if let iage = currentIage { updateIage(data: [iage]) - } else if Storage.shared.infoVisible.value[InfoType.iage.rawValue] { + } else if Storage.shared.infoDisplayItems.value.isVisible(.iage) { webLoadNSIage() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift index c78aba78a..cc4435212 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift @@ -2,7 +2,6 @@ // Notes.swift import Foundation -import UIKit extension MainViewController { // NS Note Response Processor diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift index 3b61dad8e..00dd745bd 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift @@ -2,12 +2,12 @@ // Overrides.swift import Foundation -import UIKit extension MainViewController { func processNSOverrides(entries: [[String: AnyObject]]) { overrideGraphData.removeAll() var activeOverrideNote: String? + var activeOverrideEndAt: TimeInterval? let sorted = entries.sorted { lhs, rhs in guard @@ -36,19 +36,28 @@ extension MainViewController { let start = max(startDate.timeIntervalSince1970, graphHorizon) - var end: TimeInterval - if (e["durationType"] as? String) == "indefinite" { // Only for Loop overrides - end = maxEndDate - } else { - end = start + (e["duration"] as? Double ?? 5) * 60 - } + let nextStart: TimeInterval? = { + guard i + 1 < sorted.count, + let nextDateStr = (sorted[i + 1]["timestamp"] as? String) ?? (sorted[i + 1]["created_at"] as? String) + else { return nil } + return NightscoutUtils.parseDate(nextDateStr)?.timeIntervalSince1970 + }() + + let durationSeconds = (e["duration"] as? Double ?? 5) * 60 + // Loop marks indefinite overrides explicitly; Trio represents them + // as a ~30-day duration. Treat a week or longer as indefinite. + let isIndefinite = (e["durationType"] as? String) == "indefinite" + || durationSeconds >= 7 * 24 * 3600 - if i + 1 < sorted.count, - let nextDateStr = (sorted[i + 1]["timestamp"] as? String) ?? (sorted[i + 1]["created_at"] as? String), - let nextStart = NightscoutUtils.parseDate(nextDateStr)? - .timeIntervalSince1970 - { + var end: TimeInterval = isIndefinite ? maxEndDate : start + durationSeconds + + // True end for countdown display: based on the raw start and never + // clamped to the graph edge; nil while indefinite. + var trueEnd: TimeInterval? = isIndefinite ? nil : startDate.timeIntervalSince1970 + durationSeconds + + if let nextStart = nextStart { end = min(end, nextStart - 60) // avoid overlapping overrides + trueEnd = trueEnd.map { min($0, nextStart - 60) } } end = min(end, maxEndDate) @@ -69,17 +78,24 @@ extension MainViewController { return [lo, hi] }(), enteredBy: e["enteredBy"] as? String ?? "unknown", - reason: e["reason"] as? String ?? "", + // Loop stores the override name in "reason"; Trio stores it in + // "notes". Prefer notes so Trio override names surface on the + // graph, matching the info table and treatments list. + reason: (e["notes"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) + ?? (e["reason"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) + ?? "", sgv: -20 ) overrideGraphData.append(dot) if now >= start, now < end { activeOverrideNote = e["notes"] as? String ?? e["reason"] as? String + activeOverrideEndAt = trueEnd } } Observable.shared.override.value = activeOverrideNote + Observable.shared.overrideEndAt.value = activeOverrideEndAt if Storage.shared.device.value != "Loop" { if let note = activeOverrideNote { infoManager.updateInfoData(type: .override, value: note) @@ -90,5 +106,9 @@ extension MainViewController { if Storage.shared.graphOtherTreatments.value { updateOverrideGraph() } + + #if !targetEnvironment(macCatalyst) + LiveActivityManager.shared.refreshFromCurrentState(reason: "overrideChanged") + #endif } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift index 34cbb7ebd..f896e4941 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift @@ -35,7 +35,6 @@ extension MainViewController { if Storage.shared.graphBolus.value { updateSmbGraph() - updateChartRenderers() } } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift index 7e9d8e918..592e47f7a 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift @@ -3,13 +3,13 @@ import Foundation import HealthKit -import UIKit extension MainViewController { // NS Temporary Target Response Processor func processNSTemporaryTarget(entries: [[String: AnyObject]]) { tempTargetGraphData.removeAll() var activeTempTarget: Int? + var activeTempTargetEndAt: TimeInterval? for (index, currentEntry) in entries.reversed().enumerated() { guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } @@ -28,6 +28,7 @@ extension MainViewController { if let activeIndex = tempTargetGraphData.lastIndex(where: { $0.endDate > dateTimeStamp }) { tempTargetGraphData[activeIndex].endDate = dateTimeStamp activeTempTarget = nil + activeTempTargetEndAt = nil } continue } @@ -59,12 +60,14 @@ extension MainViewController { let currentTime = Date().timeIntervalSince1970 if currentTime < endDate { activeTempTarget = Int(targetValue!) + // Trio represents indefinite temp targets as a ~30-day duration; + // treat a week or longer as indefinite (no end to display). + activeTempTargetEndAt = duration >= 7 * 24 * 3600 ? nil : endDate } } if Storage.shared.graphOtherTreatments.value { updateTempTargetGraph() - updateChartRenderers() } if let target = activeTempTarget { @@ -74,5 +77,10 @@ extension MainViewController { } else { Observable.shared.tempTarget.value = nil } + Observable.shared.tempTargetEndAt.value = activeTempTargetEndAt + + #if !targetEnvironment(macCatalyst) + LiveActivityManager.shared.refreshFromCurrentState(reason: "tempTargetChanged") + #endif } } diff --git a/LoopFollow/Controllers/StatsDisplayView.swift b/LoopFollow/Controllers/StatsDisplayView.swift index f19be5543..3597a5260 100644 --- a/LoopFollow/Controllers/StatsDisplayView.swift +++ b/LoopFollow/Controllers/StatsDisplayView.swift @@ -15,7 +15,8 @@ struct StatsDisplayView: View { pieRange: model.pieRange, pieHigh: model.pieHigh ) - .frame(width: 100, height: 100) + .frame(width: 80, height: 80) + .padding(.leading, 8) VStack(spacing: 10) { HStack { @@ -48,37 +49,31 @@ struct StatsDisplayView: View { } } -struct StatsPieChartView: UIViewRepresentable { +struct StatsPieChartView: View { var pieLow: Double var pieRange: Double var pieHigh: Double - func makeUIView(context _: Context) -> PieChartView { - let chart = PieChartView() - chart.legend.enabled = false - chart.drawEntryLabelsEnabled = false - chart.drawHoleEnabled = false - chart.rotationEnabled = false - chart.isUserInteractionEnabled = false - chart.backgroundColor = .clear - return chart + private struct Slice: Identifiable { + let id: String + let value: Double + let color: Color } - func updateUIView(_ chart: PieChartView, context _: Context) { - let entries = [ - PieChartDataEntry(value: max(pieLow, 0.1)), - PieChartDataEntry(value: max(pieRange, 0.1)), - PieChartDataEntry(value: max(pieHigh, 0.1)), + private var slices: [Slice] { + [ + Slice(id: "low", value: max(pieLow, 0.1), color: .red), + Slice(id: "range", value: max(pieRange, 0.1), color: .green), + Slice(id: "high", value: max(pieHigh, 0.1), color: .yellow), ] + } - let dataSet = PieChartDataSet(entries: entries, label: "") - dataSet.drawIconsEnabled = false - dataSet.sliceSpace = 0 - dataSet.drawValuesEnabled = false - dataSet.valueLineWidth = 0 - dataSet.formLineWidth = 0 - dataSet.colors = [.systemRed, .systemGreen, .systemYellow] - - chart.data = PieChartData(dataSet: dataSet) + var body: some View { + Chart(slices) { slice in + SectorMark(angle: .value("share", slice.value)) + .foregroundStyle(slice.color) + } + .chartLegend(.hidden) + .allowsHitTesting(false) } } diff --git a/LoopFollow/Controllers/Timers.swift b/LoopFollow/Controllers/Timers.swift index e5ee1bcda..b13ba4b4a 100644 --- a/LoopFollow/Controllers/Timers.swift +++ b/LoopFollow/Controllers/Timers.swift @@ -2,10 +2,10 @@ // Timers.swift import Foundation -import UIKit extension MainViewController { func startGraphNowTimer(time: TimeInterval = 60) { + graphNowTimer.invalidate() graphNowTimer = Timer.scheduledTimer(timeInterval: time, target: self, selector: #selector(MainViewController.graphNowTimerDidEnd(_:)), @@ -14,6 +14,6 @@ extension MainViewController { } @objc func graphNowTimerDidEnd(_: Timer) { - createVerticalLines() + chartModel.rebuild() } } diff --git a/LoopFollow/Extensions/EKEventStore+Extensions.swift b/LoopFollow/Extensions/EKEventStore+Extensions.swift index c9b81e2f2..25b065959 100644 --- a/LoopFollow/Extensions/EKEventStore+Extensions.swift +++ b/LoopFollow/Extensions/EKEventStore+Extensions.swift @@ -6,14 +6,8 @@ import Foundation extension EKEventStore { func requestCalendarAccess(completion: @escaping (Bool, Error?) -> Void) { - if #available(iOS 17, *) { - requestFullAccessToEvents { granted, error in - completion(granted, error) - } - } else { - requestAccess(to: .event) { granted, error in - completion(granted, error) - } + requestFullAccessToEvents { granted, error in + completion(granted, error) } } } diff --git a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift b/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift deleted file mode 100644 index 466421e70..000000000 --- a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift +++ /dev/null @@ -1,31 +0,0 @@ -// LoopFollow -// HKQuantity+AnyConvertible.swift - -import HealthKit - -class HKQuantityWrapper: AnyConvertible { - let quantity: HKQuantity - - init(quantity: HKQuantity) { - self.quantity = quantity - } - - func toAny() -> Any { - return ["unit": UserDefaultsRepository.getPreferredUnit().unitString, - "value": quantity.doubleValue(for: UserDefaultsRepository.getPreferredUnit())] - } - - static func fromAny(_ anyValue: Any) -> HKQuantityWrapper? { - // Convert dictionary back to HKQuantity - guard let dict = anyValue as? [String: Any], - let unitString = dict["unit"] as? String, - let value = dict["value"] as? Double - else { - return nil - } - - let unit = HKUnit(from: unitString) - let quantity = HKQuantity(unit: unit, doubleValue: value) - return HKQuantityWrapper(quantity: quantity) - } -} diff --git a/LoopFollow/Helpers/AnyConvertible.swift b/LoopFollow/Helpers/AnyConvertible.swift deleted file mode 100644 index 7c7badfa1..000000000 --- a/LoopFollow/Helpers/AnyConvertible.swift +++ /dev/null @@ -1,130 +0,0 @@ -// LoopFollow -// AnyConvertible.swift - -import Foundation - -/// A type that can be converted to/from Any -protocol AnyConvertible { - func toAny() -> Any - static func fromAny(_ anyValue: Any) -> Self? -} - -// common type implementations -extension Bool: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Bool? { - return anyValue as? Bool - } -} - -extension String: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> String? { - return anyValue as? String - } -} - -extension Int: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Int? { - return anyValue as? Int - } -} - -extension Float: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Float? { - return anyValue as? Float - } -} - -extension Double: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Double? { - return anyValue as? Double - } -} - -extension Date: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Date? { - return anyValue as? Date - } -} - -extension Data: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Data? { - return anyValue as? Data - } -} - -extension UUID: AnyConvertible { - func toAny() -> Any { - return uuidString - } - - static func fromAny(_ anyValue: Any) -> UUID? { - guard let uuidString = anyValue as? String else { - return nil - } - - return UUID(uuidString: uuidString) - } -} - -// extension Array: AnyConvertible { -// func toAny() -> Any { -// return self -// } -// -// static func fromAny(_ anyValue: Any) -> Array? { -// return anyValue as? Array -// } -// } - -extension Array: AnyConvertible where Element: AnyConvertible { - func toAny() -> Any { - return map { $0.toAny() } - } - - static func fromAny(_ anyValue: Any) -> Array? { - return (anyValue as? Array)?.compactMap { Element.fromAny($0) } - } -} - -extension Optional: AnyConvertible where Wrapped: AnyConvertible { - func toAny() -> Any { - switch self { - case let .some(value): - return value.toAny() - case .none: - return self as Any - } - } - - static func fromAny(_ anyValue: Any) -> Wrapped?? { - return Wrapped.fromAny(anyValue) - } -} diff --git a/LoopFollow/Helpers/AppConstants.swift b/LoopFollow/Helpers/AppConstants.swift index e81ba159c..7c88dea52 100644 --- a/LoopFollow/Helpers/AppConstants.swift +++ b/LoopFollow/Helpers/AppConstants.swift @@ -5,8 +5,6 @@ import Foundation // Class that contains general constants used in different classes class AppConstants { - static let APP_GROUP_ID = "group.com.$(unique_id).LoopFollow" - /// Extracts the app suffix from the bundle identifier /// Bundle identifier format: com.$(unique_id).LoopFollow$(app_suffix) /// Returns the suffix part (e.g., "2" for "com.example.LoopFollow2") diff --git a/LoopFollow/Helpers/BackgroundRefreshManager.swift b/LoopFollow/Helpers/BackgroundRefreshManager.swift index ee34a4d3c..ab2b42e67 100644 --- a/LoopFollow/Helpers/BackgroundRefreshManager.swift +++ b/LoopFollow/Helpers/BackgroundRefreshManager.swift @@ -2,7 +2,7 @@ // BackgroundRefreshManager.swift import BackgroundTasks -import UIKit +import Foundation class BackgroundRefreshManager { static let shared = BackgroundRefreshManager() diff --git a/LoopFollow/Helpers/Chart.swift b/LoopFollow/Helpers/Chart.swift deleted file mode 100644 index 7bab094ea..000000000 --- a/LoopFollow/Helpers/Chart.swift +++ /dev/null @@ -1,216 +0,0 @@ -// LoopFollow -// Chart.swift - -import Charts -import Foundation - -final class OverrideFillFormatter: FillFormatter { - func getFillLinePosition(dataSet: Charts.LineChartDataSetProtocol, dataProvider _: Charts.LineChartDataProvider) -> CGFloat { - return CGFloat(dataSet.entryForIndex(0)!.y) - // return 375 - } -} - -final class basalFillFormatter: FillFormatter { - func getFillLinePosition(dataSet _: Charts.LineChartDataSetProtocol, dataProvider _: Charts.LineChartDataProvider) -> CGFloat { - return 0 - } -} - -final class ChartXValueFormatter: AxisValueFormatter { - func stringForValue(_ value: Double, axis _: AxisBase?) -> String { - let dateFormatter = DateFormatter() - // let timezoneOffset = TimeZone.current.secondsFromGMT() - // let epochTimezoneOffset = value + Double(timezoneOffset) - if dateTimeUtils.is24Hour() { - dateFormatter.setLocalizedDateFormatFromTemplate("HH:mm") - } else { - dateFormatter.setLocalizedDateFormatFromTemplate("hh:mm") - } - - if Storage.shared.graphTimeZoneEnabled.value, - let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) - { - dateFormatter.timeZone = tz - } - - // let date = Date(timeIntervalSince1970: epochTimezoneOffset) - let date = Date(timeIntervalSince1970: value) - let formattedDate = dateFormatter.string(from: date) - - return formattedDate - } -} - -final class ChartYDataValueFormatter: ValueFormatter { - func stringForValue(_: Double, entry: ChartDataEntry, dataSetIndex _: Int, viewPortHandler _: ViewPortHandler?) -> String { - guard let text = entry.data as? String else { return "" } - // Treatment entries store "label\r\rpillText" — extract only the label portion. - if let range = text.range(of: "\r\r") { - return String(text[.. String { - if entry.data != nil { - return entry.data as? String ?? "" - } else { - return "" - } - } -} - -final class ChartYMMOLValueFormatter: AxisValueFormatter { - func stringForValue(_ value: Double, axis _: AxisBase?) -> String { - return Localizer.toDisplayUnits(String(value)) - } -} - -class PillMarker: MarkerImage { - private(set) var color: UIColor - private(set) var font: UIFont - private(set) var textColor: UIColor - private var labelText: String = "" - private var attrs: [NSAttributedString.Key: AnyObject]! - - static let formatter: DateComponentsFormatter = { - let f = DateComponentsFormatter() - f.allowedUnits = [.minute, .second] - f.unitsStyle = .short - return f - }() - - init(color: UIColor, font: UIFont, textColor: UIColor) { - self.color = color - self.font = font - self.textColor = textColor - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - attrs = [.font: font, .paragraphStyle: paragraphStyle, .foregroundColor: textColor, .baselineOffset: NSNumber(value: -4)] - super.init() - } - - override func draw(context: CGContext, point: CGPoint) { - // custom padding around text - let labelWidth = labelText.size(withAttributes: attrs).width + 10 - // if you modify labelHeigh you will have to tweak baselineOffset in attrs - let labelHeight = labelText.size(withAttributes: attrs).height + 4 - - // place pill above the marker, centered along x - var rectangle = CGRect(x: point.x, y: point.y, width: labelWidth, height: labelHeight) - rectangle.origin.x -= rectangle.width / 2.0 - var spacing: CGFloat = 20 - if point.y < 300 { spacing = -40 } - - rectangle.origin.y -= rectangle.height + spacing - - // rounded rect - let clipPath = UIBezierPath(roundedRect: rectangle, cornerRadius: 6.0).cgPath - context.addPath(clipPath) - context.setFillColor(UIColor.secondarySystemBackground.cgColor) - context.setStrokeColor(UIColor.label.cgColor) - context.closePath() - context.drawPath(using: .fillStroke) - - // add the text - labelText.draw(with: rectangle, options: .usesLineFragmentOrigin, attributes: attrs, context: nil) - } - - private static let pillSeparator = "\r\r" - - override func refreshContent(entry: ChartDataEntry, highlight _: Highlight) { - if let text = entry.data as? String { - // Treatment entries use pillSeparator to separate the value label from the marker text. - if let range = text.range(of: Self.pillSeparator) { - labelText = String(text[range.upperBound...]) - } else { - labelText = text - } - } else { - labelText = String(entry.y) - } - } - - private func customString(_ value: Double) -> String { - let formattedString = PillMarker.formatter.string(from: TimeInterval(value))! - // using this to convert the left axis values formatting, ie 2 min - return "\(formattedString)" - } -} - -// MARK: - Cone of Uncertainty - -class ConeChartDataEntry: ChartDataEntry { - var yMin: Double = 0.0 - var yMax: Double = 0.0 - - required init() { - super.init() - } - - init(x: Double, yMin: Double, yMax: Double) { - self.yMin = yMin - self.yMax = yMax - super.init(x: x, y: yMax) - } - - override func copy(with _: NSZone? = nil) -> Any { - let copy = ConeChartDataEntry(x: x, yMin: yMin, yMax: yMax) - copy.data = data - return copy - } -} - -class ConeOfUncertaintyRenderer: LineChartRenderer { - let coneDataSetIndex: Int - - init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, coneDataSetIndex: Int) { - self.coneDataSetIndex = coneDataSetIndex - super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!) - } - - override func drawExtras(context: CGContext) { - super.drawExtras(context: context) - - guard let dataProvider = dataProvider, - dataProvider.lineData?.dataSets.count ?? 0 > coneDataSetIndex, - let lineDataSet = dataProvider.lineData?.dataSets[coneDataSetIndex] as? LineChartDataSet, - lineDataSet.entryCount > 1 else { return } - - let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency) - let phaseY = animator.phaseY - - var upperPoints = [CGPoint]() - var lowerPoints = [CGPoint]() - - for i in 0 ..< lineDataSet.entryCount { - guard let entry = lineDataSet.entryForIndex(i) as? ConeChartDataEntry else { continue } - upperPoints.append(trans.pixelForValues(x: entry.x, y: entry.yMax * phaseY)) - lowerPoints.append(trans.pixelForValues(x: entry.x, y: entry.yMin * phaseY)) - } - - guard upperPoints.count > 1 else { return } - - context.saveGState() - - let path = CGMutablePath() - path.move(to: upperPoints[0]) - for i in 1 ..< upperPoints.count { - path.addLine(to: upperPoints[i]) - } - for i in stride(from: lowerPoints.count - 1, through: 0, by: -1) { - path.addLine(to: lowerPoints[i]) - } - path.closeSubpath() - - context.addPath(path) - context.setFillColor(UIColor.systemBlue.withAlphaComponent(0.4).cgColor) - context.fillPath() - - context.restoreGState() - } -} diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index 04c5ff14b..07a6246bc 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -1,6 +1,7 @@ // LoopFollow // NightscoutUtils.swift +import CryptoKit import Foundation class NightscoutUtils { @@ -44,6 +45,7 @@ class NightscoutUtils { case iage = "Insulin Change" case temporaryOverride = "Temporary Override" case temporaryOverrideCancel = "Temporary Override Cancel" + case dbSize var endpoint: String { switch self { @@ -57,6 +59,8 @@ class NightscoutUtils { return "/api/v1/devicestatus.json" case .temporaryOverride, .temporaryOverrideCancel: return "/api/v2/notifications/loop" + case .dbSize: + return "/api/v2/properties/dbsize" } } } @@ -162,6 +166,17 @@ class NightscoutUtils { return request } + // Strip whitespace/newlines/control characters that can sneak in via paste. + // Neither a URL nor a token may legally contain them, and a stray one breaks + // WebSocket connect (invalid percent-encoded query traps on iOS 26) or makes + // URL parsing fall back to a lossy cleanup that mangles the address. + static func sanitizeConnectionInput(_ input: String) -> String { + input.unicodeScalars + .filter { !CharacterSet.whitespacesAndNewlines.contains($0) && !CharacterSet.controlCharacters.contains($0) } + .map(String.init) + .joined() + } + static func constructURL(baseURL: String, token: String?, endpoint: String, parameters: [String: String]) -> URL? { var components = URLComponents(string: baseURL) components?.path = endpoint @@ -176,7 +191,10 @@ class NightscoutUtils { queryItems.append(URLQueryItem(name: key, value: value)) } - components?.queryItems = queryItems + // Assigning an empty array would still append a bare "?" to the URL. + if !queryItems.isEmpty { + components?.queryItems = queryItems + } return components?.url } @@ -240,8 +258,14 @@ class NightscoutUtils { completion(.unknown, nil, false, false) } } else { - if let _ = error { - completion(.siteNotFound, nil, false, false) + if let urlError = error as? URLError { + switch urlError.code { + case .notConnectedToInternet, .networkConnectionLost, .timedOut, .dataNotAllowed: + completion(.networkError, nil, false, false) + default: + // cannotFindHost, cannotConnectToHost, etc. — the address is wrong or the site is down. + completion(.siteNotFound, nil, false, false) + } } else { completion(.networkError, nil, false, false) } @@ -385,6 +409,163 @@ class NightscoutUtils { return responseString } + // MARK: - Token Provisioning + + /// Name of the Nightscout authorization subject LoopFollow creates when a + /// user provisions a token from their API secret. + static let provisionedSubjectName = "LoopFollow" + + private struct AuthSubject: Decodable { + let id: String? + let name: String? + let accessToken: String? + let roles: [String]? + + enum CodingKeys: String, CodingKey { + case id = "_id" + case name, accessToken, roles + } + } + + /// Creates (or reuses) a read-only Nightscout access token using the site's + /// API secret. The secret only authorizes these requests and is never + /// persisted. Returns the access token for a `readable` subject named + /// `provisionedSubjectName`. + /// + /// The full API secret authenticates as Nightscout's `admin` role (the `*` + /// permission), which includes `admin:api:subjects:create`. + /// + /// Nightscout serves the subjects list from an in-memory cache that doesn't + /// refresh promptly after a write, so a freshly-created subject (and its + /// token) can't be read back reliably right after creating it. Instead we + /// derive the token locally: it's a pure function of the subject's `_id` + /// (returned by the create call) and the API secret. See `accessToken(for:)`. + static func provisionReadOnlyToken(url: String, secret: String) async throws -> String { + let trimmedURL = url.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedURL.isEmpty else { throw NightscoutError.emptyAddress } + guard let baseURL = URL(string: trimmedURL), + trimmedURL.hasPrefix("http://") || trimmedURL.hasPrefix("https://") + else { throw NightscoutError.invalidURL } + + let secretHash = sha1Hex(secret) + + // Reuse an existing subject if one is already visible (idempotent re-runs + // once the site's cache has caught up). + if let existing = try await fetchProvisionedToken(baseURL: baseURL, secretHash: secretHash) { + return existing + } + + let id = try await createReadOnlySubject(baseURL: baseURL, secretHash: secretHash) + return accessToken(forName: provisionedSubjectName, id: id, secretHash: secretHash) + } + + /// Returns `true` when `secret` authenticates as the site's API secret. + /// Read-only: it probes an admin-gated endpoint with the hashed secret and + /// creates nothing, so it's safe to call just to find out what the user pasted. + static func verifyAPISecret(url: String, secret: String) async -> Bool { + let trimmedURL = url.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedSecret = secret.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedSecret.isEmpty, + let baseURL = URL(string: trimmedURL), + trimmedURL.hasPrefix("http://") || trimmedURL.hasPrefix("https://") + else { return false } + + let endpoint = baseURL.appendingPathComponent("api/v2/authorization/subjects") + var request = URLRequest(url: endpoint) + request.httpMethod = "GET" + request.setValue(sha1Hex(trimmedSecret), forHTTPHeaderField: "api-secret") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.cachePolicy = .reloadIgnoringLocalCacheData + + do { + let (_, response) = try await URLSession.shared.data(for: request) + return (response as? HTTPURLResponse)?.statusCode == 200 + } catch { + return false + } + } + + private static func fetchProvisionedToken(baseURL: URL, secretHash: String) async throws -> String? { + let url = baseURL.appendingPathComponent("api/v2/authorization/subjects") + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue(secretHash, forHTTPHeaderField: "api-secret") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.cachePolicy = .reloadIgnoringLocalCacheData + + let (data, response) = try await URLSession.shared.data(for: request) + try validateProvisioningResponse(response) + + let subjects = try JSONDecoder().decode([AuthSubject].self, from: data) + return subjects.first(where: { $0.name == provisionedSubjectName })?.accessToken + } + + /// Creates the subject and returns its `_id`. + private static func createReadOnlySubject(baseURL: URL, secretHash: String) async throws -> String { + let url = baseURL.appendingPathComponent("api/v2/authorization/subjects") + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue(secretHash, forHTTPHeaderField: "api-secret") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONSerialization.data(withJSONObject: [ + "name": provisionedSubjectName, + "roles": ["readable"], + ]) + + let (data, response) = try await URLSession.shared.data(for: request) + try validateProvisioningResponse(response) + + // Nightscout returns the created subject wrapped in an array + // (`[{…}]`) on current versions, but a bare object on some older ones, + // so accept either shape. + let subject = try decodeCreatedSubject(from: data) + guard let id = subject.id, !id.isEmpty else { throw NightscoutError.unknown } + return id + } + + private static func decodeCreatedSubject(from data: Data) throws -> AuthSubject { + let decoder = JSONDecoder() + if let array = try? decoder.decode([AuthSubject].self, from: data) { + guard let first = array.first else { throw NightscoutError.unknown } + return first + } + return try decoder.decode(AuthSubject.self, from: data) + } + + /// Reproduces Nightscout's subject-token derivation (`lib/authorization`): + /// abbrev = name lowercased, non-`\w` characters removed, first 10 chars + /// digest = sha1( sha1Hex(apiSecret) + subjectId ) + /// token = "\(abbrev)-\(digest[0..<16])" + private static func accessToken(forName name: String, id: String, secretHash: String) -> String { + let allowed = Set("abcdefghijklmnopqrstuvwxyz0123456789_") + let abbrev = String(name.lowercased().filter { allowed.contains($0) }.prefix(10)) + let digest = sha1Hex(secretHash + id) + return abbrev + "-" + String(digest.prefix(16)) + } + + private static func validateProvisioningResponse(_ response: URLResponse) throws { + guard let http = response as? HTTPURLResponse else { + throw NightscoutError.networkError + } + switch http.statusCode { + case 200 ..< 300: + return + case 401, 403: + // The API secret was missing or wrong. + throw NightscoutError.invalidToken + case 404: + throw NightscoutError.siteNotFound + default: + throw NightscoutError.unknown + } + } + + private static func sha1Hex(_ string: String) -> String { + Insecure.SHA1.hash(data: Data(string.utf8)) + .map { String(format: "%02x", $0) } + .joined() + } + static func extractErrorReason(from responseString: String) -> String { // 1) Try to parse the entire string as JSON and return the "message" if let data = responseString.data(using: .utf8) { diff --git a/LoopFollow/Helpers/NotificationAuthorization.swift b/LoopFollow/Helpers/NotificationAuthorization.swift new file mode 100644 index 000000000..e8ed69afd --- /dev/null +++ b/LoopFollow/Helpers/NotificationAuthorization.swift @@ -0,0 +1,30 @@ +// LoopFollow +// NotificationAuthorization.swift + +import UserNotifications + +/// Requests notification authorization lazily, the first time the user opts into +/// a feature that needs it (alarms). This keeps the system prompt off the very +/// first launch so it doesn't front the onboarding flow. +enum NotificationAuthorization { + /// Asks for authorization only when the user hasn't decided yet. Safe to call + /// repeatedly — it's a no-op once the status is determined. `completion` runs + /// on the main queue after the prompt is dismissed (or immediately when the + /// status was already determined), so a caller can wait for the system prompt + /// before moving on. + static func requestIfNeeded(completion: @escaping () -> Void = {}) { + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { settings in + guard settings.authorizationStatus == .notDetermined else { + DispatchQueue.main.async { completion() } + return + } + center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in + if !granted { + LogManager.shared.log(category: .general, message: "User has declined notifications") + } + DispatchQueue.main.async { completion() } + } + } + } +} diff --git a/LoopFollow/Helpers/ObservationToken.swift b/LoopFollow/Helpers/ObservationToken.swift deleted file mode 100644 index c2092a5f1..000000000 --- a/LoopFollow/Helpers/ObservationToken.swift +++ /dev/null @@ -1,17 +0,0 @@ -// LoopFollow -// ObservationToken.swift - -import Foundation - -/// The token received by an observe when subscribes to its subject. The observer can cancel observation, so the subject will remove it from its observers list. -class ObservationToken { - private let cancellationClosure: () -> Void - - init(cancellationClosure: @escaping () -> Void) { - self.cancellationClosure = cancellationClosure - } - - func cancel() { - cancellationClosure() - } -} diff --git a/LoopFollow/Helpers/Telemetry.swift b/LoopFollow/Helpers/Telemetry.swift index 26246a728..afd90117f 100644 --- a/LoopFollow/Helpers/Telemetry.swift +++ b/LoopFollow/Helpers/Telemetry.swift @@ -348,7 +348,7 @@ struct TelemetryConsentView: View { VStack(alignment: .leading, spacing: 16) { Text("You can choose to share anonymous information with the developers to help improve LoopFollow—such as app and iOS version, device type, which app you're following, and a few settings. Your health data, credentials, time zone, and logs remain on your device.") - Text("You can change this any time in Settings → Diagnostics.") + Text("You can change this any time in Settings → General → Diagnostics.") .font(.subheadline) .foregroundColor(.secondary) diff --git a/LoopFollow/Helpers/Views/NavigationRow.swift b/LoopFollow/Helpers/Views/NavigationRow.swift index 2198a83e2..5c98e929c 100644 --- a/LoopFollow/Helpers/Views/NavigationRow.swift +++ b/LoopFollow/Helpers/Views/NavigationRow.swift @@ -5,6 +5,7 @@ import SwiftUI struct NavigationRow: View { let title: String + var subtitle: String? = nil let icon: String var iconTint: Color = .white let value: Value @@ -13,7 +14,14 @@ struct NavigationRow: View { NavigationLink(value: value) { HStack { Glyph(symbol: icon, tint: iconTint) - Text(title) + VStack(alignment: .leading, spacing: 2) { + Text(title) + if let subtitle { + Text(subtitle) + .font(.caption) + .foregroundStyle(.secondary) + } + } } } } diff --git a/LoopFollow/Helpers/Views/TogglableSecureInput.swift b/LoopFollow/Helpers/Views/TogglableSecureInput.swift index f6ed70343..fc88a40e9 100644 --- a/LoopFollow/Helpers/Views/TogglableSecureInput.swift +++ b/LoopFollow/Helpers/Views/TogglableSecureInput.swift @@ -28,13 +28,14 @@ struct TogglableSecureInput: View { .submitLabel(.done) .focused($isFocused) } else { - HStack { - Spacer() - Text(maskString) - .font(.body.monospaced()) - .foregroundColor(.primary) - .allowsHitTesting(false) - } + // A real (masked) SecureField, not static text, so the + // value stays editable while hidden and — crucially — + // iOS AutoFill/keychain has a secure field to fill. + SecureField(placeholder, text: $text) + .multilineTextAlignment(.trailing) + .textContentType(textContentType) + .submitLabel(.done) + .focused($isFocused) } } @@ -82,19 +83,15 @@ struct TogglableSecureInput: View { } .contentShape(Rectangle()) .onTapGesture { - if !isVisible { - isVisible = true - if style == .singleLine { - isFocused = true - } else if style == .multiLine { - isMultilineFocused = true - } - } else { - if style == .singleLine { - isFocused = true - } else if style == .multiLine { - isMultilineFocused = true - } + switch style { + case .singleLine: + // The hidden state is already an editable SecureField, so tapping + // just focuses it — no need to reveal the plaintext to type. + isFocused = true + case .multiLine: + // The multi-line editor is only editable once revealed. + if !isVisible { isVisible = true } + isMultilineFocused = true } } } diff --git a/LoopFollow/Helpers/carbBolusArrays.swift b/LoopFollow/Helpers/carbBolusArrays.swift index 42d09d67d..691372088 100644 --- a/LoopFollow/Helpers/carbBolusArrays.swift +++ b/LoopFollow/Helpers/carbBolusArrays.swift @@ -32,32 +32,4 @@ extension MainViewController { return (false, 0) } - - func findNextCarbTime(timeWithin: Int, needle: TimeInterval, haystack: [carbGraphStruct], startingIndex: Int) -> Bool { - if startingIndex > haystack.count - 2 { return false } - if haystack[startingIndex + 1].date - needle < Double(timeWithin) { - return true - } - - return false - } - - func findNextBolusTime(timeWithin: Int, needle: TimeInterval, haystack: [bolusGraphStruct], startingIndex: Int) -> Bool { - var last = false - var next = true - if startingIndex > haystack.count - 2 { return false } - if startingIndex == 0 { return false } - - // Nothing to right that requires shift - if haystack[startingIndex + 1].date - needle > Double(timeWithin) { - return false - } else { - // Nothing to left preventing shift - if needle - haystack[startingIndex - 1].date > Double(timeWithin) { - return true - } - } - - return false - } } diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift index d3b589f5d..c43a195c4 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift @@ -5,6 +5,7 @@ import SwiftUI struct InfoDisplaySettingsView: View { @ObservedObject var viewModel: InfoDisplaySettingsViewModel + @State private var selectedID: Int? var body: some View { Form { @@ -17,28 +18,56 @@ struct InfoDisplaySettingsView: View { } } - Section(header: Text("Information Display Settings")) { - ForEach(viewModel.infoSort, id: \.self) { sortedIndex in - HStack { - Text(viewModel.getName(for: sortedIndex)) - Spacer() - Toggle("", isOn: Binding( - get: { viewModel.infoVisible[sortedIndex] }, - set: { _ in - viewModel.toggleVisibility(for: sortedIndex) - } - )) - .labelsHidden() + Section( + header: Text("Information Display Settings"), + footer: Text("Drag to reorder. Tap a row to set its visibility and colors.") + ) { + // The list stays in edit mode so rows are always draggable; a + // Button (unlike NavigationLink) still receives taps in edit + // mode, so reordering and navigation work at the same time. + ForEach(viewModel.items) { item in + Button { + selectedID = item.id + } label: { + rowLabel(for: item) } + .buttonStyle(.plain) } .onMove(perform: viewModel.move) } } .environment(\.editMode, .constant(.active)) + .navigationDestination(isPresented: Binding( + get: { selectedID != nil }, + set: { if !$0 { selectedID = nil } } + )) { + if let id = selectedID { + InfoRowSettingsView(item: viewModel.binding(for: id)) + } + } .onDisappear { NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) } .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Information Display Settings", displayMode: .inline) } + + private func rowLabel(for item: InfoDisplayItem) -> some View { + HStack { + Text(item.type.name) + Spacer() + if item.type.isColorable, item.coloring.enabled { + Circle() + .fill(Color.orange) + .frame(width: 8, height: 8) + .accessibilityLabel("Coloring enabled") + } + Text(item.isVisible ? "On" : "Off") + .foregroundStyle(.secondary) + Image(systemName: "chevron.right") + .font(.footnote.weight(.semibold)) + .foregroundStyle(.tertiary) + } + .contentShape(Rectangle()) + } } diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift index e77d67f69..c951805d4 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift @@ -5,28 +5,31 @@ import Foundation import SwiftUI class InfoDisplaySettingsViewModel: ObservableObject { - @Published var infoSort: [Int] - @Published var infoVisible: [Bool] + @Published var items: [InfoDisplayItem] init() { - infoSort = Storage.shared.infoSort.value - infoVisible = Storage.shared.infoVisible.value + items = Storage.shared.infoDisplayItems.value } - func toggleVisibility(for sortedIndex: Int) { - infoVisible[sortedIndex].toggle() - Storage.shared.infoVisible.value = infoVisible + func move(from source: IndexSet, to destination: Int) { + items.move(fromOffsets: source, toOffset: destination) + persist() } - func move(from source: IndexSet, to destination: Int) { - infoSort.move(fromOffsets: source, toOffset: destination) - Storage.shared.infoSort.value = infoSort + /// A binding to a single item, looked up by id so it survives reordering. + /// Writes persist to Storage immediately. + func binding(for id: Int) -> Binding { + Binding( + get: { self.items.first(where: { $0.id == id }) ?? self.items[0] }, + set: { newValue in + guard let index = self.items.firstIndex(where: { $0.id == id }) else { return } + self.items[index] = newValue + self.persist() + } + ) } - func getName(for index: Int) -> String { - guard let infoType = InfoType(rawValue: index) else { - return "Unknown" - } - return infoType.name + private func persist() { + Storage.shared.infoDisplayItems.value = items } } diff --git a/LoopFollow/InfoDisplaySettings/InfoRowSettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoRowSettingsView.swift new file mode 100644 index 000000000..9387949cc --- /dev/null +++ b/LoopFollow/InfoDisplaySettings/InfoRowSettingsView.swift @@ -0,0 +1,99 @@ +// LoopFollow +// InfoRowSettingsView.swift + +import SwiftUI + +struct InfoRowSettingsView: View { + @Binding var item: InfoDisplayItem + + var body: some View { + Form { + Section { + Toggle("Show in Info Display", isOn: $item.isVisible) + } + + if let config = item.type.colorConfig { + Section( + header: Text("Color"), + footer: Text(colorFooter(config)) + ) { + Toggle("Enable coloring", isOn: enabledBinding(config)) + + if item.coloring.enabled { + thresholdRow(title: "Yellow at", value: $item.coloring.warning, default: config.defaultWarning, config: config) + thresholdRow(title: "Red at", value: $item.coloring.urgent, default: config.defaultUrgent, config: config) + + if let thresholdWarning = thresholdWarning(config) { + Label(thresholdWarning, systemImage: "exclamationmark.triangle.fill") + .font(.footnote) + .foregroundStyle(.orange) + } + } + } + } + } + .navigationBarTitle(item.type.name, displayMode: .inline) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) + } + + /// Turning coloring on seeds any unset threshold, so the steppers and the + /// stored values always agree. + private func enabledBinding(_ config: InfoColorConfig) -> Binding { + Binding( + get: { item.coloring.enabled }, + set: { isOn in + if isOn { + item.coloring.warning = item.coloring.warning ?? config.defaultWarning + item.coloring.urgent = item.coloring.urgent ?? config.defaultUrgent + } + item.coloring.enabled = isOn + } + ) + } + + private func thresholdRow(title: String, value: Binding, default defaultValue: Double, config: InfoColorConfig) -> some View { + SettingsStepperRow( + title: title, + range: config.range, + step: config.step, + value: Binding( + get: { value.wrappedValue ?? defaultValue }, + set: { value.wrappedValue = $0 } + ), + format: { formatted($0, config) } + ) + } + + private func formatted(_ value: Double, _ config: InfoColorConfig) -> String { + let number = Localizer.formatToLocalizedString( + value, + maxFractionDigits: config.fractionDigits, + minFractionDigits: config.fractionDigits + ) + return "\(number) \(config.unit)" + } + + /// Non-blocking sanity check: red should be more severe than yellow, in the + /// metric's fixed direction. `nil` when consistent or a threshold is unset. + private func thresholdWarning(_ config: InfoColorConfig) -> String? { + guard item.coloring.enabled, + let warning = item.coloring.warning, + let urgent = item.coloring.urgent + else { return nil } + switch config.direction { + case .above: + return urgent < warning ? "Red should be at or above yellow." : nil + case .below: + return urgent > warning ? "Red should be at or below yellow." : nil + } + } + + private func colorFooter(_ config: InfoColorConfig) -> String { + switch config.direction { + case .above: + return "The value turns yellow at or above the yellow level and red at or above the red level. In range it shows green. A visual cue only — it never triggers an alarm." + case .below: + return "The value turns yellow at or below the yellow level and red at or below the red level. In range it shows green. A visual cue only — it never triggers an alarm." + } + } +} diff --git a/LoopFollow/InfoTable/InfoData.swift b/LoopFollow/InfoTable/InfoData.swift index 30f5bf9e9..56a8e8e88 100644 --- a/LoopFollow/InfoTable/InfoData.swift +++ b/LoopFollow/InfoTable/InfoData.swift @@ -7,10 +7,14 @@ class InfoData: Identifiable { let id: Int let name: String var value: String + /// Raw numeric value behind `value`, when the row carries a single number. + /// Used for threshold-based coloring; `nil` for text or combined values. + var numericValue: Double? - init(id: Int, name: String, value: String = "") { + init(id: Int, name: String, value: String = "", numericValue: Double? = nil) { self.id = id self.name = name self.value = value + self.numericValue = numericValue } } diff --git a/LoopFollow/InfoTable/InfoDisplayItem.swift b/LoopFollow/InfoTable/InfoDisplayItem.swift new file mode 100644 index 000000000..cf3a3c1b9 --- /dev/null +++ b/LoopFollow/InfoTable/InfoDisplayItem.swift @@ -0,0 +1,73 @@ +// LoopFollow +// InfoDisplayItem.swift + +import SwiftUI + +/// One configurable row in the Info Display: which info it shows, whether it is +/// visible, and its optional threshold-based coloring. The ordered array of +/// these replaces the old parallel `infoSort` / `infoVisible` storage. +struct InfoDisplayItem: Codable, Equatable, Identifiable { + var type: InfoType + var isVisible: Bool + var coloring: InfoColoring + + var id: Int { type.rawValue } +} + +extension Array where Element == InfoDisplayItem { + func item(for type: InfoType) -> InfoDisplayItem? { + first { $0.type == type } + } + + func isVisible(_ type: InfoType) -> Bool { + item(for: type)?.isVisible ?? false + } +} + +/// Which direction is "concerning" for a metric. Fixed per InfoType (battery is +/// bad when low, sensor age is bad when high) rather than user-configurable. +enum InfoColorDirection { + case above // color as the value rises (e.g. SAGE, IOB) + case below // color as the value falls (e.g. Battery, Pump) +} + +/// What threshold coloring looks like for one InfoType: the concerning +/// direction, the unit shown next to the value, and the bounds the threshold +/// steppers move in. Only types that expose one of these can be colored. +struct InfoColorConfig { + let direction: InfoColorDirection + let unit: String + let range: ClosedRange + let step: Double + let defaultWarning: Double + let defaultUrgent: Double + + /// Decimals to show, derived from the step (0.5 → 1, 5 → 0). + var fractionDigits: Int { + step.truncatingRemainder(dividingBy: 1) == 0 ? 0 : 1 + } +} + +/// Per-row, opt-in color thresholds. Purely visual — never triggers an alarm. +struct InfoColoring: Codable, Equatable { + var enabled: Bool = false + var warning: Double? = nil // yellow + var urgent: Double? = nil // red + + /// Resolves the color for a raw numeric value, given the metric's fixed + /// direction. Returns `nil` only when coloring is off (callers map that to + /// the default text color); when enabled, an in-range value reads green, + /// mirroring the app's BG text coloring. + func color(for value: Double, direction: InfoColorDirection) -> Color? { + guard enabled else { return nil } + switch direction { + case .above: + if let urgent, value >= urgent { return .red } + if let warning, value >= warning { return .yellow } + case .below: + if let urgent, value <= urgent { return .red } + if let warning, value <= warning { return .yellow } + } + return .green + } +} diff --git a/LoopFollow/InfoTable/InfoManager.swift b/LoopFollow/InfoTable/InfoManager.swift index f3205511e..3fd32e0eb 100644 --- a/LoopFollow/InfoTable/InfoManager.swift +++ b/LoopFollow/InfoTable/InfoManager.swift @@ -12,14 +12,16 @@ class InfoManager: ObservableObject { tableData = InfoType.allCases.map { InfoData(id: $0.rawValue, name: $0.name) } } - func updateInfoData(type: InfoType, value: String) { + func updateInfoData(type: InfoType, value: String, numericValue: Double? = nil) { tableData[type.rawValue].value = value + tableData[type.rawValue].numericValue = numericValue objectWillChange.send() } func updateInfoData(type: InfoType, value: HKQuantity) { let formattedValue = Localizer.formatQuantity(value) - updateInfoData(type: type, value: formattedValue) + let numericValue = value.doubleValue(for: Localizer.getPreferredUnit()) + updateInfoData(type: type, value: formattedValue, numericValue: numericValue) } func updateInfoData(type: InfoType, firstValue: HKQuantity, secondValue: HKQuantity, separator: InfoDataSeparator) { @@ -35,7 +37,7 @@ class InfoManager: ObservableObject { func updateInfoData(type: InfoType, value: Double, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) { let formattedValue = Localizer.formatToLocalizedString(value, maxFractionDigits: maxFractionDigits, minFractionDigits: minFractionDigits) - updateInfoData(type: type, value: formattedValue) + updateInfoData(type: type, value: formattedValue, numericValue: value) } func updateInfoData(type: InfoType, value: Double, enactedValue: Double, separator: InfoDataSeparator, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) { @@ -48,25 +50,28 @@ class InfoManager: ObservableObject { func updateInfoData(type: InfoType, value: Metric) { let formattedValue = value.formattedValue() - updateInfoData(type: type, value: formattedValue) + updateInfoData(type: type, value: formattedValue, numericValue: value.value) } func clearInfoData(type: InfoType) { tableData[type.rawValue].value = "" + tableData[type.rawValue].numericValue = nil objectWillChange.send() } func clearInfoData(types: [InfoType]) { for type in types { tableData[type.rawValue].value = "" + tableData[type.rawValue].numericValue = nil } objectWillChange.send() } var visibleRows: [InfoData] { - Storage.shared.infoSort.value - .filter { $0 < Storage.shared.infoVisible.value.count && Storage.shared.infoVisible.value[$0] } - .compactMap { index in + Storage.shared.infoDisplayItems.value + .filter { $0.isVisible } + .compactMap { item in + let index = item.type.rawValue guard index < tableData.count else { return nil } return tableData[index] } diff --git a/LoopFollow/InfoTable/InfoTableView.swift b/LoopFollow/InfoTable/InfoTableView.swift index 948cd94d2..6c36c6bbe 100644 --- a/LoopFollow/InfoTable/InfoTableView.swift +++ b/LoopFollow/InfoTable/InfoTableView.swift @@ -16,14 +16,24 @@ struct InfoTableView: View { row(name: "Time Zone", value: tz) } ForEach(infoManager.visibleRows) { item in - row(name: item.name, value: item.value) + row(name: item.name, value: item.value, valueColor: color(for: item)) } } .listStyle(.plain) .environment(\.defaultMinListRowHeight, rowHeight) } - private func row(name: String, value: String) -> some View { + /// Threshold-based color for a row's value, or nil to use the default color. + private func color(for item: InfoData) -> Color? { + guard let numericValue = item.numericValue, + let type = InfoType(rawValue: item.id), + let config = type.colorConfig + else { return nil } + return Storage.shared.infoDisplayItems.value.item(for: type)? + .coloring.color(for: numericValue, direction: config.direction) + } + + private func row(name: String, value: String, valueColor: Color? = nil) -> some View { // Show a placeholder for any field that has no value yet, // so the row reads as "no data" rather than appearing empty. let displayValue = value.isEmpty ? "—" : value @@ -34,14 +44,14 @@ struct InfoTableView: View { Text(name) Spacer() Text(displayValue) - .foregroundStyle(.primary) + .foregroundStyle(valueColor ?? .primary) } // Fallback when the single line won't fit: label over value VStack(alignment: .leading, spacing: 0) { Text(name) Text(displayValue) - .foregroundStyle(.primary) + .foregroundStyle(valueColor ?? .primary) .frame(maxWidth: .infinity, alignment: .trailing) } } diff --git a/LoopFollow/InfoTable/InfoType.swift b/LoopFollow/InfoTable/InfoType.swift index 14d0eb335..173ca416e 100644 --- a/LoopFollow/InfoTable/InfoType.swift +++ b/LoopFollow/InfoTable/InfoType.swift @@ -3,8 +3,8 @@ import Foundation -enum InfoType: Int, CaseIterable { - case iob, cob, basal, override, battery, pump, pumpBattery, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio, updated, tdd, iage +enum InfoType: Int, CaseIterable, Codable { + case iob, cob, basal, override, battery, pump, pumpBattery, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio, updated, tdd, iage, dbSize var name: String { switch self { @@ -28,6 +28,7 @@ enum InfoType: Int, CaseIterable { case .updated: return "Updated" case .tdd: return "TDD" case .iage: return "IAGE" + case .dbSize: return "DB Size" } } @@ -43,4 +44,36 @@ enum InfoType: Int, CaseIterable { var sortOrder: Int { return rawValue } + + /// Rows that carry a single numeric value can offer color thresholds. + /// Combined rows (basal, min/max), BG-unit rows (target, ISF, CR) and + /// pure-text rows have no config and are therefore not colorable. + /// Steps mirror the equivalent alarm editors, so a value that takes decimals + /// in an alarm takes decimals here too. + var colorConfig: InfoColorConfig? { + switch self { + case .iob: + return InfoColorConfig(direction: .above, unit: "U", range: 0 ... 20, step: 0.5, defaultWarning: 3, defaultUrgent: 5) + case .cob: + return InfoColorConfig(direction: .above, unit: "g", range: 0 ... 200, step: 1, defaultWarning: 30, defaultUrgent: 60) + case .battery, .pumpBattery: + return InfoColorConfig(direction: .below, unit: "%", range: 0 ... 100, step: 5, defaultWarning: 30, defaultUrgent: 15) + case .pump: + return InfoColorConfig(direction: .below, unit: "U", range: 0 ... 50, step: 1, defaultWarning: 20, defaultUrgent: 10) + case .tdd: + return InfoColorConfig(direction: .above, unit: "U", range: 0 ... 200, step: 1, defaultWarning: 60, defaultUrgent: 80) + case .recBolus: + return InfoColorConfig(direction: .above, unit: "U", range: 0 ... 20, step: 0.1, defaultWarning: 1, defaultUrgent: 2) + case .carbsToday: + return InfoColorConfig(direction: .above, unit: "g", range: 0 ... 500, step: 5, defaultWarning: 150, defaultUrgent: 250) + case .sage: + return InfoColorConfig(direction: .above, unit: "days", range: 0.5 ... 15, step: 0.5, defaultWarning: 9, defaultUrgent: 9.5) + case .cage, .iage: + return InfoColorConfig(direction: .above, unit: "days", range: 0.5 ... 10, step: 0.5, defaultWarning: 2.5, defaultUrgent: 3) + default: + return nil + } + } + + var isColorable: Bool { colorConfig != nil } } diff --git a/LoopFollow/LiveActivity/APNSClient.swift b/LoopFollow/LiveActivity/APNSClient.swift index d4871f067..ff20144c0 100644 --- a/LoopFollow/LiveActivity/APNSClient.swift +++ b/LoopFollow/LiveActivity/APNSClient.swift @@ -246,6 +246,9 @@ class APNSClient { if let cob = snapshot.cob { snapshotDict["cob"] = cob } if let projected = snapshot.projected { snapshotDict["projected"] = projected } if let override = snapshot.override { snapshotDict["override"] = override } + if let overrideEndAt = snapshot.overrideEndAt { snapshotDict["overrideEndAt"] = overrideEndAt } + if let tempTargetMgdl = snapshot.tempTargetMgdl { snapshotDict["tempTargetMgdl"] = tempTargetMgdl } + if let tempTargetEndAt = snapshot.tempTargetEndAt { snapshotDict["tempTargetEndAt"] = tempTargetEndAt } if let recBolus = snapshot.recBolus { snapshotDict["recBolus"] = recBolus } if let battery = snapshot.battery { snapshotDict["battery"] = battery } if let pumpBattery = snapshot.pumpBattery { snapshotDict["pumpBattery"] = pumpBattery } diff --git a/LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift b/LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift index 40ff076af..61dd92ab7 100644 --- a/LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift +++ b/LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift @@ -33,6 +33,15 @@ protocol CurrentGlucoseStateProviding { /// Active override name (nil if no active override). var override: String? { get } + /// End of the active override as Unix epoch seconds (nil = no override or indefinite). + var overrideEndAt: TimeInterval? { get } + + /// Active temporary target in mg/dL (nil if no active temp target). + var tempTargetMgdl: Double? { get } + + /// End of the active temporary target as Unix epoch seconds. + var tempTargetEndAt: TimeInterval? { get } + /// Recommended bolus in units. var recBolus: Double? { get } @@ -141,6 +150,9 @@ enum GlucoseSnapshotBuilder { cob: provider.cob, projected: provider.projectedMgdl, override: provider.override, + overrideEndAt: provider.overrideEndAt, + tempTargetMgdl: provider.tempTargetMgdl, + tempTargetEndAt: provider.tempTargetEndAt, recBolus: provider.recBolus, battery: provider.battery, pumpBattery: provider.pumpBattery, diff --git a/LoopFollow/LiveActivity/LiveActivityManager.swift b/LoopFollow/LiveActivity/LiveActivityManager.swift index c3b354a82..125ba4e09 100644 --- a/LoopFollow/LiveActivity/LiveActivityManager.swift +++ b/LoopFollow/LiveActivity/LiveActivityManager.swift @@ -10,19 +10,8 @@ import os import UIKit import UserNotifications -// Live Activity manager for LoopFollow. -// -// iOS 17.2+: every LA creation (initial start, renewal, forced -// restart) goes through APNs push-to-start. Updates -// ride the same APNs transport. One transport, one -// credential failure mode that surfaces in settings. -// -// iOS 16.6 – 17.1: legacy Activity.request() for everything; -// renewal-failed notification when backgrounded. -// The entry-point `if #available(iOS 17.2, *)` checks -// isolate every iOS 17.2 code path, so the legacy -// helpers can be deleted in one commit when the -// deployment target reaches 17.2. +// Live Activity manager for LoopFollow. Every LA creation (start, renewal, +// restart) and update goes through APNs push-to-start. final class LiveActivityManager { static let shared = LiveActivityManager() @@ -55,50 +44,42 @@ final class LiveActivityManager { startActivityUpdatesObservation() } - // MARK: - Push-to-start observation (iOS 17.2+) + // MARK: - Push-to-start observation - /// Observes the type-level push-to-start token (iOS 17.2+) and persists it. + /// Observes the type-level push-to-start token and persists it. /// The token survives app relaunches but is reissued by iOS periodically or when /// the user toggles LA permissions — each new delivery overwrites the stored value. private func startPushToStartTokenObservation() { - if #available(iOS 17.2, *) { - pushToStartObservationTask?.cancel() - LogManager.shared.log( - category: .general, - message: "[LA] pushToStartTokenUpdates observation starting (iOS 17.2+)" - ) - pushToStartObservationTask = Task { - var deliveries = 0 - for await tokenData in Activity.pushToStartTokenUpdates { - deliveries += 1 - let token = tokenData.map { String(format: "%02x", $0) }.joined() - let previousTail = Storage.shared.laPushToStartToken.value.isEmpty - ? "nil" - : String(Storage.shared.laPushToStartToken.value.suffix(8)) - let tail = String(token.suffix(8)) - let changed = tail != previousTail - Storage.shared.laPushToStartToken.value = token - LogManager.shared.log( - category: .general, - message: "[LA] push-to-start token received #\(deliveries) token=…\(tail) (prev=…\(previousTail))\(changed ? " CHANGED" : " same")" - ) - } + pushToStartObservationTask?.cancel() + LogManager.shared.log( + category: .general, + message: "[LA] pushToStartTokenUpdates observation starting" + ) + pushToStartObservationTask = Task { + var deliveries = 0 + for await tokenData in Activity.pushToStartTokenUpdates { + deliveries += 1 + let token = tokenData.map { String(format: "%02x", $0) }.joined() + let previousTail = Storage.shared.laPushToStartToken.value.isEmpty + ? "nil" + : String(Storage.shared.laPushToStartToken.value.suffix(8)) + let tail = String(token.suffix(8)) + let changed = tail != previousTail + Storage.shared.laPushToStartToken.value = token LogManager.shared.log( category: .general, - message: "[LA] pushToStartTokenUpdates stream ended after \(deliveries) deliveries — no further tokens will arrive" + message: "[LA] push-to-start token received #\(deliveries) token=…\(tail) (prev=…\(previousTail))\(changed ? " CHANGED" : " same")" ) } - } else { LogManager.shared.log( category: .general, - message: "[LA] pushToStartTokenUpdates unavailable (iOS <17.2) — push-to-start will never fire" + message: "[LA] pushToStartTokenUpdates stream ended after \(deliveries) deliveries — no further tokens will arrive" ) } } - /// Observes new Activity creations. When an activity is started by - /// push-to-start (iOS 17.2+), the app discovers it through this stream and - /// adopts it via the same bind/update path as an app-initiated start. + /// Observes new Activity creations so push-to-start activities are adopted + /// via the same bind/update path as an app-initiated start. private func startActivityUpdatesObservation() { activityUpdatesObservationTask?.cancel() LogManager.shared.log( @@ -471,7 +452,7 @@ final class LiveActivityManager { /// The actual end+restart is run from handleDidBecomeActive() because /// Activity.request() returns `visibility` during willEnterForeground. private var pendingForegroundRestart = false - /// Observes `pushToStartTokenUpdates` (iOS 17.2+) and persists the token. + /// Observes `pushToStartTokenUpdates` and persists the token. /// Long-lived — started once at init and never cancelled. private var pushToStartObservationTask: Task? /// Observes `Activity<>.activityUpdates` so activities started out-of-band @@ -515,118 +496,28 @@ final class LiveActivityManager { let startReason = nextStartReasonOverride ?? "user-start" nextStartReasonOverride = nil - if #available(iOS 17.2, *) { - // iOS 17.2+ uses push-to-start for every creation path. If an - // activity is already running and not stale we adopt/reuse it - // (covers warm starts where the LA survived a relaunch); only - // truly new starts dispatch APNs. - if let existing = Activity.activities.first { - let renewBy = Storage.shared.laRenewBy.value - let now = Date().timeIntervalSince1970 - let staleDatePassed = existing.content.staleDate.map { $0 <= Date() } ?? false - let inRenewalWindow = renewBy > 0 && now >= renewBy - LiveActivityManager.renewalWarning - let needsRestart = Storage.shared.laRenewalFailed.value || inRenewalWindow || staleDatePassed - if !needsRestart { - bind(to: existing, logReason: "reuse") - Storage.shared.laRenewalFailed.value = false - return - } - LogManager.shared.log( - category: .general, - message: "[LA] existing activity is stale on startIfNeeded (iOS 17.2+) — push-to-start replace (staleDatePassed=\(staleDatePassed), inRenewalWindow=\(inRenewalWindow))" - ) - attemptPushToStartCreate(reason: startReason, oldActivity: existing) - return - } - attemptPushToStartCreate(reason: startReason, oldActivity: nil) - } else { - startIfNeededLegacy() - } - } - - /// Pre-17.2 path (iOS 16.6 – 17.1). Identical to dev's `startIfNeeded` — - /// Activity.request() for everything. Removable when the deployment target - /// reaches 17.2. - @MainActor - private func startIfNeededLegacy() { + // Push-to-start is used for every creation path. If an activity is + // already running and not stale we adopt/reuse it (covers warm starts + // where the LA survived a relaunch); only truly new starts dispatch APNs. if let existing = Activity.activities.first { - // Before reusing, check whether this activity needs a restart. This covers cold - // starts (app was killed while the overlay was showing — willEnterForeground is - // never sent, so handleForeground never runs) and any other path that lands here - // without first going through handleForeground. let renewBy = Storage.shared.laRenewBy.value let now = Date().timeIntervalSince1970 let staleDatePassed = existing.content.staleDate.map { $0 <= Date() } ?? false let inRenewalWindow = renewBy > 0 && now >= renewBy - LiveActivityManager.renewalWarning let needsRestart = Storage.shared.laRenewalFailed.value || inRenewalWindow || staleDatePassed - - if needsRestart { - LogManager.shared.log( - category: .general, - message: "[LA] existing activity is stale on startIfNeeded — ending and restarting (staleDatePassed=\(staleDatePassed), inRenewalWindow=\(inRenewalWindow))" - ) - - endingForRestart = true - dismissedByUser = false - - Storage.shared.laRenewBy.value = 0 + if !needsRestart { + bind(to: existing, logReason: "reuse") Storage.shared.laRenewalFailed.value = false - cancelRenewalFailedNotification() - - Task { - await existing.end(nil, dismissalPolicy: .immediate) - await MainActor.run { self.startIfNeededLegacy() } - } return } - - bind(to: existing, logReason: "reuse") - Storage.shared.laRenewalFailed.value = false - return - } - - do { - let attributes = GlucoseLiveActivityAttributes(title: "LoopFollow") - - let provider = StorageCurrentGlucoseStateProvider() - let seedSnapshot = GlucoseSnapshotBuilder.build(from: provider) - ?? GlucoseSnapshotStore.shared.load() - ?? GlucoseSnapshot( - glucose: 0, - delta: 0, - trend: .unknown, - updatedAt: Date(), - iob: nil, - cob: nil, - projected: nil, - unit: .mgdl, - isNotLooping: false, - ) - - let initialState = GlucoseLiveActivityAttributes.ContentState( - snapshot: seedSnapshot, - seq: 0, - reason: "start", - producedAt: Date(), - ) - - let renewDeadline = Date().addingTimeInterval(LiveActivityManager.renewalThreshold) - let content = ActivityContent(state: initialState, staleDate: renewDeadline) - LALivenessStore.clear() - let activity = try Activity.request(attributes: attributes, content: content, pushType: .token) - - bind(to: activity, logReason: "start-new") - Storage.shared.laRenewBy.value = renewDeadline.timeIntervalSince1970 - Storage.shared.laRenewalFailed.value = false - LogManager.shared.log(category: .general, message: "Live Activity started id=\(activity.id)") - } catch { - let ns = error as NSError - let scene = isAppVisibleForLiveActivityStart() LogManager.shared.log( category: .general, - message: "Live Activity failed to start: \(error) domain=\(ns.domain) code=\(ns.code) — authorized=\(ActivityAuthorizationInfo().areActivitiesEnabled), sceneActive=\(scene), activities=\(Activity.activities.count)" + message: "[LA] existing activity is stale on startIfNeeded — push-to-start replace (staleDatePassed=\(staleDatePassed), inRenewalWindow=\(inRenewalWindow))" ) + attemptPushToStartCreate(reason: startReason, oldActivity: existing) + return } + attemptPushToStartCreate(reason: startReason, oldActivity: nil) } /// Called from applicationWillTerminate. Ends the LA synchronously (blocking @@ -771,102 +662,31 @@ final class LiveActivityManager { let overdueBy = Date().timeIntervalSince1970 - renewBy LogManager.shared.log(category: .general, message: "[LA] renewal deadline passed by \(Int(overdueBy))s, requesting new LA") - if #available(iOS 17.2, *) { - // iOS 17.2+: renewal goes through push-to-start. The dispatch hops - // to MainActor and returns immediately; adoption (or failure) lands - // in the observer. Return true so performRefresh stops processing - // this tick. - Task { @MainActor [weak self] in - self?.attemptPushToStartCreate(reason: "renew", oldActivity: oldActivity, snapshot: snapshot) - } - return true - } else { - return attemptLegacyRenewal(snapshot: snapshot, oldActivity: oldActivity) - } - } - - /// Pre-17.2 renewal (iOS 16.6 – 17.1): foreground Activity.request, mark - /// renewal-failed if it throws. Removable when the deployment target - /// reaches 17.2. - private func attemptLegacyRenewal( - snapshot: GlucoseSnapshot, - oldActivity: Activity - ) -> Bool { - let renewDeadline = Date().addingTimeInterval(LiveActivityManager.renewalThreshold) - let attributes = GlucoseLiveActivityAttributes(title: "LoopFollow") - - // Build the fresh snapshot with showRenewalOverlay: false — the new LA has a - // fresh deadline so no overlay is needed from the first frame. We pass the - // deadline as staleDate to ActivityContent below, not to Storage yet; Storage - // is only updated after Activity.request succeeds so a crash between the two - // can't leave the deadline permanently stuck in the future. - let freshSnapshot = snapshot.withRenewalOverlay(false) - - let state = GlucoseLiveActivityAttributes.ContentState( - snapshot: freshSnapshot, - seq: seq, - reason: "renew", - producedAt: Date(), - ) - let content = ActivityContent(state: state, staleDate: renewDeadline) - - do { - let newActivity = try Activity.request(attributes: attributes, content: content, pushType: .token) - - Task { - await oldActivity.end(nil, dismissalPolicy: .immediate) - } - - updateTask?.cancel() - updateTask = nil - tokenObservationTask?.cancel() - tokenObservationTask = nil - stateObserverTask?.cancel() - stateObserverTask = nil - pushToken = nil - - // Write deadline only on success — avoids a stuck future deadline if we crash - // between the write and the Activity.request call. - Storage.shared.laRenewBy.value = renewDeadline.timeIntervalSince1970 - bind(to: newActivity, logReason: "renew") - Storage.shared.laRenewalFailed.value = false - cancelRenewalFailedNotification() - GlucoseSnapshotStore.shared.save(freshSnapshot) - LogManager.shared.log(category: .general, message: "[LA] Live Activity renewed successfully id=\(newActivity.id)") - return true - } catch { - // Renewal failed — deadline was never written, so no rollback needed. - let isFirstFailure = !Storage.shared.laRenewalFailed.value - Storage.shared.laRenewalFailed.value = true - let ns = error as NSError - LogManager.shared.log( - category: .general, - message: "[LA] renewal failed, keeping existing LA: \(error) domain=\(ns.domain) code=\(ns.code) — authorized=\(ActivityAuthorizationInfo().areActivitiesEnabled), activities=\(Activity.activities.count)" - ) - if isFirstFailure { - scheduleRenewalFailedNotification() - } - return false + // Renewal goes through push-to-start. The dispatch hops to MainActor + // and returns immediately; adoption (or failure) lands in the observer. + // Return true so performRefresh stops processing this tick. + Task { @MainActor [weak self] in + self?.attemptPushToStartCreate(reason: "renew", oldActivity: oldActivity, snapshot: snapshot) } + return true } - // MARK: - Push-to-start (iOS 17.2+) + // MARK: - Push-to-start - /// Single creation path for iOS 17.2+. Handles initial start, renewal, and - /// forced restart. Verifies token + APNs credentials, applies backoff, then - /// dispatches the APNs push-to-start call. The old activity is only ended - /// after a confirmed successful send, preserving it if the send fails. - /// Adoption is delivered via the `activityUpdates` observer — - /// `handlePushToStartResult` only updates backoff/state. - @available(iOS 17.2, *) + /// Single creation path. Handles initial start, renewal, and forced restart. + /// Verifies token + APNs credentials, applies backoff, then dispatches the + /// APNs push-to-start call. The old activity is only ended after a confirmed + /// successful send, preserving it if the send fails. Adoption is delivered + /// via the `activityUpdates` observer — `handlePushToStartResult` only + /// updates backoff/state. @MainActor private func attemptPushToStartCreate( reason: String, oldActivity: Activity?, snapshot: GlucoseSnapshot? = nil ) { - // Validate APNs credentials up-front — push-to-start is the ONLY transport - // on iOS 17.2+, so missing/invalid creds mean the LA will never display. + // Validate APNs credentials up-front — push-to-start is the only + // transport, so missing/invalid creds mean the LA will never display. let keyId = Storage.shared.lfKeyId.value let apnsKey = Storage.shared.lfApnsKey.value guard APNsCredentialValidator.isFullyConfigured(keyId: keyId, apnsKey: apnsKey) else { @@ -919,7 +739,6 @@ final class LiveActivityManager { } } - @available(iOS 17.2, *) private func dispatchPushToStart( reason: String, oldActivity: Activity?, @@ -1019,7 +838,6 @@ final class LiveActivityManager { } } - @available(iOS 17.2, *) @MainActor private func handlePushToStartResult( _ result: APNSClient.PushToStartResult, @@ -1306,10 +1124,8 @@ final class LiveActivityManager { // bind-existing path rebinds to the just-ended activity — clearing // endingForRestart and turning the eventual iOS dismissal into a misclassified // user swipe. Drive the restart synchronously instead. - if #available(iOS 17.2, *) { - Task { @MainActor [weak self] in - self?.attemptPushToStartCreate(reason: "expired-token", oldActivity: nil) - } + Task { @MainActor [weak self] in + self?.attemptPushToStartCreate(reason: "expired-token", oldActivity: nil) } } diff --git a/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift b/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift index 00740e10e..e92ee5778 100644 --- a/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift +++ b/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift @@ -1,39 +1,45 @@ // LoopFollow // RestartLiveActivityIntent.swift -import AppIntents -import UIKit +#if !targetEnvironment(macCatalyst) + import AppIntents + import UIKit -struct RestartLiveActivityIntent: AppIntent { - static var title: LocalizedStringResource = "Restart Live Activity" - static var description = IntentDescription("Starts or restarts the LoopFollow Live Activity.") + struct RestartLiveActivityIntent: AppIntent, ForegroundContinuableIntent { + static var title: LocalizedStringResource = "Restart Live Activity" + static var description = IntentDescription("Starts or restarts the LoopFollow Live Activity.") - func perform() async throws -> some IntentResult & ProvidesDialog { - Storage.shared.laEnabled.value = true + func perform() async throws -> some IntentResult & ProvidesDialog { + Storage.shared.laEnabled.value = true - let keyId = Storage.shared.lfKeyId.value - let apnsKey = Storage.shared.lfApnsKey.value + let keyId = Storage.shared.lfKeyId.value + let apnsKey = Storage.shared.lfApnsKey.value - if keyId.isEmpty || apnsKey.isEmpty { - if let url = URL(string: "\(AppGroupID.urlScheme)://settings/live-activity") { - await MainActor.run { UIApplication.shared.open(url) } + if keyId.isEmpty || apnsKey.isEmpty { + if let url = URL(string: "loopfollow://settings/live-activity") { + await MainActor.run { UIApplication.shared.open(url) } + } + return .result(dialog: "Please enter your APNs credentials in LoopFollow settings to use the Live Activity.") + } + + if #available(iOS 26.0, *) { + try await continueInForeground() } - return .result(dialog: "Please enter your APNs credentials in LoopFollow settings to use the Live Activity.") - } - await MainActor.run { LiveActivityManager.shared.forceRestart() } + await MainActor.run { LiveActivityManager.shared.forceRestart() } - return .result(dialog: "Live Activity restarted.") + return .result(dialog: "Live Activity restarted.") + } } -} - -struct LoopFollowAppShortcuts: AppShortcutsProvider { - static var appShortcuts: [AppShortcut] { - AppShortcut( - intent: RestartLiveActivityIntent(), - phrases: ["Restart Live Activity in \(.applicationName)"], - shortTitle: "Restart Live Activity", - systemImageName: "dot.radiowaves.left.and.right", - ) + + struct LoopFollowAppShortcuts: AppShortcutsProvider { + static var appShortcuts: [AppShortcut] { + AppShortcut( + intent: RestartLiveActivityIntent(), + phrases: ["Restart Live Activity in \(.applicationName)"], + shortTitle: "Restart Live Activity", + systemImageName: "dot.radiowaves.left.and.right" + ) + } } -} +#endif diff --git a/LoopFollow/LiveActivity/StorageCurrentGlucoseStateProvider.swift b/LoopFollow/LiveActivity/StorageCurrentGlucoseStateProvider.swift index 1722d49fe..76b3664a4 100644 --- a/LoopFollow/LiveActivity/StorageCurrentGlucoseStateProvider.swift +++ b/LoopFollow/LiveActivity/StorageCurrentGlucoseStateProvider.swift @@ -2,6 +2,7 @@ // StorageCurrentGlucoseStateProvider.swift import Foundation +import HealthKit /// Reads the latest glucose state from LoopFollow's Storage and Observable layers. /// This is the only file in the pipeline that is allowed to touch Storage.shared @@ -47,6 +48,18 @@ struct StorageCurrentGlucoseStateProvider: CurrentGlucoseStateProviding { Observable.shared.override.value } + var overrideEndAt: TimeInterval? { + Observable.shared.overrideEndAt.value + } + + var tempTargetMgdl: Double? { + Observable.shared.tempTarget.value?.doubleValue(for: .milligramsPerDeciliter) + } + + var tempTargetEndAt: TimeInterval? { + Observable.shared.tempTargetEndAt.value + } + var recBolus: Double? { Observable.shared.deviceRecBolus.value } @@ -134,19 +147,10 @@ struct StorageCurrentGlucoseStateProvider: CurrentGlucoseStateProviding { #if targetEnvironment(macCatalyst) return false #else - // iOS 17.2+ renews silently via push-to-start at the deadline, so the - // pre-emptive 30-minute "tap to update" overlay would be misleading - // during normal operation. Only show it once renewal has actually - // failed (no token, bad creds, rate-limited) — that is genuinely - // user-actionable. iOS 16.x keeps the time-based warning because - // renewal there requires the user to foreground the app. - if #available(iOS 17.2, *) { - return Storage.shared.laRenewalFailed.value - } else { - let renewBy = Storage.shared.laRenewBy.value - let now = Date().timeIntervalSince1970 - return renewBy > 0 && now >= renewBy - LiveActivityManager.renewalWarning - } + // Push-to-start renews silently at the deadline, so only show the + // "tap to update" overlay once renewal has actually failed (no token, + // bad creds, rate-limited) — that is genuinely user-actionable. + return Storage.shared.laRenewalFailed.value #endif } } diff --git a/LoopFollow/LiveActivitySettingsView.swift b/LoopFollow/LiveActivitySettingsView.swift index 694e2779b..6d654f72d 100644 --- a/LoopFollow/LiveActivitySettingsView.swift +++ b/LoopFollow/LiveActivitySettingsView.swift @@ -59,7 +59,7 @@ get: { slots[index] }, set: { selectSlot($0, at: index) } )) { - ForEach(LiveActivitySlotOption.allCases, id: \.self) { option in + ForEach(LiveActivitySlotOption.gridCases, id: \.self) { option in Text(option.displayName).tag(option) } } diff --git a/LoopFollow/Nightscout/NightscoutSettingsView.swift b/LoopFollow/Nightscout/NightscoutSettingsView.swift index a5245e23c..8d8573abf 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsView.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsView.swift @@ -49,16 +49,43 @@ struct NightscoutSettingsView: View { } private var tokenSection: some View { - Section(header: Text("Token")) { + Section { HStack { Text("Access Token") TogglableSecureInput( - placeholder: "Enter Token", + placeholder: "Token or API secret", text: $viewModel.nightscoutToken, style: .singleLine, textContentType: .password ) } + + if viewModel.tokenIsVerifiedSecret || viewModel.isProvisioningToken { + Button { + viewModel.createReadOnlyToken(fromSecret: viewModel.nightscoutToken) + } label: { + HStack { + if viewModel.isProvisioningToken { + ProgressView() + Text("Creating read-only token…") + } else { + Image(systemName: "wand.and.stars") + Text("That's your API secret — create a read-only token") + } + } + } + .disabled(viewModel.isProvisioningToken) + } + + if let error = viewModel.tokenProvisionError { + Text(error) + .font(.footnote) + .foregroundColor(.red) + } + } header: { + Text("Token") + } footer: { + Text("Paste a Nightscout token. If your site needs one and you only have the API secret, paste that instead — LoopFollow can create a read-only token for you.") } } diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 559a12916..1065cc892 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -27,7 +27,7 @@ class NightscoutSettingsViewModel: ObservableObject { @Published var nightscoutToken: String = Storage.shared.token.value { willSet { if newValue != nightscoutToken { - Storage.shared.token.value = newValue + Storage.shared.token.value = NightscoutUtils.sanitizeConnectionInput(newValue) triggerCheckStatus() } } @@ -35,6 +35,29 @@ class NightscoutSettingsViewModel: ObservableObject { @Published var nightscoutStatus: String = "Checking..." + /// The most recent verification error, kept so the onboarding address page can + /// tell "reachable Nightscout that needs a token" apart from "can't reach it". + @Published var lastError: NightscoutUtils.NightscoutError? + + /// True when the most recent error means the site is a reachable Nightscout + /// that simply needs (a different) token. + private var errorIsTokenRelated: Bool { + switch lastError { + case .tokenRequired, .invalidToken: return true + default: return false + } + } + + /// The site responded as a Nightscout instance, even if it needs a token. + var addressReachable: Bool { + isConnected || errorIsTokenRelated + } + + /// The site is reachable but requires a token we don't have yet. + var addressNeedsToken: Bool { + !isConnected && errorIsTokenRelated + } + @Published var webSocketEnabled: Bool = Storage.shared.webSocketEnabled.value { didSet { Storage.shared.webSocketEnabled.value = webSocketEnabled @@ -62,6 +85,25 @@ class NightscoutSettingsViewModel: ObservableObject { private var checkStatusSubject = PassthroughSubject() private var checkStatusWorkItem: DispatchWorkItem? + /// While confirming a freshly provisioned token, the retry loop owns the + /// status label, so the ordinary debounced check is suppressed to avoid + /// flickering "Invalid Token" before the server has caught up. + private var isConfirmingProvisionedToken = false + + /// Set when a token we just created is correct (the create call returned its + /// id, so the secret was valid and the token is a deterministic function of + /// it) but the site hasn't started accepting it yet. Some hosts only reload + /// their auth cache on a restart, which can take minutes — far longer than we + /// can spin during onboarding — so this is treated as a success-pending state + /// the user can proceed from, not an error. + @Published private(set) var provisionedTokenPending = false + + /// True while a read-only token is being created from the API secret. + @Published var isProvisioningToken = false + + /// The most recent token-provisioning failure, for inline display. + @Published var tokenProvisionError: String? + init() { initialURL = Storage.shared.url.value initialToken = Storage.shared.token.value @@ -84,6 +126,10 @@ class NightscoutSettingsViewModel: ObservableObject { private func triggerCheckStatus() { checkStatusWorkItem?.cancel() + // Any manual edit invalidates a pending-provisioned state and any earlier + // "this is your API secret" verdict. + provisionedTokenPending = false + tokenIsVerifiedSecret = false nightscoutStatus = "Checking..." checkStatusWorkItem = DispatchWorkItem { @@ -93,6 +139,12 @@ class NightscoutSettingsViewModel: ObservableObject { } func processURL(_ value: String) { + // Strip whitespace/newlines/control chars first. A URL can't legally contain + // them, and a stray one (e.g. a trailing newline from a paste) otherwise makes + // URLComponents parsing fail and falls through to the lossy fallback below, + // mangling a URL-with-embedded-token into an invalid address. + let value = NightscoutUtils.sanitizeConnectionInput(value) + var useTokenUrl = false if let urlComponents = URLComponents(string: value), let queryItems = urlComponents.queryItems { @@ -121,6 +173,7 @@ class NightscoutSettingsViewModel: ObservableObject { } func checkNightscoutStatus() { + if isConfirmingProvisionedToken { return } NightscoutUtils.verifyURLAndToken { error, _, nsWriteAuth, nsAdminAuth in DispatchQueue.main.async { Storage.shared.nsWriteAuth.value = nsWriteAuth @@ -131,7 +184,139 @@ class NightscoutSettingsViewModel: ObservableObject { } } + /// Applies a token that LoopFollow just created and confirms it works. + /// + /// A freshly created subject isn't always recognized immediately: each + /// Nightscout server instance only reloads its in-memory subject cache on a + /// write, and multi-instance deployments don't share that cache — so the + /// first validation can be routed to an instance that hasn't caught up yet. + /// Rather than fail (and make the user tap again), we poll for a few seconds + /// with a reassuring status before surfacing any error. + func confirmProvisionedToken(_ token: String) { + isConfirmingProvisionedToken = true + provisionedTokenPending = false + isConnected = false + nightscoutStatus = "Finishing connection…" + nightscoutToken = token + verifyProvisionedTokenLoop(attempt: 0) + } + + private func verifyProvisionedTokenLoop(attempt: Int) { + let maxAttempts = 8 + NightscoutUtils.verifyURLAndToken { [weak self] error, _, nsWriteAuth, nsAdminAuth in + DispatchQueue.main.async { + guard let self else { return } + if error == nil { + self.isConfirmingProvisionedToken = false + self.provisionedTokenPending = false + Storage.shared.nsWriteAuth.value = nsWriteAuth + Storage.shared.nsAdminAuth.value = nsAdminAuth + self.updateStatusLabel(error: nil) + } else if attempt + 1 < maxAttempts { + self.nightscoutStatus = "Finishing connection…" + let delay = min(0.5 + Double(attempt) * 0.25, 2.0) + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.verifyProvisionedTokenLoop(attempt: attempt + 1) + } + } else { + // The token is correct but the site hasn't started accepting + // it yet. Surface a calm "pending" state the user can proceed + // from rather than a red error. + self.isConfirmingProvisionedToken = false + self.provisionedTokenPending = true + self.lastError = nil + } + } + } + } + + // MARK: - Token provisioning from API secret + + /// Nightscout access tokens look like `name-<16 hex>`; anything else the user + /// puts in the token field — most often their API secret — won't match. + private static let tokenFormat = "^[^-\\s]+-[0-9a-fA-F]{16}$" + + private func looksLikeToken(_ value: String) -> Bool { + value.range(of: Self.tokenFormat, options: .regularExpression) != nil + } + + /// Set once the value in the token field has been confirmed to authenticate as + /// the site's API secret. Drives the "create a token from this" suggestion. + /// This is a verified result (an actual auth probe), not a format guess, so it + /// only appears when creating a token will genuinely work. + @Published private(set) var tokenIsVerifiedSecret = false + + /// Bumped on every edit so a slow verification for an old value can't land on a + /// newer one. + private var secretCheckGeneration = 0 + + /// After a failed token check, find out whether what the user pasted is in fact + /// the API secret — verified against the server, not guessed from its shape. + private func verifyTokenIsSecret() { + let candidate = nightscoutToken + guard !candidate.isEmpty, !looksLikeToken(candidate) else { + tokenIsVerifiedSecret = false + return + } + secretCheckGeneration += 1 + let generation = secretCheckGeneration + let url = nightscoutURL + Task { + let isSecret = await NightscoutUtils.verifyAPISecret(url: url, secret: candidate) + await MainActor.run { + guard generation == self.secretCheckGeneration, + self.nightscoutToken == candidate else { return } + self.tokenIsVerifiedSecret = isSecret + } + } + } + + /// Creates (or reuses) a read-only token from the given API secret and applies + /// it. The secret authorizes the create call only and is never stored. + func createReadOnlyToken(fromSecret secret: String) { + let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + + tokenProvisionError = nil + isProvisioningToken = true + let url = nightscoutURL + + Task { + do { + let token = try await NightscoutUtils.provisionReadOnlyToken(url: url, secret: trimmed) + await MainActor.run { + self.isProvisioningToken = false + self.confirmProvisionedToken(token) + } + } catch { + await MainActor.run { + self.isProvisioningToken = false + self.tokenProvisionError = NightscoutSettingsViewModel.provisioningMessage(for: error) + } + } + } + } + + static func provisioningMessage(for error: Error) -> String { + guard let nsError = error as? NightscoutUtils.NightscoutError else { + return "Could not create a token. Please try again." + } + switch nsError { + case .invalidToken: + return "That API secret was rejected. Check it and try again." + case .invalidURL, .emptyAddress: + return "Please enter a valid site URL first." + case .siteNotFound: + return "Couldn't reach that site. Check the URL." + case .networkError: + return "Network error. Check your connection and try again." + case .tokenRequired, .unknown: + return "Could not create a token. Please try again." + } + } + func updateStatusLabel(error: NightscoutUtils.NightscoutError?) { + lastError = error if let error = error { isConnected = false switch error { @@ -151,8 +336,19 @@ class NightscoutSettingsViewModel: ObservableObject { nightscoutStatus = "Address Empty" } NightscoutSocketManager.shared.disconnect() + + // A site that's reachable but rejects the value as a token is the one + // case where the user may have pasted their API secret — verify it so + // we can offer to turn it into a token. + switch error { + case .invalidToken, .tokenRequired: + verifyTokenIsSecret() + default: + tokenIsVerifiedSecret = false + } } else { isConnected = true + tokenIsVerifiedSecret = false let authStatus: String if Storage.shared.nsAdminAuth.value { authStatus = "Admin" @@ -172,6 +368,52 @@ class NightscoutSettingsViewModel: ObservableObject { NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) } + // MARK: - Adaptive status (onboarding) + + enum ConnectionStatusKind { + case idle + case checking + case needsToken + case pending + case connected + case error + } + + /// A coarse status used to drive the onboarding status pill's color and icon. + var statusKind: ConnectionStatusKind { + if isConfirmingProvisionedToken { return .checking } + if nightscoutURL.isEmpty { return .idle } + if isConnected { return .connected } + // Token created and correct, just not accepted by the site yet. + if provisionedTokenPending { return .pending } + if nightscoutStatus == "Checking..." { return .checking } + // The site is reachable and simply needs a token — that's an expected + // step, not an error, so it's shown positively rather than red. + if addressNeedsToken { return .needsToken } + return .error + } + + /// A friendly, contextual status line that updates as the user fills fields, + /// rather than a fixed "Status" label that can read as stale. + var friendlyStatus: String { + switch statusKind { + case .idle: + return "Enter your site address to connect." + case .checking: + return isConfirmingProvisionedToken ? "Finishing connection…" : "Checking your connection…" + case .needsToken: + return "Site found — it needs a token." + case .pending: + return "Token created. Your site can take a few minutes to start accepting it — you can continue." + case .connected: + if Storage.shared.nsAdminAuth.value { return "Connected — admin access" } + if Storage.shared.nsWriteAuth.value { return "Connected — read & write" } + return "Connected — read-only" + case .error: + return nightscoutStatus + } + } + private func observeWebSocketState() { updateWebSocketStatus() NotificationCenter.default.publisher(for: .nightscoutSocketStateChanged) diff --git a/LoopFollow/Onboarding/ConnectionStatusPill.swift b/LoopFollow/Onboarding/ConnectionStatusPill.swift new file mode 100644 index 000000000..48e5df230 --- /dev/null +++ b/LoopFollow/Onboarding/ConnectionStatusPill.swift @@ -0,0 +1,49 @@ +// LoopFollow +// ConnectionStatusPill.swift + +import SwiftUI + +/// A pinned, color-coded status banner shown at the top of the onboarding +/// connect screens (Nightscout and Dexcom). It morphs as the connection state +/// changes, giving both screens the same live, above-the-fold status treatment. +/// +/// The pill itself is state-agnostic: each connect screen maps its own view +/// model's status into a `color`, an optional `systemImage`, and whether to show +/// a spinner, so the two different status enums can share one look. +struct ConnectionStatusPill: View { + let color: Color + let message: String + var isLoading: Bool = false + var systemImage: String? + + var body: some View { + HStack(spacing: 10) { + Group { + if isLoading { + ProgressView().scaleEffect(0.85) + } else if let systemImage { + Image(systemName: systemImage).foregroundColor(color) + } + } + .frame(width: 20) + + Text(message) + .font(.subheadline.weight(.medium)) + .foregroundColor(color) + .fixedSize(horizontal: false, vertical: true) + + Spacer(minLength: 0) + } + .padding(.horizontal, 14) + .padding(.vertical, 12) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(color.opacity(0.12)) + ) + .overlay( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .stroke(color.opacity(0.3), lineWidth: 1) + ) + } +} diff --git a/LoopFollow/Onboarding/LoopFollowLogo.swift b/LoopFollow/Onboarding/LoopFollowLogo.swift new file mode 100644 index 000000000..2288ff91f --- /dev/null +++ b/LoopFollow/Onboarding/LoopFollowLogo.swift @@ -0,0 +1,176 @@ +// LoopFollow +// LoopFollowLogo.swift + +import SwiftUI + +/// The LoopFollow mark, rebuilt in SwiftUI as the full app-icon face — a glassy +/// rounded square with the blue "loop" ring — so it has real visual mass when +/// tilted in 3D (a bare ring collapses to a line edge-on). +/// +/// Geometry and layers mirror the icon source artwork (loopfollow-icon.svg, +/// 1024×1024): all fractions below are the SVG coordinates divided by 1024. +struct LoopFollowLogo: View { + var size: CGFloat = 120 + + // Colors sampled from the app icon (loopfollow-icon.svg). + private let lightBlue = Color(red: 0.357, green: 0.639, blue: 0.961) // #5BA3F5 + private let midBlue = Color(red: 0.290, green: 0.565, blue: 0.886) // #4A90E2 + private let darkBlue = Color(red: 0.227, green: 0.482, blue: 0.784) // #3A7BC8 + + var body: some View { + let corner = size * 0.225 + let ringDiameter = size * 0.879 // outer r = 450 + let holeDiameter = size * 0.615 // inner r = 315 + + ZStack { + // Glassy near-white card (the icon face). + RoundedRectangle(cornerRadius: corner, style: .continuous) + .fill( + LinearGradient( + colors: [ + Color(red: 0.973, green: 0.976, blue: 0.980), // #F8F9FA + .white, + Color(red: 0.941, green: 0.949, blue: 0.961), // #F0F2F5 + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + + // Soft highlight fading down the top half of the card. + LinearGradient( + stops: [ + .init(color: .white.opacity(0.6), location: 0), + .init(color: .white.opacity(0.3), location: 0.2), + .init(color: .clear, location: 0.5), + ], + startPoint: .top, + endPoint: .bottom + ) + + // Curved glass reflection over the upper half. + radialGlow( + width: size * 1.172, height: size * 0.781, + stops: [ + .init(color: .white.opacity(0.5), location: 0), + .init(color: .white.opacity(0.2), location: 0.5), + .init(color: .clear, location: 1), + ] + ) + .offset(y: -size * 0.25) + .opacity(0.8) + + // Blue glass disc. + Circle() + .fill( + LinearGradient( + stops: [ + .init(color: lightBlue, location: 0), + .init(color: midBlue, location: 0.3), + .init(color: midBlue, location: 0.7), + .init(color: darkBlue, location: 1), + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: ringDiameter, height: ringDiameter) + + // Darkening toward the disc's outer edge for depth. + Circle() + .fill( + RadialGradient( + stops: [ + .init(color: .clear, location: 0.7), + .init(color: .black.opacity(0.15), location: 0.85), + .init(color: .black.opacity(0.25), location: 1), + ], + center: .center, + startRadius: 0, + endRadius: ringDiameter / 2 + ) + ) + .frame(width: ringDiameter, height: ringDiameter) + + // White hole that turns the disc into the loop ring. + Circle() + .fill(Color.white.opacity(0.98)) + .frame(width: holeDiameter, height: holeDiameter) + + // Glass highlight on the white hole. + radialGlow( + width: size * 0.742, height: size * 0.391, + stops: [ + .init(color: .white.opacity(0.4), location: 0), + .init(color: .white.opacity(0.1), location: 0.6), + .init(color: .clear, location: 1), + ] + ) + .offset(y: -size * 0.129) + .opacity(0.7) + + // Broad sheen across the top of the ring; its lower edge draws the + // glass "cut line" just below the middle of the icon. + Ellipse() + .fill(Color.white.opacity(0.25)) + .frame(width: size * 0.82, height: size * 0.488) + .offset(y: -size * 0.188) + .blur(radius: size * 0.003) + + // Subtle shadow pooling under the ring. + Ellipse() + .fill(Color.black.opacity(0.05)) + .frame(width: size * 0.879, height: size * 0.195) + .offset(y: size * 0.184) + .blur(radius: size * 0.002) + } + .frame(width: size, height: size) + .clipShape(RoundedRectangle(cornerRadius: corner, style: .continuous)) + } + + /// Elliptical radial glow: SwiftUI's RadialGradient is circular, so draw it + /// in a circle and squash vertically to match the SVG's elliptical gradients. + private func radialGlow(width: CGFloat, height: CGFloat, stops: [Gradient.Stop]) -> some View { + Circle() + .fill( + RadialGradient( + gradient: Gradient(stops: stops), + center: .center, + startRadius: 0, + endRadius: width / 2 + ) + ) + .frame(width: width, height: width) + .scaleEffect(x: 1, y: height / width) + } +} + +/// LoopFollow logo that lands like a coin: it starts edge-on (rotated 90° about +/// the vertical axis) and springs open to face the viewer, overshooting a little +/// past flat and rocking back to rest. Respects Reduce Motion by rendering flat. +struct AnimatedLoopFollowLogo: View { + var size: CGFloat = 140 + + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var angle: Double = 90 + + var body: some View { + LoopFollowLogo(size: size) + .rotation3DEffect( + .degrees(angle), + axis: (x: 0, y: 1, z: 0), + perspective: 0.7 + ) + // Grounding shadow so the landing reads as dimensional. + .shadow(color: .black.opacity(0.28), radius: size * 0.08, x: 0, y: size * 0.06) + .onAppear { + if reduceMotion { + angle = 0 + } else { + withAnimation(.spring(response: 0.85, dampingFraction: 0.5).delay(0.15)) { + angle = 0 + } + } + } + } +} diff --git a/LoopFollow/Onboarding/OnboardingContainerView.swift b/LoopFollow/Onboarding/OnboardingContainerView.swift new file mode 100644 index 000000000..35ca8dc73 --- /dev/null +++ b/LoopFollow/Onboarding/OnboardingContainerView.swift @@ -0,0 +1,138 @@ +// LoopFollow +// OnboardingContainerView.swift + +import SwiftUI + +/// Root of the first-run onboarding wizard. Owns the shared chrome — progress +/// bar and Back/Next footer — and swaps in the view for the current step. +struct OnboardingContainerView: View { + @StateObject private var viewModel: OnboardingViewModel + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + init(onClose: @escaping () -> Void) { + _viewModel = StateObject(wrappedValue: OnboardingViewModel(onClose: onClose)) + } + + var body: some View { + VStack(spacing: 0) { + if viewModel.step.showsProgressHeader { + header + } + + stepContent + .frame(maxWidth: .infinity, maxHeight: .infinity) + + if viewModel.step.usesSharedFooter { + footer + } + } + .background(Color(.systemGroupedBackground).ignoresSafeArea()) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) + } + + // MARK: - Chrome + + private var header: some View { + VStack(spacing: 6) { + HStack(spacing: 12) { + OnboardingProgressBar(progress: viewModel.progress) + Button("Skip") { viewModel.skip() } + .font(.subheadline) + .foregroundColor(.secondary) + } + + if !viewModel.progressLabel.isEmpty { + Text(viewModel.progressLabel) + .font(.caption) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(.horizontal) + .padding(.top, 12) + .padding(.bottom, 4) + } + + @ViewBuilder + private var stepContent: some View { + let content = Group { + switch viewModel.step { + case .welcome: + WelcomeStepView(viewModel: viewModel) + case .overview: + OverviewStepView(onboarding: viewModel) + case .dataSource: + DataSourceChoiceStepView(viewModel: viewModel) + case .connect: + switch viewModel.dataSource { + case .dexcom: + DexcomConnectStepView(viewModel: viewModel.dexcomViewModel, onboarding: viewModel) + case .copyFromPhone: + ConnectImportStepView(viewModel: viewModel) + default: + NightscoutConnectStepView(viewModel: viewModel.nightscoutViewModel, onboarding: viewModel) + } + case .units: + UnitsStepView(onboarding: viewModel) + case .generalAlarms: + GeneralAlarmsStepView() + case .alarms: + AlarmsStepView(viewModel: viewModel) + case .tabOrder: + TabOrderStepView() + case .notifications: + NotificationsStepView(viewModel: viewModel) + case .telemetry: + TelemetryStepView(viewModel: viewModel) + case .completion: + CompletionStepView(viewModel: viewModel) + } + } + + if reduceMotion { + content.id(viewModel.step) + } else { + content + .id(viewModel.step) + .transition(.asymmetric( + insertion: .move(edge: .trailing).combined(with: .opacity), + removal: .move(edge: .leading).combined(with: .opacity) + )) + } + } + + private var footer: some View { + OnboardingNavFooter( + continueEnabled: viewModel.canProceed, + showBack: viewModel.canGoBack, + onBack: { withStepAnimation { viewModel.goBack() } }, + onContinue: { withStepAnimation { viewModel.advance() } } + ) + } + + private func withStepAnimation(_ change: () -> Void) { + if reduceMotion { + change() + } else { + withAnimation(.easeInOut(duration: 0.3)) { change() } + } + } +} + +/// Thin segmented progress indicator shown at the top of each chrome'd step. +private struct OnboardingProgressBar: View { + let progress: Double + + var body: some View { + GeometryReader { geo in + ZStack(alignment: .leading) { + Capsule() + .fill(Color(.systemGray5)) + Capsule() + .fill(Color.accentColor) + .frame(width: max(0, min(1, progress)) * geo.size.width) + } + } + .frame(height: 6) + } +} diff --git a/LoopFollow/Onboarding/OnboardingNavFooter.swift b/LoopFollow/Onboarding/OnboardingNavFooter.swift new file mode 100644 index 000000000..1b0f3e2d4 --- /dev/null +++ b/LoopFollow/Onboarding/OnboardingNavFooter.swift @@ -0,0 +1,39 @@ +// LoopFollow +// OnboardingNavFooter.swift + +import SwiftUI + +/// The shared Back / Continue footer used both by the container's chrome and by +/// phases that manage their own internal pages (connect, alarms), so the controls +/// look and behave identically everywhere. +struct OnboardingNavFooter: View { + var continueTitle: String = "Continue" + var continueEnabled: Bool = true + var showBack: Bool = true + var onBack: () -> Void + var onContinue: () -> Void + + var body: some View { + HStack { + if showBack { + Button(action: onBack) { + Label("Back", systemImage: "chevron.left") + .font(.body.weight(.medium)) + } + .buttonStyle(.bordered) + } + + Spacer() + + Button(action: onContinue) { + Text(continueTitle) + .font(.body.weight(.semibold)) + .frame(minWidth: 120) + } + .buttonStyle(.borderedProminent) + .disabled(!continueEnabled) + } + .padding() + .background(.bar) + } +} diff --git a/LoopFollow/Onboarding/OnboardingStep.swift b/LoopFollow/Onboarding/OnboardingStep.swift new file mode 100644 index 000000000..0ae033ef4 --- /dev/null +++ b/LoopFollow/Onboarding/OnboardingStep.swift @@ -0,0 +1,69 @@ +// LoopFollow +// OnboardingStep.swift + +import Foundation + +/// The phases of the first-run onboarding wizard. +/// +/// Progress is tracked by phase, not by page: the set of phases is stable, while +/// some phases contain a variable number of internal pages (the Nightscout connect +/// phase can be one or two pages; the alarms phase is one page per offered alarm, +/// which differs between Nightscout and Dexcom). Those phases report their +/// within-phase position through `OnboardingViewModel.phaseProgress`, so the +/// overall progress bar stays smooth no matter how many pages a phase has. +/// +/// `connect` renders the Nightscout, Dexcom, or import view depending on the data +/// source the user picks in `dataSource`. +enum OnboardingStep: CaseIterable, Hashable { + case welcome + case overview + case dataSource + case connect + case units + case generalAlarms + case alarms + case tabOrder + case notifications + case telemetry + case completion + + /// Steps that show the progress bar + phase label + Skip at the top. The + /// welcome and completion screens are full-bleed and provide their own CTA. + var showsProgressHeader: Bool { + switch self { + case .welcome, .completion: + return false + default: + return true + } + } + + /// Steps that use the shared Back / Continue footer. Phases that contain their + /// own internal pages or custom primary buttons (overview, connect, units, + /// alarms, notifications, telemetry) supply their own footer instead, as do the + /// full-bleed welcome and completion screens. + var usesSharedFooter: Bool { + switch self { + case .dataSource, .generalAlarms, .tabOrder: + return true + default: + return false + } + } + + /// Short name shown in the progress header for this phase. + var phaseTitle: String { + switch self { + case .welcome, .completion: return "" + case .overview: return "Overview" + case .dataSource: return "Data source" + case .connect: return "Connect" + case .units: return "Units & metrics" + case .generalAlarms: return "Alarm basics" + case .alarms: return "Alarms" + case .tabOrder: return "Tabs" + case .notifications: return "Notifications" + case .telemetry: return "Privacy" + } + } +} diff --git a/LoopFollow/Onboarding/OnboardingStepHeader.swift b/LoopFollow/Onboarding/OnboardingStepHeader.swift new file mode 100644 index 000000000..1b6395ef8 --- /dev/null +++ b/LoopFollow/Onboarding/OnboardingStepHeader.swift @@ -0,0 +1,38 @@ +// LoopFollow +// OnboardingStepHeader.swift + +import SwiftUI + +/// Consistent icon + title + subtitle header used at the top of each step body. +/// +/// The header deliberately carries no horizontal margin: it shares whatever +/// leading edge its container uses, so it lines up exactly with the content +/// below it — the section cards in a `Form`, or the `.padding(.horizontal)` +/// cards in a plain `VStack`. Callers add their own horizontal padding to match. +struct OnboardingStepHeader: View { + let systemImage: String + let title: String + let subtitle: String + + var body: some View { + // Left-aligned: justified/centered body copy is harder to read, so the + // header reads as a natural top-down intro. + VStack(alignment: .leading, spacing: 12) { + Image(systemName: systemImage) + .font(.system(size: 44, weight: .semibold)) + .foregroundStyle(Color.accentColor) + .padding(.bottom, 2) + + Text(title) + .font(.title2.weight(.bold)) + .multilineTextAlignment(.leading) + + Text(subtitle) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 8) + } +} diff --git a/LoopFollow/Onboarding/OnboardingViewModel.swift b/LoopFollow/Onboarding/OnboardingViewModel.swift new file mode 100644 index 000000000..5cd894857 --- /dev/null +++ b/LoopFollow/Onboarding/OnboardingViewModel.swift @@ -0,0 +1,387 @@ +// LoopFollow +// OnboardingViewModel.swift + +import Combine +import SwiftUI +import UserNotifications + +/// Drives the onboarding wizard: tracks the current phase, the chosen data +/// source, the seeded alarms, and persists everything when the user finishes. +/// +/// The child connection view models are the same ones used by the regular +/// settings screens, so URL/token/credential entry and validation behave +/// identically here and there. +@MainActor +final class OnboardingViewModel: ObservableObject { + enum DataSource: Hashable { + case nightscout + case dexcom + case copyFromPhone + } + + /// A single recommended alarm offered on the alarms phase. Display fields are + /// carried explicitly (rather than derived from `AlarmType`) so two alarms of + /// the same type — a warning Low and an Urgent Low — can read differently. + struct SeedAlarm: Identifiable { + let id = UUID() + var alarm: Alarm + var isEnabled: Bool + var title: String + var detail: String + /// Needs Nightscout loop/uploader data, so it's hidden for Dexcom-only. + var requiresNightscout: Bool + + var type: AlarmType { alarm.type } + + /// Use each alarm type's own icon rather than a bespoke one. + var icon: String { alarm.type.icon } + } + + /// Position within a multi-page phase, reported by that phase's view so the + /// overall progress bar and label can reflect it. + struct PhaseProgress: Equatable { + var page: Int + var count: Int + } + + @Published var step: OnboardingStep = .welcome + @Published var dataSource: DataSource? + @Published var seedAlarms: [SeedAlarm] + + /// Within-phase position for phases that own internal pages (connect, alarms). + /// Reset to `nil` whenever the phase changes. + @Published var phaseProgress: PhaseProgress? + + /// Set once a QR settings import on the "copy from another phone" path + /// succeeds, so the connect phase can be considered complete. + @Published var didImportSettings = false + + /// Whether the notification permission is still undecided. The notifications + /// phase is only shown when it is — there's no point prompting someone who has + /// already granted or denied it (iOS won't show the prompt again anyway). + /// Defaults to `true`; resolved asynchronously at launch, well before the user + /// reaches that phase. + @Published private var notificationsUndecided = true + + let nightscoutViewModel = NightscoutSettingsViewModel() + let dexcomViewModel = DexcomSettingsViewModel() + + /// Called to dismiss the onboarding cover. + private let onClose: () -> Void + private var cancellables = Set() + + /// Whether to show the in-flow telemetry consent phase. Captured once at + /// launch so the decision the phase records doesn't remove it from under the + /// navigation while the user is still on it. + private let includeTelemetryStep: Bool + + /// Alarm types the user already has, captured at launch. Onboarding only + /// offers recommended alarms whose type the user doesn't already own, so a + /// returning user is helped to add new ones without touching their existing + /// (possibly custom-named) alarms. + private let existingAlarmTypes: Set + + init(onClose: @escaping () -> Void) { + self.onClose = onClose + includeTelemetryStep = !Storage.shared.telemetryConsentDecisionMade.value + existingAlarmTypes = Set(Storage.shared.alarms.value.map(\.type)) + let hasNightscout = !Storage.shared.url.value.isEmpty + let hasDexcom = !Storage.shared.shareUserName.value.isEmpty + && !Storage.shared.sharePassword.value.isEmpty + isAlreadyConfigured = hasNightscout || hasDexcom + seedAlarms = OnboardingViewModel.defaultSeedAlarms() + + // Re-publish child changes so the footer's `canProceed` stays in sync + // with live connection validation. + nightscoutViewModel.objectWillChange + .sink { [weak self] in self?.objectWillChange.send() } + .store(in: &cancellables) + dexcomViewModel.objectWillChange + .sink { [weak self] in self?.objectWillChange.send() } + .store(in: &cancellables) + + UNUserNotificationCenter.current().getNotificationSettings { settings in + let undecided = settings.authorizationStatus == .notDetermined + Task { @MainActor [weak self] in + self?.notificationsUndecided = undecided + } + } + } + + // MARK: - Derived state + + /// True when the user already had a working data source at launch — used to + /// make skipping prominent for returning users and to decide whether the + /// data-source/connect phases are shown. Captured once in `init`, not read + /// live: the connect step persists the URL/credentials as the user types, so + /// a live read would flip this to `true` mid-`.connect`, drop those phases + /// from `activeSteps`, and make `advance()` fall through to `finish()` — + /// silently skipping units, alarms, and the rest of setup. + let isAlreadyConfigured: Bool + + var canProceed: Bool { + switch step { + case .welcome, .overview, .units, .generalAlarms, .alarms, + .tabOrder, .notifications, .telemetry, .completion: + return true + case .dataSource: + return dataSource != nil + case .connect: + switch dataSource { + case .nightscout: return nightscoutViewModel.isConnected || nightscoutViewModel.provisionedTokenPending + case .dexcom: return dexcomViewModel.canVerifyProceed + case .copyFromPhone: return didImportSettings + case .none: return false + } + } + } + + /// Whether Nightscout loop/uploader data is (or will be) available — used to + /// gate alarms that depend on it. True for the Nightscout source, or any path + /// that ends with a Nightscout URL configured (including a QR import). + private var hasNightscoutData: Bool { + dataSource == .nightscout || !Storage.shared.url.value.isEmpty + } + + /// Whether a seeded alarm should be offered: its data source must be available + /// and the user must not already have an alarm of that type. + func isOffered(_ seed: SeedAlarm) -> Bool { + guard !seed.requiresNightscout || hasNightscoutData else { return false } + return !existingAlarmTypes.contains(seed.type) + } + + var offeredSeedAlarms: [SeedAlarm] { + seedAlarms.filter { isOffered($0) } + } + + private var alarmsOffered: Bool { !offeredSeedAlarms.isEmpty } + + /// True when a returning user already has a working data source and there are + /// no recommended alarms left to add — nothing essential to configure, so the + /// overview becomes a short "you're all set" confirmation instead of a plan. + var hasNothingToSetUp: Bool { + isAlreadyConfigured && !alarmsOffered + } + + /// The phases actually shown for the current configuration, in order. Optional + /// phases are included only when relevant; a returning user skips the phases + /// they've already completed. + var activeSteps: [OnboardingStep] { + var steps: [OnboardingStep] = [.welcome, .overview] + + // Already set up and nothing to add — the overview is the last stop, with + // a "Done" that finishes. Notification/telemetry consent is still handled + // after dismissal, the same as when skipping. + if hasNothingToSetUp { + return steps + } + + // A returning user with a working data source skips source selection and + // the connection screen; everyone else sets them up. + if !isAlreadyConfigured { + steps.append(contentsOf: [.dataSource, .connect]) + } + + steps.append(.units) + if alarmsOffered { + steps.append(contentsOf: [.generalAlarms, .alarms]) + } + steps.append(.tabOrder) + if notificationsUndecided { + steps.append(.notifications) + } + if includeTelemetryStep { + steps.append(.telemetry) + } + steps.append(.completion) + return steps + } + + /// Phases that show the progress header — the unit over which the bar fills. + private var chromePhases: [OnboardingStep] { + activeSteps.filter { $0.showsProgressHeader } + } + + /// Progress fraction (0...1). Each phase is one slot; a multi-page phase fills + /// its slot proportionally via `phaseProgress`, so the bar stays smooth no + /// matter how many pages a phase turns out to have. + var progress: Double { + guard !chromePhases.isEmpty else { return 0 } + guard let index = chromePhases.firstIndex(of: step) else { + return step == .completion ? 1 : 0 + } + let within: Double + if let progress = phaseProgress, progress.count > 1 { + within = Double(progress.page) / Double(progress.count) + } else { + within = 0 + } + return (Double(index) + within) / Double(chromePhases.count) + } + + /// The phase name shown in the header, with a local page count for multi-page + /// phases (e.g. "Alarms · 3 of 13"). + var progressLabel: String { + let title = step.phaseTitle + if let progress = phaseProgress, progress.count > 1 { + return "\(title) · \(min(progress.page + 1, progress.count)) of \(progress.count)" + } + return title + } + + // MARK: - Navigation + + var canGoBack: Bool { + guard let index = activeSteps.firstIndex(of: step) else { return false } + return index > 0 + } + + func advance() { + phaseProgress = nil + let steps = activeSteps + guard let index = steps.firstIndex(of: step) else { + // Defensive: the current step should always be in `activeSteps`. + // If it ever isn't (a state change removed it while we were on it), + // continue to the next still-active step in canonical order rather + // than silently ending setup via `finish()`. + let canonical = OnboardingStep.allCases + let currentRank = canonical.firstIndex(of: step) ?? canonical.count + if let next = steps.first(where: { (canonical.firstIndex(of: $0) ?? 0) > currentRank }) { + step = next + } else { + finish() + } + return + } + guard index + 1 < steps.count else { + finish() + return + } + step = steps[index + 1] + } + + func goBack() { + phaseProgress = nil + guard let index = activeSteps.firstIndex(of: step), index > 0 else { return } + step = activeSteps[index - 1] + } + + /// Skip the rest of setup. Marks onboarding complete without seeding alarms + /// or touching units, leaving any existing configuration untouched. + func skip() { + Storage.shared.hasCompletedOnboarding.value = true + onClose() + } + + /// Finish setup: seed selected alarms, mark units/onboarding complete, and + /// kick a refresh so the home screen loads data immediately. + func finish() { + persistSeededAlarms() + Storage.shared.hasConfiguredUnits.value = true + Storage.shared.hasCompletedOnboarding.value = true + onClose() + NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) + } + + // MARK: - Alarm seeding + + private func persistSeededAlarms() { + var alarms = Storage.shared.alarms.value + let existingTypes = Set(alarms.map(\.type)) + + for seed in offeredSeedAlarms where seed.isEnabled { + // Don't re-add a type the user already configured. The two seeded Low + // alarms (warning + urgent) are both added on a fresh install because + // `existingTypes` is sampled once, before any are appended. + guard !existingTypes.contains(seed.type) else { continue } + alarms.append(seed.alarm) + } + + Storage.shared.alarms.value = alarms + } + + // MARK: - Default seed set + + /// The recommended alarms, in page order. Glucose and phone alarms are offered + /// to everyone; loop/pump/insulin alarms require Nightscout data. + private static func defaultSeedAlarms() -> [SeedAlarm] { + func seed( + _ type: AlarmType, + title: String, + detail: String, + requiresNightscout: Bool, + configure: (inout Alarm) -> Void = { _ in } + ) -> SeedAlarm { + var alarm = Alarm(type: type) + configure(&alarm) + return SeedAlarm( + alarm: alarm, + isEnabled: true, + title: title, + detail: detail, + requiresNightscout: requiresNightscout + ) + } + + return [ + seed(.low, title: "Low glucose", + detail: "Warns when glucose is low, now or soon.", + requiresNightscout: false) + { + $0.name = "Low glucose" + $0.belowBG = 70 + $0.predictiveMinutes = 20 + }, + seed(.low, title: "Urgent low", + detail: "A separate warning for when glucose is very low.", + requiresNightscout: false) + { + $0.name = "Urgent low" + $0.belowBG = 54 + $0.predictiveMinutes = 0 + $0.persistentMinutes = 0 + }, + seed(.high, title: "High glucose", + detail: "Warns when glucose is high.", + requiresNightscout: false) + { + $0.aboveBG = 180 + }, + seed(.fastDrop, title: "Fast drop", + detail: "Warns when glucose is falling quickly.", + requiresNightscout: false), + seed(.missedReading, title: "Missed readings", + detail: "Warns when glucose stops updating.", + requiresNightscout: false), + seed(.notLooping, title: "Not looping", + detail: "Warns when the loop stops running.", + requiresNightscout: true), + seed(.battery, title: "Looping phone battery", + detail: "Warns when the battery of the phone running Loop or Trio is low.", + requiresNightscout: true), + seed(.iob, title: "IOB", + detail: "Warns when insulin on board is high.", + requiresNightscout: true), + seed(.cob, title: "COB", + detail: "Warns when carbs on board are high.", + requiresNightscout: true), + seed(.sensorChange, title: "Sensor change", + detail: "Reminds you when the CGM sensor is due.", + requiresNightscout: true) + { + $0.threshold = 10 + $0.activeOption = .day + }, + seed(.pumpChange, title: "Pump change", + detail: "Reminds you when the pump site is due.", + requiresNightscout: true) + { + $0.threshold = 3 + $0.activeOption = .day + }, + seed(.pump, title: "Pump insulin", + detail: "Warns when pump reservoir insulin is low.", + requiresNightscout: true), + ] + } +} diff --git a/LoopFollow/Onboarding/Steps/AlarmsStepView.swift b/LoopFollow/Onboarding/Steps/AlarmsStepView.swift new file mode 100644 index 000000000..5efe46da7 --- /dev/null +++ b/LoopFollow/Onboarding/Steps/AlarmsStepView.swift @@ -0,0 +1,260 @@ +// LoopFollow +// AlarmsStepView.swift + +import HealthKit +import SwiftUI + +/// The alarms phase: one recommended alarm per page, each with a toggle and a few +/// of its most useful settings. The number of pages depends on the data source +/// (Dexcom-only followers don't see loop/pump alarms), which the phase reports via +/// `phaseProgress` so the overall progress bar stays accurate. +struct AlarmsStepView: View { + @ObservedObject var viewModel: OnboardingViewModel + + /// Page index into `offeredIndices`. + @State private var index = 0 + + /// Indices into `viewModel.seedAlarms` that are offered for this data source. + private var offeredIndices: [Int] { + viewModel.seedAlarms.indices.filter { viewModel.isOffered(viewModel.seedAlarms[$0]) } + } + + var body: some View { + VStack(spacing: 0) { + if let seedIndex = offeredIndices[safe: index] { + alarmPage(seedIndex: seedIndex) + } + + OnboardingNavFooter( + continueEnabled: true, + showBack: true, + onBack: goBack, + onContinue: goForward + ) + } + .onAppear(perform: reportProgress) + .onChange(of: index) { _ in reportProgress() } + } + + private func reportProgress() { + viewModel.phaseProgress = .init(page: index, count: offeredIndices.count) + } + + private func goForward() { + if index < offeredIndices.count - 1 { + withAnimation(.easeInOut(duration: 0.25)) { index += 1 } + } else { + viewModel.advance() + } + } + + private func goBack() { + if index > 0 { + withAnimation(.easeInOut(duration: 0.25)) { index -= 1 } + } else { + viewModel.goBack() + } + } + + // MARK: - Page + + private func alarmPage(seedIndex: Int) -> some View { + let seed = $viewModel.seedAlarms[seedIndex] + let display = viewModel.seedAlarms[seedIndex] + return Form { + Section { + EmptyView() + } header: { + VStack(spacing: 8) { + Image(systemName: display.icon) + .font(.system(size: 40, weight: .semibold)) + .foregroundStyle(Color.accentColor) + Text(display.title) + .font(.title2.weight(.bold)) + Text(display.detail) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .textCase(nil) + .padding(.bottom, 8) + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + + Section { + Toggle("Enable this alarm", isOn: seed.isEnabled) + + if seed.wrappedValue.isEnabled { + controls(for: seed) + } + } footer: { + if seed.wrappedValue.isEnabled, let text = explanation(for: seed.wrappedValue) { + Text(text) + } + } + } + } + + // MARK: - Per-alarm controls + + @ViewBuilder + private func controls(for seed: Binding) -> some View { + switch seed.wrappedValue.type { + case .low: + bgPicker(seed, title: "Alert below", range: 40 ... 150, keyPath: \.belowBG) + intStepper(seed, label: "Warn early by", range: 0 ... 30, step: 5, unit: "min", keyPath: \.predictiveMinutes) + case .high: + bgPicker(seed, title: "Alert above", range: 120 ... 350, keyPath: \.aboveBG) + intStepper(seed, label: "Only after high for", range: 0 ... 60, step: 5, unit: "min", keyPath: \.persistentMinutes) + case .fastDrop: + bgPicker(seed, title: "Drop per reading", range: 3 ... 54, keyPath: \.delta) + intStepper(seed, label: "Readings in a row", range: 1 ... 3, step: 1, unit: "", keyPath: \.monitoringWindow) + case .missedReading: + doubleStepper(seed, label: "No reading for", range: 11 ... 121, step: 5, unit: "min", keyPath: \.threshold) + case .notLooping: + doubleStepper(seed, label: "No loop for", range: 16 ... 61, step: 5, unit: "min", keyPath: \.threshold) + case .battery: + doubleStepper(seed, label: "At or below", range: 0 ... 100, step: 5, unit: "%", keyPath: \.threshold) + case .iob: + doubleStepper(seed, label: "Alert above", range: 0 ... 30, step: 1, unit: "U", keyPath: \.threshold) + case .cob: + doubleStepper(seed, label: "Alert above", range: 0 ... 200, step: 5, unit: "g", keyPath: \.threshold) + case .sensorChange: + doubleStepper(seed, label: "Remind after", range: 1 ... 15, step: 1, unit: "days", keyPath: \.threshold) + activePicker(seed) + case .pumpChange: + doubleStepper(seed, label: "Remind after", range: 1 ... 7, step: 1, unit: "days", keyPath: \.threshold) + activePicker(seed) + case .pump: + doubleStepper(seed, label: "Alert below", range: 0 ... 100, step: 5, unit: "U", keyPath: \.threshold) + default: + EmptyView() + } + } + + private func bgPicker( + _ seed: Binding, + title: String, + range: ClosedRange, + keyPath: WritableKeyPath + ) -> some View { + BGPicker(title: title, range: range, value: doubleBinding(seed, keyPath: keyPath)) + } + + private func doubleStepper( + _ seed: Binding, + label: String, + range: ClosedRange, + step: Double, + unit: String, + keyPath: WritableKeyPath + ) -> some View { + let value = doubleBinding(seed, keyPath: keyPath) + return Stepper(value: value, in: range, step: step) { + labelRow(label, value: "\(formatted(value.wrappedValue)) \(unit)") + } + } + + private func intStepper( + _ seed: Binding, + label: String, + range: ClosedRange, + step: Int, + unit: String, + keyPath: WritableKeyPath + ) -> some View { + let value = intBinding(seed, keyPath: keyPath) + let text = unit.isEmpty ? "\(value.wrappedValue)" : "\(value.wrappedValue) \(unit)" + return Stepper(value: value, in: range, step: step) { + labelRow(label, value: text) + } + } + + /// Plain-language summary of what a recommended alarm will do, shown as the + /// section footer. Fast drop earns one because its per-reading-times-window + /// behaviour isn't obvious from the two controls alone. + private func explanation(for seed: OnboardingViewModel.SeedAlarm) -> String? { + switch seed.type { + case .fastDrop: + let alarm = seed.alarm + let fallback = Alarm(type: .fastDrop) + let delta = alarm.delta ?? fallback.delta ?? 0 + let window = alarm.monitoringWindow ?? fallback.monitoringWindow ?? 0 + guard delta > 0, window > 0 else { return nil } + + let unit = Localizer.getPreferredUnit().localizedShortUnitString + let perReading = Localizer.formatQuantity(delta) + + if window == 1 { + return "Warns when glucose falls by at least \(perReading) \(unit) between two readings." + } + let total = Localizer.formatQuantity(delta * Double(window)) + let minutes = window * 5 + return "Warns when glucose falls by at least \(perReading) \(unit) on each of \(window) readings in a row — about \(total) \(unit) over roughly \(minutes) minutes." + default: + return nil + } + } + + /// Day/night picker for reminder alarms (sensor/pump change), so they don't + /// fire overnight by default. Reuses the same menu picker as the full editor. + private func activePicker(_ seed: Binding) -> some View { + let binding = Binding( + get: { seed.wrappedValue.alarm.activeOption }, + set: { seed.wrappedValue.alarm.activeOption = $0 } + ) + return AlarmEnumMenuPicker(title: "Active during", selection: binding) + } + + private func labelRow(_ label: String, value: String) -> some View { + HStack { + Text(label) + Spacer() + Text(value).foregroundColor(.secondary) + } + } + + private func formatted(_ value: Double) -> String { + value == value.rounded() ? String(Int(value)) : String(format: "%.1f", value) + } + + private func doubleBinding( + _ seed: Binding, + keyPath: WritableKeyPath + ) -> Binding { + Binding( + get: { seed.wrappedValue.alarm[keyPath: keyPath] ?? typeDefault(seed, keyPath) }, + set: { seed.wrappedValue.alarm[keyPath: keyPath] = $0 } + ) + } + + private func intBinding( + _ seed: Binding, + keyPath: WritableKeyPath + ) -> Binding { + Binding( + get: { seed.wrappedValue.alarm[keyPath: keyPath] ?? typeDefault(seed, keyPath) }, + set: { seed.wrappedValue.alarm[keyPath: keyPath] = $0 } + ) + } + + /// Fallback value for a control, read from a fresh `Alarm(type:)` so the + /// onboarding controls share the one source of truth for per-type defaults + /// and can't drift from them. Only a safety net — seeded alarms already carry + /// these values, so the fallback is rarely exercised. + private func typeDefault(_ seed: Binding, _ keyPath: KeyPath) -> Double { + Alarm(type: seed.wrappedValue.type)[keyPath: keyPath] ?? 0 + } + + private func typeDefault(_ seed: Binding, _ keyPath: KeyPath) -> Int { + Alarm(type: seed.wrappedValue.type)[keyPath: keyPath] ?? 0 + } +} + +private extension Array { + subscript(safe index: Int) -> Element? { + indices.contains(index) ? self[index] : nil + } +} diff --git a/LoopFollow/Onboarding/Steps/CompletionStepView.swift b/LoopFollow/Onboarding/Steps/CompletionStepView.swift new file mode 100644 index 000000000..b0e9cb261 --- /dev/null +++ b/LoopFollow/Onboarding/Steps/CompletionStepView.swift @@ -0,0 +1,108 @@ +// LoopFollow +// CompletionStepView.swift + +import SwiftUI + +struct CompletionStepView: View { + @ObservedObject var viewModel: OnboardingViewModel + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var animate = false + + /// A short tease of optional features worth discovering later — not part of + /// setup, just a nudge toward a few things that are easy to miss. + private struct Gem: Identifiable { + let id = UUID() + let icon: String + let title: String + let detail: String + } + + private let gems: [Gem] = [ + Gem(icon: "person.crop.square", + title: "Contact image", + detail: "Show your glucose on your Apple Watch face through a contact."), + Gem(icon: "bell.badge", + title: "Custom alarms", + detail: "Build your own alarms beyond the recommended set."), + Gem(icon: "dot.radiowaves.left.and.right", + title: "Remote commands", + detail: "Send carbs, boluses, and temp targets to Trio or Loop."), + ] + + var body: some View { + VStack(spacing: 0) { + Spacer(minLength: 24) + + VStack(spacing: 16) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 84, weight: .semibold)) + .foregroundStyle(.green.gradient) + .scaleEffect(animate || reduceMotion ? 1 : 0.6) + .opacity(animate || reduceMotion ? 1 : 0) + + Text("You're all set") + .font(.largeTitle.weight(.bold)) + .multilineTextAlignment(.center) + + Text("You're ready to go. Adjust everything later from the Menu — and when you have a moment, these are worth a look:") + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + } + + gemsCard + .padding(.top, 28) + .padding(.horizontal, 24) + + Spacer(minLength: 24) + + Button { viewModel.finish() } label: { + Text("Finish") + .font(.body.weight(.semibold)) + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + } + .buttonStyle(.borderedProminent) + .padding(.horizontal, 24) + .padding(.bottom, 32) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear { + guard !reduceMotion else { return } + withAnimation(.spring(response: 0.5, dampingFraction: 0.6).delay(0.1)) { + animate = true + } + } + } + + private var gemsCard: some View { + VStack(alignment: .leading, spacing: 14) { + ForEach(gems) { gem in + HStack(alignment: .top, spacing: 12) { + Image(systemName: gem.icon) + .font(.body) + .foregroundStyle(Color.accentColor) + .frame(width: 26) + + VStack(alignment: .leading, spacing: 2) { + Text(gem.title) + .font(.subheadline.weight(.semibold)) + Text(gem.detail) + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + } + + Spacer(minLength: 0) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color(.secondarySystemGroupedBackground)) + ) + } +} diff --git a/LoopFollow/Onboarding/Steps/ConnectImportStepView.swift b/LoopFollow/Onboarding/Steps/ConnectImportStepView.swift new file mode 100644 index 000000000..ef25b59ae --- /dev/null +++ b/LoopFollow/Onboarding/Steps/ConnectImportStepView.swift @@ -0,0 +1,84 @@ +// LoopFollow +// ConnectImportStepView.swift + +import SwiftUI + +/// The "copy from another phone" connect path: scan a QR code exported from an +/// already-configured LoopFollow and apply those settings. Reuses the same +/// scanner, preview, and import logic as Settings → Import/Export. +struct ConnectImportStepView: View { + @ObservedObject var viewModel: OnboardingViewModel + @StateObject private var importVM = ImportExportSettingsViewModel() + + private var importSucceeded: Bool { + importVM.qrCodeErrorMessage.contains("Successfully imported") + } + + var body: some View { + VStack(spacing: 0) { + form + OnboardingNavFooter( + continueEnabled: viewModel.didImportSettings, + showBack: viewModel.canGoBack, + onBack: { viewModel.goBack() }, + onContinue: { viewModel.advance() } + ) + } + } + + private var form: some View { + Form { + Section { + EmptyView() + } header: { + OnboardingStepHeader( + systemImage: "qrcode", + title: "Copy from another phone", + subtitle: "On your other phone, open Settings → Nightscout (or Dexcom Share) → Import/Export and export to a QR code. Then scan it here." + ) + .textCase(nil) + .padding(.bottom, 8) + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + + Section { + Button { + importVM.isShowingQRCodeScanner = true + } label: { + HStack { + Image(systemName: "qrcode.viewfinder") + .foregroundColor(.accentColor) + Text(viewModel.didImportSettings ? "Scan a different code" : "Scan QR Code") + } + } + } + + if viewModel.didImportSettings { + Section { + Label("Settings imported — you're connected.", systemImage: "checkmark.circle.fill") + .foregroundColor(.green) + } + } else if !importVM.qrCodeErrorMessage.isEmpty { + Section { + Text(importVM.qrCodeErrorMessage) + .font(.caption) + .foregroundColor(.red) + } + } + } + .sheet(isPresented: $importVM.isShowingQRCodeScanner) { + SimpleQRCodeScannerView { result in + importVM.handleQRCodeScanResult(result) + } + } + .sheet(isPresented: $importVM.showImportConfirmation) { + ImportConfirmationView(viewModel: importVM) + } + .onChange(of: importVM.qrCodeErrorMessage) { message in + if message.contains("Successfully imported") { + viewModel.didImportSettings = true + } + } + } +} diff --git a/LoopFollow/Onboarding/Steps/DataSourceChoiceStepView.swift b/LoopFollow/Onboarding/Steps/DataSourceChoiceStepView.swift new file mode 100644 index 000000000..7021ae46e --- /dev/null +++ b/LoopFollow/Onboarding/Steps/DataSourceChoiceStepView.swift @@ -0,0 +1,84 @@ +// LoopFollow +// DataSourceChoiceStepView.swift + +import SwiftUI + +struct DataSourceChoiceStepView: View { + @ObservedObject var viewModel: OnboardingViewModel + + var body: some View { + ScrollView { + VStack(spacing: 24) { + OnboardingStepHeader( + systemImage: "antenna.radiowaves.left.and.right", + title: "Choose a data source", + subtitle: "LoopFollow needs a glucose data source. Pick one now — you can change or add more later in Settings." + ) + .padding(.horizontal) + + VStack(spacing: 14) { + choiceCard( + source: .nightscout, + icon: "globe", + title: "Nightscout", + detail: "Follow a Nightscout site. Works with Loop, Trio, and most uploaders. Enables the full set of LoopFollow features." + ) + choiceCard( + source: .dexcom, + icon: "sensor.tag.radiowaves.forward.fill", + title: "Dexcom Share", + detail: "Follow glucose directly from a Dexcom Share account, without a Nightscout site." + ) + choiceCard( + source: .copyFromPhone, + icon: "qrcode", + title: "Copy from another phone", + detail: "Already using LoopFollow on another phone? Scan its QR code to copy the connection here." + ) + } + .padding(.horizontal) + } + .padding(.bottom, 24) + } + } + + private func choiceCard(source: OnboardingViewModel.DataSource, icon: String, title: String, detail: String) -> some View { + let isSelected = viewModel.dataSource == source + return Button { + viewModel.dataSource = source + } label: { + HStack(alignment: .top, spacing: 14) { + Image(systemName: icon) + .font(.title2) + .foregroundStyle(isSelected ? Color.accentColor : .secondary) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.headline) + .foregroundColor(.primary) + Text(detail) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + } + + Spacer(minLength: 0) + + Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") + .font(.title3) + .foregroundStyle(isSelected ? Color.accentColor : Color(.systemGray3)) + } + .padding() + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color(.secondarySystemGroupedBackground)) + ) + .overlay( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 2) + ) + } + .buttonStyle(.plain) + } +} diff --git a/LoopFollow/Onboarding/Steps/DexcomConnectStepView.swift b/LoopFollow/Onboarding/Steps/DexcomConnectStepView.swift new file mode 100644 index 000000000..a932d9f68 --- /dev/null +++ b/LoopFollow/Onboarding/Steps/DexcomConnectStepView.swift @@ -0,0 +1,97 @@ +// LoopFollow +// DexcomConnectStepView.swift + +import SwiftUI + +struct DexcomConnectStepView: View { + @ObservedObject var viewModel: DexcomSettingsViewModel + @ObservedObject var onboarding: OnboardingViewModel + + var body: some View { + VStack(spacing: 0) { + ConnectionStatusPill( + color: statusColor, + message: viewModel.statusMessage, + isLoading: viewModel.statusKind == .checking, + systemImage: statusIcon + ) + .padding(.horizontal) + .padding(.top, 12) + .padding(.bottom, 8) + .animation(.easeInOut(duration: 0.25), value: viewModel.statusKind) + + form + OnboardingNavFooter( + continueEnabled: viewModel.canVerifyProceed, + showBack: onboarding.canGoBack, + onBack: { onboarding.goBack() }, + onContinue: { onboarding.advance() } + ) + } + } + + private var form: some View { + Form { + Section { + EmptyView() + } header: { + OnboardingStepHeader( + systemImage: "sensor.tag.radiowaves.forward.fill", + title: "Connect Dexcom Share", + subtitle: "Sign in with the Dexcom Share account that shares glucose data." + ) + .textCase(nil) + .padding(.bottom, 8) + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + + Section(header: Text("Dexcom Share")) { + HStack { + Text("Username") + TextField("Enter Username", text: $viewModel.userName) + .textContentType(.username) + .autocapitalization(.none) + .disableAutocorrection(true) + .multilineTextAlignment(.trailing) + } + + HStack { + Text("Password") + TogglableSecureInput( + placeholder: "Enter Password", + text: $viewModel.password, + style: .singleLine, + textContentType: .password + ) + } + + Picker("Server", selection: $viewModel.server) { + Text("US").tag("US") + Text("Outside US").tag("NON-US") + } + .pickerStyle(.segmented) + } + } + } + + // MARK: - Status pill mapping + + private var statusColor: Color { + switch viewModel.statusKind { + case .idle: return .secondary + case .checking: return .orange + case .connected: return .green + case .error: return .red + } + } + + private var statusIcon: String? { + switch viewModel.statusKind { + case .idle: return "circle" + case .checking: return nil + case .connected: return "checkmark.circle.fill" + case .error: return "exclamationmark.triangle.fill" + } + } +} diff --git a/LoopFollow/Onboarding/Steps/GeneralAlarmsStepView.swift b/LoopFollow/Onboarding/Steps/GeneralAlarmsStepView.swift new file mode 100644 index 000000000..e7d4added --- /dev/null +++ b/LoopFollow/Onboarding/Steps/GeneralAlarmsStepView.swift @@ -0,0 +1,83 @@ +// LoopFollow +// GeneralAlarmsStepView.swift + +import SwiftUI + +/// A short set of the alarm-wide settings that matter most up front: when the +/// day and night periods begin (used by day/night alarm options) and how alarm +/// sound is handled. The full set lives in the Menu. +struct GeneralAlarmsStepView: View { + @ObservedObject private var cfgStore = Storage.shared.alarmConfiguration + + private var dayBinding: Binding { + timeBinding(\.dayStart) + } + + private var nightBinding: Binding { + timeBinding(\.nightStart) + } + + var body: some View { + Form { + Section { + EmptyView() + } header: { + OnboardingStepHeader( + systemImage: "slider.horizontal.3", + title: "Alarm basics", + subtitle: "Set when your day and night begin, and how alarm sound behaves. You can fine-tune everything later in the Menu." + ) + .textCase(nil) + .padding(.bottom, 8) + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + + Section { + DatePicker("Day starts", selection: dayBinding, displayedComponents: [.hourAndMinute]) + .datePickerStyle(.compact) + DatePicker("Night starts", selection: nightBinding, displayedComponents: [.hourAndMinute]) + .datePickerStyle(.compact) + } header: { + Text("Day / Night") + } footer: { + Text("Alarms can behave differently during the day and at night.") + } + + Section { + Toggle("Override system volume", isOn: $cfgStore.value.overrideSystemOutputVolume) + + if cfgStore.value.overrideSystemOutputVolume { + HStack { + Image(systemName: "speaker.fill") + .foregroundColor(.secondary) + Slider(value: $cfgStore.value.forcedOutputVolume, in: 0 ... 1) + Image(systemName: "speaker.wave.3.fill") + .foregroundColor(.secondary) + } + } + + Toggle("Play sound during calls", isOn: $cfgStore.value.audioDuringCalls) + } header: { + Text("Sound") + } footer: { + Text("Overriding the system volume lets alarms be heard even when your phone is silenced or in a Focus mode.") + } + } + } + + private func timeBinding(_ keyPath: WritableKeyPath) -> Binding { + Binding( + get: { + var components = Calendar.current.dateComponents([.year, .month, .day], from: Date()) + components.hour = cfgStore.value[keyPath: keyPath].hour + components.minute = cfgStore.value[keyPath: keyPath].minute + return Calendar.current.date(from: components) ?? Date() + }, + set: { newDate in + let hm = Calendar.current.dateComponents([.hour, .minute], from: newDate) + cfgStore.value[keyPath: keyPath] = TimeOfDay(hour: hm.hour ?? 0, minute: hm.minute ?? 0) + } + ) + } +} diff --git a/LoopFollow/Onboarding/Steps/NightscoutConnectStepView.swift b/LoopFollow/Onboarding/Steps/NightscoutConnectStepView.swift new file mode 100644 index 000000000..10dee8c01 --- /dev/null +++ b/LoopFollow/Onboarding/Steps/NightscoutConnectStepView.swift @@ -0,0 +1,293 @@ +// LoopFollow +// NightscoutConnectStepView.swift + +import SwiftUI + +/// The Nightscout connect phase, split into two internal pages: +/// 1. **Address** — enter the site URL; we validate it. A public site (or a URL +/// that already carries a token) is done here. A reachable site that needs a +/// token advances to page 2. An unreachable/invalid address stays put with the +/// error shown in the status pill so the user can correct it. +/// 2. **Token** — paste a token or create a read-only one from the API secret. +struct NightscoutConnectStepView: View { + @ObservedObject var viewModel: NightscoutSettingsViewModel + @ObservedObject var onboarding: OnboardingViewModel + + private enum Page { case address, token } + private enum TokenMode: Hashable { case haveToken, createFromSecret } + + @State private var page: Page = .address + @State private var mode: TokenMode = .haveToken + @State private var apiSecret: String = "" + + var body: some View { + VStack(spacing: 0) { + ConnectionStatusPill( + color: statusColor, + message: viewModel.friendlyStatus, + isLoading: viewModel.statusKind == .checking, + systemImage: statusIcon + ) + .padding(.horizontal) + .padding(.top, 12) + .padding(.bottom, 8) + .animation(.easeInOut(duration: 0.25), value: viewModel.statusKind) + + switch page { + case .address: + addressForm + case .token: + tokenForm + } + + footer + } + } + + // MARK: - Pages + + private var addressForm: some View { + Form { + titleSection( + "Connect to Nightscout", + "Enter your site address. We'll check it, and only ask for a token if your site needs one." + ) + urlSection + } + } + + private var tokenForm: some View { + Form { + if viewModel.isConnected || viewModel.provisionedTokenPending { + tokenDoneSection + } else { + titleSection( + "Add a token", + "This site needs a token. Paste one, or have LoopFollow create a read-only token from your API secret." + ) + tokenModeSection + + switch mode { + case .haveToken: + tokenSection + case .createFromSecret: + secretSection + } + } + } + } + + /// Shown once a token is in place, so the page reads as "done — continue" + /// rather than still asking the user to do something. + private var tokenDoneSection: some View { + Section { + EmptyView() + } header: { + VStack(alignment: .leading, spacing: 10) { + Text(viewModel.provisionedTokenPending ? "Token created" : "You're connected") + .font(.title2.weight(.bold)) + .foregroundColor(.primary) + Text(viewModel.provisionedTokenPending + ? "Your read-only token is ready. Tap Continue to keep going — your site may take a few minutes to start accepting it." + : "Your read-only token is set up. Tap Continue to keep going.") + .font(.subheadline) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + .textCase(nil) + .padding(.bottom, 8) + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + } + + @ViewBuilder + private var footer: some View { + switch page { + case .address: + OnboardingNavFooter( + continueEnabled: viewModel.isConnected || viewModel.addressNeedsToken, + showBack: onboarding.canGoBack, + onBack: { onboarding.goBack() }, + onContinue: addressContinue + ) + case .token: + OnboardingNavFooter( + continueEnabled: viewModel.isConnected || viewModel.provisionedTokenPending, + showBack: true, + onBack: { withAnimation(.easeInOut(duration: 0.25)) { page = .address } }, + onContinue: { onboarding.advance() } + ) + } + } + + private func addressContinue() { + if viewModel.isConnected { + onboarding.advance() + } else if viewModel.addressNeedsToken { + withAnimation(.easeInOut(duration: 0.25)) { page = .token } + } + } + + // MARK: - Sections + + private func titleSection(_ title: String, _ subtitle: String) -> some View { + Section { + EmptyView() + } header: { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.title2.weight(.bold)) + .foregroundColor(.primary) + Text(subtitle) + .font(.subheadline) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + .textCase(nil) + .padding(.bottom, 8) + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + } + + private var urlSection: some View { + Section { + // `verbatim:` keeps SwiftUI from auto-linking the example URL in accent + // blue (which reads as an "active" field). + ZStack(alignment: .leading) { + if viewModel.nightscoutURL.isEmpty { + Text(verbatim: "https://your-site.example.com") + .foregroundColor(.secondary) + } + TextField("", text: $viewModel.nightscoutURL) + .textContentType(.URL) + .keyboardType(.URL) + .autocapitalization(.none) + .disableAutocorrection(true) + .foregroundColor(.primary) + .onChange(of: viewModel.nightscoutURL) { newValue in + viewModel.processURL(newValue) + } + } + } header: { + Text("Site URL") + } footer: { + Text("Enter or paste your full Nightscout address. If the link already includes a token, we'll fill both in for you.") + } + } + + private var tokenModeSection: some View { + Section { + Picker("Token", selection: $mode) { + Text("I have a token").tag(TokenMode.haveToken) + Text("Create one for me").tag(TokenMode.createFromSecret) + } + .pickerStyle(.segmented) + } footer: { + if mode == .createFromSecret { + Text("Your API secret is used once to create a read-only access token and is never stored.") + } else { + Text("Type or paste a token, or a full Nightscout URL that includes a token.") + } + } + } + + private var tokenSection: some View { + Section(header: Text("Access Token")) { + HStack { + Text("Token") + TogglableSecureInput( + placeholder: "Enter Token", + text: $viewModel.nightscoutToken, + style: .singleLine, + textContentType: .password + ) + } + + if viewModel.tokenIsVerifiedSecret || viewModel.isProvisioningToken { + Button { + viewModel.createReadOnlyToken(fromSecret: viewModel.nightscoutToken) + } label: { + HStack { + if viewModel.isProvisioningToken { + ProgressView() + Text("Creating read-only token…") + } else { + Image(systemName: "wand.and.stars") + Text("That's your API secret — create a read-only token") + } + } + } + .disabled(viewModel.isProvisioningToken) + } + + if let error = viewModel.tokenProvisionError { + Text(error) + .font(.footnote) + .foregroundColor(.red) + } + } + } + + private var secretSection: some View { + Section(header: Text("API Secret")) { + HStack { + Text("Secret") + TogglableSecureInput( + placeholder: "Enter API Secret", + text: $apiSecret, + style: .singleLine, + textContentType: .password + ) + } + + Button { + viewModel.createReadOnlyToken(fromSecret: apiSecret) + } label: { + HStack { + Spacer() + if viewModel.isProvisioningToken { + ProgressView() + } else { + Text("Create Read-Only Token") + .fontWeight(.semibold) + } + Spacer() + } + } + .disabled(viewModel.isProvisioningToken + || apiSecret.isEmpty + || viewModel.nightscoutURL.isEmpty) + + if let error = viewModel.tokenProvisionError { + Text(error) + .font(.footnote) + .foregroundColor(.red) + } + } + } + + // MARK: - Status pill mapping + + private var statusColor: Color { + switch viewModel.statusKind { + case .idle: return .secondary + case .checking: return .orange + case .pending: return .blue + case .needsToken, .connected: return .green + case .error: return .red + } + } + + private var statusIcon: String? { + switch viewModel.statusKind { + case .idle: return "globe" + case .checking: return nil + case .pending: return "clock.badge.checkmark" + case .needsToken: return "checkmark.circle" + case .connected: return "checkmark.circle.fill" + case .error: return "exclamationmark.triangle.fill" + } + } +} diff --git a/LoopFollow/Onboarding/Steps/NotificationsStepView.swift b/LoopFollow/Onboarding/Steps/NotificationsStepView.swift new file mode 100644 index 000000000..cdae54c31 --- /dev/null +++ b/LoopFollow/Onboarding/Steps/NotificationsStepView.swift @@ -0,0 +1,66 @@ +// LoopFollow +// NotificationsStepView.swift + +import SwiftUI + +/// A short context screen ahead of the system notification prompt, following +/// Apple's pre-alert guidance. The system prompt is triggered from here — before +/// the final screen — so it never appears on top of "You're all set". +struct NotificationsStepView: View { + @ObservedObject var viewModel: OnboardingViewModel + @State private var requesting = false + + var body: some View { + VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 20) { + Image(systemName: "bell.badge.fill") + .font(.system(size: 44, weight: .semibold)) + .foregroundStyle(Color.accentColor) + + Text("Stay informed") + .font(.title2.weight(.bold)) + + Text("LoopFollow uses notifications to deliver your alarms — like low or high glucose, or a missed reading. Without them, alarms can't reach you when the app is in the background.") + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + + Text("We'll ask iOS for permission next. You can change this any time in the Settings app.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + .padding(.top, 8) + + Spacer() + + VStack(spacing: 12) { + Button { + guard !requesting else { return } + requesting = true + NotificationAuthorization.requestIfNeeded { + viewModel.advance() + } + } label: { + Text("Enable Notifications") + .font(.body.weight(.semibold)) + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + } + .buttonStyle(.borderedProminent) + .disabled(requesting) + + Button { viewModel.advance() } label: { + Text("Not now") + .font(.body.weight(.medium)) + } + .disabled(requesting) + } + .padding(.horizontal, 24) + .padding(.bottom, 24) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} diff --git a/LoopFollow/Onboarding/Steps/OverviewStepView.swift b/LoopFollow/Onboarding/Steps/OverviewStepView.swift new file mode 100644 index 000000000..9854ecdce --- /dev/null +++ b/LoopFollow/Onboarding/Steps/OverviewStepView.swift @@ -0,0 +1,132 @@ +// LoopFollow +// OverviewStepView.swift + +import SwiftUI + +/// A quick map of what the rest of setup covers, so the user knows what to +/// expect. The list reflects the phases that will actually run — a returning +/// user who's already connected won't see "Connect your data" — and collapses to +/// a short "you're all set" when there's nothing left to configure. +struct OverviewStepView: View { + @ObservedObject var onboarding: OnboardingViewModel + + private struct Item: Identifiable { + let id = UUID() + let icon: String + let title: String + let detail: String + } + + /// The overview rows, derived from the phases that will actually run. + private var items: [Item] { + let steps = onboarding.activeSteps + var result: [Item] = [] + if steps.contains(.connect) { + result.append(Item(icon: "antenna.radiowaves.left.and.right", + title: "Connect your data", + detail: "Link a Nightscout site or a Dexcom Share account.")) + } + if steps.contains(.units) { + result.append(Item(icon: "ruler", + title: "Units & metrics", + detail: "Choose how glucose and statistics are shown.")) + } + if steps.contains(.alarms) { + result.append(Item(icon: "bell.badge.fill", + title: "Recommended alarms", + detail: "Turn on a few useful alarms with sensible defaults.")) + } + if steps.contains(.tabOrder) { + result.append(Item(icon: "square.grid.2x2", + title: "Finish up", + detail: "Arrange your tabs and set notification preferences.")) + } + return result + } + + var body: some View { + VStack(spacing: 0) { + if onboarding.hasNothingToSetUp { + allSet + } else { + checklist + } + footer + } + } + + // MARK: - Content + + private var checklist: some View { + ScrollView { + VStack(alignment: .leading, spacing: 24) { + OnboardingStepHeader( + systemImage: "list.bullet.rectangle", + title: "Here's what we'll do", + subtitle: "A quick, straightforward setup — about 5 minutes. You can change anything later in Settings." + ) + .padding(.horizontal) + + VStack(spacing: 14) { + ForEach(items) { item in + HStack(alignment: .top, spacing: 14) { + Image(systemName: item.icon) + .font(.title3) + .foregroundStyle(Color.accentColor) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 4) { + Text(item.title) + .font(.headline) + Text(item.detail) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + } + + Spacer(minLength: 0) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color(.secondarySystemGroupedBackground)) + ) + } + } + .padding(.horizontal) + } + .padding(.bottom, 24) + } + } + + private var allSet: some View { + ScrollView { + OnboardingStepHeader( + systemImage: "checkmark.circle", + title: "You're all set", + subtitle: "Your data source is connected and your alarms are in place — there's nothing to set up here. You can fine-tune anything later in Settings." + ) + .padding(.horizontal) + .padding(.top, 24) + } + } + + // MARK: - Footer + + private var footer: some View { + OnboardingNavFooter( + continueTitle: onboarding.hasNothingToSetUp ? "Done" : "Continue", + continueEnabled: true, + showBack: onboarding.canGoBack, + onBack: { onboarding.goBack() }, + onContinue: { + if onboarding.hasNothingToSetUp { + onboarding.finish() + } else { + onboarding.advance() + } + } + ) + } +} diff --git a/LoopFollow/Onboarding/Steps/TabOrderStepView.swift b/LoopFollow/Onboarding/Steps/TabOrderStepView.swift new file mode 100644 index 000000000..268857898 --- /dev/null +++ b/LoopFollow/Onboarding/Steps/TabOrderStepView.swift @@ -0,0 +1,23 @@ +// LoopFollow +// TabOrderStepView.swift + +import SwiftUI + +/// Lets the user arrange which features live in the tab bar versus the Menu, +/// during onboarding. Reuses the same drag-to-reorder list as the Settings +/// "Tabs" screen so behavior stays identical. +struct TabOrderStepView: View { + var body: some View { + VStack(spacing: 0) { + OnboardingStepHeader( + systemImage: "square.grid.2x2", + title: "Arrange your tabs", + subtitle: "Pick which features sit in the tab bar. You can always reach everything through the Menu." + ) + .padding(.horizontal) + .padding(.top, 8) + + TabCustomizationModal() + } + } +} diff --git a/LoopFollow/Onboarding/Steps/TelemetryStepView.swift b/LoopFollow/Onboarding/Steps/TelemetryStepView.swift new file mode 100644 index 000000000..02bf5d70c --- /dev/null +++ b/LoopFollow/Onboarding/Steps/TelemetryStepView.swift @@ -0,0 +1,79 @@ +// LoopFollow +// TelemetryStepView.swift + +import SwiftUI + +/// In-flow telemetry consent. Mirrors `TelemetryConsentView` but records the +/// decision and continues the wizard instead of dismissing a sheet. +struct TelemetryStepView: View { + @ObservedObject var viewModel: OnboardingViewModel + @State private var showPreview = false + + var body: some View { + VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 16) { + Image(systemName: "chart.bar.doc.horizontal") + .font(.system(size: 44, weight: .semibold)) + .foregroundStyle(Color.accentColor) + + Text("Help us help you") + .font(.title2.weight(.bold)) + + Text("You can choose to share anonymous information with the developers to help improve LoopFollow — such as app and iOS version, device type, which app you're following, and a few settings. Your health data, credentials, time zone, and logs remain on your device.") + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + + Button { showPreview = true } label: { + Label("See exactly what's sent", systemImage: "doc.text.magnifyingglass") + .font(.subheadline) + } + + Text("You can change this any time in Settings → General → Diagnostics.") + .font(.subheadline) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + .padding(.top, 8) + + Spacer() + + VStack(spacing: 12) { + Button { decide(true) } label: { + Text("Yes, send anonymous stats") + .font(.body.weight(.semibold)) + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + } + .buttonStyle(.borderedProminent) + + Button { decide(false) } label: { + Text("No thanks") + .font(.body.weight(.medium)) + } + } + .padding(.horizontal, 24) + .padding(.bottom, 24) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .sheet(isPresented: $showPreview) { + NavigationStack { + TelemetryPreviewView() + } + } + } + + private func decide(_ enabled: Bool) { + Storage.shared.telemetryEnabled.value = enabled + Storage.shared.telemetryConsentDecisionMade.value = true + if enabled { + // Fire the inaugural ping immediately, then start the 24h cadence. + Task.detached { + await TelemetryClient.shared.maybeSend() + TelemetryClient.shared.scheduleRecurring() + } + } + viewModel.advance() + } +} diff --git a/LoopFollow/Onboarding/Steps/UnitsStepView.swift b/LoopFollow/Onboarding/Steps/UnitsStepView.swift new file mode 100644 index 000000000..25138e611 --- /dev/null +++ b/LoopFollow/Onboarding/Steps/UnitsStepView.swift @@ -0,0 +1,86 @@ +// LoopFollow +// UnitsStepView.swift + +import SwiftUI + +/// The units phase, split into two lighter pages so it doesn't feel heavy: +/// 1. **Units** — the glucose display unit on its own. +/// 2. **Statistics** — range mode and the glycemic/variability metrics LoopFollow +/// shows. +/// +/// Both pages reuse `UnitsConfigurationView`, each rendering only its subset of +/// sections. The phase reports its within-phase position through +/// `phaseProgress` so the progress bar fills smoothly across the two pages. +struct UnitsStepView: View { + @ObservedObject var onboarding: OnboardingViewModel + + private enum Page: Int, CaseIterable { case units, statistics } + @State private var page: Page = .units + + var body: some View { + VStack(spacing: 0) { + Form { + switch page { + case .units: + header( + systemImage: "ruler", + title: "Units", + subtitle: "Choose how glucose values are displayed. You can change this later in Settings." + ) + UnitsConfigurationView(sections: .units) + case .statistics: + header( + systemImage: "chart.bar.xaxis", + title: "Statistics", + subtitle: "Choose how LoopFollow's statistics are measured and displayed." + ) + UnitsConfigurationView(sections: .statistics) + } + } + + footer + } + .onAppear { reportProgress() } + .onChange(of: page) { _ in reportProgress() } + } + + private func reportProgress() { + onboarding.phaseProgress = .init(page: page.rawValue, count: Page.allCases.count) + } + + private func header(systemImage: String, title: String, subtitle: String) -> some View { + Section { + EmptyView() + } header: { + OnboardingStepHeader( + systemImage: systemImage, + title: title, + subtitle: subtitle + ) + .textCase(nil) + .padding(.bottom, 8) + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + } + + @ViewBuilder + private var footer: some View { + switch page { + case .units: + OnboardingNavFooter( + continueEnabled: true, + showBack: onboarding.canGoBack, + onBack: { onboarding.goBack() }, + onContinue: { withAnimation(.easeInOut(duration: 0.25)) { page = .statistics } } + ) + case .statistics: + OnboardingNavFooter( + continueEnabled: true, + showBack: true, + onBack: { withAnimation(.easeInOut(duration: 0.25)) { page = .units } }, + onContinue: { onboarding.advance() } + ) + } + } +} diff --git a/LoopFollow/Onboarding/Steps/WelcomeStepView.swift b/LoopFollow/Onboarding/Steps/WelcomeStepView.swift new file mode 100644 index 000000000..d619d722d --- /dev/null +++ b/LoopFollow/Onboarding/Steps/WelcomeStepView.swift @@ -0,0 +1,84 @@ +// LoopFollow +// WelcomeStepView.swift + +import SwiftUI + +struct WelcomeStepView: View { + @ObservedObject var viewModel: OnboardingViewModel + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var animate = false + + var body: some View { + VStack(spacing: 0) { + Spacer() + + VStack(spacing: 20) { + AnimatedLoopFollowLogo(size: 140) + .frame(height: 160) + .opacity(animate || reduceMotion ? 1 : 0) + + Text("Welcome to LoopFollow") + .font(.largeTitle.weight(.bold)) + .multilineTextAlignment(.center) + + Text(viewModel.isAlreadyConfigured + ? "You're already set up. You can skip this guide, or walk through it to review your settings." + : "Let's get you connected to your data and set up a few recommended alarms. It only takes a few minutes.") + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + } + + Spacer() + + VStack(spacing: 12) { + if viewModel.isAlreadyConfigured { + Button { viewModel.skip() } label: { + primaryLabel("Skip") + } + .buttonStyle(.borderedProminent) + + Button { advance() } label: { + Text("Review setup anyway") + .font(.body.weight(.medium)) + } + } else { + Button { advance() } label: { + primaryLabel("Get Started") + } + .buttonStyle(.borderedProminent) + + Button { viewModel.skip() } label: { + Text("Skip") + .font(.body.weight(.medium)) + } + } + } + .padding(.horizontal, 24) + .padding(.bottom, 32) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear { + guard !reduceMotion else { return } + withAnimation(.spring(response: 0.6, dampingFraction: 0.7).delay(0.1)) { + animate = true + } + } + } + + private func primaryLabel(_ text: String) -> some View { + Text(text) + .font(.body.weight(.semibold)) + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + } + + private func advance() { + if reduceMotion { + viewModel.advance() + } else { + withAnimation(.easeInOut(duration: 0.3)) { viewModel.advance() } + } + } +} diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSRemoteView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSRemoteView.swift index b57688b20..73de2aae4 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSRemoteView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSRemoteView.swift @@ -6,6 +6,7 @@ import SwiftUI struct LoopAPNSRemoteView: View { @Environment(\.presentationMode) var presentationMode @StateObject private var viewModel = RemoteSettingsViewModel() + @ObservedObject private var activeOverrideNote = Observable.shared.override var body: some View { NavigationView { @@ -20,7 +21,7 @@ struct LoopAPNSRemoteView: View { LazyVGrid(columns: columns, spacing: 16) { CommandButtonView(command: "Meal", iconName: "fork.knife", destination: LoopAPNSCarbsView()) CommandButtonView(command: "Bolus", iconName: "syringe", destination: LoopAPNSBolusView()) - CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: OverridePresetsView()) + CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: OverridePresetsView(), isActive: activeOverrideNote.value != nil) } .padding(.horizontal) } else { diff --git a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift index ce88f0b90..4d29aa6f6 100644 --- a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift +++ b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift @@ -7,6 +7,7 @@ struct OverridePresetsView: View { @StateObject private var viewModel = OverridePresetsViewModel() @Environment(\.presentationMode) var presentationMode @ObservedObject var overrideNote = Observable.shared.override + @ObservedObject var overrideEndAt = Observable.shared.overrideEndAt var body: some View { NavigationView { @@ -28,6 +29,12 @@ struct OverridePresetsView: View { Text(activeNote) .font(.subheadline) .foregroundColor(.secondary) + + if let endAt = overrideEndAt.value { + RemainingTimeText(endAt: endAt) + .font(.subheadline) + .foregroundColor(.secondary) + } } Spacer() diff --git a/LoopFollow/Remote/RemainingTimeText.swift b/LoopFollow/Remote/RemainingTimeText.swift new file mode 100644 index 000000000..297c0d4bc --- /dev/null +++ b/LoopFollow/Remote/RemainingTimeText.swift @@ -0,0 +1,34 @@ +// LoopFollow +// RemainingTimeText.swift + +import SwiftUI + +/// Remaining time for an active override / temp target: a live countdown when +/// 2 hours or less remain, otherwise a static "29d 23h"-style duration — +/// a multi-day ticker reads poorly. +struct RemainingTimeText: View { + let endAt: TimeInterval + + static let tickerMaxRemaining: TimeInterval = 2 * 3600 + + private static let longFormatter: DateComponentsFormatter = { + let f = DateComponentsFormatter() + f.unitsStyle = .abbreviated + f.allowedUnits = [.day, .hour, .minute] + f.maximumUnitCount = 2 + return f + }() + + var body: some View { + let end = Date(timeIntervalSince1970: endAt) + let remaining = end.timeIntervalSinceNow + if remaining <= 0 { + EmptyView() + } else if remaining <= Self.tickerMaxRemaining { + Text(timerInterval: Date() ... end, countsDown: true) + .monospacedDigit() + } else { + Text(Self.longFormatter.string(from: remaining) ?? "") + } + } +} diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 879ca047a..6e9b2acaa 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -4,7 +4,6 @@ import AVFoundation import HealthKit import SwiftUI -import UIKit struct RemoteSettingsView: View { @ObservedObject var viewModel: RemoteSettingsViewModel diff --git a/LoopFollow/Remote/TRC/OverrideView.swift b/LoopFollow/Remote/TRC/OverrideView.swift index 3a402f847..703201b07 100644 --- a/LoopFollow/Remote/TRC/OverrideView.swift +++ b/LoopFollow/Remote/TRC/OverrideView.swift @@ -10,6 +10,7 @@ struct OverrideView: View { @ObservedObject var device = Storage.shared.device @ObservedObject var overrideNote = Observable.shared.override + @ObservedObject var overrideEndAt = Observable.shared.overrideEndAt @State private var showAlert: Bool = false @State private var alertType: AlertType? = nil @@ -49,6 +50,14 @@ struct OverrideView: View { Text(activeNote) .foregroundColor(.secondary) } + if let endAt = overrideEndAt.value { + HStack { + Text("Time Remaining") + Spacer() + RemainingTimeText(endAt: endAt) + .foregroundColor(.secondary) + } + } Button { alertType = .confirmCancellation showAlert = true diff --git a/LoopFollow/Remote/TRC/TempTargetView.swift b/LoopFollow/Remote/TRC/TempTargetView.swift index 3a047e66f..ac2fa7303 100644 --- a/LoopFollow/Remote/TRC/TempTargetView.swift +++ b/LoopFollow/Remote/TRC/TempTargetView.swift @@ -10,6 +10,7 @@ struct TempTargetView: View { @ObservedObject var device = Storage.shared.device @ObservedObject var tempTarget = Observable.shared.tempTarget + @ObservedObject var tempTargetEndAt = Observable.shared.tempTargetEndAt @State private var newHKTarget = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0.0) @State private var duration = HKQuantity(unit: .minute(), doubleValue: 0.0) @@ -51,6 +52,14 @@ struct TempTargetView: View { Text(Localizer.formatQuantity(tempTargetValue)) Text(Localizer.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) } + if let endAt = tempTargetEndAt.value { + HStack { + Text("Time Remaining") + Spacer() + RemainingTimeText(endAt: endAt) + .foregroundColor(.secondary) + } + } Button { alertType = .confirmCancellation showAlert = true diff --git a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift index 5335f6451..0ad641a8b 100644 --- a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift +++ b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift @@ -5,6 +5,8 @@ import SwiftUI struct TrioRemoteControlView: View { @ObservedObject var viewModel: TrioRemoteControlViewModel + @ObservedObject private var activeOverrideNote = Observable.shared.override + @ObservedObject private var activeTempTarget = Observable.shared.tempTarget @Environment(\.presentationMode) var presentationMode var body: some View { @@ -18,8 +20,8 @@ struct TrioRemoteControlView: View { LazyVGrid(columns: columns, spacing: 16) { CommandButtonView(command: "Meal", iconName: "fork.knife", destination: MealView()) CommandButtonView(command: "Bolus", iconName: "syringe", destination: BolusView()) - CommandButtonView(command: "Temp Target", iconName: "scope", destination: TempTargetView()) - CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: OverrideView()) + CommandButtonView(command: "Temp Target", iconName: "scope", destination: TempTargetView(), isActive: activeTempTarget.value != nil) + CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: OverrideView(), isActive: activeOverrideNote.value != nil) } .padding(.horizontal) @@ -34,6 +36,9 @@ struct CommandButtonView: View { let command: String let iconName: String let destination: Destination + /// Lights the button up with a glow while the corresponding override / + /// temp target is active. + var isActive: Bool = false var body: some View { NavigationLink(destination: destination) { @@ -49,6 +54,13 @@ struct CommandButtonView: View { .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) + .overlay { + if isActive { + RoundedRectangle(cornerRadius: 8) + .stroke(Color.green, lineWidth: 2.5) + } + } + .shadow(color: isActive ? Color.green.opacity(0.7) : .clear, radius: isActive ? 9 : 0) } .buttonStyle(PlainButtonStyle()) } diff --git a/LoopFollow/Settings/ContactSettingsViewModel.swift b/LoopFollow/Settings/ContactSettingsViewModel.swift index 15ec8411b..c0ed95857 100644 --- a/LoopFollow/Settings/ContactSettingsViewModel.swift +++ b/LoopFollow/Settings/ContactSettingsViewModel.swift @@ -106,7 +106,6 @@ class ContactSettingsViewModel: ObservableObject { } } - private let storage = ObservableUserDefaults.shared private var cancellables = Set() init() { diff --git a/LoopFollow/Settings/DexcomSettingsView.swift b/LoopFollow/Settings/DexcomSettingsView.swift index 99a4dc32b..2f43360fd 100644 --- a/LoopFollow/Settings/DexcomSettingsView.swift +++ b/LoopFollow/Settings/DexcomSettingsView.swift @@ -14,8 +14,9 @@ struct DexcomSettingsView: View { Form { Section(header: Text("Dexcom Settings")) { HStack { - Text("User Name") - TextField("Enter User Name", text: $viewModel.userName) + Text("Username") + TextField("Enter Username", text: $viewModel.userName) + .textContentType(.username) .autocapitalization(.none) .disableAutocorrection(true) .multilineTextAlignment(.trailing) @@ -26,13 +27,14 @@ struct DexcomSettingsView: View { TogglableSecureInput( placeholder: "Enter Password", text: $viewModel.password, - style: .singleLine + style: .singleLine, + textContentType: .password ) } Picker("Server", selection: $viewModel.server) { Text("US").tag("US") - Text("NON-US").tag("NON-US") + Text("Outside US").tag("NON-US") } .pickerStyle(SegmentedPickerStyle()) } diff --git a/LoopFollow/Settings/DexcomSettingsViewModel.swift b/LoopFollow/Settings/DexcomSettingsViewModel.swift index 8c7fd5324..5a929cbe6 100644 --- a/LoopFollow/Settings/DexcomSettingsViewModel.swift +++ b/LoopFollow/Settings/DexcomSettingsViewModel.swift @@ -3,8 +3,16 @@ import Combine import Foundation +import ShareClient class DexcomSettingsViewModel: ObservableObject { + enum ConnectionStatusKind { + case idle + case checking + case connected + case error + } + /// Whether this is a fresh setup (credentials were empty when view appeared) private(set) var isFreshSetup: Bool = false @@ -12,6 +20,7 @@ class DexcomSettingsViewModel: ObservableObject { willSet { if newValue != userName { Storage.shared.shareUserName.value = newValue + scheduleVerification() } } } @@ -20,6 +29,7 @@ class DexcomSettingsViewModel: ObservableObject { willSet { if newValue != password { Storage.shared.sharePassword.value = newValue + scheduleVerification() } } } @@ -28,6 +38,7 @@ class DexcomSettingsViewModel: ObservableObject { willSet { if newValue != server { Storage.shared.shareServer.value = newValue + scheduleVerification() } } } @@ -37,7 +48,94 @@ class DexcomSettingsViewModel: ObservableObject { !userName.isEmpty && !password.isEmpty } + // MARK: - Verification + + @Published var statusKind: ConnectionStatusKind = .idle + @Published var statusMessage: String = "Enter your username and password" + + /// True when a real Dexcom Share login succeeded. + @Published private(set) var isVerified: Bool = false + + /// The credentials were explicitly rejected by Dexcom (as opposed to a network + /// failure we can't draw a conclusion from). + private(set) var loginRejected: Bool = false + + /// Can move on: verified, or the only problem is that we couldn't reach Dexcom + /// (so we don't trap a user on a flaky network). A rejected login always blocks. + var canVerifyProceed: Bool { + hasCredentials && statusKind != .checking && !loginRejected + } + + private var verifyGeneration = 0 + private var cancellables = Set() + private let verifySubject = PassthroughSubject() + init() { isFreshSetup = Storage.shared.shareUserName.value.isEmpty + + verifySubject + .debounce(for: .seconds(1.5), scheduler: DispatchQueue.main) + .sink { [weak self] in self?.verify() } + .store(in: &cancellables) + + scheduleVerification() + } + + /// Resets status to "checking" and queues a debounced verification. + private func scheduleVerification() { + verifyGeneration += 1 + loginRejected = false + isVerified = false + if hasCredentials { + statusKind = .checking + statusMessage = "Checking your account…" + verifySubject.send() + } else { + statusKind = .idle + statusMessage = "Enter your username and password" + } + } + + private func verify() { + guard hasCredentials else { return } + + let generation = verifyGeneration + let serverURL = server == "US" + ? KnownShareServers.US.rawValue + : KnownShareServers.NON_US.rawValue + let client = ShareClient(username: userName, password: password, shareServer: serverURL) + + client.fetchData(1) { [weak self] error, _ in + DispatchQueue.main.async { + guard let self, generation == self.verifyGeneration else { return } + + if let error = error { + switch error { + case .loginError: + self.statusKind = .error + self.statusMessage = "Username or password not accepted" + self.isVerified = false + self.loginRejected = true + case .httpError: + self.statusKind = .error + self.statusMessage = "Network error — check your connection" + self.isVerified = false + self.loginRejected = false + default: + // Login succeeded but there's no recent reading yet; the + // credentials are valid, which is all we're confirming. + self.statusKind = .connected + self.statusMessage = "Connected" + self.isVerified = true + self.loginRejected = false + } + } else { + self.statusKind = .connected + self.statusMessage = "Connected" + self.isVerified = true + self.loginRejected = false + } + } + } } } diff --git a/LoopFollow/Settings/GraphSettingsView.swift b/LoopFollow/Settings/GraphSettingsView.swift index d1de43c97..07d9d8d91 100644 --- a/LoopFollow/Settings/GraphSettingsView.swift +++ b/LoopFollow/Settings/GraphSettingsView.swift @@ -56,9 +56,12 @@ struct GraphSettingsView: View { if nightscoutEnabled { Section("Treatments") { Toggle("Show Carb/Bolus Values", isOn: $showValues.value) + .onChange(of: showValues.value) { _ in markDirty() } Toggle("Show Carb Absorption", isOn: $showAbsorption.value) + .onChange(of: showAbsorption.value) { _ in markDirty() } Toggle("Treatments on Small Graph", isOn: $smallGraphTreatments.value) + .onChange(of: smallGraphTreatments.value) { _ in markDirty() } } } diff --git a/LoopFollow/Settings/ImportExport/AlarmSelectionView.swift b/LoopFollow/Settings/ImportExport/AlarmSelectionView.swift index 3574eb2b2..a758ae2ac 100644 --- a/LoopFollow/Settings/ImportExport/AlarmSelectionView.swift +++ b/LoopFollow/Settings/ImportExport/AlarmSelectionView.swift @@ -239,6 +239,8 @@ struct AlarmSelectionRow: View { return "Temp Target Ended" case .buildExpire: return "Looping app expiration" + case .dbSize: + return "Nightscout Database Size" } } } diff --git a/LoopFollow/Settings/ImportExport/ExportableSettings.swift b/LoopFollow/Settings/ImportExport/ExportableSettings.swift index cd222417c..3571d8cfb 100644 --- a/LoopFollow/Settings/ImportExport/ExportableSettings.swift +++ b/LoopFollow/Settings/ImportExport/ExportableSettings.swift @@ -389,6 +389,44 @@ struct RemoteSettingsExport: Codable { } } +// MARK: - APNS Settings Export + +struct APNSSettingsExport: Codable { + let version: String + let keyId: String + let apnsKey: String + + static func fromCurrentStorage() -> APNSSettingsExport { + let storage = Storage.shared + return APNSSettingsExport( + version: AppVersionManager().version(), + keyId: storage.lfKeyId.value, + apnsKey: storage.lfApnsKey.value + ) + } + + func applyToStorage() { + let storage = Storage.shared + storage.lfKeyId.value = keyId + + let apnsService = LoopAPNSService() + storage.lfApnsKey.value = apnsService.validateAndFixAPNSKey(apnsKey) + } + + func encodeToJSON() -> String? { + do { + let data = try JSONEncoder().encode(self) + return String(data: data, encoding: .utf8) + } catch { + return nil + } + } + + func hasValidSettings() -> Bool { + APNsCredentialValidator.isFullyConfigured(keyId: keyId, apnsKey: apnsKey) + } +} + // MARK: - Combined Settings Export struct CombinedSettingsExport: Codable { @@ -398,6 +436,7 @@ struct CombinedSettingsExport: Codable { let dexcom: DexcomSettingsExport? let remote: RemoteSettingsExport? let alarms: AlarmSettingsExport? + let apns: APNSSettingsExport? let exportType: String let timestamp: Date @@ -405,6 +444,7 @@ struct CombinedSettingsExport: Codable { dexcom: DexcomSettingsExport? = nil, remote: RemoteSettingsExport? = nil, alarms: AlarmSettingsExport? = nil, + apns: APNSSettingsExport? = nil, exportType: String) { version = "1.0" @@ -413,6 +453,7 @@ struct CombinedSettingsExport: Codable { self.dexcom = dexcom self.remote = remote self.alarms = alarms + self.apns = apns self.exportType = exportType timestamp = Date() } diff --git a/LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift b/LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift index 6082b2187..3627f7ec5 100644 --- a/LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift +++ b/LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift @@ -3,7 +3,6 @@ import AVFoundation import SwiftUI -import UIKit struct ImportExportSettingsView: View { @StateObject private var viewModel = ImportExportSettingsViewModel() @@ -83,7 +82,7 @@ struct ImportExportSettingsView: View { ) .padding() - Text("Scan this QR code with another LoopFollow app to import \(viewModel.exportType.rawValue.lowercased())") + Text("Scan this QR code with another LoopFollow app to import \(viewModel.exportType.importDescription)") .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) @@ -187,6 +186,15 @@ struct ImportConfirmationView: View { color: .red ) } + + if let apnsKeyId = preview.apnsKeyId, !apnsKeyId.isEmpty { + SettingRowView( + icon: "key.horizontal", + title: "APNS Key ID", + value: apnsKeyId, + color: .purple + ) + } } .padding(.horizontal) } @@ -206,6 +214,13 @@ struct ImportConfirmationView: View { .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) + + if let apnsKeyId = viewModel.importPreview?.apnsKeyId, !apnsKeyId.isEmpty { + Text("APNS warning: importing APNS settings will overwrite your existing APNS Key ID and APNS Key.") + .font(.caption) + .foregroundColor(.orange) + .multilineTextAlignment(.center) + } } .padding(.horizontal) diff --git a/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift b/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift index cea659f85..0550e609e 100644 --- a/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift +++ b/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift @@ -10,6 +10,7 @@ struct ImportPreview { let remoteType: String? let alarmCount: Int let alarmNames: [String] + let apnsKeyId: String? } class ImportExportSettingsViewModel: ObservableObject { @@ -38,6 +39,7 @@ class ImportExportSettingsViewModel: ObservableObject { case nightscout = "Nightscout Settings" case dexcom = "Dexcom Share Settings" case remote = "Remote Settings" + case apns = "APNS Settings" case alarms = "Alarm Settings" var icon: String { @@ -46,6 +48,17 @@ class ImportExportSettingsViewModel: ObservableObject { case .dexcom: return "network" case .remote: return "antenna.radiowaves.left.and.right" case .alarms: return "bell" + case .apns: return "key.horizontal" + } + } + + var importDescription: String { + switch self { + case .nightscout: return "Nightscout Settings" + case .dexcom: return "Dexcom Share Settings" + case .remote: return "Remote Settings" + case .alarms: return "Alarm Settings" + case .apns: return "APNS Settings" } } } @@ -91,7 +104,7 @@ class ImportExportSettingsViewModel: ObservableObject { return } - LogManager.shared.log(category: .general, message: "QR code decoded successfully. Components: nightscout=\(settings.nightscout != nil), remote=\(settings.remote != nil), alarms=\(settings.alarms != nil)") + LogManager.shared.log(category: .general, message: "QR code decoded successfully. Components: nightscout=\(settings.nightscout != nil), remote=\(settings.remote != nil), alarms=\(settings.alarms != nil), apns=\(settings.apns != nil)") // Check version compatibility let currentVersion = AppVersionManager().version() @@ -163,6 +176,16 @@ class ImportExportSettingsViewModel: ObservableObject { LogManager.shared.log(category: .general, message: "Alarm settings imported from \(source) (version: \(alarms.version), \(alarms.alarms.count) alarms)") } + if let apns = settings.apns { + if apns.hasValidSettings() { + apns.applyToStorage() + importedComponents.append("APNS settings") + LogManager.shared.log(category: .general, message: "APNS settings imported from \(source) (version: \(apns.version), keyId: \(apns.keyId))") + } else { + throw NSError(domain: "SettingsImport", code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid APNS settings in \(source)"]) + } + } + // Update the success message with what was imported if !importedComponents.isEmpty { let componentsList = importedComponents.joined(separator: ", ") @@ -220,6 +243,16 @@ class ImportExportSettingsViewModel: ObservableObject { dexcom: dexcomSettings, exportType: exportType.rawValue ) + case .apns: + let apnsSettings = APNSSettingsExport.fromCurrentStorage() + if !apnsSettings.hasValidSettings() { + qrCodeErrorMessage = "Please configure your APNS settings first (valid APNS Key ID and APNS Key)" + return nil + } + settings = CombinedSettingsExport( + apns: apnsSettings, + exportType: exportType.rawValue + ) } return settings?.encodeToJSON() @@ -257,14 +290,16 @@ class ImportExportSettingsViewModel: ObservableObject { let remoteType = settings.remote?.remoteType != .none ? settings.remote?.remoteType.rawValue : nil let alarmCount = settings.alarms?.alarms.count ?? 0 let alarmNames = settings.alarms?.alarms.map { $0.name } ?? [] + let apnsKeyId = settings.apns?.keyId.isEmpty == false ? settings.apns?.keyId : nil // Check if any settings are actually present let hasAnySettings = (nightscoutURL != nil && !nightscoutURL!.isEmpty) || (dexcomUsername != nil && !dexcomUsername!.isEmpty) || (remoteType != nil && !remoteType!.isEmpty && remoteType != "None") || - alarmCount > 0 + alarmCount > 0 || + (apnsKeyId != nil && !apnsKeyId!.isEmpty) - LogManager.shared.log(category: .general, message: "Import preview check - nightscoutURL: \(nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), dexcomUsername: \(dexcomUsername.map { LogRedactor.username($0) } ?? "nil"), remoteType: \(remoteType ?? "nil"), alarmCount: \(alarmCount), hasAnySettings: \(hasAnySettings)") + LogManager.shared.log(category: .general, message: "Import preview check - nightscoutURL: \(nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), dexcomUsername: \(dexcomUsername.map { LogRedactor.username($0) } ?? "nil"), remoteType: \(remoteType ?? "nil"), alarmCount: \(alarmCount), apnsKeyIdPresent: \(apnsKeyId != nil), hasAnySettings: \(hasAnySettings)") if hasAnySettings { LogManager.shared.log(category: .general, message: "Creating import preview with settings") @@ -273,7 +308,8 @@ class ImportExportSettingsViewModel: ObservableObject { dexcomUsername: dexcomUsername, remoteType: remoteType, alarmCount: alarmCount, - alarmNames: alarmNames + alarmNames: alarmNames, + apnsKeyId: apnsKeyId ) LogManager.shared.log(category: .general, message: "Created importPreview - nightscoutURL: \(importPreview?.nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), remoteType: \(importPreview?.remoteType ?? "nil"), alarmCount: \(importPreview?.alarmCount ?? 0)") showImportConfirmation = true diff --git a/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift b/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift index c4e2a6b20..0c47f2dc9 100644 --- a/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift +++ b/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift @@ -61,6 +61,15 @@ class SettingsMigrationManager { ) } + // Try to decode as APNSSettingsExport + if let apnsSettings = try? JSONDecoder().decode(APNSSettingsExport.self, from: data) { + LogManager.shared.log(category: .general, message: "Successfully decoded as APNSSettingsExport", isDebug: true) + return CombinedSettingsExport( + apns: apnsSettings, + exportType: "APNS Settings" + ) + } + LogManager.shared.log(category: .general, message: "Failed to decode as any known component", isDebug: true) return nil } diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index f354c51b6..38fb991fb 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -9,100 +9,44 @@ struct SettingsMenuView: View { var body: some View { List { - dataSection - - Section("Display Settings") { - NavigationRow(title: "General", - icon: "gearshape", - value: SettingsRoute.general) - NavigationRow(title: "Graph", - icon: "chart.xyaxis.line", - value: SettingsRoute.graph) - - if !nightscoutURL.value.isEmpty { - NavigationRow(title: "Information Display", - icon: "info.circle", - value: SettingsRoute.infoDisplay) - } - - NavigationRow(title: "Units and Metrics", - icon: "scalemass", - value: SettingsRoute.units) - - NavigationRow(title: "Tabs", - icon: "rectangle.3.group", - value: SettingsRoute.tabSettings) - } - - Section("App Settings") { - NavigationRow(title: "Background Refresh", - icon: "arrow.clockwise", - value: SettingsRoute.backgroundRefresh) - - NavigationRow(title: "Import/Export", - icon: "square.and.arrow.down", - value: SettingsRoute.importExport) - - NavigationRow(title: "APN", - icon: "bell.and.waves.left.and.right", - value: SettingsRoute.apn) - - #if !targetEnvironment(macCatalyst) - NavigationRow(title: "Live Activity", - icon: "dot.radiowaves.left.and.right", - value: SettingsRoute.liveActivity) - #endif - - if !nightscoutURL.value.isEmpty { - NavigationRow(title: "Remote", - icon: "antenna.radiowaves.left.and.right", - value: SettingsRoute.remote) + ForEach(SettingsRoute.menuSections(nightscoutConfigured: !nightscoutURL.value.isEmpty), id: \.0) { section, routes in + Section(section.rawValue) { + ForEach(routes) { route in + NavigationRow(title: route.title, + icon: route.icon, + value: route) + } } } - - Section("Alarms") { - NavigationRow(title: "Alarms", - icon: "bell.badge", - value: SettingsRoute.alarmSettings) - } - - Section("Integrations") { - NavigationRow(title: "Calendar", - icon: "calendar", - value: SettingsRoute.calendar) - - NavigationRow(title: "Contact", - icon: "person.circle", - value: SettingsRoute.contact) - } - - Section("Advanced Settings") { - NavigationRow(title: "Advanced", - icon: "exclamationmark.shield", - value: SettingsRoute.advanced) - } } .navigationTitle("Settings") .navigationBarTitleDisplayMode(.large) } +} - // MARK: – Section builders +// MARK: – Sheet routing - @ViewBuilder - private var dataSection: some View { - Section("Data Settings") { - NavigationRow(title: "Nightscout", - icon: "network", - value: SettingsRoute.nightscout) - - NavigationRow(title: "Dexcom", - icon: "sensor.tag.radiowaves.forward", - value: SettingsRoute.dexcom) - } - } +enum SettingsSection: String, CaseIterable, Hashable { + case data = "Data Settings" + case display = "Display Settings" + case app = "App Settings" + case alarms = "Alarms" + case integrations = "Integrations" + case advanced = "Advanced Settings" } -// MARK: – Sheet routing +/// A single setting inside a settings screen, exposed to Menu search. Leaves +/// are not navigable on their own — a search hit opens the screen +/// (`SettingsRoute`) that contains it. +struct SettingsLeaf: Hashable { + let title: String + let keywords: [String] + + init(_ title: String, _ keywords: [String] = []) { + self.title = title + self.keywords = keywords + } +} enum SettingsRoute: Hashable, Identifiable { case settings @@ -125,6 +69,226 @@ enum SettingsRoute: Hashable, Identifiable { var id: Self { self } + // MARK: – Row presentation (single source of truth) + + /// Title shown in the Settings list and used for search matching. + /// Non-row cases (`.settings`, `.aggregatedStats`) return "" and are never + /// included in `menuSections`. + var title: String { + switch self { + case .nightscout: return "Nightscout" + case .dexcom: return "Dexcom" + case .general: return "General" + case .graph: return "Graph" + case .infoDisplay: return "Information Display" + case .units: return "Units and Metrics" + case .tabSettings: return "Tabs" + case .backgroundRefresh: return "Background Refresh" + case .importExport: return "Import/Export" + case .apn: return "APN" + #if !targetEnvironment(macCatalyst) + case .liveActivity: return "Live Activity" + #endif + case .remote: return "Remote" + case .alarmSettings: return "Alarms" + case .calendar: return "Calendar" + case .contact: return "Contact" + case .advanced: return "Advanced" + case .settings, .aggregatedStats: return "" + } + } + + var icon: String { + switch self { + case .nightscout: return "network" + case .dexcom: return "sensor.tag.radiowaves.forward" + case .general: return "gearshape" + case .graph: return "chart.xyaxis.line" + case .infoDisplay: return "info.circle" + case .units: return "scalemass" + case .tabSettings: return "rectangle.3.group" + case .backgroundRefresh: return "arrow.clockwise" + case .importExport: return "square.and.arrow.down" + case .apn: return "bell.and.waves.left.and.right" + #if !targetEnvironment(macCatalyst) + case .liveActivity: return "dot.radiowaves.left.and.right" + #endif + case .remote: return "antenna.radiowaves.left.and.right" + case .alarmSettings: return "bell.badge" + case .calendar: return "calendar" + case .contact: return "person.circle" + case .advanced: return "exclamationmark.shield" + case .settings, .aggregatedStats: return "" + } + } + + /// Extra synonyms so search finds a page by related terms, not just its title. + var keywords: [String] { + switch self { + case .graph: return ["chart"] + case .units: return ["mmol", "mgdl", "metrics"] + case .infoDisplay: return ["info"] + case .apn: return ["push", "notification"] + #if !targetEnvironment(macCatalyst) + case .liveActivity: return ["dynamic island", "lock screen"] + #endif + case .importExport: return ["import", "export", "backup"] + default: return [] + } + } + + /// The individual settings inside this screen, so Menu search can find a + /// bottom-level setting (e.g. "Graph Basal") and open the screen containing + /// it. Titles mirror the row (or section) titles in each screen's view — + /// keep them in sync when adding or renaming rows. + var leaves: [SettingsLeaf] { + switch self { + case .nightscout: return [ + SettingsLeaf("URL"), + SettingsLeaf("Access Token", ["token"]), + SettingsLeaf("Enable WebSocket", ["websocket", "real-time"]), + ] + case .dexcom: return [ + SettingsLeaf("User Name", ["username"]), + SettingsLeaf("Password"), + SettingsLeaf("Server"), + ] + case .general: return [ + SettingsLeaf("Display App Badge", ["badge"]), + SettingsLeaf("Persistent Notification"), + SettingsLeaf("Appearance", ["dark mode", "light mode", "theme"]), + SettingsLeaf("Display Stats"), + SettingsLeaf("Display Small Graph"), + SettingsLeaf("Color BG Text"), + SettingsLeaf("Keep Screen Active", ["screen lock", "screenlock"]), + SettingsLeaf("Show Display Name"), + SettingsLeaf("Snoozer emoji"), + SettingsLeaf("Force portrait mode", ["orientation", "landscape"]), + SettingsLeaf("Time Zone Override", ["timezone"]), + SettingsLeaf("Speak BG", ["voice", "speech"]), + SettingsLeaf("Send anonymous usage stats", ["telemetry", "diagnostics"]), + ] + case .graph: return [ + SettingsLeaf("Display Dots"), + SettingsLeaf("Display Lines"), + SettingsLeaf("Show DIA Lines", ["dia"]), + SettingsLeaf("Show −30 min Line", ["-30"]), + SettingsLeaf("Show −90 min Line", ["-90"]), + SettingsLeaf("Show Yesterday's BG", ["yesterday"]), + SettingsLeaf("Show Midnight Lines"), + SettingsLeaf("Show Carb/Bolus Values", ["carbs"]), + SettingsLeaf("Show Carb Absorption"), + SettingsLeaf("Treatments on Small Graph"), + SettingsLeaf("Small Graph Height", ["height"]), + SettingsLeaf("Hours of Prediction", ["prediction"]), + SettingsLeaf("Prediction Style"), + SettingsLeaf("Min Basal", ["basal scale"]), + SettingsLeaf("Min BG Scale"), + SettingsLeaf("Show Days Back", ["history", "days back"]), + ] + case .infoDisplay: + return [SettingsLeaf("Hide Information Table")] + + InfoType.allCases.map { SettingsLeaf($0.name) } + case .units: return [ + SettingsLeaf("Glucose Unit"), + SettingsLeaf("Range Mode", ["tir", "titr", "time in range"]), + SettingsLeaf("Glycemic Metrics", ["hba1c", "ehba1c", "gmi"]), + SettingsLeaf("Variability", ["standard deviation", "cv"]), + ] + case .backgroundRefresh: return [ + SettingsLeaf("Background Refresh Type", ["silent tune", "bluetooth", "rileylink", "omnipod", "heartbeat"]), + ] + case .importExport: return [ + SettingsLeaf("Scan QR Code to Import Settings", ["qr"]), + SettingsLeaf("Export Settings To QR Code", ["qr"]), + ] + case .apn: return [ + SettingsLeaf("APNS Key ID", ["apns"]), + SettingsLeaf("APNS Key", ["apns", "p8"]), + ] + #if !targetEnvironment(macCatalyst) + case .liveActivity: return [ + SettingsLeaf("Enable Live Activity"), + SettingsLeaf("Restart Live Activity"), + SettingsLeaf("Grid Slots", ["carplay", "watch"]), + ] + #endif + case .remote: return [ + SettingsLeaf("Loop Remote Control"), + SettingsLeaf("Trio Remote Control", ["trc"]), + SettingsLeaf("Meal with Bolus"), + SettingsLeaf("Meal with Fat/Protein"), + SettingsLeaf("Guardrails", ["max bolus", "max carbs", "max fat", "max protein"]), + SettingsLeaf("Bolus Increment"), + SettingsLeaf("Shared Secret"), + SettingsLeaf("QR Code URL", ["qr"]), + ] + case .alarmSettings: return [ + SettingsLeaf("All Alerts Snoozed", ["snooze all"]), + SettingsLeaf("All Sounds Muted", ["mute all"]), + SettingsLeaf("Day starts", ["schedule", "day/night"]), + SettingsLeaf("Night starts", ["schedule", "day/night"]), + SettingsLeaf("Override System Volume", ["volume"]), + SettingsLeaf("Audio During Calls"), + SettingsLeaf("Ignore Zero BG"), + SettingsLeaf("Auto-Snooze CGM Start", ["autosnooze"]), + SettingsLeaf("Volume Buttons Snooze Alarms"), + ] + case .calendar: return [ + SettingsLeaf("Save BG to Calendar", ["watch", "carplay"]), + SettingsLeaf("Calendar Text"), + ] + case .contact: return [ + SettingsLeaf("Enable Contact BG Updates", ["watch face"]), + SettingsLeaf("Background Color"), + SettingsLeaf("Color Mode"), + SettingsLeaf("Text Color"), + SettingsLeaf("Show Trend"), + SettingsLeaf("Show Delta"), + SettingsLeaf("Show IOB"), + ] + case .advanced: return [ + SettingsLeaf("Download Treatments"), + SettingsLeaf("Download Prediction"), + SettingsLeaf("Graph Basal"), + SettingsLeaf("Graph Bolus"), + SettingsLeaf("Graph Carbs"), + SettingsLeaf("Graph Other Treatments"), + SettingsLeaf("BG Update Delay", ["delay"]), + SettingsLeaf("Debug Log Level", ["logging"]), + ] + case .tabSettings, .settings, .aggregatedStats: return [] + } + } + + /// Ordered, grouped list of the routes shown as rows in the Settings menu. + /// Encodes the conditional visibility in one place so both the list and the + /// Menu search stay in sync. + static func menuSections(nightscoutConfigured: Bool) -> [(SettingsSection, [SettingsRoute])] { + var display: [SettingsRoute] = [.general, .graph] + if nightscoutConfigured { + display.append(.infoDisplay) + } + display += [.units, .tabSettings] + + var app: [SettingsRoute] = [.backgroundRefresh, .importExport, .apn] + #if !targetEnvironment(macCatalyst) + app.append(.liveActivity) + #endif + if nightscoutConfigured { + app.append(.remote) + } + + return [ + (.data, [.nightscout, .dexcom]), + (.display, display), + (.app, app), + (.alarms, [.alarmSettings]), + (.integrations, [.calendar, .contact]), + (.advanced, [.advanced]), + ] + } + @ViewBuilder var destination: some View { switch self { @@ -177,8 +341,6 @@ struct AggregatedStatsViewWrapper: View { // MARK: – UIKit helpers (unchanged) -import UIKit - extension UIApplication { var topMost: UIViewController? { // `keyWindow` is deprecated and returns nil on Mac Catalyst / multi-window iPad. diff --git a/LoopFollow/Settings/UnitsConfigurationView.swift b/LoopFollow/Settings/UnitsConfigurationView.swift index 427c4e5bd..2124077c7 100644 --- a/LoopFollow/Settings/UnitsConfigurationView.swift +++ b/LoopFollow/Settings/UnitsConfigurationView.swift @@ -5,26 +5,69 @@ import SwiftUI /// Reusable view for configuring units and metrics. /// Can be embedded in Forms or used standalone during onboarding. +/// +/// Onboarding splits this into two lighter pages — the glucose unit on its own, +/// and the LoopFollow statistics (range, glycemic, variability) on a second page +/// — by rendering a subset of the sections via `sections`. Settings shows them +/// all together (the default). struct UnitsConfigurationView: View { + /// Which groups of sections to render. Onboarding uses `.units` and + /// `.statistics` on separate pages; everywhere else uses `.all`. + struct Sections: OptionSet { + let rawValue: Int + /// Glucose display unit (mg/dL vs mmol/L). + static let units = Sections(rawValue: 1 << 0) + /// Range mode plus the glycemic and variability metrics. + static let statistics = Sections(rawValue: 1 << 1) + static let all: Sections = [.units, .statistics] + } + + var sections: Sections = .all + @State private var rangeMode = UnitSettingsStore.shared.timeInRangeMode @State private var glucoseUnit = UnitSettingsStore.shared.glucoseUnit @State private var lowValue = Storage.shared.lowLine.value @State private var highValue = Storage.shared.highLine.value + /// Formats a mg/dL threshold pair in the currently selected glucose unit, + /// e.g. "70–180 mg/dL" or "3.9–10.0 mmol/L". + private func rangeBounds(_ lowMgdl: Double, _ highMgdl: Double) -> String { + let factor = glucoseUnit == .mmolL ? GlucoseConversion.mgDlToMmolL : 1.0 + let digits = glucoseUnit.fractionDigits + let low = Localizer.formatToLocalizedString(lowMgdl * factor, maxFractionDigits: digits, minFractionDigits: digits) + let high = Localizer.formatToLocalizedString(highMgdl * factor, maxFractionDigits: digits, minFractionDigits: digits) + return "\(low)–\(high) \(glucoseUnit.rawValue)" + } + var body: some View { Group { - Section("Glucose") { - Picker("Glucose Unit", selection: $glucoseUnit) { - Text("mg/dL").tag(GlucoseDisplayUnit.mgdL) - Text("mmol/L").tag(GlucoseDisplayUnit.mmolL) - } - .pickerStyle(.segmented) - .onChange(of: glucoseUnit) { newValue in - UnitSettingsStore.shared.glucoseUnit = newValue - } + if sections.contains(.units) { + unitsSections + } + if sections.contains(.statistics) { + statisticsSections } + } + } + + @ViewBuilder + private var unitsSections: some View { + Section("Glucose") { + Picker("Glucose Unit", selection: $glucoseUnit) { + Text("mg/dL").tag(GlucoseDisplayUnit.mgdL) + Text("mmol/L").tag(GlucoseDisplayUnit.mmolL) + } + .pickerStyle(.segmented) + .onChange(of: glucoseUnit) { newValue in + UnitSettingsStore.shared.glucoseUnit = newValue + } + } + } - Section("Range") { + @ViewBuilder + private var statisticsSections: some View { + Group { + Section { Picker("Range Mode", selection: $rangeMode) { Text("TIR").tag(TimeInRangeDisplayMode.tir) Text("TITR").tag(TimeInRangeDisplayMode.titr) @@ -58,9 +101,13 @@ struct UnitsConfigurationView: View { Observable.shared.chartSettingsChanged.value = true } } + } header: { + Text("Range") + } footer: { + Text("TIR — Time in Range, the share of readings within \(rangeBounds(70, 180)).\nTITR — Time in Tight Range, within \(rangeBounds(70, 140)).\nCustom — set your own low and high.") } - Section("Glycemic Metrics") { + Section { Picker("Metric", selection: Binding( get: { UnitSettingsStore.shared.glycemicMetricMode }, set: { UnitSettingsStore.shared.glycemicMetricMode = $0 } @@ -78,9 +125,13 @@ struct UnitsConfigurationView: View { Text("mmol/mol").tag(GlycemicOutputUnit.mmolMol) } .pickerStyle(.segmented) + } header: { + Text("Glycemic Metrics") + } footer: { + Text("eHbA1c — an A1c estimate from your average glucose. GMI — Glucose Management Indicator, another A1c estimate from average glucose. % and mmol/mol (IFCC) are two scales for the result.") } - Section("Variability") { + Section { Picker("Metric", selection: Binding( get: { UnitSettingsStore.shared.variabilityMetricMode }, set: { UnitSettingsStore.shared.variabilityMetricMode = $0 } @@ -89,6 +140,10 @@ struct UnitsConfigurationView: View { Text("CV").tag(VariabilityMetricMode.cv) } .pickerStyle(.segmented) + } header: { + Text("Variability") + } footer: { + Text("Std Dev — Standard Deviation, how much glucose swings around the average. CV — Coefficient of Variation, that swing relative to the average (Std Dev ÷ mean).") } } } diff --git a/LoopFollow/Stats/AGP/AGPGraphView.swift b/LoopFollow/Stats/AGP/AGPGraphView.swift index 4d38b138c..11e8dbb04 100644 --- a/LoopFollow/Stats/AGP/AGPGraphView.swift +++ b/LoopFollow/Stats/AGP/AGPGraphView.swift @@ -4,141 +4,138 @@ import Charts import SwiftUI -struct AGPGraphView: UIViewRepresentable { +struct AGPGraphView: View { let agpData: [AGPDataPoint] - func makeCoordinator() -> Coordinator { - Coordinator() + private enum Percentile: String, CaseIterable, Plottable { + case p5 = "5th" + case p25 = "25th" + case median = "Median" + case p75 = "75th" + case p95 = "95th" + + var color: Color { + switch self { + case .p5, .p95: return Color(.systemGray).opacity(0.6) + case .p25, .p75: return Color(.systemBlue).opacity(0.7) + case .median: return Color(.systemBlue) + } + } + + var lineWidth: CGFloat { + self == .median ? 3 : 1.5 + } } - func makeUIView(context _: Context) -> UIView { - let containerView = NonInteractiveContainerView() - containerView.backgroundColor = .systemBackground - - let chartView = LineChartView() - chartView.rightAxis.enabled = true - chartView.leftAxis.enabled = false - chartView.xAxis.labelPosition = .bottom - chartView.rightAxis.drawGridLinesEnabled = false - chartView.leftAxis.drawGridLinesEnabled = false - chartView.xAxis.drawGridLinesEnabled = false - chartView.rightAxis.valueFormatter = ChartYMMOLValueFormatter() - chartView.legend.enabled = false - chartView.chartDescription.enabled = false - chartView.isUserInteractionEnabled = false - - containerView.addSubview(chartView) - chartView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - chartView.topAnchor.constraint(equalTo: containerView.topAnchor), - chartView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - chartView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - chartView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - ]) - - return containerView + private struct Point: Identifiable { + let percentile: Percentile + let hour: Double + let value: Double + var id: String { "\(percentile.rawValue)-\(hour)" } } - class Coordinator {} - - func updateUIView(_ containerView: UIView, context _: Context) { - guard let chartView = containerView.subviews.first as? LineChartView else { return } - guard !agpData.isEmpty else { return } - var p5Entries: [ChartDataEntry] = [] - var p25Entries: [ChartDataEntry] = [] - var p50Entries: [ChartDataEntry] = [] - var p75Entries: [ChartDataEntry] = [] - var p95Entries: [ChartDataEntry] = [] - - for point in agpData { - let x = Double(point.timeOfDay) / 60.0 - p5Entries.append(ChartDataEntry(x: x, y: point.p5)) - p25Entries.append(ChartDataEntry(x: x, y: point.p25)) - p50Entries.append(ChartDataEntry(x: x, y: point.p50)) - p75Entries.append(ChartDataEntry(x: x, y: point.p75)) - p95Entries.append(ChartDataEntry(x: x, y: point.p95)) + private var points: [Point] { + let sortedData = agpData.sorted { $0.timeOfDay < $1.timeOfDay } + return sortedData.flatMap { dp -> [Point] in + let hour = Double(dp.timeOfDay) / 60.0 + return [ + Point(percentile: .p5, hour: hour, value: dp.p5), + Point(percentile: .p25, hour: hour, value: dp.p25), + Point(percentile: .median, hour: hour, value: dp.p50), + Point(percentile: .p75, hour: hour, value: dp.p75), + Point(percentile: .p95, hour: hour, value: dp.p95), + ] } + } + + private var activeThresholds: (low: Double, high: Double) { + UnitSettingsStore.shared.effectiveThresholds() + } + + private var lowThresholdLabel: String { + Localizer.toDisplayUnits(String(activeThresholds.low)) + } - let sortedP5 = p5Entries.sorted { $0.x < $1.x } - let sortedP25 = p25Entries.sorted { $0.x < $1.x } - let sortedP50 = p50Entries.sorted { $0.x < $1.x } - let sortedP75 = p75Entries.sorted { $0.x < $1.x } - let sortedP95 = p95Entries.sorted { $0.x < $1.x } + private var highThresholdLabel: String { + Localizer.toDisplayUnits(String(activeThresholds.high)) + } + + private var plottedMinValue: Double { + min(points.map(\.value).min() ?? activeThresholds.low, activeThresholds.low) + } - guard !sortedP5.isEmpty, !sortedP25.isEmpty, !sortedP50.isEmpty, - !sortedP75.isEmpty, !sortedP95.isEmpty - else { - return + private var plottedMaxValue: Double { + max(points.map(\.value).max() ?? activeThresholds.high, activeThresholds.high) + } + + private var yDomain: ClosedRange { + let span = max(plottedMaxValue - plottedMinValue, 1) + let topPadding = max(span * 0.05, 1) + return plottedMinValue ... (plottedMaxValue + topPadding) + } + + private var yAxisEndpoints: [Double] { + if abs(plottedMaxValue - plottedMinValue) < 0.0001 { + return [plottedMinValue] } - let p5DataSet = LineChartDataSet(entries: sortedP5, label: "5th") - p5DataSet.colors = [NSUIColor.systemGray.withAlphaComponent(0.6)] - p5DataSet.lineWidth = 1.5 - p5DataSet.drawCirclesEnabled = false - p5DataSet.drawValuesEnabled = false - p5DataSet.drawFilledEnabled = false - p5DataSet.mode = .linear - - let p25DataSet = LineChartDataSet(entries: sortedP25, label: "25th") - p25DataSet.colors = [NSUIColor.systemBlue.withAlphaComponent(0.7)] - p25DataSet.lineWidth = 1.5 - p25DataSet.drawCirclesEnabled = false - p25DataSet.drawValuesEnabled = false - p25DataSet.drawFilledEnabled = false - p25DataSet.mode = .linear - - let p50DataSet = LineChartDataSet(entries: sortedP50, label: "Median") - p50DataSet.colors = [NSUIColor.systemBlue] - p50DataSet.lineWidth = 3 - p50DataSet.drawCirclesEnabled = false - p50DataSet.drawValuesEnabled = false - p50DataSet.drawFilledEnabled = false - p50DataSet.mode = .linear - - let p75DataSet = LineChartDataSet(entries: sortedP75, label: "75th") - p75DataSet.colors = [NSUIColor.systemBlue.withAlphaComponent(0.7)] - p75DataSet.lineWidth = 1.5 - p75DataSet.drawCirclesEnabled = false - p75DataSet.drawValuesEnabled = false - p75DataSet.drawFilledEnabled = false - p75DataSet.mode = .linear - - let p95DataSet = LineChartDataSet(entries: sortedP95, label: "95th") - p95DataSet.colors = [NSUIColor.systemGray.withAlphaComponent(0.6)] - p95DataSet.lineWidth = 1.5 - p95DataSet.drawCirclesEnabled = false - p95DataSet.drawValuesEnabled = false - p95DataSet.drawFilledEnabled = false - p95DataSet.mode = .linear - let maxY = max(sortedP95.map { $0.y }.max() ?? 300, 300) + 10 - let hourMinY = min(sortedP5.map { $0.y }.min() ?? 0, 0) - 10 - - var hourLines: [ChartDataEntry] = [] - for hour in 0 ... 24 { - let x = Double(hour) - hourLines.append(ChartDataEntry(x: x, y: hourMinY)) - hourLines.append(ChartDataEntry(x: x, y: maxY)) - if hour < 24 { - hourLines.append(ChartDataEntry(x: x + 0.0001, y: hourMinY)) + return [plottedMinValue, plottedMaxValue] + } + + var body: some View { + Chart { + RuleMark(y: .value("lowBoundary", activeThresholds.low)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 4])) + .annotation(position: .leading, alignment: .leading) { + Text(lowThresholdLabel) + .font(.caption2) + } + + RuleMark(y: .value("highBoundary", activeThresholds.high)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 4])) + .annotation(position: .leading, alignment: .leading) { + Text(highThresholdLabel) + .font(.caption2) + } + + ForEach(points) { point in + LineMark( + x: .value("hour", point.hour), + y: .value("bg", point.value) + ) + .foregroundStyle(by: .value("percentile", point.percentile)) + .lineStyle(StrokeStyle(lineWidth: point.percentile.lineWidth)) + .interpolationMethod(.linear) } } - - let hourLinesDataSet = LineChartDataSet(entries: hourLines, label: "Hours") - hourLinesDataSet.colors = [NSUIColor.label.withAlphaComponent(0.3)] - hourLinesDataSet.lineWidth = 1 - hourLinesDataSet.drawCirclesEnabled = false - hourLinesDataSet.drawValuesEnabled = false - hourLinesDataSet.drawFilledEnabled = false - - let data = LineChartData() - data.append(p5DataSet) - data.append(p25DataSet) - data.append(p50DataSet) - data.append(p75DataSet) - data.append(p95DataSet) - data.append(hourLinesDataSet) - - chartView.data = data - chartView.notifyDataSetChanged() - chartView.setNeedsDisplay() + .chartForegroundStyleScale( + domain: Percentile.allCases, + range: Percentile.allCases.map(\.color) + ) + .chartLegend(.hidden) + .chartXScale(domain: 0 ... 24) + .chartYScale(domain: yDomain) + .chartXAxis { + AxisMarks(position: .bottom, values: Array(stride(from: 0, through: 24, by: 3))) { value in + AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5)) + .foregroundStyle(Color(.label).opacity(0.3)) + AxisTick() + AxisValueLabel { + if let hour = value.as(Int.self) { + Text("\(hour)") + } + } + } + } + .chartYAxis { + AxisMarks(position: .leading, values: yAxisEndpoints) { value in + AxisValueLabel { + if let raw = value.as(Double.self) { + Text(Localizer.toDisplayUnits(String(raw))) + } + } + } + } + .allowsHitTesting(false) + .background(Color(.systemBackground)) } } diff --git a/LoopFollow/Stats/AGP/AGPView.swift b/LoopFollow/Stats/AGP/AGPView.swift index 6179b7b39..9d0cfa6d2 100644 --- a/LoopFollow/Stats/AGP/AGPView.swift +++ b/LoopFollow/Stats/AGP/AGPView.swift @@ -15,8 +15,10 @@ struct AGPView: View { AGPGraphView(agpData: viewModel.agpData) .frame(height: 200) + .padding(.top, 10) + .padding(.horizontal, 8) + .padding(.bottom, 4) .allowsHitTesting(false) - .clipped() // Legend HStack(spacing: 16) { diff --git a/LoopFollow/Stats/AggregatedStatsView.swift b/LoopFollow/Stats/AggregatedStatsView.swift index 343831003..0e9ad3a4d 100644 --- a/LoopFollow/Stats/AggregatedStatsView.swift +++ b/LoopFollow/Stats/AggregatedStatsView.swift @@ -2,7 +2,6 @@ // AggregatedStatsView.swift import SwiftUI -import UIKit struct AggregatedStatsView: View { @ObservedObject var viewModel: AggregatedStatsViewModel diff --git a/LoopFollow/Stats/ChartContainerView.swift b/LoopFollow/Stats/ChartContainerView.swift deleted file mode 100644 index e201e954e..000000000 --- a/LoopFollow/Stats/ChartContainerView.swift +++ /dev/null @@ -1,25 +0,0 @@ -// LoopFollow -// ChartContainerView.swift - -import Charts -import UIKit - -class NonInteractiveContainerView: UIView { - override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setup() - } - - private func setup() { - isUserInteractionEnabled = false - } - - override func hitTest(_: CGPoint, with _: UIEvent?) -> UIView? { - return nil - } -} diff --git a/LoopFollow/Stats/GRI/GRIRiskGridView.swift b/LoopFollow/Stats/GRI/GRIRiskGridView.swift index 8ff7e763b..814942cb9 100644 --- a/LoopFollow/Stats/GRI/GRIRiskGridView.swift +++ b/LoopFollow/Stats/GRI/GRIRiskGridView.swift @@ -4,138 +4,108 @@ import Charts import SwiftUI -struct GRIRiskGridView: UIViewRepresentable { +struct GRIRiskGridView: View { let hypoComponent: Double let hyperComponent: Double let gri: Double - func makeCoordinator() -> Coordinator { - Coordinator() + private enum Zone: String, CaseIterable, Plottable { + case a = "A" + case b = "B" + case c = "C" + case d = "D" + case e = "E" + + var color: Color { + switch self { + case .a: return .green.opacity(0.3) + case .b: return .yellow.opacity(0.3) + case .c: return .orange.opacity(0.3) + case .d: return .red.opacity(0.3) + case .e: return .red.opacity(0.5) + } + } } - func makeUIView(context _: Context) -> UIView { - let containerView = NonInteractiveContainerView() - containerView.backgroundColor = .systemBackground - - let chartView = ScatterChartView() - chartView.backgroundColor = .systemBackground - chartView.rightAxis.enabled = false - chartView.leftAxis.enabled = true - chartView.legend.enabled = false - chartView.chartDescription.enabled = false - chartView.leftAxis.axisMinimum = 0 - chartView.leftAxis.axisMaximum = 60 - chartView.leftAxis.forceLabelsEnabled = true - chartView.leftAxis.labelPosition = .outsideChart - chartView.rightAxis.enabled = false - chartView.xAxis.labelPosition = .bottom - chartView.xAxis.axisMinimum = 0 - chartView.xAxis.axisMaximum = 30 - chartView.isUserInteractionEnabled = false - - containerView.addSubview(chartView) - chartView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - chartView.topAnchor.constraint(equalTo: containerView.topAnchor), - chartView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - chartView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - chartView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - ]) - - return containerView + private struct GridPoint: Identifiable { + let hypo: Double + let hyper: Double + let zone: Zone + var id: String { "\(hypo)-\(hyper)" } } - class Coordinator {} - - func updateUIView(_ containerView: UIView, context _: Context) { - guard let chartView = containerView.subviews.first as? ScatterChartView else { return } - - chartView.data = nil - - let xHypoValue = hypoComponent - let yHyperValue = hyperComponent - - var zoneAEntries: [ChartDataEntry] = [] - var zoneBEntries: [ChartDataEntry] = [] - var zoneCEntries: [ChartDataEntry] = [] - var zoneDEntries: [ChartDataEntry] = [] - var zoneEEntries: [ChartDataEntry] = [] - + private var gridPoints: [GridPoint] { + var points: [GridPoint] = [] let step = 0.5 for hypo in stride(from: 0.0, through: 30.0, by: step) { for hyper in stride(from: 0.0, through: 60.0, by: step) { let griValue = (3.0 * hypo) + (1.6 * hyper) - guard griValue <= 100 else { continue } - let entry = ChartDataEntry(x: hypo, y: hyper) - + let zone: Zone if griValue <= 20 { - zoneAEntries.append(entry) + zone = .a } else if griValue <= 40 { - zoneBEntries.append(entry) + zone = .b } else if griValue <= 60 { - zoneCEntries.append(entry) + zone = .c } else if griValue <= 80 { - zoneDEntries.append(entry) + zone = .d } else { - zoneEEntries.append(entry) + zone = .e } + points.append(GridPoint(hypo: hypo, hyper: hyper, zone: zone)) } } + return points + } - let zoneADataSet = ScatterChartDataSet(entries: zoneAEntries, label: "Zone A") - zoneADataSet.setColor(NSUIColor.systemGreen.withAlphaComponent(0.3)) - zoneADataSet.scatterShapeSize = 4 - zoneADataSet.drawValuesEnabled = false - - let zoneBDataSet = ScatterChartDataSet(entries: zoneBEntries, label: "Zone B") - zoneBDataSet.setColor(NSUIColor.systemYellow.withAlphaComponent(0.3)) - zoneBDataSet.scatterShapeSize = 4 - zoneBDataSet.drawValuesEnabled = false - - let zoneCDataSet = ScatterChartDataSet(entries: zoneCEntries, label: "Zone C") - zoneCDataSet.setColor(NSUIColor.systemOrange.withAlphaComponent(0.3)) - zoneCDataSet.scatterShapeSize = 4 - zoneCDataSet.drawValuesEnabled = false - - let zoneDDataSet = ScatterChartDataSet(entries: zoneDEntries, label: "Zone D") - zoneDDataSet.setColor(NSUIColor.systemRed.withAlphaComponent(0.3)) - zoneDDataSet.scatterShapeSize = 4 - zoneDDataSet.drawValuesEnabled = false - - let zoneEDataSet = ScatterChartDataSet(entries: zoneEEntries, label: "Zone E") - zoneEDataSet.setColor(NSUIColor.systemRed.withAlphaComponent(0.5)) - zoneEDataSet.scatterShapeSize = 4 - zoneEDataSet.drawValuesEnabled = false - - let currentPoint = ChartDataEntry(x: xHypoValue, y: yHyperValue) - let currentDataSet = ScatterChartDataSet(entries: [currentPoint], label: "Current GRI") - currentDataSet.setColor(NSUIColor.label) - currentDataSet.scatterShapeSize = 12 - currentDataSet.setScatterShape(.circle) - currentDataSet.drawValuesEnabled = false - - let data = ScatterChartData() - data.append(zoneADataSet) - data.append(zoneBDataSet) - data.append(zoneCDataSet) - data.append(zoneDDataSet) - data.append(zoneEDataSet) - data.append(currentDataSet) - - chartView.data = data + var body: some View { + Chart { + ForEach(gridPoints) { point in + PointMark( + x: .value("hypo", point.hypo), + y: .value("hyper", point.hyper) + ) + .symbolSize(16) + .foregroundStyle(by: .value("zone", point.zone)) + } - chartView.xAxis.valueFormatter = DefaultAxisValueFormatter { value, _ in - String(format: "%.0f", value) + PointMark( + x: .value("hypo", hypoComponent), + y: .value("hyper", hyperComponent) + ) + .symbolSize(120) + .foregroundStyle(Color(.label)) } - chartView.leftAxis.valueFormatter = DefaultAxisValueFormatter { value, _ in - String(format: "%.0f", value) + .chartForegroundStyleScale( + domain: Zone.allCases, + range: Zone.allCases.map(\.color) + ) + .chartLegend(.hidden) + .chartXScale(domain: 0 ... 30) + .chartYScale(domain: 0 ... 61) + .chartXAxis { + AxisMarks(position: .bottom) { value in + AxisTick() + AxisValueLabel { + if let v = value.as(Double.self) { + Text(String(format: "%.0f", v)) + } + } + } } - - chartView.xAxis.labelTextColor = .label - chartView.leftAxis.labelTextColor = .label - - chartView.notifyDataSetChanged() + .chartYAxis { + AxisMarks(position: .leading) { value in + AxisTick() + AxisValueLabel { + if let v = value.as(Double.self) { + Text(String(format: "%.0f", v)) + } + } + } + } + .allowsHitTesting(false) + .background(Color(.systemBackground)) } } diff --git a/LoopFollow/Stats/GRI/GRIView.swift b/LoopFollow/Stats/GRI/GRIView.swift index cd6889008..4480b5df5 100644 --- a/LoopFollow/Stats/GRI/GRIView.swift +++ b/LoopFollow/Stats/GRI/GRIView.swift @@ -43,9 +43,11 @@ struct GRIView: View { gri: viewModel.gri ?? 0 ) .frame(height: 250) + .padding(.top, 10) .padding(.leading, yAxisLabelInset) + .padding(.trailing, 8) + .padding(.bottom, 4) .allowsHitTesting(false) - .clipped() Text("Hyperglycemia Component (%)") .font(.caption2) diff --git a/LoopFollow/Stats/TIR/TIRGraphView.swift b/LoopFollow/Stats/TIR/TIRGraphView.swift index 5a509c44b..03a84a14b 100644 --- a/LoopFollow/Stats/TIR/TIRGraphView.swift +++ b/LoopFollow/Stats/TIR/TIRGraphView.swift @@ -3,99 +3,76 @@ import Charts import SwiftUI -import UIKit -struct TIRGraphView: UIViewRepresentable { +struct TIRGraphView: View { let tirData: [TIRDataPoint] - func makeCoordinator() -> Coordinator { - Coordinator() + private enum Band: String, CaseIterable, Plottable { + case veryLow = "Very Low" + case low = "Low" + case inRange = "In Range" + case high = "High" + case veryHigh = "Very High" + + var color: Color { + switch self { + case .veryLow: return .red.opacity(0.8) + case .low: return .red.opacity(0.5) + case .inRange: return .green.opacity(0.7) + case .high: return .yellow.opacity(0.7) + case .veryHigh: return .orange.opacity(0.7) + } + } } - func makeUIView(context _: Context) -> UIView { - let containerView = NonInteractiveContainerView() - containerView.backgroundColor = .systemBackground - - let chartView = BarChartView() - chartView.backgroundColor = .systemBackground - chartView.rightAxis.enabled = false - chartView.leftAxis.enabled = true - chartView.xAxis.labelPosition = .bottom - chartView.xAxis.granularity = 1.0 - chartView.leftAxis.axisMinimum = 0.0 - chartView.leftAxis.axisMaximum = 100.0 - chartView.leftAxis.valueFormatter = PercentageAxisValueFormatter() - chartView.leftAxis.labelCount = 5 - chartView.rightAxis.drawGridLinesEnabled = false - chartView.leftAxis.drawGridLinesEnabled = true - chartView.leftAxis.gridLineDashLengths = [5, 5] - chartView.xAxis.drawGridLinesEnabled = false - chartView.legend.enabled = false - chartView.chartDescription.enabled = false - chartView.isUserInteractionEnabled = false - - containerView.addSubview(chartView) - chartView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - chartView.topAnchor.constraint(equalTo: containerView.topAnchor), - chartView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - chartView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - chartView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - ]) - - return containerView + private struct Slice: Identifiable { + let period: TIRPeriod + let band: Band + let value: Double + var id: String { "\(period.rawValue)-\(band.rawValue)" } } - class Coordinator {} - - func updateUIView(_ containerView: UIView, context _: Context) { - guard let chartView = containerView.subviews.first as? BarChartView else { return } - guard !tirData.isEmpty else { return } - - var dataEntries: [BarChartDataEntry] = [] - var xAxisLabels: [String] = [] - - for (index, point) in tirData.enumerated() { - let entry = BarChartDataEntry( - x: Double(index), - yValues: [ - point.veryLow, - point.low, - point.inRange, - point.high, - point.veryHigh, - ] - ) - dataEntries.append(entry) - xAxisLabels.append(point.period.rawValue) + private var slices: [Slice] { + tirData.flatMap { point in + [ + Slice(period: point.period, band: .veryLow, value: point.veryLow), + Slice(period: point.period, band: .low, value: point.low), + Slice(period: point.period, band: .inRange, value: point.inRange), + Slice(period: point.period, band: .high, value: point.high), + Slice(period: point.period, band: .veryHigh, value: point.veryHigh), + ] } - - let dataSet = BarChartDataSet(entries: dataEntries, label: "Time in Range") - dataSet.colors = [ - UIColor.systemRed.withAlphaComponent(0.8), - UIColor.systemRed.withAlphaComponent(0.5), - UIColor.systemGreen.withAlphaComponent(0.7), - UIColor.systemYellow.withAlphaComponent(0.7), - UIColor.systemOrange.withAlphaComponent(0.7), - ] - dataSet.stackLabels = ["Very Low", "Low", "In Range", "High", "Very High"] - dataSet.drawValuesEnabled = false - - let data = BarChartData(dataSet: dataSet) - data.barWidth = 0.6 - - chartView.data = data - - chartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: xAxisLabels) - chartView.xAxis.labelRotationAngle = 0 - chartView.xAxis.labelCount = xAxisLabels.count - - chartView.notifyDataSetChanged() } -} -class PercentageAxisValueFormatter: AxisValueFormatter { - func stringForValue(_ value: Double, axis _: AxisBase?) -> String { - return String(format: "%.0f%%", value) + var body: some View { + Chart(slices) { slice in + BarMark( + x: .value("period", slice.period.rawValue), + y: .value("pct", slice.value), + width: .ratio(0.6) + ) + .foregroundStyle(by: .value("band", slice.band)) + } + .chartForegroundStyleScale(domain: Band.allCases, range: Band.allCases.map(\.color)) + .chartLegend(.hidden) + .chartYScale(domain: 0 ... 102) + .chartYAxis { + AxisMarks(position: .leading, values: [0, 25, 50, 75, 100]) { value in + AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [5, 5])) + AxisTick() + AxisValueLabel { + if let pct = value.as(Int.self) { + Text("\(pct)%") + } + } + } + } + .chartXAxis { + AxisMarks(position: .bottom) { _ in + AxisValueLabel() + } + } + .allowsHitTesting(false) + .background(Color(.systemBackground)) } } diff --git a/LoopFollow/Stats/TIR/TIRView.swift b/LoopFollow/Stats/TIR/TIRView.swift index fade4cc2e..8ff66e273 100644 --- a/LoopFollow/Stats/TIR/TIRView.swift +++ b/LoopFollow/Stats/TIR/TIRView.swift @@ -27,8 +27,10 @@ struct TIRView: View { if !viewModel.tirData.isEmpty { TIRGraphView(tirData: viewModel.tirData) .frame(height: 250) + .padding(.top, 10) + .padding(.horizontal, 8) + .padding(.bottom, 4) .allowsHitTesting(false) - .clipped() VStack(alignment: .leading, spacing: 8) { if let average = viewModel.tirData.first(where: { $0.period == .average }) { diff --git a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift deleted file mode 100644 index bc2c1a98d..000000000 --- a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift +++ /dev/null @@ -1,111 +0,0 @@ -// LoopFollow -// ObservableUserDefaultsValue.swift - -import Combine -import Foundation - -class ObservableUserDefaultsValue: ObservableObject, UserDefaultsAnyValue { - // user defaults key (UserDefaultsAnyValue protocol implementation) - let key: String - typealias ValueType = T - - // The value (strong typed) - @Published var value: T { - didSet { - // Continue only if the new value is different from the old value - guard value != oldValue else { return } - - if let validation = validation { - guard let validatedValue = validation(value) else { - DispatchQueue.main.async { - self.value = oldValue - } - return - } - value = validatedValue - } - - // Store value to user defaults - ObservableUserDefaultsValue.defaults.setValue(value.toAny(), forKey: key) - - // Execute custom closure - DispatchQueue.main.async { - self.onChange?(self.value) - - // Notify observers - self.observers.values.forEach { $0(self.value) } - - // Notify UserDefaultsValueGroups that value has changed - UserDefaultsValueGroups.valueChanged(self) - } - } - } - - /// Get/set the value from Any value (UserDefaultsAnyValue protocol implementation) - var anyValue: Any? { - get { - return value.toAny() - } - set { - guard let newValue = T.fromAny(newValue) as T? else { - return - } - DispatchQueue.main.async { - self.value = newValue - } - } - } - - /// Is there this key already stored in UserDefaults? - var exists: Bool { - return ObservableUserDefaultsValue.defaults.object(forKey: key) != nil - } - - // On change closure - private let onChange: ((T) -> Void)? - - // Validate & transform closure : given the new value, validate it; if validation passes, return the new value; - // if validation fails, transform the value, returning a modified version or return nil and the change will not happen - private let validation: ((T) -> T?)? - - // Value change observers - private var observers: [UUID: (T) -> Void] = [:] - - // User defaults used for persistence - private class var defaults: UserDefaults { - return UserDefaults(suiteName: AppConstants.APP_GROUP_ID)! - } - - init(key: String, default defaultValue: T, onChange: ((T) -> Void)? = nil, validation: ((T) -> T?)? = nil) { - self.key = key - self.onChange = onChange - self.validation = validation - - if let anyValue = ObservableUserDefaultsValue.defaults.object(forKey: key), let value = T.fromAny(anyValue) as T? { - self.value = validation?(value) ?? value - } else { - value = defaultValue - } - } - - /// Insert this value in a group, useful for observing changes in the whole group, instead of particular values - func group(_ groupName: String) -> Self { - UserDefaultsValueGroups.add(self, to: groupName) - return self - } - - /// Register observers, will be notified when value changes - @discardableResult - func observeChanges(using closure: @escaping (T) -> Void) -> ObservationToken { - let id = UUID() - observers[id] = closure - - return ObservationToken { [weak self] in - self?.observers.removeValue(forKey: id) - } - } - - func setNil(key: String) { - ObservableUserDefaultsValue.defaults.removeObject(forKey: key) - } -} diff --git a/LoopFollow/Storage/Framework/SecureStorageValue.swift b/LoopFollow/Storage/Framework/SecureStorageValue.swift index 5ca4b7328..b76e7cc04 100644 --- a/LoopFollow/Storage/Framework/SecureStorageValue.swift +++ b/LoopFollow/Storage/Framework/SecureStorageValue.swift @@ -4,12 +4,17 @@ import Combine import Foundation -class SecureStorageValue: ObservableObject { +class SecureStorageValue: ObservableObject, BFUReloadable { let key: String @Published var value: T { didSet { guard value != oldValue else { return } + // Memory-only during a suspect BFU window; see StorageValue's guard. + guard !StorageReadiness.isSuppressingWrites else { + StorageReadiness.noteSuppressedWrite(key: key) + return + } if let data = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true) { SecureStorageValue.defaults.set(data, forKey: key) } @@ -33,9 +38,22 @@ class SecureStorageValue: ObservableOb } else { value = defaultValue } + + StorageReadiness.register(self) } func remove() { SecureStorageValue.defaults.removeObject(forKey: key) } + + /// Re-reads from UserDefaults during BFU recovery. Same class-C backing as + /// StorageValue, so it recovers identically. + func reload() { + if let data = SecureStorageValue.defaults.data(forKey: key), + let decodedValue = try? NSKeyedUnarchiver.unarchivedObject(ofClass: T.self, from: data), + decodedValue != value + { + value = decodedValue + } + } } diff --git a/LoopFollow/Storage/Framework/StorageReadiness.swift b/LoopFollow/Storage/Framework/StorageReadiness.swift new file mode 100644 index 000000000..fc3bf2c4d --- /dev/null +++ b/LoopFollow/Storage/Framework/StorageReadiness.swift @@ -0,0 +1,124 @@ +// LoopFollow +// StorageReadiness.swift + +import Combine +import Foundation + +/// A cached value that can re-read itself from disk after a BFU launch. +protocol BFUReloadable: AnyObject { + func reload() +} + +/// Before-First-Unlock (BFU) storage recovery + the app-wide readiness gate. +/// +/// On a launch before the device's first unlock since boot, `UserDefaults` is +/// encrypted, so every `StorageValue`/`SecureStorageValue` initialises to its +/// default. This distinguishes true BFU from an ordinary locked launch (where the +/// store reads fine), gates the UI until storage is trustworthy, hydrates every +/// registered value once readable, and suppresses writes meanwhile so poisoned +/// defaults can't be flushed over real settings on unlock. +/// +/// All mutators run on the main thread (launch + the protected-data/foreground +/// notifications); `register(_:)` runs during `Storage` init. So no locking. +enum StorageReadiness { + /// Published gate; `false` until storage is known-good. The root shows a + /// storage-free loading view until `true`. + static let ready = ObservableValue(default: false) + + /// True for the duration of a suspected true-BFU launch, until hydration runs. + private(set) static var suspectBFU = false + + /// While true, storage values keep writes in memory only (hydration reconciles + /// from disk). Off-main reads see a benign eventually-consistent Bool snapshot. + static var isSuppressingWrites: Bool { suspectBFU && !ready.value } + + /// Debug surface for any writer active during the suspect window — such a write + /// is memory-only and lost if it was needed on disk. None exist today. + static func noteSuppressedWrite(key: String) { + #if DEBUG + LogManager.shared.log(category: .general, message: "Storage write to '\(key)' suppressed during BFU window") + #endif + } + + // MARK: - Sentinel + + private static let markerKey = "storageReadinessMarker" + + /// Present once storage has been confirmed readable. Absent in true BFU (store + /// encrypted) — presence, not a value, so it can't drift. + static var markerExists: Bool { + UserDefaults.standard.object(forKey: markerKey) != nil + } + + // MARK: - Registry + + private struct WeakReloadable { + weak var ref: BFUReloadable? + } + + private static var registry: [WeakReloadable] = [] + + /// Called by every storage value at init, so hydration covers all by construction. + static func register(_ reloadable: BFUReloadable) { + registry.append(WeakReloadable(ref: reloadable)) + } + + // MARK: - Deferred work + + private static var pending: [() -> Void] = [] + + /// Run `block` now if ready, else once ready. For launch work that reads real + /// config or mutates persisted history (telemetry, network). + static func whenReady(_ block: @escaping () -> Void) { + if ready.value { + block() + } else { + pending.append(block) + } + } + + // MARK: - Lifecycle + + /// Call once at launch. A non-suspect launch is marked ready synchronously (the + /// common path); a suspect launch stays gated until `recover()`. + static func configure(suspectBFU: Bool) { + self.suspectBFU = suspectBFU + if !suspectBFU { + markReady() + } + } + + /// Hydrate every registered value and open the gate. Idempotent; returns `true` + /// only on the transition that did the work. + @discardableResult + static func recover() -> Bool { + guard suspectBFU, !ready.value else { return false } + registry.removeAll { $0.ref == nil } // drop dead weak slots + for entry in registry { + entry.ref?.reload() + } + markReady() + return true + } + + private static func markReady() { + guard !ready.value else { return } + + // Clear suspect BEFORE publishing: @Published emits in willSet, so a + // ready.$value subscriber (BLEManager reconnect) runs while ready is still + // false — clearing first lets its persists land. + suspectBFU = false + + // Store is readable here (proven by the probes, or by protected data + // becoming available), and class-C UserDefaults is writable while readable. + if !markerExists { + UserDefaults.standard.set(true, forKey: markerKey) + } + + ready.value = true + + let blocks = pending + pending.removeAll() + blocks.forEach { $0() } + } +} diff --git a/LoopFollow/Storage/Framework/StorageValue.swift b/LoopFollow/Storage/Framework/StorageValue.swift index 86faa34da..fc9de99b1 100644 --- a/LoopFollow/Storage/Framework/StorageValue.swift +++ b/LoopFollow/Storage/Framework/StorageValue.swift @@ -4,13 +4,20 @@ import Combine import Foundation -class StorageValue: ObservableObject { +class StorageValue: ObservableObject, BFUReloadable { let key: String @Published var value: T { didSet { guard value != oldValue else { return } + // Suspect BFU: keep in memory only — a poisoned-default write could be + // flushed over real data on unlock. Hydration reconciles from disk. + guard !StorageReadiness.isSuppressingWrites else { + StorageReadiness.noteSuppressedWrite(key: key) + return + } + if let encodedData = try? JSONEncoder().encode(value) { StorageValue.defaults.set(encodedData, forKey: key) } @@ -35,6 +42,8 @@ class StorageValue: ObservableObject { } else { value = defaultValue } + + StorageReadiness.register(self) } func remove() { diff --git a/LoopFollow/Storage/Framework/UserData.xcdatamodeld/UserData.xcdatamodel/contents b/LoopFollow/Storage/Framework/UserData.xcdatamodeld/UserData.xcdatamodel/contents deleted file mode 100644 index 7dcee9cb7..000000000 --- a/LoopFollow/Storage/Framework/UserData.xcdatamodeld/UserData.xcdatamodel/contents +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/LoopFollow/Storage/Framework/UserDefaultsValue.swift b/LoopFollow/Storage/Framework/UserDefaultsValue.swift deleted file mode 100644 index 797ae94f1..000000000 --- a/LoopFollow/Storage/Framework/UserDefaultsValue.swift +++ /dev/null @@ -1,121 +0,0 @@ -// LoopFollow -// UserDefaultsValue.swift - -import Foundation - -/// A type that presents a key and a mutable value (type erased key-value) -protocol UserDefaultsAnyValue { - var key: String { get } - var anyValue: Any? { get set } -} - -/// A value holder that keeps its internal data (the value) synchronized with its UserDefaults value. The value is read from UserDefaults on initialization (if exists, otherwise a default value is used) and written when the value changes. Also a custom validation & onChange closure can be set on initialization. -/// -/// Another feature of this class is that when the value changes, the change can be observed by multiple observers. There are two observation levels: the instance level, when the observers register directly to the UserDefaultsValue instance, and group level, if the UserDefaultsValue instance is embeded in a group (UserDefaultsValueGroups class manages the groups). It is very convenient to declare related values in the same group, so when one of them changes, the group observers are notified that a change occured in that group (no need to observe each particular UserDefaultsValue instance). -class UserDefaultsValue: UserDefaultsAnyValue { - // user defaults key (UserDefaultsAnyValue protocol implementation) - let key: String - - typealias ValueType = T - - // the value (strong typed) - var value: T { - didSet { - // continue only if the new value is different than old value - guard value != oldValue else { - return - } - - if let validation = validation { - guard let validatedValue = validation(value) else { - value = oldValue - return - } - - value = validatedValue - } - - // store value to user defaults - UserDefaultsValue.defaults.setValue(value.toAny(), forKey: key) - - // execute custom closure - onChange?(value) - - // notify observers - observers.values.forEach { $0(value) } - - // notify UserDefaultsValueGroups that value has changed - UserDefaultsValueGroups.valueChanged(self) - } - } - - /// get/set the value from Any value (UserDefaultsAnyValue protocol implementation) - var anyValue: Any? { - get { - return value.toAny() - } - - set { - guard let newValue = T.fromAny(newValue) as T? else { - return - } - - value = newValue - } - } - - /// is there this key already stored in UserDefaults? - var exists: Bool { - return UserDefaultsValue.defaults.object(forKey: key) != nil - } - - // on change closure - private let onChange: ((T) -> Void)? - - // validate & transform closure : giving the new value, validate it; if validations passes, return the new value; if fails, transform the value, returning a modified version or ... return nil and the change will not gonna happen - private let validation: ((T) -> T?)? - - // value change observers - private var observers: [UUID: (T) -> Void] = [:] - - // user defaults used for persistence - private class var defaults: UserDefaults { - return UserDefaults(suiteName: AppConstants.APP_GROUP_ID)! - } - - init(key: String, default defaultValue: T, onChange: ((T) -> Void)? = nil, validation: ((T) -> T?)? = nil) { - self.key = key - if let anyValue = UserDefaultsValue.defaults.object(forKey: key), let value = T.fromAny(anyValue) as T? { - if let validation = validation { - self.value = validation(value) ?? defaultValue - } else { - self.value = value - } - } else { - value = defaultValue - } - self.onChange = onChange - self.validation = validation - } - - /// Insert this value in a group, useful for observing changes in the whole group, instead of particular values - func group(_ groupName: String) -> Self { - UserDefaultsValueGroups.add(self, to: groupName) - return self - } - - /// register observers, will be notified when value changes - @discardableResult - func observeChanges(using closure: @escaping (T) -> Void) -> ObservationToken { - let id = UUID() - observers[id] = closure - - return ObservationToken { [weak self] in - self?.observers.removeValue(forKey: id) - } - } - - func setNil(key: String) { - UserDefaultsValue.defaults.removeObject(forKey: key) - } -} diff --git a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift deleted file mode 100644 index cd03a53cb..000000000 --- a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift +++ /dev/null @@ -1,66 +0,0 @@ -// LoopFollow -// UserDefaultsValueGroups.swift - -import Foundation - -/// UserDefaultValue groups manager class, providing change observation capabilities (keep clients informed when a change occured in a given group). -class UserDefaultsValueGroups { - class func values(from groupName: String) -> [UserDefaultsAnyValue]? { - return groupNameToValues[groupName] - } - - class func add(_ value: UserDefaultsAnyValue, to groupName: String) { - // add to "value-key to groupNames" dictionary - var groupNames = valueKeyToGroupNames[value.key] ?? [] - guard !groupNames.contains(groupName) else { - // already added value to this group! - return - } - - groupNames.append(groupName) - valueKeyToGroupNames[value.key] = groupNames - - // add to "groupName to value" dictionary - var values = groupNameToValues[groupName] ?? [] - values.append(value) - groupNameToValues[groupName] = values - } - - @discardableResult - class func observeChanges(in groupName: String, using closure: @escaping (UserDefaultsAnyValue, String) -> Void) -> ObservationToken { - let id = UUID() - - var observers = groupNameToObservers[groupName] ?? [:] - observers[id] = closure - groupNameToObservers[groupName] = observers - - return ObservationToken { - groupNameToObservers[groupName]?.removeValue(forKey: id) - } - } - - // called by UserDefaultsValue instances when value changes - class func valueChanged(_ value: UserDefaultsAnyValue) { - valueKeyToGroupNames[value.key]?.forEach { groupName in - notifyValueChanged(value, in: groupName) - } - } - - private class func notifyValueChanged(_ value: UserDefaultsAnyValue, in groupName: String) { - groupNameToObservers[groupName]?.values.forEach { closure in - closure(value, groupName) - } - } - - private static var groupNameToValues: [String: [UserDefaultsAnyValue]] = [:] - private static var valueKeyToGroupNames: [String: [String]] = [:] - private static var groupNameToObservers: [String: [UUID: (UserDefaultsAnyValue, String) -> Void]] = [:] -} - -// user default values group definitions -extension UserDefaultsValueGroups { - enum GroupNames { - static let watchSync = "watchSync" - static let alarm = "alarm" - } -} diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 4128918db..07877f8a8 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -15,6 +15,11 @@ class Observable { var tempTarget = ObservableValue(default: nil) var override = ObservableValue(default: nil) + /// End of the active override as Unix epoch seconds; nil = no override or indefinite. + var overrideEndAt = ObservableValue(default: nil) + /// End of the active temp target as Unix epoch seconds; nil = no active temp target. + var tempTargetEndAt = ObservableValue(default: nil) + var minAgoText = ObservableValue(default: "?? min ago") var bgText = ObservableValue(default: "BG") var bg = ObservableValue(default: nil) @@ -41,7 +46,9 @@ class Observable { var previousAlertLastLoopTime = ObservableValue(default: nil) var deviceRecBolus = ObservableValue(default: nil) var deviceBatteryLevel = ObservableValue(default: nil) + var deviceBatteryIsCharging = ObservableValue(default: nil) var pumpBatteryLevel = ObservableValue(default: nil) + var dbSizePercentage = ObservableValue(default: nil) var enactedOrSuggested = ObservableValue(default: nil) var lastSentTOTP = ObservableValue(default: nil) @@ -53,5 +60,12 @@ class Observable { /// Selected tab index used by SwiftUI TabView — set from MainViewController to switch tabs var selectedTabIndex = ObservableValue(default: 0) + /// Currently visible app-wide banner (nil = hidden). Managed by BannerManager. + var activeBanner = ObservableValue(default: nil) + + /// True while the onboarding cover is on screen; AlarmManager doesn't act + /// on alarms while set. In-memory, so background launches are unaffected. + var isOnboardingActive = ObservableValue(default: false) + private init() {} } diff --git a/LoopFollow/Storage/ObservableUserDefaults.swift b/LoopFollow/Storage/ObservableUserDefaults.swift deleted file mode 100644 index d4282db10..000000000 --- a/LoopFollow/Storage/ObservableUserDefaults.swift +++ /dev/null @@ -1,20 +0,0 @@ -// LoopFollow -// ObservableUserDefaults.swift - -import Combine -import Foundation - -/* - Legacy storage, we are moving away from this - */ - -class ObservableUserDefaults { - static let shared = ObservableUserDefaults() - - var old_url = ObservableUserDefaultsValue(key: "url", default: "") - var old_device = ObservableUserDefaultsValue(key: "device", default: "") - var old_nsWriteAuth = ObservableUserDefaultsValue(key: "nsWriteAuth", default: false) - var old_nsAdminAuth = ObservableUserDefaultsValue(key: "nsAdminAuth", default: false) - - private init() {} -} diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index b2274675d..34022aff0 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -60,6 +60,45 @@ extension Storage { UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: legacyNotificationIDs) } + func migrateStep10() { + LogManager.shared.log(category: .general, message: "Running migrateStep10 — infoSort/infoVisible → infoDisplayItems") + migrateInfoDisplayItems() + } + + /// Converts the legacy parallel `infoSort` / `infoVisible` arrays into the + /// unified `infoDisplayItems` store, then removes the legacy keys. + /// + /// Idempotent — does nothing once the legacy keys are gone. Called from + /// `migrateStep10()` and, as a safety net, unconditionally from + /// `synchronizeInfoTypes()`: fresh installs never persist a `migrationStep`, + /// so a user who customized their info display before this release would + /// otherwise have the step-10 guard skip their conversion. + func migrateInfoDisplayItems() { + let legacySort = StorageValue<[Int]>(key: "infoSort", defaultValue: []) + let legacyVisible = StorageValue<[Bool]>(key: "infoVisible", defaultValue: []) + guard legacySort.exists || legacyVisible.exists else { return } + + let sort = legacySort.value + let visible = legacyVisible.value + + var items: [InfoDisplayItem] = [] + var seen = Set() + // Honor the saved order and per-index visibility. + for index in sort { + guard let type = InfoType(rawValue: index), seen.insert(index).inserted else { continue } + let isVisible = index < visible.count ? visible[index] : type.defaultVisible + items.append(InfoDisplayItem(type: type, isVisible: isVisible, coloring: InfoColoring())) + } + // Append any InfoType missing from the saved order (new cases, gaps). + for type in InfoType.allCases where seen.insert(type.rawValue).inserted { + items.append(InfoDisplayItem(type: type, isVisible: type.defaultVisible, coloring: InfoColoring())) + } + + Storage.shared.infoDisplayItems.value = items + legacySort.remove() + legacyVisible.remove() + } + func migrateStep9() { // Default for debugLogLevel changed from false to true so users ship useful // logs when they report a problem. Force-enable for existing users. @@ -159,1679 +198,6 @@ extension Storage { nightscoutPosition.value = .position4 } - func migrateStep1() { - // Guard each field with .exists so that if the App Group suite is unreadable - // (e.g. Before-First-Unlock state after a reboot), we skip the write rather - // than overwriting the already-migrated Standard value with an empty default. - if ObservableUserDefaults.shared.old_url.exists { - Storage.shared.url.value = ObservableUserDefaults.shared.old_url.value - } - if ObservableUserDefaults.shared.old_device.exists { - Storage.shared.device.value = ObservableUserDefaults.shared.old_device.value - } - if ObservableUserDefaults.shared.old_nsWriteAuth.exists { - Storage.shared.nsWriteAuth.value = ObservableUserDefaults.shared.old_nsWriteAuth.value - } - if ObservableUserDefaults.shared.old_nsAdminAuth.exists { - Storage.shared.nsAdminAuth.value = ObservableUserDefaults.shared.old_nsAdminAuth.value - } - - // Helper: 1-to-1 type ----------------------------------------------------------------- - func move( - _ legacy: @autoclosure () -> UserDefaultsValue, - into newValue: StorageValue - ) { - let item = legacy() - guard item.exists else { return } - newValue.value = item.value - item.setNil(key: item.key) - } - - // Helper: Float → Double ------------------------------------------------------------ - func moveFloatToDouble( - _ legacy: @autoclosure () -> UserDefaultsValue, - into newValue: StorageValue - ) { - let item = legacy() - guard item.exists else { return } - newValue.value = Double(item.value) - item.setNil(key: item.key) - } - - // Remove this in a year later than the release of the new Alarms [BEGIN] - let legacyColorBGText = UserDefaultsValue(key: "colorBGText", default: true) - if legacyColorBGText.exists { - Storage.shared.colorBGText.value = legacyColorBGText.value - legacyColorBGText.setNil(key: "colorBGText") - } - - let legacyAppBadge = UserDefaultsValue(key: "appBadge", default: true) - if legacyAppBadge.exists { - Storage.shared.appBadge.value = legacyAppBadge.value - legacyAppBadge.setNil(key: "appBadge") - } - - let legacyForceDarkMode = UserDefaultsValue(key: "forceDarkMode", default: true) - if legacyForceDarkMode.exists { - // Migrate from Bool to AppearanceMode: true -> .dark, false -> .system - Storage.shared.appearanceMode.value = legacyForceDarkMode.value ? .dark : .system - legacyForceDarkMode.setNil(key: "forceDarkMode") - } - - let legacyShowStats = UserDefaultsValue(key: "showStats", default: true) - if legacyShowStats.exists { - Storage.shared.showStats.value = legacyShowStats.value - legacyShowStats.setNil(key: "showStats") - } - - let legacyUseIFCC = UserDefaultsValue(key: "useIFCC", default: false) - if legacyUseIFCC.exists { - Storage.shared.useIFCC.value = legacyUseIFCC.value - legacyUseIFCC.setNil(key: "useIFCC") - } - - let legacyShowSmallGraph = UserDefaultsValue(key: "showSmallGraph", default: true) - if legacyShowSmallGraph.exists { - Storage.shared.showSmallGraph.value = legacyShowSmallGraph.value - legacyShowSmallGraph.setNil(key: "showSmallGraph") - } - - let legacyScreenlockSwitchState = UserDefaultsValue(key: "screenlockSwitchState", default: true) - if legacyScreenlockSwitchState.exists { - Storage.shared.screenlockSwitchState.value = legacyScreenlockSwitchState.value - legacyScreenlockSwitchState.setNil(key: "screenlockSwitchState") - } - - let legacyShowDisplayName = UserDefaultsValue(key: "showDisplayName", default: false) - if legacyShowDisplayName.exists { - Storage.shared.showDisplayName.value = legacyShowDisplayName.value - legacyShowDisplayName.setNil(key: "showDisplayName") - } - - let legacySpeakBG = UserDefaultsValue(key: "speakBG", default: false) - if legacySpeakBG.exists { - Storage.shared.speakBG.value = legacySpeakBG.value - legacySpeakBG.setNil(key: "speakBG") - } - - let legacySpeakBGAlways = UserDefaultsValue(key: "speakBGAlways", default: true) - if legacySpeakBGAlways.exists { - Storage.shared.speakBGAlways.value = legacySpeakBGAlways.value - legacySpeakBGAlways.setNil(key: "speakBGAlways") - } - - let legacySpeakLowBG = UserDefaultsValue(key: "speakLowBG", default: false) - if legacySpeakLowBG.exists { - Storage.shared.speakLowBG.value = legacySpeakLowBG.value - legacySpeakLowBG.setNil(key: "speakLowBG") - } - - let legacySpeakProactiveLowBG = UserDefaultsValue(key: "speakProactiveLowBG", default: false) - if legacySpeakProactiveLowBG.exists { - Storage.shared.speakProactiveLowBG.value = legacySpeakProactiveLowBG.value - legacySpeakProactiveLowBG.setNil(key: "speakProactiveLowBG") - } - - let legacySpeakFastDropDelta = UserDefaultsValue(key: "speakFastDropDelta", default: 10.0) - if legacySpeakFastDropDelta.exists { - Storage.shared.speakFastDropDelta.value = Double(legacySpeakFastDropDelta.value) - legacySpeakFastDropDelta.setNil(key: "speakFastDropDelta") - } - - let legacySpeakLowBGLimit = UserDefaultsValue(key: "speakLowBGLimit", default: 72.0) - if legacySpeakLowBGLimit.exists { - Storage.shared.speakLowBGLimit.value = Double(legacySpeakLowBGLimit.value) - legacySpeakLowBGLimit.setNil(key: "speakLowBGLimit") - } - - let legacySpeakHighBGLimit = UserDefaultsValue(key: "speakHighBGLimit", default: 180.0) - if legacySpeakHighBGLimit.exists { - Storage.shared.speakHighBGLimit.value = Double(legacySpeakHighBGLimit.value) - legacySpeakHighBGLimit.setNil(key: "speakHighBGLimit") - } - - let legacySpeakHighBG = UserDefaultsValue(key: "speakHighBG", default: false) - if legacySpeakHighBG.exists { - Storage.shared.speakHighBG.value = legacySpeakHighBG.value - legacySpeakHighBG.setNil(key: "speakHighBG") - } - - let legacySpeakLanguage = UserDefaultsValue(key: "speakLanguage", default: "en") - if legacySpeakLanguage.exists { - Storage.shared.speakLanguage.value = legacySpeakLanguage.value - legacySpeakLanguage.setNil(key: "speakLanguage") - } - - move(UserDefaultsValue(key: "persistentNotification", default: true), into: Storage.shared.persistentNotification) - - // ── General (done earlier, but safe to repeat) ── - move(UserDefaultsValue(key: "colorBGText", default: true), into: Storage.shared.colorBGText) - move(UserDefaultsValue(key: "appBadge", default: true), into: appBadge) - // Note: forceDarkMode migration to appearanceMode is handled earlier in migrateGeneralSettings() - move(UserDefaultsValue(key: "showStats", default: true), into: showStats) - move(UserDefaultsValue(key: "useIFCC", default: false), into: useIFCC) - move(UserDefaultsValue(key: "showSmallGraph", default: true), into: showSmallGraph) - move(UserDefaultsValue(key: "screenlockSwitchState", default: false), into: screenlockSwitchState) - move(UserDefaultsValue(key: "showDisplayName", default: false), into: showDisplayName) - - // ── Speak-BG ── - move(UserDefaultsValue(key: "speakBG", default: false), into: speakBG) - move(UserDefaultsValue(key: "speakBGAlways", default: true), into: speakBGAlways) - move(UserDefaultsValue(key: "speakLowBG", default: false), into: speakLowBG) - move(UserDefaultsValue(key: "speakProactiveLowBG", default: false), into: speakProactiveLowBG) - move(UserDefaultsValue(key: "speakHighBG", default: false), into: speakHighBG) - moveFloatToDouble(UserDefaultsValue(key: "speakLowBGLimit", default: 72.0), into: speakLowBGLimit) - moveFloatToDouble(UserDefaultsValue(key: "speakHighBGLimit", default: 180.0), into: speakHighBGLimit) - moveFloatToDouble(UserDefaultsValue(key: "speakFastDropDelta", default: 10.0), into: speakFastDropDelta) - move(UserDefaultsValue(key: "speakLanguage", default: "en"), into: speakLanguage) - - // ── Graph ── - move(UserDefaultsValue(key: "showDots", default: true), into: showDots) - move(UserDefaultsValue(key: "showLines", default: true), into: showLines) - move(UserDefaultsValue(key: "showValues", default: true), into: showValues) - move(UserDefaultsValue(key: "showAbsorption", default: true), into: showAbsorption) - move(UserDefaultsValue(key: "showDIAMarkers", default: true), into: showDIALines) - move(UserDefaultsValue(key: "show30MinLine", default: false), into: show30MinLine) - move(UserDefaultsValue(key: "show90MinLine", default: false), into: show90MinLine) - move(UserDefaultsValue(key: "showMidnightMarkers", default: false), into: showMidnightLines) - move(UserDefaultsValue(key: "smallGraphTreatments", default: true), into: smallGraphTreatments) - - move(UserDefaultsValue(key: "smallGraphHeight", default: 40), into: smallGraphHeight) - move(UserDefaultsValue(key: "predictionToLoad", default: 1.0), into: predictionToLoad) - move(UserDefaultsValue(key: "minBasalScale", default: 5.0), into: minBasalScale) - moveFloatToDouble(UserDefaultsValue(key: "minBGScale", default: 250.0), into: minBGScale) - moveFloatToDouble(UserDefaultsValue(key: "lowLine", default: 70.0), into: lowLine) - moveFloatToDouble(UserDefaultsValue(key: "highLine", default: 180.0), into: highLine) - move(UserDefaultsValue(key: "downloadDays", default: 1), into: downloadDays) - // Remove this in a year later than the release of the new Alarms [END] - - // ── Watch / Calendar ──────────────────────────────────────────────── - move(UserDefaultsValue(key: "writeCalendarEvent", default: false), into: writeCalendarEvent) - move(UserDefaultsValue(key: "calendarIdentifier", default: ""), into: calendarIdentifier) - move(UserDefaultsValue(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%"), into: watchLine1) - move(UserDefaultsValue(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%"), into: watchLine2) - - // Migration of generic alarm settings - // ── AlarmConfiguration migration ───────────────────────────────────────── - do { - // Work on a mutable copy, then write the whole thing back once. - var cfg = Storage.shared.alarmConfiguration.value - let cal = Calendar.current - - /// Copy *one* legacy value → struct field → delete old key - func move( - _ legacy: @autoclosure () -> UserDefaultsValue, - write: (inout AlarmConfiguration, T) -> Void - ) { - let item = legacy() - guard item.exists else { return } - write(&cfg, item.value) - item.setNil(key: item.key) - } - - // 1. Override-volume toggle - move(UserDefaultsValue(key: "overrideSystemOutputVolume", - default: cfg.overrideSystemOutputVolume)) - { - $0.overrideSystemOutputVolume = $1 - } - - // 2. Forced output volume itself. - // Prefer newer key (“forcedOutputVolume”); otherwise fall back. - if UserDefaultsValue(key: "forcedOutputVolume", - default: cfg.forcedOutputVolume).exists - { - move(UserDefaultsValue(key: "forcedOutputVolume", - default: cfg.forcedOutputVolume)) - { - $0.forcedOutputVolume = $1 - } - } else { - move(UserDefaultsValue(key: "systemOutputVolume", - default: cfg.forcedOutputVolume)) - { - $0.forcedOutputVolume = $1 - } - } - - // 3. Play audio during phone calls - move(UserDefaultsValue(key: "alertAudioDuringPhone", - default: cfg.audioDuringCalls)) - { - $0.audioDuringCalls = $1 - } - - // 4. Auto-snooze CGM-start alarm - move(UserDefaultsValue(key: "alertAutoSnoozeCGMStart", - default: cfg.autoSnoozeCGMStart)) - { - $0.autoSnoozeCGMStart = $1 - } - - // 5. Global “Snooze all” → snoozeUntil - move(UserDefaultsValue(key: "alertSnoozeAllTime", - default: cfg.snoozeUntil)) - { - $0.snoozeUntil = $1 - } - - // 6. Global “Mute all” → muteUntil - move(UserDefaultsValue(key: "alertMuteAllTime", - default: cfg.muteUntil)) - { - $0.muteUntil = $1 - } - - // 7 & 8. Legacy quiet-hours → day/night start - // (only if both dates exist and are on the same “reference” day) - let qStart = UserDefaultsValue(key: "quietHourStart", default: nil) - let qEnd = UserDefaultsValue(key: "quietHourEnd", default: nil) - if let s = qStart.value, let e = qEnd.value { - let compsStart = cal.dateComponents([.hour, .minute], from: s) - let compsEnd = cal.dateComponents([.hour, .minute], from: e) - - if let sh = compsStart.hour, let sm = compsStart.minute, - let eh = compsEnd.hour, let em = compsEnd.minute - { - cfg.nightStart = TimeOfDay(hour: sh, minute: sm) - cfg.dayStart = TimeOfDay(hour: eh, minute: em) - qStart.setNil(key: qStart.key) - qEnd.setNil(key: qEnd.key) - } - } - - // 9. Legacy “ignore zero BG” flag → ignoreZeroBG - move(UserDefaultsValue(key: "alertIgnoreZero", - default: cfg.ignoreZeroBG)) - { - $0.ignoreZeroBG = $1 - } - - // finally persist the whole struct - Storage.shared.alarmConfiguration.value = cfg - } - - // ── Dexcom Share -------------------------------------------------------- - move(UserDefaultsValue(key: "shareUserName", default: ""), - into: Storage.shared.shareUserName) - - move(UserDefaultsValue(key: "sharePassword", default: ""), - into: Storage.shared.sharePassword) - - move(UserDefaultsValue(key: "shareServer", default: "US"), - into: Storage.shared.shareServer) - - // ── Graph --------------------------------------------------------------- - moveFloatToDouble( - UserDefaultsValue(key: "chartScaleX", default: 18.0), - into: Storage.shared.chartScaleX - ) - - // ── Advanced settings --------------------------------------------------- - move(UserDefaultsValue(key: "downloadTreatments", default: true), - into: Storage.shared.downloadTreatments) - move(UserDefaultsValue(key: "downloadPrediction", default: true), - into: Storage.shared.downloadPrediction) - move(UserDefaultsValue(key: "graphOtherTreatments", default: true), - into: Storage.shared.graphOtherTreatments) - move(UserDefaultsValue(key: "graphBasal", default: true), - into: Storage.shared.graphBasal) - move(UserDefaultsValue(key: "graphBolus", default: true), - into: Storage.shared.graphBolus) - move(UserDefaultsValue(key: "graphCarbs", default: true), - into: Storage.shared.graphCarbs) - move(UserDefaultsValue(key: "bgUpdateDelay", default: 10), - into: Storage.shared.bgUpdateDelay) - - // ── Insert times -------------------------------------------------------- - move(UserDefaultsValue(key: "alertCageInsertTime", default: 0), - into: Storage.shared.cageInsertTime) - move(UserDefaultsValue(key: "alertSageInsertTime", default: 0), - into: Storage.shared.sageInsertTime) - - // ── Version-cache / notification bookkeeping --------------------------- - move(UserDefaultsValue(key: "cachedForVersion", default: nil), - into: Storage.shared.cachedForVersion) - move(UserDefaultsValue(key: "latestVersion", default: nil), - into: Storage.shared.latestVersion) - move(UserDefaultsValue(key: "latestVersionChecked", default: nil), - into: Storage.shared.latestVersionChecked) - move(UserDefaultsValue(key: "currentVersionBlackListed", default: false), - into: Storage.shared.currentVersionBlackListed) - move(UserDefaultsValue(key: "lastBlacklistNotificationShown", default: nil), - into: Storage.shared.lastBlacklistNotificationShown) - move(UserDefaultsValue(key: "lastVersionUpdateNotificationShown", default: nil), - into: Storage.shared.lastVersionUpdateNotificationShown) - move(UserDefaultsValue(key: "lastExpirationNotificationShown", default: nil), - into: Storage.shared.lastExpirationNotificationShown) - - move(UserDefaultsValue(key: "hideInfoTable", default: false), into: Storage.shared.hideInfoTable) - move(UserDefaultsValue(key: "token", default: ""), into: Storage.shared.token) - move(UserDefaultsValue(key: "units", default: "mg/dL"), into: Storage.shared.units) - - move(UserDefaultsValue<[Int]>(key: "infoSort", - default: InfoType.allCases.map { $0.sortOrder }), - into: Storage.shared.infoSort) - - move(UserDefaultsValue<[Bool]>(key: "infoVisible", - default: InfoType.allCases.map { $0.defaultVisible }), - into: Storage.shared.infoVisible) - - migrateUrgentLowAlarm() - migrateLowAlarm() - migrateHighAlarm() - migrateUrgentHighAlarm() - migrateFastDropAlarm() - migrateFastRiseAlarm() - migrateMissedReadingAlarm() - migrateNotLoopingAlarm() - migrateMissedBolusAlarm() - migrateSensorChangeAlarm() - migratePumpChangeAlarm() - migrateOverrideStartAlarm() - migrateOverrideEndAlarm() - migrateTempTargetStartAlarm() - migrateTempTargetEndAlarm() - migrateTemporaryBGAlarm() - migratePumpVolumeAlarm() - migrateIOBAlarm() - migrateCOBAlarm() - migrateBatteryAlarm() - migrateBatteryDropAlarm() - migrateRecBolusAlarm() - } - - // MARK: - One-off alarm migrations - - /// Reads *all* `alertUrgentLow*` keys, converts them into a single `Alarm`, - /// saves it to the modern `[Alarm]` store, then deletes the legacy keys. - private func migrateUrgentLowAlarm() { - // Did the user ever change that alert? (No key ⇒ nothing to do.) - guard UserDefaultsValue(key: "alertUrgentLowActive", default: false).exists else { return } - - /// Helper: fetch-then-delete a legacy value in one line. - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } - return box.value - } - - // Build the new Alarm ------------------------------------------------ - var alarm = Alarm(type: .low) - alarm.name = "Urgent Low" - alarm.isEnabled = take("alertUrgentLowActive", default: false) - alarm.belowBG = Double(take("alertUrgentLowBG", default: 55.0)) - alarm.predictiveMinutes = take("alertUrgentLowPredictiveMinutes", default: 0) - alarm.snoozeDuration = take("alertUrgentLowSnooze", default: 5) - alarm.snoozedUntil = take("alertUrgentLowSnoozedTime", default: nil as Date?) - alarm.soundFile = SoundFile( - rawValue: take("alertUrgentLowSound", - default: "Emergency_Alarm_Siren")) - ?? .emergencyAlarmSiren - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertUrgentLowAudible", - default: "Always").lowercased()) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertUrgentLowRepeat", - default: "Always").lowercased()) ?? .always - - // Day / Night active-window (“Pre-Snooze”) - let autoStr = take("alertUrgentLowAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertUrgentLowAutosnoozeDay", default: false) - let nightFlag = take("alertUrgentLowAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always // “Never” → always active - } - }() - - // Persist ----------------------------------------------------------------- - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Low-BG alarm ------------------------------------------------------- - - private func migrateLowAlarm() { - // Bail if the old keys were never written - guard UserDefaultsValue(key: "alertLowActive", default: false).exists else { return } - - // tiny helper: fetch value → erase key - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } - return box.value - } - - // Build the new Alarm ---------------------------------------------------- - var alarm = Alarm(type: .low) - alarm.name = "Low" - alarm.isEnabled = take("alertLowActive", default: false) - alarm.belowBG = Double(take("alertLowBG", default: 70.0)) - - // “Persistent ≥ X min” → `persistentMinutes` - alarm.persistentMinutes = take("alertLowPersistent", default: 0) - - // “Persistence max BG drop” -- ignoring this for now - // let persistentLowTriggerImmediatelyBG = UserDefaultsRepository.alertLowBG.value - UserDefaultsRepository.alertLowPersistenceMax.value - // (Float(persistentLowBG) <= UserDefaultsRepository.alertLowBG.value || Float(currentBG) <= persistentLowTriggerImmediatelyBG) - _ = Double(take("alertLowPersistenceMax", default: 5.0)) - - alarm.snoozeDuration = take("alertLowSnooze", default: 5) - alarm.snoozedUntil = take("alertLowSnoozedTime", default: nil as Date?) - alarm.soundFile = SoundFile( - rawValue: take("alertLowSound", - default: "Indeed")) ?? .indeed - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertLowAudible", - default: "Always").lowercased()) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertLowRepeat", - default: "Always").lowercased()) ?? .always - - // activeOption ← legacy “Pre-Snooze” flags / picker - let autoStr = take("alertLowAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertLowAutosnoozeDay", default: false) - let nightFlag = take("alertLowAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - // Done → append to the modern store - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - High-BG alarm ----------------------------------------------------- - - private func migrateHighAlarm() { - // Only run if the legacy key ever existed - guard UserDefaultsValue(key: "alertHighActive", default: false).exists else { return } - - /// Fetch → erase helper - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } // remove legacy value - return box.value - } - - // ---------- Build Alarm ----------------------------------------------- - var alarm = Alarm(type: .high) - alarm.name = "High" - alarm.isEnabled = take("alertHighActive", default: false) - alarm.aboveBG = Double(take("alertHighBG", default: 180.0)) - - alarm.persistentMinutes = take("alertHighPersistent", default: 60) - alarm.snoozeDuration = take("alertHighSnooze", default: 60) - alarm.snoozedUntil = take("alertHighSnoozedTime", default: nil as Date?) - - alarm.soundFile = SoundFile( - rawValue: take("alertHighSound", - default: "Time_Has_Come")) ?? .timeHasCome - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertHighAudible", - default: "Always").lowercased()) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertHighRepeat", - default: "Always").lowercased()) ?? .always - - // ── activeOption derived from “Pre-Snooze” picker & flags - let autoStr = take("alertHighAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertHighAutosnoozeDay", default: false) - let nightFlag = take("alertHighAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - // ---------- Persist & we’re done -------------------------------------- - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Urgent-High alarm -------------------------------------------------- - - private func migrateUrgentHighAlarm() { - // run only once, only if the user ever changed that toggle - guard UserDefaultsValue(key: "alertUrgentHighActive", default: false).exists else { return } - - // helper: read-then-erase a legacy value - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } // wipe legacy key - return box.value - } - - // ───────── Build the Alarm ──────────────────────────────────────────── - var alarm = Alarm(type: .high) // we map to the existing `.high` type - alarm.name = "Urgent High" - alarm.isEnabled = take("alertUrgentHighActive", default: false) - alarm.aboveBG = Double(take("alertUrgentHighBG", default: 250.0)) - - alarm.snoozeDuration = take("alertUrgentHighSnooze", default: 30) - alarm.snoozedUntil = take("alertUrgentHighSnoozedTime", default: nil as Date?) - - alarm.soundFile = SoundFile( - rawValue: take("alertUrgentHighSound", - default: "Pager_Beeps")) ?? .pagerBeeps - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertUrgentHighAudible", - default: "Always").lowercased()) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertUrgentHighRepeat", - default: "Always").lowercased()) ?? .always - - // activeOption comes from the old “Pre-Snooze” picker + its day/night flags - let autoStr = take("alertUrgentHighAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertUrgentHighAutosnoozeDay", default: false) - let nightFlag = take("alertUrgentHighAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { // fall back to picker value - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - // ───────── Persist in new storage ───────────────────────────────────── - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Fast-Drop alarm ---------------------------------------------------- - - private func migrateFastDropAlarm() { - guard UserDefaultsValue(key: "alertFastDropDeltaActive", - default: false).exists else { return } - - // helper: read-then-erase - func take(_ k: String, default d: V) -> V { - let box = UserDefaultsValue(key: k, default: d) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .fastDrop) - alarm.name = "Fast Drop" - alarm.isEnabled = take("alertFastDropDeltaActive", default: false) - - // core trigger parameters - alarm.delta = Double(take("alertFastDropDelta", default: 10.0)) - alarm.monitoringWindow = take("alertFastDropReadings", default: 3) - 1 // store #readings - if take("alertFastDropUseLimit", default: false) { - alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0)) - } - - // snoozing - alarm.snoozeDuration = take("alertFastDropDeltaSnooze", default: 10) - alarm.snoozedUntil = take("alertFastDropSnoozedTime", default: nil as Date?) - - // sound + options - alarm.soundFile = SoundFile( - rawValue: take("alertFastDropSound", default: "Big_Clock_Ticking") - ) ?? .bigClockTicking - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertFastDropAudible", default: "Always").lowercased() - ) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertFastDropRepeat", default: "Never").lowercased() - ) ?? .never - - // activeOption from old “Pre-Snooze” picker + day/night flags - let autoStr = take("alertFastDropAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertFastDropAutosnoozeDay", default: false) - let nightFlag = take("alertFastDropAutosnoozeNight", default: false) - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Fast-Rise alarm ---------------------------------------------------- - - private func migrateFastRiseAlarm() { - guard UserDefaultsValue(key: "alertFastRiseDeltaActive", - default: false).exists else { return } - - func take(_ k: String, default d: V) -> V { - let box = UserDefaultsValue(key: k, default: d) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .fastRise) - alarm.name = "Fast Rise" - alarm.isEnabled = take("alertFastRiseDeltaActive", default: false) - - alarm.delta = Double(take("alertFastRiseDelta", default: 10.0)) - alarm.monitoringWindow = take("alertFastRiseReadings", default: 3) - if take("alertFastRiseUseLimit", default: false) { - alarm.aboveBG = Double(take("alertFastRiseAboveBG", default: 200.0)) - } - - alarm.snoozeDuration = take("alertFastRiseDeltaSnooze", default: 10) - alarm.snoozedUntil = take("alertFastRiseSnoozedTime", default: nil as Date?) - - alarm.soundFile = SoundFile( - rawValue: take("alertFastRiseSound", - default: "Cartoon_Fail_Strings_Trumpet") - ) ?? .cartoonFailStringsTrumpet - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertFastRiseAudible", default: "Always").lowercased() - ) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertFastRiseRepeat", default: "Never").lowercased() - ) ?? .never - - let autoStr = take("alertFastRiseAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertFastRiseAutosnoozeDay", default: false) - let nightFlag = take("alertFastRiseAutosnoozeNight", default: false) - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Missed-Reading alarm --------------------------------------------- - - private func migrateMissedReadingAlarm() { - // Run only when the user has ever modified those settings - guard UserDefaultsValue(key: "alertMissedReadingActive", - default: false).exists else { return } - - // read-then-erase helper - func take(_ k: String, default d: V) -> V { - let b = UserDefaultsValue(key: k, default: d) - defer { b.setNil(key: k) } - return b.value - } - - var alarm = Alarm(type: .missedReading) - alarm.name = "Missed Reading" - alarm.isEnabled = take("alertMissedReadingActive", default: false) - - // “No CGM data for X minutes” - alarm.threshold = take("alertMissedReading", default: 31) - - // snoozing - alarm.snoozeDuration = take("alertMissedReadingSnooze", default: 30) - alarm.snoozedUntil = take("alertMissedReadingSnoozedTime", - default: nil as Date?) - // (legacy “is-snoozed” flag is implicit in snoozedUntil) - - // sound - alarm.soundFile = SoundFile( - rawValue: take("alertMissedReadingSound", - default: "Cartoon_Tip_Toe_Sneaky_Walk") - ) ?? .cartoonTipToeSneakyWalk - - // play / repeat options - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertMissedReadingAudible", - default: "Always").lowercased() - ) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertMissedReadingRepeat", - default: "Never").lowercased() - ) ?? .never - - // activeOption ← “Pre-Snooze” picker + day/night flags - let autoStr = take("alertMissedReadingAutosnooze", default: "Never") - .lowercased() - let dayFlag = take("alertMissedReadingAutosnoozeDay", default: false) - let nightFlag = take("alertMissedReadingAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always // “Never” → always on - } - }() - - // store in the new world - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - “Not Looping” (legacy → Storage.shared.alarms) - - private func migrateNotLoopingAlarm() { - // Check if the user ever configured this alarm - let activeFlag = UserDefaultsValue(key: "alertNotLoopingActive", default: false) - guard activeFlag.exists else { return } // nothing to migrate - - // Convenience: read-then-erase - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } // wipe after reading - return box.value - } - - // Build the new Alarm --------------------------------------------------- - var alarm = Alarm(type: .notLooping) - alarm.name = "Not Looping Alert" - alarm.isEnabled = take("alertNotLoopingActive", default: false) - alarm.threshold = Double(take("alertNotLooping", default: 31)) // minutes - alarm.snoozeDuration = take("alertNotLoopingSnooze", default: 30) - alarm.snoozedUntil = take("alertNotLoopingSnoozedTime", default: nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertNotLoopingSound", - default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown - - // ── ACTIVE-DURING (day/night) ← old **Pre-Snooze** flags -------------- - let actDay = take("alertNotLoopingAutosnoozeDay", default: false) - let actNight = take("alertNotLoopingAutosnoozeNight", default: false) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always // “Never” in old UI - } - }() - - // ── PLAY-SOUND option --------------------------------------------------- - let playStr = take("alertNotLoopingAudible", default: "Always").lowercased() - let playDay = take("alertNotLoopingDayTimeAudible", default: true) - let playNight = take("alertNotLoopingNightTimeAudible", default: true) - alarm.playSoundOption = { - if !playDay, !playNight { return .never } - else if playDay, playNight { return .always } - else if playDay { return .day } - else { return .night } - }() - - // ── REPEAT-SOUND option ------------------------------------------------- - let repStr = take("alertNotLoopingRepeat", default: "Never").lowercased() - let repDay = take("alertNotLoopingDayTime", default: false) - let repNight = take("alertNotLoopingNightTime", default: false) - alarm.repeatSoundOption = { - if repDay, repNight { return .always } - else if repDay, !repNight { return .day } - else if repNight, !repDay { return .night } - else { return .never } - }() - - // ── BG-limit guard ------------------------------------------------------ - if take("alertNotLoopingUseLimits", default: false) { - alarm.belowBG = Double(take("alertNotLoopingLowerLimit", default: 100.0)) - alarm.aboveBG = Double(take("alertNotLoopingUpperLimit", default: 160.0)) - } - - // ── Per-alarm snooze state --------------------------------------------- - if !take("alertNotLoopingIsSnoozed", default: false) { - alarm.snoozedUntil = nil // ignore stored date if flag isn’t set - } - - // Persist & finish ------------------------------------------------------- - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // MARK: – Missed-Bolus alarm ------------------------------------------------- - - private func migrateMissedBolusAlarm() { - // Was the old alarm ever configured? - let legacyActive = UserDefaultsValue(key: "alertMissedBolusActive", - default: false) - guard legacyActive.exists else { return } // nothing to do - - // helper: read-then-delete ------------------------------------------------ - func take(_ k: String, - _ def: V) -> V - { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } // wipe after reading - return box.value - } - - // ──────────────────────────────────────────────────────────────────────── - // Build the new Alarm - // ──────────────────────────────────────────────────────────────────────── - var alarm = Alarm(type: .missedBolus) - alarm.name = "Missed Bolus Alert" - alarm.isEnabled = take("alertMissedBolusActive", false) - - // core timings - alarm.monitoringWindow = take("alertMissedBolus", 10) // delay - alarm.predictiveMinutes = take("alertMissedBolusPrebolus", 20) // pre-bolus window - alarm.snoozeDuration = take("alertMissedBolusSnooze", 10) - - // snooze-state - if take("alertMissedBolusIsSnoozed", false) { - alarm.snoozedUntil = take("alertMissedBolusSnoozedTime", - nil as Date?) - } - - // carb / bolus filters - alarm.delta = take("alertMissedBolusIgnoreBolus", 0.5) - alarm.threshold = Double( - take("alertMissedBolusLowGrams", 10)) - alarm.aboveBG = Double( - take("alertMissedBolusLowGramsBG", 70.0)) - - // sound & tone - alarm.soundFile = SoundFile(rawValue: - take("alertMissedBolusSound", - "Dhol_Shuffleloop")) ?? .dholShuffleloop - - // ── ACTIVE-DURING ← old “Pre-Snooze” flags - let actDay = take("alertMissedBolusAutosnoozeDay", false) - let actNight = take("alertMissedBolusAutosnoozeNight", false) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always // “Never” → always - } - }() - - // ── PLAY-SOUND ← old “PlaySound” picker - let playDay = take("alertMissedBolusDayTimeAudible", true) - let playNight = take("alertMissedBolusNightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // ── REPEAT-SOUND ← old “Repeat Sound” picker - let repDay = take("alertMissedBolusDayTime", false) - let repNight = take("alertMissedBolusNightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // (The deprecated “alertMissedBolusQuiet” key is ignored.) - - // ── Store & finish ------------------------------------------------------ - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // ───────────────────────────────────────────────────────────────────────────── - - // MARK: SAGE → .sensorChange - - // ───────────────────────────────────────────────────────────────────────────── - private func migrateSensorChangeAlarm() { - // Was the old setting ever stored? - let flag = UserDefaultsValue(key: "alertSAGEActive", default: false) - guard flag.exists else { return } - - // tiny helper that *reads + wipes* a legacy key - func take(_ k: String, _ def: V) -> V { - let b = UserDefaultsValue(key: k, default: def) - defer { b.setNil(key: k) } - return b.value - } - - var alarm = Alarm(type: .sensorChange) - alarm.name = "Sensor Change Reminder" - alarm.isEnabled = take("alertSAGEActive", false) - alarm.threshold = Double(take("alertSAGE", 8)) // hours - alarm.snoozeDuration = take("alertSAGESnooze", 2) // hours - if take("alertSAGEIsSnoozed", false) { - alarm.snoozedUntil = take("alertSAGESnoozedTime", nil as Date?) - } - alarm.soundFile = SoundFile(rawValue: - take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou - - // ACTIVE (day / night) - let actDay = take("alertSAGEAutosnoozeDay", false) - let actNight = take("alertSAGEAutosnoozeNight", true) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // PLAY sound - let playDay = take("alertSAGEDayTimeAudible", true) - let playNight = take("alertSAGENightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // REPEAT sound - let repDay = take("alertSAGEDayTime", false) - let repNight = take("alertSAGENightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // Persist - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // ───────────────────────────────────────────────────────────────────────────── - - // MARK: CAGE → .pumpChange - - // ───────────────────────────────────────────────────────────────────────────── - private func migratePumpChangeAlarm() { - let flag = UserDefaultsValue(key: "alertCAGEActive", default: false) - guard flag.exists else { return } - - func take(_ k: String, _ def: V) -> V { - let b = UserDefaultsValue(key: k, default: def) - defer { b.setNil(key: k) } - return b.value - } - - var alarm = Alarm(type: .pumpChange) - alarm.name = "Pump / Cannula Change" - alarm.isEnabled = take("alertCAGEActive", false) - alarm.threshold = Double(take("alertCAGE", 4)) // hours - alarm.snoozeDuration = take("alertCAGESnooze", 2) // hours - if take("alertCAGEIsSnoozed", false) { - alarm.snoozedUntil = take("alertCAGESnoozedTime", nil as Date?) - } - alarm.soundFile = SoundFile(rawValue: - take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou - - // ACTIVE - let actDay = take("alertCAGEAutosnoozeDay", false) - let actNight = take("alertCAGEAutosnoozeNight", true) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // PLAY - let playDay = take("alertCAGEDayTimeAudible", true) - let playNight = take("alertCAGENightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // REPEAT - let repDay = take("alertCAGEDayTime", false) - let repNight = take("alertCAGENightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // ───────────────────────────────────────────────────────────────────────────── - - // MARK: Override-Start → .overrideStart - - // ───────────────────────────────────────────────────────────────────────────── - private func migrateOverrideStartAlarm() { - let exists = UserDefaultsValue(key: "alertOverrideStart", default: false) - guard exists.exists else { return } // user never touched it - - func take(_ k: String, _ def: V) -> V { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } // wipe after reading - return box.value - } - - var alarm = Alarm(type: .overrideStart) - alarm.name = "Override Started" - - alarm.isEnabled = take("alertOverrideStart", false) - alarm.snoozeDuration = 5 // legacy UI had no stepper - if take("alertOverrideStartIsSnoozed", false) { - alarm.snoozedUntil = take("alertOverrideStartSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile( - rawValue: take("alertOverrideStartSound", "Ending_Reached") - ) ?? .endingReached - - // ── ACTIVE (legacy “Pre-Snooze” day/night flags) ────────────── - let actDay = take("alertOverrideStartAutosnoozeDay", false) - let actNight = take("alertOverrideStartAutosnoozeNight", false) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // ── PLAY (legacy “Play Sound” day/night flags) ─────────────── - let playDay = take("alertOverrideStartDayTimeAudible", true) - let playNight = take("alertOverrideStartNightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // ── REPEAT (legacy “Repeat Sound” day/night flags) ─────────── - let repDay = take("alertOverrideStartDayTime", false) - let repNight = take("alertOverrideStartNightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // ignore & wipe unused keys - _ = take("alertOverrideStartQuiet", false as Bool) - _ = take("alertOverrideStartRepeatAudible", "Always" as String) - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // ───────────────────────────────────────────────────────────────────────────── - - // MARK: Override-End → .overrideEnd - - // ───────────────────────────────────────────────────────────────────────────── - private func migrateOverrideEndAlarm() { - let exists = UserDefaultsValue(key: "alertOverrideEnd", default: false) - guard exists.exists else { return } - - func take(_ k: String, _ def: V) -> V { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .overrideEnd) - alarm.name = "Override Ended" - - alarm.isEnabled = take("alertOverrideEnd", false) - alarm.snoozeDuration = 5 - if take("alertOverrideEndIsSnoozed", false) { - alarm.snoozedUntil = take("alertOverrideEndSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile( - rawValue: take("alertOverrideEndSound", "Alert_Tone_Busy") - ) ?? .alertToneBusy - - // ACTIVE - let actDay = take("alertOverrideEndAutosnoozeDay", false) - let actNight = take("alertOverrideEndAutosnoozeNight", false) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // PLAY - let playDay = take("alertOverrideEndDayTimeAudible", true) - let playNight = take("alertOverrideEndNightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // REPEAT - let repDay = take("alertOverrideEndDayTime", false) - let repNight = take("alertOverrideEndNightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // wipe unused keys - _ = take("alertOverrideEndQuiet", false as Bool) - _ = take("alertOverrideEndRepeatAudible", "Always" as String) - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // MARK: ––––– Temp-Target START → .tempTargetStart ––––– - - private func migrateTempTargetStartAlarm() { - let touched = UserDefaultsValue(key: "alertTempTargetStart", default: false) - guard touched.exists else { return } - - func take(_ k: String, _ def: V) -> V { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } // scrub after read - return box.value - } - - var alarm = Alarm(type: .tempTargetStart) - alarm.name = "Temp Target Started" - - alarm.isEnabled = take("alertTempTargetStart", false) - alarm.snoozeDuration = 5 - if take("alertTempTargetStartIsSnoozed", false) { - alarm.snoozedUntil = take("alertTempTargetStartSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile( - rawValue: take("alertTempTargetStartSound", "Ending_Reached") - ) ?? .endingReached - - // ACTIVE ← legacy Pre-Snooze day/night flags - alarm.activeOption = { - let d = take("alertTempTargetStartAutosnoozeDay", false) - let n = take("alertTempTargetStartAutosnoozeNight", false) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always } - }() - - // PLAY - alarm.playSoundOption = { - let d = take("alertTempTargetStartDayTimeAudible", true) - let n = take("alertTempTargetStartNightTimeAudible", true) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never } - }() - - // REPEAT - alarm.repeatSoundOption = { - let d = take("alertTempTargetStartDayTime", false) - let n = take("alertTempTargetStartNightTime", false) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never } - }() - - // wipe “quiet / RepeatAudible” extras - _ = take("alertTempTargetStartQuiet", false as Bool) - _ = take("alertTempTargetStartRepeatAudible", "Always" as String) - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // MARK: ––––– Temp-Target END → .tempTargetEnd ––––– - - private func migrateTempTargetEndAlarm() { - let touched = UserDefaultsValue(key: "alertTempTargetEnd", default: false) - guard touched.exists else { return } - - func take(_ k: String, _ def: V) -> V { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .tempTargetEnd) - alarm.name = "Temp Target Ended" - - alarm.isEnabled = take("alertTempTargetEnd", false) - alarm.snoozeDuration = 5 - if take("alertTempTargetEndIsSnoozed", false) { - alarm.snoozedUntil = take("alertTempTargetEndSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile( - rawValue: take("alertTempTargetEndSound", "Alert_Tone_Busy") - ) ?? .alertToneBusy - - alarm.activeOption = { - let d = take("alertTempTargetEndAutosnoozeDay", false) - let n = take("alertTempTargetEndAutosnoozeNight", false) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always } - }() - - alarm.playSoundOption = { - let d = take("alertTempTargetEndDayTimeAudible", true) - let n = take("alertTempTargetEndNightTimeAudible", true) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never } - }() - - alarm.repeatSoundOption = { - let d = take("alertTempTargetEndDayTime", false) - let n = take("alertTempTargetEndNightTime", false) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never } - }() - - _ = take("alertTempTargetEndQuiet", false as Bool) - _ = take("alertTempTargetEndRepeatAudible", "Always" as String) - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // MARK: ––––– TEMPORARY BG LIMIT → .temporary ––––– - - private func migrateTemporaryBGAlarm() { - let flag = UserDefaultsValue(key: "alertTemporaryActive", default: false) - guard flag.exists else { return } - - func take(_ k: String, _ d: V) -> V { - let box = UserDefaultsValue(key: k, default: d) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .temporary) - alarm.name = "Temporary BG Limit" - alarm.isEnabled = take("alertTemporaryActive", false) - - // limit direction ↓ / ↑ - let limit = Double(take("alertTemporaryBG", 90.0 as Float)) - if take("alertTemporaryBelow", true) { - alarm.belowBG = limit - } else { - alarm.aboveBG = limit - } - - // audio & repeat - alarm.soundFile = SoundFile(rawValue: - take("alertTemporarySound", "Indeed")) ?? .indeed - - alarm.playSoundOption = take("alertTemporaryBGAudible", true) ? .always : .never - alarm.repeatSoundOption = take("alertTemporaryBGRepeat", false) ? .always : .never - - Storage.shared.alarms.value.append(alarm) - } - - // MARK: ––––– PUMP RESERVOIR LEVEL → .pump ––––– - - private func migratePumpVolumeAlarm() { - let flag = UserDefaultsValue(key: "alertPump", default: false) - guard flag.exists else { return } - - func take(_ k: String, _ d: V) -> V { - let box = UserDefaultsValue(key: k, default: d) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .pump) - alarm.name = "Pump Reservoir" - alarm.isEnabled = take("alertPump", false) - alarm.threshold = Double(take("alertPumpAt", 10)) // units left - - // Snooze — stored in hours, so keep as-is. - alarm.snoozeDuration = take("alertPumpSnoozeHours", 5) - - if take("alertPumpIsSnoozed", false) { - alarm.snoozedUntil = take("alertPumpSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile(rawValue: - take("alertPumpSound", "Marimba_Descend")) ?? .marimbaDescend - - // PLAY-sound option (day / night / never) - alarm.playSoundOption = { - let d = take("alertPumpDayTimeAudible", true) - let n = take("alertPumpNightTimeAudible", true) - switch (d, n) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // REPEAT-sound option – derived from legacy picker flags - alarm.repeatSoundOption = { - let d = take("alertPumpDayTime", false) - let n = take("alertPumpNightTime", false) - switch (d, n) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // ACTIVE day/night ← old “Pre-Snooze” flags - alarm.activeOption = { - let d = take("alertPumpAutosnoozeDay", false) - let n = take("alertPumpAutosnoozeNight", false) - switch (d, n) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // Discard no-longer-needed extras - _ = take("alertPumpQuiet", false as Bool) - _ = take("alertPumpRepeat", "Never" as String) - _ = take("alertPumpAudible", "Always" as String) - _ = take("alertPumpAutosnooze", "Never" as String) - - Storage.shared.alarms.value.append(alarm) - } - - // ----------------------------------------------------------------------------- - - // MARK: - Helpers (place them once, near the top of the migrate() file) - - // ----------------------------------------------------------------------------- - - /// Legacy picker + day/night flags → PlaySoundOption - private func playOption(from picker: String, - dayFlag: Bool, - nightFlag: Bool) -> PlaySoundOption - { - let p = picker.lowercased() - if p == "never" { return .never } - if p == "always" { return .always } - - switch (dayFlag, nightFlag) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - } - - /// Legacy picker + day/night flags → RepeatSoundOption - private func repeatOption(from picker: String, - dayFlag: Bool, - nightFlag: Bool) -> RepeatSoundOption - { - let p = picker.lowercased() - if p == "never" { return .never } - if p == "always" { return .always } - - switch (dayFlag, nightFlag) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - } - - /// Convenience: load-then-erase a legacy `UserDefaultsValue` - private func take( - _ key: String, - _ defaultValue: V - ) -> V { - let box = UserDefaultsValue(key: key, default: defaultValue) - defer { box.setNil(key: key) } - return box.value - } - - // ----------------------------------------------------------------------------- - - // MARK: - IOB Alarm migration - - // ----------------------------------------------------------------------------- - - private func migrateIOBAlarm() { - // Only migrate if the user ever touched IOB alarm settings - guard UserDefaultsValue(key: "alertIOB", default: false).exists else { - return - } - - var alarm = Alarm(type: .iob) - alarm.name = "High IOB" - alarm.isEnabled = take("alertIOB", false) - alarm.aboveBG = nil // BG band not used - alarm.threshold = Double(take("alertIOBAt", 1.5)) // units threshold - alarm.bolusCountThreshold = take("alertIOBNumber", 3) - alarm.bolusWindowMinutes = take("alertIOBBolusesWithin", 60) - alarm.delta = Double(take("alertIOBMaxBoluses", 10)) // max total bolus - alarm.snoozeDuration = take("alertIOBSnoozeHours", 1) * 60 // hours → minutes - alarm.snoozedUntil = take("alertIOBSnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertIOBSound", "Alert_Tone_Ringtone_1")) - ?? .alertToneRingtone1 - - // Audio options - alarm.playSoundOption = playOption( - from: take("alertIOBAudible", "Always"), - dayFlag: take("alertIOBDayTimeAudible", true), - nightFlag: take("alertIOBNightTimeAudible", true) - ) - alarm.repeatSoundOption = repeatOption( - from: take("alertIOBRepeat", "Always"), - dayFlag: take("alertIOBDayTime", true), - nightFlag: take("alertIOBNightTime", true) - ) - - // Active (day/night) option comes from the legacy “Pre-Snooze” picker, - // but the IOB alarm never had one, so treat it as always-on. - alarm.activeOption = .always - - // Persist - Storage.shared.alarms.value.append(alarm) - } - - // ----------------------------------------------------------------------------- - - // MARK: - COB Alarm migration - - // ----------------------------------------------------------------------------- - - private func migrateCOBAlarm() { - // Only migrate if the user ever touched COB alarm settings - guard UserDefaultsValue(key: "alertCOB", default: false).exists else { - return - } - - var alarm = Alarm(type: .cob) - alarm.name = "High COB" - alarm.isEnabled = take("alertCOB", false) - alarm.threshold = Double(take("alertCOBAt", 50)) // grams threshold - alarm.snoozeDuration = take("alertCOBSnoozeHours", 1) * 60 // hours → minutes - alarm.snoozedUntil = take("alertCOBSnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertCOBSound", "Alert_Tone_Ringtone_2")) - ?? .alertToneRingtone2 - - // Audio options - alarm.playSoundOption = playOption( - from: take("alertCOBAudible", "Always"), - dayFlag: take("alertCOBDayTimeAudible", true), - nightFlag: take("alertCOBNightTimeAudible", true) - ) - alarm.repeatSoundOption = repeatOption( - from: take("alertCOBRepeat", "Always"), - dayFlag: take("alertCOBDayTime", true), - nightFlag: take("alertCOBNightTime", true) - ) - - alarm.activeOption = .always // same reason as above - - // Persist - Storage.shared.alarms.value.append(alarm) - } - - // ============================================================================= - // BATTERY-LEVEL alarm (old keys → .battery) - // ============================================================================= - private func migrateBatteryAlarm() { - guard UserDefaultsValue(key: "alertBatteryActive", - default: false).exists else { return } - - var alarm = Alarm(type: .battery) - alarm.name = "Low Battery" - alarm.isEnabled = take("alertBatteryActive", false) - alarm.threshold = Double(take("alertBatteryLevel", 25)) // % - alarm.snoozeDuration = take("alertBatterySnoozeHours", 1) * 60 - alarm.snoozedUntil = take("alertBatterySnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertBatterySound", "Machine_Charge")) - ?? .machineCharge - - // ── audio – legacy had a simple Bool “repeat / no-repeat” - let rpt = take("alertBatteryRepeat", true) - alarm.playSoundOption = .always // no day/night picker in legacy UI - alarm.repeatSoundOption = rpt ? .always : .never - - alarm.activeOption = .always // no day/night activation picker - Storage.shared.alarms.value.append(alarm) - } - - // ============================================================================= - // BATTERY-DROP alarm (old keys → .batteryDrop) - // ============================================================================= - private func migrateBatteryDropAlarm() { - guard UserDefaultsValue(key: "alertBatteryDropActive", - default: false).exists else { return } - - var alarm = Alarm(type: .batteryDrop) - alarm.name = "Battery Drop" - alarm.isEnabled = take("alertBatteryDropActive", false) - alarm.delta = Double(take("alertBatteryDropPercentage", 5)) // % drop - alarm.monitoringWindow = take("alertBatteryDropPeriod", 15) // min - alarm.snoozeDuration = take("alertBatteryDropSnoozeHours", 1) * 60 - alarm.snoozedUntil = take("alertBatteryDropSnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertBatteryDropSound", "Machine_Charge")) - ?? .machineCharge - - let rpt = take("alertBatteryDropRepeat", true) - alarm.playSoundOption = .always - alarm.repeatSoundOption = rpt ? .always : .never - alarm.activeOption = .always - - Storage.shared.alarms.value.append(alarm) - } - - // ============================================================================= - // REC-BOLUS alarm (old keys → .recBolus) - // ============================================================================= - private func migrateRecBolusAlarm() { - guard UserDefaultsValue(key: "alertRecBolusActive", - default: false).exists else { return } - - var alarm = Alarm(type: .recBolus) - alarm.name = "Recommended Bolus" - alarm.isEnabled = take("alertRecBolusActive", false) - alarm.delta = take("alertRecBolusLevel", 1.0) // units - alarm.snoozeDuration = take("alertRecBolusSnooze", 5) // min - alarm.snoozedUntil = take("alertRecBolusSnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertRecBolusSound", "Dhol_Shuffleloop")) - ?? .dholShuffleloop - - let rpt = take("alertRecBolusRepeat", false) - alarm.playSoundOption = .always - alarm.repeatSoundOption = rpt ? .always : .never - alarm.activeOption = .always - - Storage.shared.alarms.value.append(alarm) - } - // MARK: - Migration helpers (can be removed when step 5 is removed) /// Check whether a tab item's position was explicitly stored in UserDefaults diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 4dd7370a8..4876924e2 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -3,7 +3,6 @@ import Foundation import HealthKit -import UIKit /* Observable persistant storage @@ -202,9 +201,14 @@ class Storage { var token = StorageValue(key: "token", defaultValue: "") var units = StorageValue(key: "units", defaultValue: "mg/dL") var hasConfiguredUnits = StorageValue(key: "hasConfiguredUnits", defaultValue: false) + var hasCompletedOnboarding = StorageValue(key: "hasCompletedOnboarding", defaultValue: false) - var infoSort = StorageValue<[Int]>(key: "infoSort", defaultValue: InfoType.allCases.map(\.sortOrder)) - var infoVisible = StorageValue<[Bool]>(key: "infoVisible", defaultValue: InfoType.allCases.map(\.defaultVisible)) + var infoDisplayItems = StorageValue<[InfoDisplayItem]>( + key: "infoDisplayItems", + defaultValue: InfoType.allCases.map { + InfoDisplayItem(type: $0, isVisible: $0.defaultVisible, coloring: InfoColoring()) + } + ) var url = StorageValue(key: "url", defaultValue: "") var device = StorageValue(key: "device", defaultValue: "") @@ -215,7 +219,7 @@ class Storage { // When adding a new migration step in `runMigrationsIfNeeded()`, bump this default // to the new latest step number so fresh installs skip all migrations. Other defaults // in this file must reflect the post-migration final state for a fresh install. - var migrationStep = StorageValue(key: "migrationStep", defaultValue: 9) + var migrationStep = StorageValue(key: "migrationStep", defaultValue: 10) var persistentNotification = StorageValue(key: "persistentNotification", defaultValue: false) var persistentNotificationLastBGTime = StorageValue(key: "persistentNotificationLastBGTime", defaultValue: .distantPast) @@ -249,199 +253,6 @@ class Storage { static let shared = Storage() private init() {} - /// Set to true at launch if isProtectedDataAvailable was false (BFU state). - /// Consumed and cleared on the first foreground after that launch. - var needsBFUReload = false - - /// Re-reads every StorageValue from UserDefaults, firing @Published only where the value - /// actually changed. Call this when foregrounding after a Before-First-Unlock (BFU) background - /// launch, where Storage was initialized while UserDefaults was encrypted and all values were - /// cached as their defaults. - /// - /// `migrationStep` is intentionally excluded: viewDidLoad writes it to the latest step during - /// the BFU launch; if we reloaded it and the flush had somehow not landed yet, migrations would re-run. - /// - /// SecureStorageValue properties (maxBolus, maxCarbs, maxProtein, maxFat, bolusIncrement) are - /// not covered here — SecureStorageValue does not implement reload() and Keychain has the same - /// BFU inaccessibility; that is a separate problem. - func reloadAll() { - remoteType.reload() - deviceToken.reload() - expirationDate.reload() - sharedSecret.reload() - productionEnvironment.reload() - remoteApnsKey.reload() - teamId.reload() - remoteKeyId.reload() - - lfApnsKey.reload() - lfKeyId.reload() - bundleId.reload() - user.reload() - - mealWithBolus.reload() - mealWithFatProtein.reload() - hasSeenFatProteinOrderChange.reload() - - backgroundRefreshType.reload() - selectedBLEDevice.reload() - debugLogLevel.reload() - - contactTrend.reload() - contactDelta.reload() - contactEnabled.reload() - contactBackgroundColor.reload() - contactTextColor.reload() - - sensorScheduleOffset.reload() - alarms.reload() - alarmConfiguration.reload() - - lastOverrideStartNotified.reload() - lastOverrideEndNotified.reload() - lastTempTargetStartNotified.reload() - lastTempTargetEndNotified.reload() - lastRecBolusNotified.reload() - lastCOBNotified.reload() - lastMissedBolusNotified.reload() - - appBadge.reload() - colorBGText.reload() - appearanceMode.reload() - showStats.reload() - useIFCC.reload() - showSmallGraph.reload() - screenlockSwitchState.reload() - showDisplayName.reload() - snoozerEmoji.reload() - forcePortraitMode.reload() - - speakBG.reload() - speakBGAlways.reload() - speakLowBG.reload() - speakProactiveLowBG.reload() - speakFastDropDelta.reload() - speakLowBGLimit.reload() - speakHighBGLimit.reload() - speakHighBG.reload() - speakLanguage.reload() - - lastBgReadingTimeSeconds.reload() - lastDeltaMgdl.reload() - lastTrendCode.reload() - lastIOB.reload() - lastCOB.reload() - projectedBgMgdl.reload() - - lastBasal.reload() - lastPumpReservoirU.reload() - lastAutosens.reload() - lastTdd.reload() - lastTargetLowMgdl.reload() - lastTargetHighMgdl.reload() - lastIsfMgdlPerU.reload() - lastCarbRatio.reload() - lastCarbsToday.reload() - lastProfileName.reload() - iageInsertTime.reload() - lastMinBgMgdl.reload() - lastMaxBgMgdl.reload() - - laEnabled.reload() - laRenewBy.reload() - laRenewalFailed.reload() - laPushToStartToken.reload() - laLastPushToStartAt.reload() - laPushToStartBackoff.reload() - - showDots.reload() - showLines.reload() - showValues.reload() - showAbsorption.reload() - showDIALines.reload() - show30MinLine.reload() - show90MinLine.reload() - showMidnightLines.reload() - smallGraphTreatments.reload() - smallGraphHeight.reload() - predictionToLoad.reload() - predictionDisplayType.reload() - minBasalScale.reload() - minBGScale.reload() - lowLine.reload() - highLine.reload() - downloadDays.reload() - graphTimeZoneEnabled.reload() - graphTimeZoneIdentifier.reload() - - writeCalendarEvent.reload() - calendarIdentifier.reload() - watchLine1.reload() - watchLine2.reload() - - shareUserName.reload() - sharePassword.reload() - shareServer.reload() - - chartScaleX.reload() - - downloadTreatments.reload() - downloadPrediction.reload() - graphOtherTreatments.reload() - graphBasal.reload() - graphBolus.reload() - graphCarbs.reload() - bgUpdateDelay.reload() - - cageInsertTime.reload() - sageInsertTime.reload() - - cachedForVersion.reload() - latestVersion.reload() - latestVersionChecked.reload() - currentVersionBlackListed.reload() - lastBlacklistNotificationShown.reload() - lastVersionUpdateNotificationShown.reload() - lastExpirationNotificationShown.reload() - - hideInfoTable.reload() - token.reload() - units.reload() - infoSort.reload() - infoVisible.reload() - - url.reload() - device.reload() - nsWriteAuth.reload() - nsAdminAuth.reload() - - // migrationStep intentionally excluded — see method comment above. - - persistentNotification.reload() - persistentNotificationLastBGTime.reload() - - lastLoopingChecked.reload() - lastBGChecked.reload() - lastLoopTime.reload() - - homePosition.reload() - alarmsPosition.reload() - snoozerPosition.reload() - nightscoutPosition.reload() - remotePosition.reload() - statisticsPosition.reload() - treatmentsPosition.reload() - - loopAPNSQrCodeURL.reload() - bolusIncrementDetected.reload() - remoteBolusHistory.reload() - remoteMealHistory.reload() - showGMI.reload() - showStdDev.reload() - showTITR.reload() - timeInRangeModeRaw.reload() - } - // MARK: - Tab Position Helpers /// Get the position for a given tab item diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 0102d66ed..c665c738d 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -33,9 +33,7 @@ extension MainViewController { bgReadings: self.bgData .suffix(24) .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest - predictionData: self.predictionData - .prefix(12) - .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest, Predictions not currently available for Trio + predictionData: self.alarmPredictionData(), /// These are oldest .. newest expireDate: Storage.shared.expirationDate.value, lastLoopTime: Observable.shared.alertLastLoopTime.value, latestOverrideStart: latestOverrideStart, @@ -50,9 +48,11 @@ extension MainViewController { IOB: self.latestIOB?.value, recentBoluses: bolusEntries, latestBattery: latestBattery, + latestBatteryIsCharging: Observable.shared.deviceBatteryIsCharging.value, latestPumpBattery: latestPumpBattery, batteryHistory: self.deviceBatteryData, - recentCarbs: recentCarbs + recentCarbs: recentCarbs, + dbSizePercentage: Observable.shared.dbSizePercentage.value ) let finalAlarmData: AlarmData @@ -72,6 +72,59 @@ extension MainViewController { } } + /// Builds the forward glucose series the low alarm looks ahead in, + /// oldest .. newest at 5-minute spacing. + /// + /// Loop reports a single forecast, already stored in `predictionData`. Trio + /// reports four forecasts, collapsed into the lowest value per point in time + /// by `lowestForecast(forecasts:start:cap:)`. + func alarmPredictionData() -> [GlucoseValue] { + if Storage.shared.device.value == "Loop" { + return predictionData + .prefix(MainViewController.alarmForecastPointCap) + .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) } + } + + guard let predBGs = openAPSPredBGs else { return [] } + + let forecasts = ["ZT", "IOB", "COB", "UAM"].compactMap { predBGs[$0] } + return MainViewController.lowestForecast( + forecasts: forecasts, + start: openAPSPredUpdatedTime ?? Date().timeIntervalSince1970 + ) + } + + /// Maximum number of forward points (5-minute spacing) the low alarm looks at: + /// 12 points = 60 minutes, matching the predictive look-ahead's upper bound. + static let alarmForecastPointCap = 12 + + /// Collapses several forecasts into a single series by taking the **lowest** + /// value at each point in time, oldest .. newest at 5-minute spacing. + /// + /// Trio/OpenAPS reports four forecasts (ZT, IOB, COB, UAM) rather than the + /// single one Loop provides, so this lets the predictive-low alarm fire if + /// *any* forecast dips to or below the threshold. Empty forecasts are ignored, + /// and each point uses whichever forecasts still extend that far. + static func lowestForecast( + forecasts: [[Double]], + start: TimeInterval, + cap: Int = alarmForecastPointCap + ) -> [GlucoseValue] { + let nonEmpty = forecasts.filter { !$0.isEmpty } + guard !nonEmpty.isEmpty else { return [] } + + let count = min(nonEmpty.map { $0.count }.max() ?? 0, cap) + + return (0 ..< count).compactMap { i in + let valuesAtIndex = nonEmpty.compactMap { i < $0.count ? $0[i] : nil } + guard let lowest = valuesAtIndex.min() else { return nil } + return GlucoseValue( + sgv: Int(lowest.rounded()), + date: Date(timeIntervalSince1970: start + Double(i) * 300) + ) + } + } + func saveLatestAlarmDataToFile(_ alarmData: AlarmData) { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 diff --git a/LoopFollow/Task/BGTask.swift b/LoopFollow/Task/BGTask.swift index 08b5703a3..b5728970c 100644 --- a/LoopFollow/Task/BGTask.swift +++ b/LoopFollow/Task/BGTask.swift @@ -19,6 +19,14 @@ extension MainViewController { to: Date().addingTimeInterval(60) ) + // A deconfigured source can no longer be in an error state. + if Storage.shared.shareUserName.value == "" || Storage.shared.sharePassword.value == "" { + BannerManager.shared.clear(.dexcom) + } + if !IsNightscoutEnabled() { + BannerManager.shared.clear(.nightscout) + } + // If no Dexcom credentials and no Nightscout, schedule a retry in 60 seconds. if Storage.shared.shareUserName.value == "", Storage.shared.sharePassword.value == "", diff --git a/LoopFollow/Task/DBSizeTask.swift b/LoopFollow/Task/DBSizeTask.swift new file mode 100644 index 000000000..0edf50a7e --- /dev/null +++ b/LoopFollow/Task/DBSizeTask.swift @@ -0,0 +1,30 @@ +// LoopFollow +// DBSizeTask.swift + +import Foundation + +extension MainViewController { + /// Nightscout recomputes the database size on every data load, but the value only moves + /// by a few MiB per day, so a slow poll is enough. + private static let dbSizeInterval: TimeInterval = 6 * 60 * 60 + + func scheduleDBSizeTask(initialDelay: TimeInterval = 5) { + let firstRun = Date().addingTimeInterval(initialDelay) + + TaskScheduler.shared.scheduleTask(id: .dbSize, nextRun: firstRun) { [weak self] in + guard let self = self else { return } + self.dbSizeTaskAction() + } + } + + func dbSizeTaskAction() { + guard IsNightscoutEnabled() else { + TaskScheduler.shared.rescheduleTask(id: .dbSize, to: Date().addingTimeInterval(60)) + return + } + + webLoadNSDBSize() + + TaskScheduler.shared.rescheduleTask(id: .dbSize, to: Date().addingTimeInterval(Self.dbSizeInterval)) + } +} diff --git a/LoopFollow/Task/DeviceStatusTask.swift b/LoopFollow/Task/DeviceStatusTask.swift index 0273937d1..412ffbf0f 100644 --- a/LoopFollow/Task/DeviceStatusTask.swift +++ b/LoopFollow/Task/DeviceStatusTask.swift @@ -15,6 +15,10 @@ extension MainViewController { func deviceStatusAction() { // If no NS config, we wait 60s before trying again: guard IsNightscoutEnabled() else { + // No device-status source (e.g. Dexcom-only): drop any forecast left over + // from a previous Loop/Trio source so it doesn't linger on the chart. + clearLoopPredictionGraph() + clearOpenAPSPredictionGraph() TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(60)) return } diff --git a/LoopFollow/Task/Task.swift b/LoopFollow/Task/Task.swift index 41a7df57b..05098891a 100644 --- a/LoopFollow/Task/Task.swift +++ b/LoopFollow/Task/Task.swift @@ -12,5 +12,6 @@ extension MainViewController { scheduleMinAgoTask() scheduleCalendarTask() scheduleAlarmTask() + scheduleDBSizeTask() } } diff --git a/LoopFollow/Task/TaskScheduler.swift b/LoopFollow/Task/TaskScheduler.swift index 51588fb7a..b76ac3022 100644 --- a/LoopFollow/Task/TaskScheduler.swift +++ b/LoopFollow/Task/TaskScheduler.swift @@ -2,7 +2,6 @@ // TaskScheduler.swift import Foundation -import UIKit enum TaskID: CaseIterable { case profile @@ -13,6 +12,7 @@ enum TaskID: CaseIterable { case calendarWrite case alarmCheck case telemetry + case dbSize } struct ScheduledTask { diff --git a/LoopFollow/ViewControllers/AppStateViewController.swift b/LoopFollow/ViewControllers/AppStateViewController.swift deleted file mode 100644 index dcbf0cd63..000000000 --- a/LoopFollow/ViewControllers/AppStateViewController.swift +++ /dev/null @@ -1,8 +0,0 @@ -// LoopFollow -// AppStateViewController.swift - -import Foundation - -class AppStateViewController { - var appStateController: AppStateController? -} diff --git a/LoopFollow/ViewControllers/LineChartWrapper.swift b/LoopFollow/ViewControllers/LineChartWrapper.swift deleted file mode 100644 index b08edf8a7..000000000 --- a/LoopFollow/ViewControllers/LineChartWrapper.swift +++ /dev/null @@ -1,20 +0,0 @@ -// LoopFollow -// LineChartWrapper.swift - -import Charts -import SwiftUI - -struct LineChartWrapper: UIViewRepresentable { - let chartView: LineChartView - - func makeUIView(context _: Context) -> LineChartView { - chartView - } - - func updateUIView(_: LineChartView, context _: Context) { - // Intentionally empty. MainViewController owns the chart and calls - // notifyDataSetChanged itself whenever it mutates the data; doing it - // here too would redo that work on every unrelated SwiftUI re-render - // of MainHomeView (e.g. the once-a-second minAgoText tick). - } -} diff --git a/LoopFollow/ViewControllers/MainHomeView.swift b/LoopFollow/ViewControllers/MainHomeView.swift index e8509d94d..599120f7d 100644 --- a/LoopFollow/ViewControllers/MainHomeView.swift +++ b/LoopFollow/ViewControllers/MainHomeView.swift @@ -1,12 +1,13 @@ // LoopFollow // MainHomeView.swift -import Charts import SwiftUI struct MainHomeView: View { - let bgChart: LineChartView - let bgChartFull: LineChartView + // Deliberately not @ObservedObject: this view only passes the model on. + // The chart views observe what they each need, so data and (especially) + // per-frame pan/zoom updates don't re-evaluate this whole screen. + let chartModel: BGChartModel @ObservedObject var infoManager: InfoManager @ObservedObject var statsModel: StatsDisplayModel @@ -53,11 +54,11 @@ struct MainHomeView: View { .fixedSize(horizontal: false, vertical: true) // Main chart (fills remaining space) - LineChartWrapper(chartView: bgChart) + BGChartView(model: chartModel, config: .main) // Small overview chart if showSmallGraph.value { - LineChartWrapper(chartView: bgChartFull) + BGChartView(model: chartModel, config: .small) .frame(height: CGFloat(smallGraphHeight.value)) } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 105873f1c..ed1ce880f 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -2,7 +2,6 @@ // MainViewController.swift import AVFAudio -import Charts import Combine import CoreBluetooth import EventKit @@ -23,7 +22,7 @@ private struct APNSCredentialSnapshot: Equatable { let lfKeyId: String } -class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificationCenterDelegate { +class MainViewController: UIViewController, UNUserNotificationCenterDelegate { /// The single, long-lived MainViewController that owns the app's data /// pipeline (scheduleAllTasks). Held strongly so it stays alive — and the /// engine keeps running — regardless of which tabs are visible or whether @@ -41,8 +40,6 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio vc.loadViewIfNeeded() } - var BGChart: LineChartView! - var BGChartFull: LineChartView! var statsDisplayModel = StatsDisplayModel() /// The hosting controller's view — hidden during loading / first-time setup. @@ -90,6 +87,10 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio var overrideGraphData: [DataStructs.overrideStruct] = [] var tempTargetGraphData: [DataStructs.tempTargetStruct] = [] var predictionData: [ShareGlucoseData] = [] + var ztPredictionData: [ShareGlucoseData] = [] + var iobPredictionData: [ShareGlucoseData] = [] + var cobPredictionData: [ShareGlucoseData] = [] + var uamPredictionData: [ShareGlucoseData] = [] var openAPSPredBGs: [String: [Double]]? var openAPSPredUpdatedTime: TimeInterval? var bgCheckData: [ShareGlucoseData] = [] @@ -97,7 +98,6 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio var resumeGraphData: [DataStructs.timestampOnlyStruct] = [] var sensorStartGraphData: [DataStructs.timestampOnlyStruct] = [] var noteGraphData: [DataStructs.noteStruct] = [] - var chartData = LineChartData() var deviceBatteryData: [DataStructs.batteryStruct] = [] var lastCalDate: Double = 0 var latestLoopStatusString = "" @@ -127,12 +127,12 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio // Stores the timestamp of the last BG value that was spoken. var lastSpokenBGDate: TimeInterval = 0 - var autoScrollPauseUntil: Date? - var IsNotLooping = false let contactImageUpdater = ContactImageUpdater() + let chartModel = BGChartModel() + private var cancellables = Set() // Loading state management @@ -150,17 +150,10 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio private func setupUI() { view.backgroundColor = .systemBackground - BGChart = LineChartView() - BGChart.backgroundColor = .systemBackground - - BGChartFull = LineChartView() - BGChartFull.backgroundColor = .systemBackground - infoManager = InfoManager() let mainView = MainHomeView( - bgChart: BGChart, - bgChartFull: BGChartFull, + chartModel: chartModel, infoManager: infoManager, statsModel: statsDisplayModel, onRefresh: { [weak self] in self?.refresh() }, @@ -206,17 +199,11 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio // Synchronize info types to ensure arrays are the correct size synchronizeInfoTypes() - let shareUserName = Storage.shared.shareUserName.value - let sharePassword = Storage.shared.sharePassword.value - let shareServer = Storage.shared.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue - dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer) + configureDexShareClient() // setup show/hide graphs (first-time setup check) updateGraphVisibility() - BGChart.delegate = self - BGChartFull.delegate = self - // Apply initial appearance mode updateAppearance(Storage.shared.appearanceMode.value) @@ -228,9 +215,9 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio // when runMigrationsIfNeeded() is called. This catches migrations deferred by a // background BGAppRefreshTask launch in Before-First-Unlock state. notificationCenter.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) - // Posted by AppDelegate after Storage.reloadAll has refreshed every StorageValue - // following a BFU launch. If we're alive when this fires, our scheduled tasks - // were set up with BFU defaults (url='') and need to be redone. + // Posted by AppDelegate after BFU recovery. Vestigial with the readiness gate + // (this controller is built only after storage is ready, so it never fires + // while we're alive); retained one release as a safety net. notificationCenter.addObserver(self, selector: #selector(handleBFUReloadCompleted), name: .bfuReloadCompleted, object: nil) #if !targetEnvironment(macCatalyst) @@ -333,19 +320,18 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio } .store(in: &cancellables) - Storage.shared.shareUserName.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.checkAndShowImportButtonIfNeeded() - } - .store(in: &cancellables) - - Storage.shared.sharePassword.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.checkAndShowImportButtonIfNeeded() - } - .store(in: &cancellables) + // ShareClient captures the credentials at init, so it's rebuilt on any change. + Publishers.MergeMany( + Storage.shared.shareUserName.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.sharePassword.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.shareServer.$value.map { _ in () }.eraseToAnyPublisher() + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.configureDexShareClient() + self?.checkAndShowImportButtonIfNeeded() + } + .store(in: &cancellables) Publishers.CombineLatest4( Storage.shared.remoteApnsKey.$value, @@ -421,6 +407,14 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio checkAndShowImportButtonIfNeeded() } + /// Builds the Dexcom Share client from the stored credentials. + private func configureDexShareClient() { + let shareUserName = Storage.shared.shareUserName.value + let sharePassword = Storage.shared.sharePassword.value + let shareServer = Storage.shared.shareServer.value == "US" ? KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue + dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer) + } + // MARK: - Loading Overlay private func isDataSourceConfigured() -> Bool { @@ -565,29 +559,6 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio @objc func refresh() { LogManager.shared.log(category: .general, message: "Refreshing") - // Clear prediction for both Loop or OpenAPS - - // Check if Loop prediction data exists and clear it if necessary - if !predictionData.isEmpty { - predictionData.removeAll() - updatePredictionGraph() - } - - // Check if OpenAPS prediction data exists and clear it if necessary - let openAPSDataIndices = [12, 13, 14, 15] - for dataIndex in openAPSDataIndices { - let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet - let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - if !mainChart.entries.isEmpty || !smallChart.entries.isEmpty { - updatePredictionGraphGeneric( - dataIndex: dataIndex, - predictionData: [], - chartLabel: "", - color: UIColor.systemGray - ) - } - } - Observable.shared.minAgoText.value = "Refreshing" scheduleAllTasks() NightscoutSocketManager.shared.connectIfNeeded() @@ -646,12 +617,6 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio // 2. Update any other StorageValue defaults in Storage.swift that this new step // mutates, so a fresh install ends up in the same state as a migrated user. - // Step 1: Released in v3.0.0 (2025-07-07). Can be removed after 2026-07-07. - if Storage.shared.migrationStep.value < 1 { - Storage.shared.migrateStep1() - Storage.shared.migrationStep.value = 1 - } - // Step 2: Released in v3.1.0 (2025-07-21). Can be removed after 2026-07-21. if Storage.shared.migrationStep.value < 2 { Storage.shared.migrateStep2() @@ -697,6 +662,11 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio Storage.shared.migrateStep9() Storage.shared.migrationStep.value = 9 } + + if Storage.shared.migrationStep.value < 10 { + Storage.shared.migrateStep10() + Storage.shared.migrationStep.value = 10 + } } @objc func appDidBecomeActive() { @@ -719,8 +689,9 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio } @objc func appCameToForeground() { - // BFU recovery (Storage.reloadAll) is driven by AppDelegate; this controller - // reacts via .bfuReloadCompleted in handleBFUReloadCompleted() above. + // BFU recovery (StorageReadiness.recover) is driven by AppDelegate before this + // controller exists (the readiness gate), so handleBFUReloadCompleted() above + // is a vestigial no-op in the gated flow. // reset screenlock state if needed UIApplication.shared.isIdleTimerDisabled = Storage.shared.screenlockSwitchState.value @@ -991,16 +962,6 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio func userNotificationCenter(_: UNUserNotificationCenter, didReceive _: UNNotificationResponse, withCompletionHandler _: @escaping () -> Void) {} - // User has scrolled the chart - func chartTranslated(_: ChartViewBase, dX _: CGFloat, dY _: CGFloat) { - let isViewingLatestData = abs(BGChart.highestVisibleX - BGChart.chartXMax) < 0.001 - if isViewingLatestData { - autoScrollPauseUntil = nil // User is back at the latest data, allow auto-scrolling - } else { - autoScrollPauseUntil = Date().addingTimeInterval(5 * 60) // User is viewing historical data, pause auto-scrolling - } - } - func calculateMaxBgGraphValue() -> Float { return max(Float(topBG), Float(topPredictionBG)) } @@ -1042,36 +1003,23 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio } private func synchronizeInfoTypes() { - var sortArray = Storage.shared.infoSort.value - var visibleArray = Storage.shared.infoVisible.value + // Safety net: fold any leftover legacy infoSort/infoVisible into + // infoDisplayItems for users whose migrationStep predates that key. + Storage.shared.migrateInfoDisplayItems() - // Current valid indices based on InfoType - let currentValidIndices = InfoType.allCases.map { $0.rawValue } + var items = Storage.shared.infoDisplayItems.value - // Add missing indices to sortArray - for index in currentValidIndices { - if !sortArray.contains(index) { - sortArray.append(index) - } - } - - // Remove deprecated indices - sortArray = sortArray.filter { currentValidIndices.contains($0) } - - // Ensure visibleArray is updated with new entries - if visibleArray.count < currentValidIndices.count { - for i in visibleArray.count ..< currentValidIndices.count { - visibleArray.append(InfoType(rawValue: i)?.defaultVisible ?? false) - } - } + // Drop items whose InfoType no longer exists. + let validTypes = Set(InfoType.allCases) + items.removeAll { !validTypes.contains($0.type) } - // Trim excess elements if there are more than needed - if visibleArray.count > currentValidIndices.count { - visibleArray = Array(visibleArray.prefix(currentValidIndices.count)) + // Append any newly added InfoType. + let present = Set(items.map(\.type)) + for type in InfoType.allCases where !present.contains(type) { + items.append(InfoDisplayItem(type: type, isVisible: type.defaultVisible, coloring: InfoColoring())) } - Storage.shared.infoSort.value = sortArray - Storage.shared.infoVisible.value = visibleArray + Storage.shared.infoDisplayItems.value = items } // MARK: - First Time Setup diff --git a/LoopFollow/ViewControllers/MoreMenuView.swift b/LoopFollow/ViewControllers/MoreMenuView.swift index ac389a122..31f3b3915 100644 --- a/LoopFollow/ViewControllers/MoreMenuView.swift +++ b/LoopFollow/ViewControllers/MoreMenuView.swift @@ -12,111 +12,239 @@ struct MoreMenuView: View { @State private var alertMessage = "" @State private var showAlert = false @State private var currentVersion: String = AppVersionManager().version() + @State private var searchText = "" + @ObservedObject private var nightscoutURL = Storage.shared.url + + private var isSearching: Bool { + !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } var body: some View { List { - // Settings - Section { - NavigationLink(value: SettingsRoute.settings) { - Label("Settings", systemImage: "gearshape") + if isSearching { + searchResultsSection + } else { + menuContent + } + } + .listStyle(.insetGrouped) + .navigationTitle("Menu") + .navigationBarTitleDisplayMode(.large) + .searchable(text: $searchText, prompt: "Search") + .overlay { + if isSearching, filteredSearchItems.isEmpty { + VStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .font(.largeTitle) + .foregroundColor(.secondary) + Text("No Results") + .font(.headline) + Text("No matches for “\(searchText)”.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) } + .padding(.horizontal) } + } + .task { + await fetchVersionInfo() + } + .alert(alertTitle, isPresented: $showAlert) { + Button("OK", role: .cancel) {} + } message: { + Text(alertMessage) + } + .navigationDestination(for: SettingsRoute.self) { $0.destination } + .navigationDestination( + isPresented: Binding( + get: { pendingRoute != nil }, + set: { if !$0 { pendingRoute = nil } } + ) + ) { + if let route = pendingRoute { + route.destination + } + } + } - // Features - Section("Features") { - ForEach(TabItem.featureOrder) { item in - FullRowButton(showsChevron: true) { - let tabs = Storage.shared.orderedTabBarItems() - if let tabIndex = tabs.firstIndex(of: item) { - Observable.shared.selectedTabIndex.value = tabIndex - } else { - pendingRoute = MenuRoute(item) - } - } label: { - Label(item.displayName, systemImage: item.icon) - } - } + // MARK: - Menu content + + @ViewBuilder + private var menuContent: some View { + // Settings + Section { + NavigationLink(value: SettingsRoute.settings) { + Label("Settings", systemImage: "gearshape") } + } - // Logging - Section("Logging") { - FullRowButton(showsChevron: true) { pendingRoute = .log } label: { - Label("View Log", systemImage: "doc.text.magnifyingglass") + // Features + Section("Features") { + ForEach(TabItem.featureOrder) { item in + FullRowButton(showsChevron: true) { + selectFeature(item) + } label: { + Label(item.displayName, systemImage: item.icon) } + } + } - FullRowButton { shareLogs() } label: { - Label("Share Logs", systemImage: "square.and.arrow.up") - } + // Logging + Section("Logging") { + FullRowButton(showsChevron: true) { pendingRoute = .log } label: { + Label("View Log", systemImage: "doc.text.magnifyingglass") } - // Support & Community - Section("Support & Community") { - Link(destination: URL(string: "https://loopfollowdocs.org/")!) { - HStack { - Label("LoopFollow Docs", systemImage: "book") - Spacer() - Image(systemName: "arrow.up.right.square") - .foregroundStyle(.tertiary) - } - } + FullRowButton { shareLogs() } label: { + Label("Share Logs", systemImage: "square.and.arrow.up") + } + } - Link(destination: URL(string: "https://discord.gg/KQgk3gzuYU")!) { + // Support & Community + Section("Support & Community") { + ForEach(MoreMenuView.supportLinks, id: \.url) { link in + Link(destination: link.url) { HStack { - Label("Loop and Learn Discord", systemImage: "bubble.left.and.bubble.right") + Label(link.title, systemImage: link.icon) Spacer() Image(systemName: "arrow.up.right.square") .foregroundStyle(.tertiary) } } + } + } - Link(destination: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) { - HStack { - Label("LoopFollow Facebook Group", systemImage: "person.2.fill") - Spacer() - Image(systemName: "arrow.up.right.square") - .foregroundStyle(.tertiary) - } - } + // Build Information + Section("Build Information") { + buildInfoRow(title: "Version", value: currentVersion, color: versionTint) + buildInfoRow(title: "Latest version", value: latestVersion ?? "Fetching…", color: .secondary) + + let build = BuildDetails.default + if !(build.isMacApp() || build.isSimulatorBuild()) { + buildInfoRow( + title: build.expirationHeaderString, + value: dateTimeUtils.formattedDate(from: build.calculateExpirationDate()), + color: .secondary + ) } - // Build Information - Section("Build Information") { - buildInfoRow(title: "Version", value: currentVersion, color: versionTint) - buildInfoRow(title: "Latest version", value: latestVersion ?? "Fetching…", color: .secondary) - - let build = BuildDetails.default - if !(build.isMacApp() || build.isSimulatorBuild()) { - buildInfoRow( - title: build.expirationHeaderString, - value: dateTimeUtils.formattedDate(from: build.calculateExpirationDate()), - color: .secondary - ) - } + buildInfoRow(title: "Built", value: dateTimeUtils.formattedDate(from: build.buildDate()), color: .secondary) + buildInfoRow(title: "Branch", value: build.branchAndSha, color: .secondary) + } + } + + private func selectFeature(_ item: TabItem) { + let tabs = Storage.shared.orderedTabBarItems() + if let tabIndex = tabs.firstIndex(of: item) { + Observable.shared.selectedTabIndex.value = tabIndex + } else { + pendingRoute = MenuRoute(item) + } + } - buildInfoRow(title: "Built", value: dateTimeUtils.formattedDate(from: build.buildDate()), color: .secondary) - buildInfoRow(title: "Branch", value: build.branchAndSha, color: .secondary) + // MARK: - Search + + /// External Support & Community links, shared by the menu and search so their + /// titles live in one place. + private static let supportLinks: [(title: String, icon: String, url: URL)] = [ + ("LoopFollow Docs", "book", URL(string: "https://loopfollowdocs.org/")!), + ("Loop and Learn Discord", "bubble.left.and.bubble.right", URL(string: "https://discord.gg/KQgk3gzuYU")!), + ("LoopFollow Facebook Group", "person.2.fill", URL(string: "https://www.facebook.com/groups/loopfollowlnl")!), + ] + + /// The searchable universe, assembled from the same sources that render the + /// menu — the Settings routes, the tab features, and the static rows — so it + /// never needs to be kept in sync by hand. + private var searchItems: [MenuSearchItem] { + var items: [MenuSearchItem] = [ + MenuSearchItem(title: "Settings", icon: "gearshape", keywords: [], kind: .settings(.settings)), + ] + + let settingsSections = SettingsRoute.menuSections(nightscoutConfigured: !nightscoutURL.value.isEmpty) + + for (_, routes) in settingsSections { + for route in routes { + items.append(MenuSearchItem(title: route.title, icon: route.icon, keywords: route.keywords, kind: .settings(route))) } } - .listStyle(.insetGrouped) - .navigationTitle("Menu") - .navigationBarTitleDisplayMode(.large) - .task { - await fetchVersionInfo() + + for item in TabItem.featureOrder { + items.append(MenuSearchItem(title: item.displayName, icon: item.icon, keywords: [], kind: .feature(item))) } - .alert(alertTitle, isPresented: $showAlert) { - Button("OK", role: .cancel) {} - } message: { - Text(alertMessage) + + items.append(MenuSearchItem(title: "View Log", icon: "doc.text.magnifyingglass", keywords: ["logging"], kind: .viewLog)) + items.append(MenuSearchItem(title: "Share Logs", icon: "square.and.arrow.up", keywords: ["logging"], kind: .shareLogs)) + + for link in MoreMenuView.supportLinks { + items.append(MenuSearchItem(title: link.title, icon: link.icon, keywords: ["support", "community"], kind: .link(link.url))) } - .navigationDestination(for: SettingsRoute.self) { $0.destination } - .navigationDestination( - isPresented: Binding( - get: { pendingRoute != nil }, - set: { if !$0 { pendingRoute = nil } } - ) - ) { - if let route = pendingRoute { - route.destination + + // Bottom-level settings inside each sub-screen. A hit opens the screen + // that contains the setting. Appended last so screen- and feature-level + // matches rank above individual settings. + for (_, routes) in settingsSections { + for route in routes { + for leaf in route.leaves { + items.append(MenuSearchItem( + title: leaf.title, + icon: route.icon, + keywords: leaf.keywords, + kind: .settings(route), + subtitle: "Settings → \(route.title)" + )) + } + } + } + + return items + } + + private var filteredSearchItems: [MenuSearchItem] { + searchItems.filter(matches) + } + + private func matches(_ item: MenuSearchItem) -> Bool { + let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !query.isEmpty else { return true } + return item.title.localizedCaseInsensitiveContains(query) + || item.keywords.contains { $0.localizedCaseInsensitiveContains(query) } + } + + @ViewBuilder + private var searchResultsSection: some View { + Section { + ForEach(filteredSearchItems) { item in + searchRow(for: item) + } + } + } + + @ViewBuilder + private func searchRow(for item: MenuSearchItem) -> some View { + switch item.kind { + case let .settings(route): + NavigationRow(title: item.title, subtitle: item.subtitle, icon: item.icon, value: route) + case let .feature(feature): + FullRowButton(showsChevron: true) { selectFeature(feature) } label: { + Label(item.title, systemImage: item.icon) + } + case .viewLog: + FullRowButton(showsChevron: true) { pendingRoute = .log } label: { + Label(item.title, systemImage: item.icon) + } + case .shareLogs: + FullRowButton { shareLogs() } label: { + Label(item.title, systemImage: item.icon) + } + case let .link(url): + Link(destination: url) { + HStack { + Label(item.title, systemImage: item.icon) + Spacer() + Image(systemName: "arrow.up.right.square") + .foregroundStyle(.tertiary) + } } } } @@ -211,6 +339,39 @@ struct MoreMenuView: View { } } +// MARK: – Search model + +/// A single searchable menu entry. Assembled from the menu's existing data +/// sources (see `MoreMenuView.searchItems`); `kind` carries how tapping it +/// should behave so results reuse the same navigation as the live rows. +private struct MenuSearchItem: Identifiable { + let title: String + let icon: String + let keywords: [String] + let kind: Kind + var subtitle: String? = nil + + enum Kind { + case settings(SettingsRoute) + case feature(TabItem) + case viewLog + case shareLogs + case link(URL) + } + + var id: String { + // Leaf settings share their screen's route, so the title is part of + // the identity. + switch kind { + case let .settings(route): return "settings.\(route).\(title)" + case let .feature(item): return "feature.\(item.rawValue)" + case .viewLog: return "viewLog" + case .shareLogs: return "shareLogs" + case let .link(url): return "link.\(url.absoluteString)" + } + } +} + // MARK: – Full-row tappable button private struct FullRowButton: View { diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index 8dd1fc4b2..27c6b584f 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -37,14 +37,12 @@ class NightscoutViewController: UIViewController { } .store(in: &cancellables) - var url = Storage.shared.url.value - let token = Storage.shared.token.value - - if token != "" { - url = url + "?token=" + token - } - - guard let myUrl = URL(string: url) else { return } + guard let myUrl = NightscoutUtils.constructURL( + baseURL: Storage.shared.url.value, + token: Storage.shared.token.value, + endpoint: "", + parameters: [:] + ) else { return } let webpagePreferences = WKWebpagePreferences() webpagePreferences.allowsContentJavaScript = true diff --git a/LoopFollowLAExtension/LoopFollowLiveActivity.swift b/LoopFollowLAExtension/LoopFollowLiveActivity.swift index f854e6615..6d438531e 100644 --- a/LoopFollowLAExtension/LoopFollowLiveActivity.swift +++ b/LoopFollowLAExtension/LoopFollowLiveActivity.swift @@ -44,60 +44,27 @@ private func makeDynamicIsland(context: ActivityViewContext some View { - if #available(iOS 17.0, *) { - contentMargins(Edge.Set.all, 0) - } else { - self + ) + .activitySystemActionForegroundColor(.white) + .contentMargins(.all, 0) + .widgetURL(URL(string: "\(AppGroupID.urlScheme)://la-tap")!) + } dynamicIsland: { context in + makeDynamicIsland(context: context) } + .supplementalActivityFamilies([.small]) } } @@ -106,7 +73,6 @@ private extension View { /// Reads the activityFamily environment value and routes to the appropriate layout. /// - `.small` → CarPlay Dashboard & Watch Smart Stack /// - everything else → full lock screen layout -@available(iOS 18.0, *) private struct LockScreenFamilyAdaptiveView: View { let state: GlucoseLiveActivityAttributes.ContentState @@ -125,7 +91,6 @@ private struct LockScreenFamilyAdaptiveView: View { // MARK: - Small family view (CarPlay Dashboard + Watch Smart Stack) -@available(iOS 18.0, *) private struct SmallFamilyView: View { let snapshot: GlucoseSnapshot @@ -270,6 +235,8 @@ private struct LockScreenLiveActivityView: View { .frame(maxWidth: .infinity, alignment: .trailing) } + ActiveAdjustmentsView(snapshot: s) + Text(LAAppGroupSettings.showDisplayName() ? "\(LAAppGroupSettings.displayName()) — \(LAFormat.updated(s))" : "Last Update: \(LAFormat.updated(s))") @@ -400,6 +367,84 @@ private struct SlotView: View { } } +/// Conditional row showing the active override and/or temp target with a +/// self-ticking countdown. Shared by the lock screen card and the expanded +/// Dynamic Island bottom region; renders nothing when neither is active. +private struct ActiveAdjustmentsView: View { + let snapshot: GlucoseSnapshot + + /// Above this remaining time the ticker is skipped (name/value only) — + /// a multi-hour or multi-day countdown reads poorly in the compact row. + private static let tickerMaxRemaining: TimeInterval = 2 * 3600 + + // Ends already in the past are stale data waiting for the next refresh — + // drop them rather than render a dead 0:00 timer. + private var overrideEnd: Date? { + guard let t = snapshot.overrideEndAt, t > Date().timeIntervalSince1970 else { return nil } + return Date(timeIntervalSince1970: t) + } + + private var tempTargetEnd: Date? { + guard let t = snapshot.tempTargetEndAt, t > Date().timeIntervalSince1970 else { return nil } + return Date(timeIntervalSince1970: t) + } + + /// nil end with a non-nil name means indefinite — keep showing the name; + /// a timed override whose end has passed is hidden entirely. + private var overrideName: String? { + guard let name = snapshot.override else { return nil } + if snapshot.overrideEndAt != nil, overrideEnd == nil { return nil } + return name + } + + private var tempTargetText: String? { + guard tempTargetEnd != nil else { return nil } + return LAFormat.tempTargetValue(snapshot) + } + + var body: some View { + if overrideName != nil || tempTargetText != nil { + HStack(spacing: 5) { + Text("⏱") + .font(.system(size: 11)) + if let name = overrideName { + Text(name) + .lineLimit(1) + .minimumScaleFactor(0.7) + .layoutPriority(1) + if let end = overrideEnd, end.timeIntervalSinceNow <= Self.tickerMaxRemaining { + countdown(to: end) + } + } + if overrideName != nil, tempTargetText != nil { + Text("·") + .foregroundStyle(.white.opacity(0.5)) + } + if let tt = tempTargetText, let end = tempTargetEnd { + Text("TT \(tt)") + .lineLimit(1) + .minimumScaleFactor(0.8) + if end.timeIntervalSinceNow <= Self.tickerMaxRemaining { + countdown(to: end) + } + } + } + .font(.system(size: 12, weight: .semibold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(.white.opacity(0.85)) + .frame(maxWidth: .infinity, alignment: .center) + } + } + + @ViewBuilder + private func countdown(to end: Date) -> some View { + // Text(timerInterval:) claims flexible width; cap it so the row stays centered. + Text(timerInterval: Date() ... end, countsDown: true) + .multilineTextAlignment(.leading) + .frame(maxWidth: end.timeIntervalSinceNow >= 3600 ? 62 : 46, alignment: .leading) + } +} + // MARK: - Dynamic Island private struct DynamicIslandLeadingView: View { @@ -470,11 +515,14 @@ private struct DynamicIslandBottomView: View { .lineLimit(1) .minimumScaleFactor(0.75) } else { - Text("Updated at: \(LAFormat.updated(snapshot))") - .font(.system(size: 13, weight: .semibold, design: .rounded)) - .foregroundStyle(.white.opacity(0.92)) - .lineLimit(1) - .minimumScaleFactor(0.85) + VStack(spacing: 2) { + ActiveAdjustmentsView(snapshot: snapshot) + Text("Updated at: \(LAFormat.updated(snapshot))") + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundStyle(.white.opacity(0.92)) + .lineLimit(1) + .minimumScaleFactor(0.85) + } } } } @@ -688,6 +736,11 @@ private enum LAFormat { s.override ?? "—" } + static func tempTargetValue(_ s: GlucoseSnapshot) -> String? { + guard let tt = s.tempTargetMgdl, tt > 0 else { return nil } + return formatGlucoseValue(tt, unit: s.unit) + } + static func profileName(_ s: GlucoseSnapshot) -> String { s.profileName ?? "—" } diff --git a/Podfile b/Podfile index 1229b37d6..fb0cf706c 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,6 @@ target 'LoopFollow' do use_frameworks! - pod 'Charts' pod 'ShareClient', :git => 'https://github.com/loopandlearn/dexcom-share-client-swift.git', :branch => 'loopfollow' end @@ -15,54 +14,4 @@ post_install do |installer| end end end - - # Patch Charts Transformer to avoid "CGAffineTransformInvert: singular matrix" - # warnings when chart views have zero dimensions (before layout). - transformer = 'Pods/Charts/Source/Charts/Utils/Transformer.swift' - if File.exist?(transformer) - code = File.read(transformer) - original = 'return valueToPixelMatrix.inverted()' - patched = <<~SWIFT.chomp - let matrix = valueToPixelMatrix - guard matrix.a * matrix.d - matrix.b * matrix.c != 0 else { - return .identity - } - return matrix.inverted() - SWIFT - if code.include?(original) - File.write(transformer, code.sub(original, patched)) - end - end - - # Inject a privacy manifest into the Charts framework (ITMS-91061). - # Charts 4.1.0 ships no PrivacyInfo.xcprivacy; it collects no data, performs - # no tracking, and uses no required-reason APIs, so this is a negative - # declaration. Re-applied here because `pod install` regenerates the project. - charts_manifest = <<~XML - - - - - \tNSPrivacyTracking - \t - \tNSPrivacyTrackingDomains - \t - \tNSPrivacyCollectedDataTypes - \t - \tNSPrivacyAccessedAPITypes - \t - - - XML - - manifest_path = installer.sandbox.root + 'Charts/PrivacyInfo.xcprivacy' - File.write(manifest_path, charts_manifest) - - charts_target = installer.pods_project.targets.find { |t| t.name == 'Charts' } - if charts_target - file_ref = installer.pods_project.new_file(manifest_path.to_s) - already_added = charts_target.resources_build_phase.files_references.include?(file_ref) - charts_target.resources_build_phase.add_file_reference(file_ref) unless already_added - installer.pods_project.save - end end diff --git a/Podfile.lock b/Podfile.lock index 9a7875837..22a172618 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,20 +1,9 @@ PODS: - - Charts (4.1.0): - - Charts/Core (= 4.1.0) - - Charts/Core (4.1.0): - - SwiftAlgorithms (~> 1.0) - ShareClient (1.2) - - SwiftAlgorithms (1.0.0) DEPENDENCIES: - - Charts - ShareClient (from `https://github.com/loopandlearn/dexcom-share-client-swift.git`, branch `loopfollow`) -SPEC REPOS: - trunk: - - Charts - - SwiftAlgorithms - EXTERNAL SOURCES: ShareClient: :branch: loopfollow @@ -26,10 +15,8 @@ CHECKOUT OPTIONS: :git: https://github.com/loopandlearn/dexcom-share-client-swift.git SPEC CHECKSUMS: - Charts: ce0768268078eee0336f122c3c4ca248e4e204c5 ShareClient: 60b911c95e73b0ea9c5aad6d194a9c6b5f34b741 - SwiftAlgorithms: 38dda4731d19027fdeee1125f973111bf3386b53 -PODFILE CHECKSUM: d241dda366d5454279fe3ff8869458d19e1bbebb +PODFILE CHECKSUM: 617b13cfc623f15ca31e14e19086cb729b408fd7 -COCOAPODS: 1.12.1 +COCOAPODS: 1.16.2 diff --git a/Pods/Charts/LICENSE b/Pods/Charts/LICENSE deleted file mode 100644 index 8198c7a0c..000000000 --- a/Pods/Charts/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2016-2019 Daniel Cohen Gindi & Philipp Jahoda - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/Pods/Charts/PrivacyInfo.xcprivacy b/Pods/Charts/PrivacyInfo.xcprivacy deleted file mode 100644 index e08a130bc..000000000 --- a/Pods/Charts/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - NSPrivacyTracking - - NSPrivacyTrackingDomains - - NSPrivacyCollectedDataTypes - - NSPrivacyAccessedAPITypes - - - diff --git a/Pods/Charts/README.md b/Pods/Charts/README.md deleted file mode 100644 index b87cfd8e4..000000000 --- a/Pods/Charts/README.md +++ /dev/null @@ -1,239 +0,0 @@ -**Version 4.0.0**, synced to [MPAndroidChart #f6a398b](https://github.com/PhilJay/MPAndroidChart/commit/f6a398b) - -![alt tag](https://raw.github.com/danielgindi/Charts/master/Assets/feature_graphic.png) - ![Supported Platforms](https://img.shields.io/cocoapods/p/Charts.svg) [![Releases](https://img.shields.io/github/release/danielgindi/Charts.svg)](https://github.com/danielgindi/Charts/releases) [![Latest pod release](https://img.shields.io/cocoapods/v/Charts.svg)](http://cocoapods.org/pods/charts) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Build Status](https://travis-ci.org/danielgindi/Charts.svg?branch=master)](https://travis-ci.org/danielgindi/Charts) [![codecov](https://codecov.io/gh/danielgindi/Charts/branch/master/graph/badge.svg)](https://codecov.io/gh/danielgindi/Charts) -[![Join the chat at https://gitter.im/danielgindi/Charts](https://badges.gitter.im/danielgindi/Charts.svg)](https://gitter.im/danielgindi/Charts?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -### Just a heads up: Charts 3.0 has some breaking changes. Please read [the release/migration notes](https://github.com/danielgindi/Charts/releases/tag/v3.0.0). -### Another heads up: ChartsRealm is now in a [separate repo](https://github.com/danielgindi/ChartsRealm). Pods is also now `Charts` and `ChartsRealm`, instead of ~`Charts/Core`~ and ~`Charts/Realm`~ -### One more heads up: As Swift evolves, if you are not using the latest Swift compiler, you shouldn't check out the master branch. Instead, you should go to the release page and pick up whatever suits you. - -* Xcode 14 / Swift 5.7 (master branch) -* iOS >= 12.0 (Use as an **Embedded** Framework) -* tvOS >= 12.0 -* macOS >= 10.13 - -Okay so there's this beautiful library called [MPAndroidChart](https://github.com/PhilJay/MPAndroidChart) by [Philipp Jahoda](https://www.linkedin.com/in/philippjahoda) which has become very popular amongst Android developers, but there was no decent solution to create charts for iOS. - -I've chosen to write it in `Swift` as it can be highly optimized by the compiler, and can be used in both `Swift` and `ObjC` project. The demo project is written in `ObjC` to demonstrate how it works. - -**An amazing feature** of this library now, for Android, iOS, tvOS and macOS, is the time it saves you when developing for both platforms, as the learning curve is singleton- it happens only once, and the code stays very similar so developers don't have to go around and re-invent the app to produce the same output with a different library. (And that's not even considering the fact that there's not really another good choice out there currently...) - -## Having trouble running the demo? - -* `ChartsDemo/ChartsDemo.xcodeproj` is the demo project for iOS/tvOS -* `ChartsDemo-OSX/ChartsDemo-OSX.xcodeproj` is the demo project for macOS -* Make sure you are running a supported version of Xcode. - * Usually it is specified here a few lines above. - * In most cases it will be the latest Xcode version. -* Make sure that your project supports Swift 5.0 -* Optional: Run `carthage checkout` in the project folder, to fetch dependencies (i.e testing dependencies). - * If you don't have Carthage - you can get it [here](https://github.com/Carthage/Carthage/releases). - - -## Usage - -In order to correctly compile: - -1. Drag the `Charts.xcodeproj` to your project -2. Go to your target's settings, hit the "+" under the "Frameworks, Libraries, and Embedded Content" section, and select the Charts.framework -3. `@import Charts` -4. When using Swift in an ObjC project: - - You need to import your Bridging Header. Usually it is "*YourProject-Swift.h*", so in ChartsDemo it's "*ChartsDemo-Swift.h*". Do not try to actually include "*ChartsDemo-Swift.h*" in your project :-) - - (Xcode 8.1 and earlier) Under "Build Options", mark "Embedded Content Contains Swift Code" - - (Xcode 8.2+) Under "Build Options", mark "Always Embed Swift Standard Libraries" -5. When using [Realm.io](https://realm.io/): - - Note that the Realm framework is not linked with Charts - it is only there for *optional* bindings. Which means that you need to have the framework in your project, and in a compatible version to whatever is compiled with Charts. We will do our best to always compile against the latest version. - - You'll need to add `ChartsRealm` as a dependency too. - -## 3rd party tutorials -#### Video tutorials - -* [Chart in Swift - Setting Up a Basic Line Chart Using iOS Charts(Alex Nagy)](https://www.youtube.com/watch?v=mWhwe_tLNE8&list=PL_csAAO9PQ8bjzg-wxEff1Fr0Y5W1hrum&index=5) -* [Charts Framework in SwiftUI - Bar Chart (Stewart Lynch)](https://youtu.be/csd7pyfEXgw) - -#### Blog posts -* [Using Realm and Charts with Swift 3 in iOS 10 (Sami Korpela)](https://medium.com/@skoli/using-realm-and-charts-with-swift-3-in-ios-10-40c42e3838c0#.2gyymwfh8) -* [Creating a Line Chart in Swift 3 and iOS 10 (Osian Smith)](https://medium.com/@OsianSmith/creating-a-line-chart-in-swift-3-and-ios-10-2f647c95392e) -* [Beginning Set-up and Example Using Charts with Swift 3](https://github.com/annalizhaz/ChartsForSwiftBasic) -* [Creating a Radar Chart in Swift (David Piper)](https://medium.com/@HeyDaveTheDev/creating-a-radar-chart-in-swift-5791afcf92f0) -* [Plotting in IOS using Charts framework with SwiftUI (Evgeny Basisty)](https://medium.com/@zzzzbh/plotting-in-ios-using-charts-framework-with-swiftui-222034a2bea6) -* [Set Up a Basic Bar Chart Using iOS-Charts (Penny Huang)](https://medium.com/@penny-huang/swift-setting-up-a-basic-bar-chart-using-ios-charts-afd6aad96ac) -* [iOS-Charts Tutorial: Highlight Selected Value With a Custom Marker (Penny Huang)](https://medium.com/@penny-huang/swift-ios-charts-tutorial-highlight-selected-value-with-a-custom-marker-30ccbf92aa1b) - -Want your tutorial to show here? Create a PR! - -## Troubleshooting - -#### Can't compile? - -* Please note the difference between installing a compiled framework from CocoaPods or Carthage, and copying the source code. -* Please read the **Usage** section again. -* Search in the issues -* Try to politely ask in the issues section - -#### Other problems / feature requests - -* Search in the issues -* Try to politely ask in the issues section - -## CocoaPods Install - -Add `pod 'Charts'` to your Podfile. "Charts" is the name of the library. -For [Realm](https://realm.io/) support, please add `pod 'ChartsRealm'` too. - -**Note:** ~~`pod 'ios-charts'`~~ is not the correct library, and refers to a different project by someone else. - -## Carthage Install - -Charts now include Carthage prebuilt binaries. - -```carthage -github "danielgindi/Charts" == 4.1.0 -github "danielgindi/Charts" ~> 4.1.0 -``` - -In order to build the binaries for a new release, use `carthage build --no-skip-current && carthage archive Charts`. - -## Swift Package Manager Install - -Swift Package Manager - -```swift -dependencies: [ - .package(url: "https://github.com/danielgindi/Charts.git", .upToNextMajor(from: "4.1.0")) -] -``` - -## 3rd party bindings - -Xamarin (by @Flash3001): *iOS* - [GitHub](https://github.com/Flash3001/iOSCharts.Xamarin)/[NuGet](https://www.nuget.org/packages/iOSCharts/). *Android* - [GitHub](https://github.com/Flash3001/MPAndroidChart.Xamarin)/[NuGet](https://www.nuget.org/packages/MPAndroidChart/). - -## Help - -If you like what you see here, and want to support the work being done in this repository, you could: -* Contribute code, issues and pull requests -* Let people know this library exists (:fire: spread the word :fire:) -* [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=68UL6Y8KUPS96) (You can buy me a beer, or you can buy me dinner :-) - -**Note:** The author of [MPAndroidChart](https://github.com/PhilJay/MPAndroidChart) is the reason that this library exists, and is accepting [donations](https://github.com/PhilJay/MPAndroidChart#donations) on his page. He deserves them! - -Questions & Issues ------ - -If you are having questions or problems, you should: - - - Make sure you are using the latest version of the library. Check the [**release-section**](https://github.com/danielgindi/Charts/releases). - - Study the Android version's [**Documentation-Wiki**](https://github.com/PhilJay/MPAndroidChart/wiki) - - Search or open questions on [**stackoverflow**](http://stackoverflow.com/questions/tagged/ios-charts) with the `ios-charts` tag - - Search [**known issues**](https://github.com/danielgindi/Charts/issues) for your problem (open and closed) - - Create new issues (please :fire: **search known issues before** :fire:, do not create duplicate issues) - - -Features -======= - -**Core features:** - - 8 different chart types - - Scaling on both axes (with touch-gesture, axes separately or pinch-zoom) - - Dragging / Panning (with touch-gesture) - - Combined-Charts (line-, bar-, scatter-, candle-stick-, bubble-) - - Dual (separate) Axes - - Customizable Axes (both x- and y-axis) - - Highlighting values (with customizable popup-views) - - Save chart to camera-roll / export to PNG/JPEG - - Predefined color templates - - Legends (generated automatically, customizable) - - Animations (build up animations, on both x- and y-axis) - - Limit lines (providing additional information, maximums, ...) - - Fully customizable (paints, typefaces, legends, colors, background, gestures, dashed lines, ...) - - Plotting data directly from [**Realm.io**](https://realm.io) mobile database ([here](https://github.com/danielgindi/ChartsRealm)) - -**Chart types:** - -*Screenshots are currently taken from the original repository, as they render exactly the same :-)* - - - - **LineChart (with legend, simple design)** -![alt tag](https://raw.github.com/PhilJay/MPChart/master/screenshots/simpledesign_linechart4.png) - - **LineChart (with legend, simple design)** -![alt tag](https://raw.github.com/PhilJay/MPChart/master/screenshots/simpledesign_linechart3.png) - - - **LineChart (cubic lines)** -![alt tag](https://raw.github.com/PhilJay/MPChart/master/screenshots/cubiclinechart.png) - - - **LineChart (gradient fill)** -![alt tag](https://raw.github.com/PhilJay/MPAndroidChart/master/screenshots/line_chart_gradient.png) - - - **Combined-Chart (bar- and linechart in this case)** -![alt tag](https://raw.github.com/PhilJay/MPChart/master/screenshots/combined_chart.png) - - - **BarChart (with legend, simple design)** - -![alt tag](https://raw.github.com/PhilJay/MPChart/master/screenshots/simpledesign_barchart3.png) - - - **BarChart (grouped DataSets)** - -![alt tag](https://raw.github.com/PhilJay/MPChart/master/screenshots/groupedbarchart.png) - - - **Horizontal-BarChart** - -![alt tag](https://raw.github.com/PhilJay/MPChart/master/screenshots/horizontal_barchart.png) - - - - **PieChart (with selection, ...)** - -![alt tag](https://raw.github.com/PhilJay/MPAndroidChart/master/screenshots/simpledesign_piechart1.png) - - - **ScatterChart** (with squares, triangles, circles, ... and more) - -![alt tag](https://raw.github.com/PhilJay/MPAndroidChart/master/screenshots/scatterchart.png) - - - **CandleStickChart** (for financial data) - -![alt tag](https://raw.github.com/PhilJay/MPAndroidChart/master/screenshots/candlestickchart.png) - - - **BubbleChart** (area covered by bubbles indicates the value) - -![alt tag](https://raw.github.com/PhilJay/MPAndroidChart/master/screenshots/bubblechart.png) - - - **RadarChart** (spider web chart) - -![alt tag](https://raw.github.com/PhilJay/MPAndroidChart/master/screenshots/radarchart.png) - - -Documentation -======= -Currently there's no need for documentation for the iOS/tvOS/macOS version, as the API is **95% the same** as on Android. -You can read the official [MPAndroidChart](https://github.com/PhilJay/MPAndroidChart) documentation here: [**Wiki**](https://github.com/PhilJay/MPAndroidChart/wiki) - -Or you can see the Charts Demo project in both Objective-C and Swift ([**ChartsDemo-iOS**](https://github.com/danielgindi/Charts/tree/master/ChartsDemo-iOS), as well as macOS [**ChartsDemo-macOS**](https://github.com/danielgindi/Charts/tree/master/ChartsDemo-macOS)) and learn the how-tos from it. - - -Special Thanks -======= - -Goes to [@liuxuan30](https://github.com/liuxuan30), [@petester42](https://github.com/petester42) and [@AlBirdie](https://github.com/AlBirdie) for new features, bugfixes, and lots and lots of involvement in our open-sourced community! You guys are a huge help to all of those coming here with questions and issues, and I couldn't respond to all of those without you. - -### Our amazing sponsors - -[Debricked](https://debricked.com/): Use open source securely - -[![debricked](https://user-images.githubusercontent.com/4375169/73585544-25bfa800-44dd-11ea-9661-82519a125302.jpg)](https://debricked.com/) - - -License -======= -Copyright 2016 Daniel Cohen Gindi & Philipp Jahoda - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/Pods/Charts/Source/Charts/Animation/Animator.swift b/Pods/Charts/Source/Charts/Animation/Animator.swift deleted file mode 100644 index 7d0afa122..000000000 --- a/Pods/Charts/Source/Charts/Animation/Animator.swift +++ /dev/null @@ -1,281 +0,0 @@ -// -// Animator.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics -import QuartzCore - -@objc(ChartAnimatorDelegate) -public protocol AnimatorDelegate -{ - /// Called when the Animator has stepped. - func animatorUpdated(_ animator: Animator) - - /// Called when the Animator has stopped. - func animatorStopped(_ animator: Animator) -} - -@objc(ChartAnimator) -open class Animator: NSObject -{ - @objc open weak var delegate: AnimatorDelegate? - @objc open var updateBlock: (() -> Void)? - @objc open var stopBlock: (() -> Void)? - - /// the phase that is animated and influences the drawn values on the x-axis - @objc open var phaseX: Double = 1.0 - - /// the phase that is animated and influences the drawn values on the y-axis - @objc open var phaseY: Double = 1.0 - - private var _startTimeX: TimeInterval = 0.0 - private var _startTimeY: TimeInterval = 0.0 - private var _displayLink: NSUIDisplayLink? - - private var _durationX: TimeInterval = 0.0 - private var _durationY: TimeInterval = 0.0 - - private var _endTimeX: TimeInterval = 0.0 - private var _endTimeY: TimeInterval = 0.0 - private var _endTime: TimeInterval = 0.0 - - private var _enabledX: Bool = false - private var _enabledY: Bool = false - - private var _easingX: ChartEasingFunctionBlock? - private var _easingY: ChartEasingFunctionBlock? - - public override init() - { - super.init() - } - - deinit - { - stop() - } - - @objc open func stop() - { - guard _displayLink != nil else { return } - - _displayLink?.remove(from: .main, forMode: RunLoop.Mode.common) - _displayLink = nil - - _enabledX = false - _enabledY = false - - // If we stopped an animation in the middle, we do not want to leave it like this - if phaseX != 1.0 || phaseY != 1.0 - { - phaseX = 1.0 - phaseY = 1.0 - - delegate?.animatorUpdated(self) - updateBlock?() - } - - delegate?.animatorStopped(self) - stopBlock?() - } - - private func updateAnimationPhases(_ currentTime: TimeInterval) - { - if _enabledX - { - let elapsedTime: TimeInterval = currentTime - _startTimeX - let duration: TimeInterval = _durationX - var elapsed: TimeInterval = elapsedTime - if elapsed > duration - { - elapsed = duration - } - - phaseX = _easingX?(elapsed, duration) ?? elapsed / duration - } - - if _enabledY - { - let elapsedTime: TimeInterval = currentTime - _startTimeY - let duration: TimeInterval = _durationY - var elapsed: TimeInterval = elapsedTime - if elapsed > duration - { - elapsed = duration - } - - phaseY = _easingY?(elapsed, duration) ?? elapsed / duration - } - } - - @objc private func animationLoop() - { - let currentTime: TimeInterval = CACurrentMediaTime() - - updateAnimationPhases(currentTime) - - delegate?.animatorUpdated(self) - updateBlock?() - - if currentTime >= _endTime - { - stop() - } - } - - /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - yAxisDuration: duration for animating the y axis - /// - easingX: an easing function for the animation on the x axis - /// - easingY: an easing function for the animation on the y axis - @objc open func animate(xAxisDuration: TimeInterval, yAxisDuration: TimeInterval, easingX: ChartEasingFunctionBlock?, easingY: ChartEasingFunctionBlock?) - { - stop() - - _startTimeX = CACurrentMediaTime() - _startTimeY = _startTimeX - _durationX = xAxisDuration - _durationY = yAxisDuration - _endTimeX = _startTimeX + xAxisDuration - _endTimeY = _startTimeY + yAxisDuration - _endTime = _endTimeX > _endTimeY ? _endTimeX : _endTimeY - _enabledX = xAxisDuration > 0.0 - _enabledY = yAxisDuration > 0.0 - - _easingX = easingX - _easingY = easingY - - // Take care of the first frame if rendering is already scheduled... - updateAnimationPhases(_startTimeX) - - if _enabledX || _enabledY - { - _displayLink = NSUIDisplayLink(target: self, selector: #selector(animationLoop)) - _displayLink?.add(to: RunLoop.main, forMode: RunLoop.Mode.common) - } - } - - /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - yAxisDuration: duration for animating the y axis - /// - easingOptionX: the easing function for the animation on the x axis - /// - easingOptionY: the easing function for the animation on the y axis - @objc open func animate(xAxisDuration: TimeInterval, yAxisDuration: TimeInterval, easingOptionX: ChartEasingOption, easingOptionY: ChartEasingOption) - { - animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easingX: easingFunctionFromOption(easingOptionX), easingY: easingFunctionFromOption(easingOptionY)) - } - - /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - yAxisDuration: duration for animating the y axis - /// - easing: an easing function for the animation - @objc open func animate(xAxisDuration: TimeInterval, yAxisDuration: TimeInterval, easing: ChartEasingFunctionBlock?) - { - animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easingX: easing, easingY: easing) - } - - /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - yAxisDuration: duration for animating the y axis - /// - easingOption: the easing function for the animation - @objc open func animate(xAxisDuration: TimeInterval, yAxisDuration: TimeInterval, easingOption: ChartEasingOption = .easeInOutSine) - { - animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easing: easingFunctionFromOption(easingOption)) - } - - /// Animates the drawing / rendering of the chart the x-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - easing: an easing function for the animation - @objc open func animate(xAxisDuration: TimeInterval, easing: ChartEasingFunctionBlock?) - { - _startTimeX = CACurrentMediaTime() - _durationX = xAxisDuration - _endTimeX = _startTimeX + xAxisDuration - _endTime = _endTimeX > _endTimeY ? _endTimeX : _endTimeY - _enabledX = xAxisDuration > 0.0 - - _easingX = easing - - // Take care of the first frame if rendering is already scheduled... - updateAnimationPhases(_startTimeX) - - if _enabledX || _enabledY, - _displayLink == nil - { - _displayLink = NSUIDisplayLink(target: self, selector: #selector(animationLoop)) - _displayLink?.add(to: .main, forMode: RunLoop.Mode.common) - } - } - - /// Animates the drawing / rendering of the chart the x-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - easingOption: the easing function for the animation - @objc open func animate(xAxisDuration: TimeInterval, easingOption: ChartEasingOption = .easeInOutSine) - { - animate(xAxisDuration: xAxisDuration, easing: easingFunctionFromOption(easingOption)) - } - - /// Animates the drawing / rendering of the chart the y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - yAxisDuration: duration for animating the y axis - /// - easing: an easing function for the animation - @objc open func animate(yAxisDuration: TimeInterval, easing: ChartEasingFunctionBlock?) - { - _startTimeY = CACurrentMediaTime() - _durationY = yAxisDuration - _endTimeY = _startTimeY + yAxisDuration - _endTime = _endTimeX > _endTimeY ? _endTimeX : _endTimeY - _enabledY = yAxisDuration > 0.0 - - _easingY = easing - - // Take care of the first frame if rendering is already scheduled... - updateAnimationPhases(_startTimeY) - - if _enabledX || _enabledY, - _displayLink == nil - { - _displayLink = NSUIDisplayLink(target: self, selector: #selector(animationLoop)) - _displayLink?.add(to: .main, forMode: RunLoop.Mode.common) - } - } - - /// Animates the drawing / rendering of the chart the y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - yAxisDuration: duration for animating the y axis - /// - easingOption: the easing function for the animation - @objc open func animate(yAxisDuration: TimeInterval, easingOption: ChartEasingOption = .easeInOutSine) - { - animate(yAxisDuration: yAxisDuration, easing: easingFunctionFromOption(easingOption)) - } -} diff --git a/Pods/Charts/Source/Charts/Animation/ChartAnimationEasing.swift b/Pods/Charts/Source/Charts/Animation/ChartAnimationEasing.swift deleted file mode 100644 index a6453d9e0..000000000 --- a/Pods/Charts/Source/Charts/Animation/ChartAnimationEasing.swift +++ /dev/null @@ -1,394 +0,0 @@ -// -// ChartAnimationUtils.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public enum ChartEasingOption: Int -{ - case linear - case easeInQuad - case easeOutQuad - case easeInOutQuad - case easeInCubic - case easeOutCubic - case easeInOutCubic - case easeInQuart - case easeOutQuart - case easeInOutQuart - case easeInQuint - case easeOutQuint - case easeInOutQuint - case easeInSine - case easeOutSine - case easeInOutSine - case easeInExpo - case easeOutExpo - case easeInOutExpo - case easeInCirc - case easeOutCirc - case easeInOutCirc - case easeInElastic - case easeOutElastic - case easeInOutElastic - case easeInBack - case easeOutBack - case easeInOutBack - case easeInBounce - case easeOutBounce - case easeInOutBounce -} - -public typealias ChartEasingFunctionBlock = ((_ elapsed: TimeInterval, _ duration: TimeInterval) -> Double) - -internal func easingFunctionFromOption(_ easing: ChartEasingOption) -> ChartEasingFunctionBlock -{ - switch easing - { - case .linear: - return EasingFunctions.Linear - case .easeInQuad: - return EasingFunctions.EaseInQuad - case .easeOutQuad: - return EasingFunctions.EaseOutQuad - case .easeInOutQuad: - return EasingFunctions.EaseInOutQuad - case .easeInCubic: - return EasingFunctions.EaseInCubic - case .easeOutCubic: - return EasingFunctions.EaseOutCubic - case .easeInOutCubic: - return EasingFunctions.EaseInOutCubic - case .easeInQuart: - return EasingFunctions.EaseInQuart - case .easeOutQuart: - return EasingFunctions.EaseOutQuart - case .easeInOutQuart: - return EasingFunctions.EaseInOutQuart - case .easeInQuint: - return EasingFunctions.EaseInQuint - case .easeOutQuint: - return EasingFunctions.EaseOutQuint - case .easeInOutQuint: - return EasingFunctions.EaseInOutQuint - case .easeInSine: - return EasingFunctions.EaseInSine - case .easeOutSine: - return EasingFunctions.EaseOutSine - case .easeInOutSine: - return EasingFunctions.EaseInOutSine - case .easeInExpo: - return EasingFunctions.EaseInExpo - case .easeOutExpo: - return EasingFunctions.EaseOutExpo - case .easeInOutExpo: - return EasingFunctions.EaseInOutExpo - case .easeInCirc: - return EasingFunctions.EaseInCirc - case .easeOutCirc: - return EasingFunctions.EaseOutCirc - case .easeInOutCirc: - return EasingFunctions.EaseInOutCirc - case .easeInElastic: - return EasingFunctions.EaseInElastic - case .easeOutElastic: - return EasingFunctions.EaseOutElastic - case .easeInOutElastic: - return EasingFunctions.EaseInOutElastic - case .easeInBack: - return EasingFunctions.EaseInBack - case .easeOutBack: - return EasingFunctions.EaseOutBack - case .easeInOutBack: - return EasingFunctions.EaseInOutBack - case .easeInBounce: - return EasingFunctions.EaseInBounce - case .easeOutBounce: - return EasingFunctions.EaseOutBounce - case .easeInOutBounce: - return EasingFunctions.EaseInOutBounce - } -} - -internal struct EasingFunctions -{ - internal static let Linear = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in return Double(elapsed / duration) } - - internal static let EaseInQuad = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - return position * position - } - - internal static let EaseOutQuad = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - return -position * (position - 2.0) - } - - internal static let EaseInOutQuad = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / (duration / 2.0)) - if position < 1.0 - { - return 0.5 * position * position - } - - return -0.5 * ((position - 1.0) * (position - 3.0) - 1.0) - } - - internal static let EaseInCubic = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - return position * position * position - } - - internal static let EaseOutCubic = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - position -= 1.0 - return (position * position * position + 1.0) - } - - internal static let EaseInOutCubic = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / (duration / 2.0)) - if position < 1.0 - { - return 0.5 * position * position * position - } - position -= 2.0 - return 0.5 * (position * position * position + 2.0) - } - - internal static let EaseInQuart = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - return position * position * position * position - } - - internal static let EaseOutQuart = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - position -= 1.0 - return -(position * position * position * position - 1.0) - } - - internal static let EaseInOutQuart = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / (duration / 2.0)) - if position < 1.0 - { - return 0.5 * position * position * position * position - } - position -= 2.0 - return -0.5 * (position * position * position * position - 2.0) - } - - internal static let EaseInQuint = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - return position * position * position * position * position - } - - internal static let EaseOutQuint = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - position -= 1.0 - return (position * position * position * position * position + 1.0) - } - - internal static let EaseInOutQuint = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / (duration / 2.0)) - if position < 1.0 - { - return 0.5 * position * position * position * position * position - } - else - { - position -= 2.0 - return 0.5 * (position * position * position * position * position + 2.0) - } - } - - internal static let EaseInSine = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position: TimeInterval = elapsed / duration - return Double( -cos(position * Double.pi / 2) + 1.0 ) - } - - internal static let EaseOutSine = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position: TimeInterval = elapsed / duration - return Double( sin(position * Double.pi / 2) ) - } - - internal static let EaseInOutSine = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position: TimeInterval = elapsed / duration - return Double( -0.5 * (cos(Double.pi * position) - 1.0) ) - } - - internal static let EaseInExpo = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - return (elapsed == 0) ? 0.0 : Double(pow(2.0, 10.0 * (elapsed / duration - 1.0))) - } - - internal static let EaseOutExpo = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - return (elapsed == duration) ? 1.0 : (-Double(pow(2.0, -10.0 * elapsed / duration)) + 1.0) - } - - internal static let EaseInOutExpo = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - if elapsed == 0 - { - return 0.0 - } - if elapsed == duration - { - return 1.0 - } - - var position: TimeInterval = elapsed / (duration / 2.0) - if position < 1.0 - { - return Double( 0.5 * pow(2.0, 10.0 * (position - 1.0)) ) - } - - position = position - 1.0 - return Double( 0.5 * (-pow(2.0, -10.0 * position) + 2.0) ) - } - - internal static let EaseInCirc = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - return -(Double(sqrt(1.0 - position * position)) - 1.0) - } - - internal static let EaseOutCirc = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position = Double(elapsed / duration) - position -= 1.0 - return Double( sqrt(1 - position * position) ) - } - - internal static let EaseInOutCirc = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position: TimeInterval = elapsed / (duration / 2.0) - if position < 1.0 - { - return Double( -0.5 * (sqrt(1.0 - position * position) - 1.0) ) - } - position -= 2.0 - return Double( 0.5 * (sqrt(1.0 - position * position) + 1.0) ) - } - - internal static let EaseInElastic = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - if elapsed == 0.0 - { - return 0.0 - } - - var position: TimeInterval = elapsed / duration - if position == 1.0 - { - return 1.0 - } - - var p = duration * 0.3 - var s = p / (2.0 * Double.pi) * asin(1.0) - position -= 1.0 - return Double( -(pow(2.0, 10.0 * position) * sin((position * duration - s) * (2.0 * Double.pi) / p)) ) - } - - internal static let EaseOutElastic = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - if elapsed == 0.0 - { - return 0.0 - } - - var position: TimeInterval = elapsed / duration - if position == 1.0 - { - return 1.0 - } - - var p = duration * 0.3 - var s = p / (2.0 * Double.pi) * asin(1.0) - return Double( pow(2.0, -10.0 * position) * sin((position * duration - s) * (2.0 * Double.pi) / p) + 1.0 ) - } - - internal static let EaseInOutElastic = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - if elapsed == 0.0 - { - return 0.0 - } - - var position: TimeInterval = elapsed / (duration / 2.0) - if position == 2.0 - { - return 1.0 - } - - var p = duration * (0.3 * 1.5) - var s = p / (2.0 * Double.pi) * asin(1.0) - if position < 1.0 - { - position -= 1.0 - return Double( -0.5 * (pow(2.0, 10.0 * position) * sin((position * duration - s) * (2.0 * Double.pi) / p)) ) - } - position -= 1.0 - return Double( pow(2.0, -10.0 * position) * sin((position * duration - s) * (2.0 * Double.pi) / p) * 0.5 + 1.0 ) - } - - internal static let EaseInBack = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - let s: TimeInterval = 1.70158 - var position: TimeInterval = elapsed / duration - return Double( position * position * ((s + 1.0) * position - s) ) - } - - internal static let EaseOutBack = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - let s: TimeInterval = 1.70158 - var position: TimeInterval = elapsed / duration - position -= 1.0 - return Double( position * position * ((s + Double(1.0)) * position + s) + Double(1.0) ) - } - - internal static let EaseInOutBack = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var s: TimeInterval = 1.70158 - var position: TimeInterval = elapsed / (duration / 2.0) - if position < 1.0 - { - s *= 1.525 - return Double( 0.5 * (position * position * ((s + 1.0) * position - s)) ) - } - s *= 1.525 - position -= 2.0 - return Double( 0.5 * (position * position * ((s + 1.0) * position + s) + 2.0) ) - } - - internal static let EaseInBounce = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - return 1.0 - EaseOutBounce(duration - elapsed, duration) - } - - internal static let EaseOutBounce = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - var position: TimeInterval = elapsed / duration - if position < (1.0 / 2.75) - { - return Double( 7.5625 * position * position ) - } - else if position < (2.0 / 2.75) - { - position -= (1.5 / 2.75) - return Double( 7.5625 * position * position + 0.75 ) - } - else if position < (2.5 / 2.75) - { - position -= (2.25 / 2.75) - return Double( 7.5625 * position * position + 0.9375 ) - } - else - { - position -= (2.625 / 2.75) - return Double( 7.5625 * position * position + 0.984375 ) - } - } - - internal static let EaseInOutBounce = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in - if elapsed < (duration / 2.0) - { - return EaseInBounce(elapsed * 2.0, duration) * 0.5 - } - return EaseOutBounce(elapsed * 2.0 - duration, duration) * 0.5 + 0.5 - } -} diff --git a/Pods/Charts/Source/Charts/Charts/BarChartView.swift b/Pods/Charts/Source/Charts/Charts/BarChartView.swift deleted file mode 100644 index 92332774d..000000000 --- a/Pods/Charts/Source/Charts/Charts/BarChartView.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// BarChartView.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// Chart that draws bars. -open class BarChartView: BarLineChartViewBase, BarChartDataProvider -{ - /// if set to true, all values are drawn above their bars, instead of below their top - private var _drawValueAboveBarEnabled = true - - /// if set to true, a grey area is drawn behind each bar that indicates the maximum value - private var _drawBarShadowEnabled = false - - internal override func initialize() - { - super.initialize() - - renderer = BarChartRenderer(dataProvider: self, animator: chartAnimator, viewPortHandler: viewPortHandler) - - self.highlighter = BarHighlighter(chart: self) - - self.xAxis.spaceMin = 0.5 - self.xAxis.spaceMax = 0.5 - } - - internal override func calcMinMax() - { - guard let data = self.data as? BarChartData - else { return } - - if fitBars - { - xAxis.calculate( - min: data.xMin - data.barWidth / 2.0, - max: data.xMax + data.barWidth / 2.0) - } - else - { - xAxis.calculate(min: data.xMin, max: data.xMax) - } - - // calculate axis range (min / max) according to provided data - leftAxis.calculate( - min: data.getYMin(axis: .left), - max: data.getYMax(axis: .left)) - rightAxis.calculate( - min: data.getYMin(axis: .right), - max: data.getYMax(axis: .right)) - } - - /// - Returns: The Highlight object (contains x-index and DataSet index) of the selected value at the given touch point inside the BarChart. - open override func getHighlightByTouchPoint(_ pt: CGPoint) -> Highlight? - { - if data === nil - { - Swift.print("Can't select by touch. No data set.") - return nil - } - - guard let h = self.highlighter?.getHighlight(x: pt.x, y: pt.y) - else { return nil } - - if !isHighlightFullBarEnabled { return h } - - // For isHighlightFullBarEnabled, remove stackIndex - return Highlight( - x: h.x, y: h.y, - xPx: h.xPx, yPx: h.yPx, - dataIndex: h.dataIndex, - dataSetIndex: h.dataSetIndex, - stackIndex: -1, - axis: h.axis) - } - - /// - Returns: The bounding box of the specified Entry in the specified DataSet. Returns null if the Entry could not be found in the charts data. - @objc open func getBarBounds(entry e: BarChartDataEntry) -> CGRect - { - guard let - data = data as? BarChartData, - let set = data.getDataSetForEntry(e) as? BarChartDataSetProtocol - else { return .null } - - let y = e.y - let x = e.x - - let barWidth = data.barWidth - - let left = x - barWidth / 2.0 - let right = x + barWidth / 2.0 - let top = y >= 0.0 ? y : 0.0 - let bottom = y <= 0.0 ? y : 0.0 - - var bounds = CGRect(x: left, y: top, width: right - left, height: bottom - top) - - getTransformer(forAxis: set.axisDependency).rectValueToPixel(&bounds) - - return bounds - } - - /// Groups all BarDataSet objects this data object holds together by modifying the x-value of their entries. - /// Previously set x-values of entries will be overwritten. Leaves space between bars and groups as specified by the parameters. - /// Calls `notifyDataSetChanged()` afterwards. - /// - /// - Parameters: - /// - fromX: the starting point on the x-axis where the grouping should begin - /// - groupSpace: the space between groups of bars in values (not pixels) e.g. 0.8f for bar width 1f - /// - barSpace: the space between individual bars in values (not pixels) e.g. 0.1f for bar width 1f - @objc open func groupBars(fromX: Double, groupSpace: Double, barSpace: Double) - { - guard let barData = self.barData - else - { - Swift.print("You need to set data for the chart before grouping bars.", terminator: "\n") - return - } - - barData.groupBars(fromX: fromX, groupSpace: groupSpace, barSpace: barSpace) - notifyDataSetChanged() - } - - /// Highlights the value at the given x-value in the given DataSet. Provide -1 as the dataSetIndex to undo all highlighting. - /// - /// - Parameters: - /// - x: - /// - dataSetIndex: - /// - stackIndex: the index inside the stack - only relevant for stacked entries - @objc open func highlightValue(x: Double, dataSetIndex: Int, stackIndex: Int) - { - highlightValue(Highlight(x: x, dataSetIndex: dataSetIndex, stackIndex: stackIndex)) - } - - // MARK: Accessors - - /// if set to true, all values are drawn above their bars, instead of below their top - @objc open var drawValueAboveBarEnabled: Bool - { - get { return _drawValueAboveBarEnabled } - set - { - _drawValueAboveBarEnabled = newValue - setNeedsDisplay() - } - } - - /// if set to true, a grey area is drawn behind each bar that indicates the maximum value - @objc open var drawBarShadowEnabled: Bool - { - get { return _drawBarShadowEnabled } - set - { - _drawBarShadowEnabled = newValue - setNeedsDisplay() - } - } - - /// Adds half of the bar width to each side of the x-axis range in order to allow the bars of the barchart to be fully displayed. - /// **default**: false - @objc open var fitBars = false - - /// Set this to `true` to make the highlight operation full-bar oriented, `false` to make it highlight single values (relevant only for stacked). - /// If enabled, highlighting operations will highlight the whole bar, even if only a single stack entry was tapped. - @objc open var highlightFullBarEnabled: Bool = false - - /// `true` the highlight is be full-bar oriented, `false` ifsingle-value - open var isHighlightFullBarEnabled: Bool { return highlightFullBarEnabled } - - // MARK: - BarChartDataProvider - - open var barData: BarChartData? { return data as? BarChartData } - - /// `true` if drawing values above bars is enabled, `false` ifnot - open var isDrawValueAboveBarEnabled: Bool { return drawValueAboveBarEnabled } - - /// `true` if drawing shadows (maxvalue) for each bar is enabled, `false` ifnot - open var isDrawBarShadowEnabled: Bool { return drawBarShadowEnabled } -} diff --git a/Pods/Charts/Source/Charts/Charts/BarLineChartViewBase.swift b/Pods/Charts/Source/Charts/Charts/BarLineChartViewBase.swift deleted file mode 100644 index 343cd6832..000000000 --- a/Pods/Charts/Source/Charts/Charts/BarLineChartViewBase.swift +++ /dev/null @@ -1,1958 +0,0 @@ -// -// BarLineChartViewBase.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import CoreGraphics -import Foundation - -#if canImport(UIKit) -import UIKit -#elseif canImport(AppKit) -import AppKit -#endif - -/// Base-class of LineChart, BarChart, ScatterChart and CandleStickChart. -open class BarLineChartViewBase: ChartViewBase, BarLineScatterCandleBubbleChartDataProvider, NSUIGestureRecognizerDelegate -{ - /// the maximum number of entries to which values will be drawn - /// (entry numbers greater than this value will cause value-labels to disappear) - internal var _maxVisibleCount = 100 - - /// flag that indicates if auto scaling on the y axis is enabled - private var _autoScaleMinMaxEnabled = false - - private var _pinchZoomEnabled = false - private var _doubleTapToZoomEnabled = true - private var _dragXEnabled = true - private var _dragYEnabled = true - - private var _scaleXEnabled = true - private var _scaleYEnabled = true - - /// the color for the background of the chart-drawing area (everything behind the grid lines). - @objc open var gridBackgroundColor = NSUIColor(red: 240/255.0, green: 240/255.0, blue: 240/255.0, alpha: 1.0) - - @objc open var borderColor = NSUIColor.black - @objc open var borderLineWidth: CGFloat = 1.0 - - /// flag indicating if the grid background should be drawn or not - @objc open var drawGridBackgroundEnabled = false - - /// When enabled, the borders rectangle will be rendered. - /// If this is enabled, there is no point drawing the axis-lines of x- and y-axis. - @objc open var drawBordersEnabled = false - - /// When enabled, the values will be clipped to contentRect, otherwise they can bleed outside the content rect. - @objc open var clipValuesToContentEnabled: Bool = false - - /// When disabled, the data and/or highlights will not be clipped to contentRect. Disabling this option can - /// be useful, when the data lies fully within the content rect, but is drawn in such a way (such as thick lines) - /// that there is unwanted clipping. - @objc open var clipDataToContentEnabled: Bool = true - - /// Sets the minimum offset (padding) around the chart, defaults to 10 - @objc open var minOffset = CGFloat(10.0) - - /// Sets whether the chart should keep its position (zoom / scroll) after a rotation (orientation change) - /// **default**: false - @objc open var keepPositionOnRotation: Bool = false - - /// The left y-axis object. In the horizontal bar-chart, this is the - /// top axis. - @objc open internal(set) var leftAxis = YAxis(position: .left) - - /// The right y-axis object. In the horizontal bar-chart, this is the - /// bottom axis. - @objc open internal(set) var rightAxis = YAxis(position: .right) - - /// The left Y axis renderer. This is a read-write property so you can set your own custom renderer here. - /// **default**: An instance of YAxisRenderer - @objc open lazy var leftYAxisRenderer = YAxisRenderer(viewPortHandler: viewPortHandler, axis: leftAxis, transformer: _leftAxisTransformer) - - /// The right Y axis renderer. This is a read-write property so you can set your own custom renderer here. - /// **default**: An instance of YAxisRenderer - @objc open lazy var rightYAxisRenderer = YAxisRenderer(viewPortHandler: viewPortHandler, axis: rightAxis, transformer: _rightAxisTransformer) - - internal var _leftAxisTransformer: Transformer! - internal var _rightAxisTransformer: Transformer! - - /// The X axis renderer. This is a read-write property so you can set your own custom renderer here. - /// **default**: An instance of XAxisRenderer - @objc open lazy var xAxisRenderer = XAxisRenderer(viewPortHandler: viewPortHandler, axis: xAxis, transformer: _leftAxisTransformer) - - internal var _tapGestureRecognizer: NSUITapGestureRecognizer! - internal var _doubleTapGestureRecognizer: NSUITapGestureRecognizer! - #if !os(tvOS) - internal var _pinchGestureRecognizer: NSUIPinchGestureRecognizer! - #endif - internal var _panGestureRecognizer: NSUIPanGestureRecognizer! - - /// flag that indicates if a custom viewport offset has been set - private var _customViewPortEnabled = false - - public override init(frame: CGRect) - { - super.init(frame: frame) - } - - public required init?(coder aDecoder: NSCoder) - { - super.init(coder: aDecoder) - } - - deinit - { - stopDeceleration() - } - - internal override func initialize() - { - super.initialize() - - _leftAxisTransformer = Transformer(viewPortHandler: viewPortHandler) - _rightAxisTransformer = Transformer(viewPortHandler: viewPortHandler) - - self.highlighter = ChartHighlighter(chart: self) - - _tapGestureRecognizer = NSUITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))) - _doubleTapGestureRecognizer = NSUITapGestureRecognizer(target: self, action: #selector(doubleTapGestureRecognized(_:))) - _doubleTapGestureRecognizer.nsuiNumberOfTapsRequired = 2 - _panGestureRecognizer = NSUIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:))) - - _panGestureRecognizer.delegate = self - - self.addGestureRecognizer(_tapGestureRecognizer) - self.addGestureRecognizer(_doubleTapGestureRecognizer) - self.addGestureRecognizer(_panGestureRecognizer) - - _doubleTapGestureRecognizer.isEnabled = _doubleTapToZoomEnabled - _panGestureRecognizer.isEnabled = _dragXEnabled || _dragYEnabled - - #if !os(tvOS) - _pinchGestureRecognizer = NSUIPinchGestureRecognizer(target: self, action: #selector(BarLineChartViewBase.pinchGestureRecognized(_:))) - _pinchGestureRecognizer.delegate = self - self.addGestureRecognizer(_pinchGestureRecognizer) - _pinchGestureRecognizer.isEnabled = _pinchZoomEnabled || _scaleXEnabled || _scaleYEnabled - #endif - } - - open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) - { - // Saving current position of chart. - var oldPoint: CGPoint? - if (keepPositionOnRotation && (keyPath == "frame" || keyPath == "bounds")) - { - oldPoint = viewPortHandler.contentRect.origin - getTransformer(forAxis: .left).pixelToValues(&oldPoint!) - } - - // Superclass transforms chart. - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) - - // Restoring old position of chart - if var newPoint = oldPoint , keepPositionOnRotation - { - getTransformer(forAxis: .left).pointValueToPixel(&newPoint) - viewPortHandler.centerViewPort(pt: newPoint, chart: self) - } - else - { - viewPortHandler.refresh(newMatrix: viewPortHandler.touchMatrix, chart: self, invalidate: true) - } - } - - open override func draw(_ rect: CGRect) - { - super.draw(rect) - - guard data != nil, let renderer = renderer else { return } - - let optionalContext = NSUIGraphicsGetCurrentContext() - guard let context = optionalContext else { return } - - // execute all drawing commands - drawGridBackground(context: context) - - - if _autoScaleMinMaxEnabled - { - autoScale() - } - - if leftAxis.isEnabled - { - leftYAxisRenderer.computeAxis(min: leftAxis._axisMinimum, max: leftAxis._axisMaximum, inverted: leftAxis.isInverted) - } - - if rightAxis.isEnabled - { - rightYAxisRenderer.computeAxis(min: rightAxis._axisMinimum, max: rightAxis._axisMaximum, inverted: rightAxis.isInverted) - } - - if xAxis.isEnabled - { - xAxisRenderer.computeAxis(min: xAxis._axisMinimum, max: xAxis._axisMaximum, inverted: false) - } - - xAxisRenderer.renderAxisLine(context: context) - leftYAxisRenderer.renderAxisLine(context: context) - rightYAxisRenderer.renderAxisLine(context: context) - - // The renderers are responsible for clipping, to account for line-width center etc. - if xAxis.drawGridLinesBehindDataEnabled - { - xAxisRenderer.renderGridLines(context: context) - leftYAxisRenderer.renderGridLines(context: context) - rightYAxisRenderer.renderGridLines(context: context) - } - - if xAxis.isEnabled && xAxis.isDrawLimitLinesBehindDataEnabled - { - xAxisRenderer.renderLimitLines(context: context) - } - - if leftAxis.isEnabled && leftAxis.isDrawLimitLinesBehindDataEnabled - { - leftYAxisRenderer.renderLimitLines(context: context) - } - - if rightAxis.isEnabled && rightAxis.isDrawLimitLinesBehindDataEnabled - { - rightYAxisRenderer.renderLimitLines(context: context) - } - - context.saveGState() - - // make sure the data cannot be drawn outside the content-rect - if clipDataToContentEnabled { - context.clip(to: viewPortHandler.contentRect) - } - - renderer.drawData(context: context) - - // The renderers are responsible for clipping, to account for line-width center etc. - if !xAxis.drawGridLinesBehindDataEnabled - { - xAxisRenderer.renderGridLines(context: context) - leftYAxisRenderer.renderGridLines(context: context) - rightYAxisRenderer.renderGridLines(context: context) - } - - // if highlighting is enabled - if (valuesToHighlight()) - { - renderer.drawHighlighted(context: context, indices: highlighted) - } - - context.restoreGState() - - renderer.drawExtras(context: context) - - if xAxis.isEnabled && !xAxis.isDrawLimitLinesBehindDataEnabled - { - xAxisRenderer.renderLimitLines(context: context) - } - - if leftAxis.isEnabled && !leftAxis.isDrawLimitLinesBehindDataEnabled - { - leftYAxisRenderer.renderLimitLines(context: context) - } - - if rightAxis.isEnabled && !rightAxis.isDrawLimitLinesBehindDataEnabled - { - rightYAxisRenderer.renderLimitLines(context: context) - } - - xAxisRenderer.renderAxisLabels(context: context) - leftYAxisRenderer.renderAxisLabels(context: context) - rightYAxisRenderer.renderAxisLabels(context: context) - - if clipValuesToContentEnabled - { - context.saveGState() - context.clip(to: viewPortHandler.contentRect) - - renderer.drawValues(context: context) - - context.restoreGState() - } - else - { - renderer.drawValues(context: context) - } - - legendRenderer.renderLegend(context: context) - - drawDescription(in: context) - - drawMarkers(context: context) - } - - private var _autoScaleLastLowestVisibleX: Double? - private var _autoScaleLastHighestVisibleX: Double? - - /// Performs auto scaling of the axis by recalculating the minimum and maximum y-values based on the entries currently in view. - internal func autoScale() - { - guard let data = data - else { return } - - data.calcMinMaxY(fromX: self.lowestVisibleX, toX: self.highestVisibleX) - - xAxis.calculate(min: data.xMin, max: data.xMax) - - // calculate axis range (min / max) according to provided data - - if leftAxis.isEnabled - { - leftAxis.calculate(min: data.getYMin(axis: .left), max: data.getYMax(axis: .left)) - } - - if rightAxis.isEnabled - { - rightAxis.calculate(min: data.getYMin(axis: .right), max: data.getYMax(axis: .right)) - } - - calculateOffsets() - } - - internal func prepareValuePxMatrix() - { - _rightAxisTransformer.prepareMatrixValuePx(chartXMin: xAxis._axisMinimum, deltaX: CGFloat(xAxis.axisRange), deltaY: CGFloat(rightAxis.axisRange), chartYMin: rightAxis._axisMinimum) - _leftAxisTransformer.prepareMatrixValuePx(chartXMin: xAxis._axisMinimum, deltaX: CGFloat(xAxis.axisRange), deltaY: CGFloat(leftAxis.axisRange), chartYMin: leftAxis._axisMinimum) - } - - internal func prepareOffsetMatrix() - { - _rightAxisTransformer.prepareMatrixOffset(inverted: rightAxis.isInverted) - _leftAxisTransformer.prepareMatrixOffset(inverted: leftAxis.isInverted) - } - - open override func notifyDataSetChanged() - { - renderer?.initBuffers() - - calcMinMax() - - leftYAxisRenderer.computeAxis(min: leftAxis._axisMinimum, max: leftAxis._axisMaximum, inverted: leftAxis.isInverted) - rightYAxisRenderer.computeAxis(min: rightAxis._axisMinimum, max: rightAxis._axisMaximum, inverted: rightAxis.isInverted) - - if let data = data - { - xAxisRenderer.computeAxis( - min: xAxis._axisMinimum, - max: xAxis._axisMaximum, - inverted: false) - - legendRenderer.computeLegend(data: data) - } - - calculateOffsets() - - setNeedsDisplay() - } - - internal override func calcMinMax() - { - // calculate / set x-axis range - xAxis.calculate(min: data?.xMin ?? 0.0, max: data?.xMax ?? 0.0) - - // calculate axis range (min / max) according to provided data - leftAxis.calculate(min: data?.getYMin(axis: .left) ?? 0.0, max: data?.getYMax(axis: .left) ?? 0.0) - rightAxis.calculate(min: data?.getYMin(axis: .right) ?? 0.0, max: data?.getYMax(axis: .right) ?? 0.0) - } - - internal func calculateLegendOffsets(offsetLeft: inout CGFloat, offsetTop: inout CGFloat, offsetRight: inout CGFloat, offsetBottom: inout CGFloat) - { - // setup offsets for legend - if legend.isEnabled, !legend.drawInside - { - switch legend.orientation - { - case .vertical: - - switch legend.horizontalAlignment - { - case .left: - offsetLeft += min(legend.neededWidth, viewPortHandler.chartWidth * legend.maxSizePercent) + legend.xOffset - - case .right: - offsetRight += min(legend.neededWidth, viewPortHandler.chartWidth * legend.maxSizePercent) + legend.xOffset - - case .center: - - switch legend.verticalAlignment - { - case .top: - offsetTop += min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) + legend.yOffset - - case .bottom: - offsetBottom += min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) + legend.yOffset - - default: - break - } - } - - case .horizontal: - - switch legend.verticalAlignment - { - case .top: - offsetTop += min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) + legend.yOffset - - case .bottom: - offsetBottom += min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) + legend.yOffset - - default: - break - } - } - } - } - - internal override func calculateOffsets() - { - if !_customViewPortEnabled - { - var offsetLeft = CGFloat(0.0) - var offsetRight = CGFloat(0.0) - var offsetTop = CGFloat(0.0) - var offsetBottom = CGFloat(0.0) - - calculateLegendOffsets(offsetLeft: &offsetLeft, - offsetTop: &offsetTop, - offsetRight: &offsetRight, - offsetBottom: &offsetBottom) - - // offsets for y-labels - if leftAxis.needsOffset - { - offsetLeft += leftAxis.requiredSize().width - } - - if rightAxis.needsOffset - { - offsetRight += rightAxis.requiredSize().width - } - - if xAxis.isEnabled && xAxis.isDrawLabelsEnabled - { - let xlabelheight = xAxis.labelRotatedHeight + xAxis.yOffset - - // offsets for x-labels - if xAxis.labelPosition == .bottom - { - offsetBottom += xlabelheight - } - else if xAxis.labelPosition == .top - { - offsetTop += xlabelheight - } - else if xAxis.labelPosition == .bothSided - { - offsetBottom += xlabelheight - offsetTop += xlabelheight - } - } - - offsetTop += self.extraTopOffset - offsetRight += self.extraRightOffset - offsetBottom += self.extraBottomOffset - offsetLeft += self.extraLeftOffset - - viewPortHandler.restrainViewPort( - offsetLeft: max(self.minOffset, offsetLeft), - offsetTop: max(self.minOffset, offsetTop), - offsetRight: max(self.minOffset, offsetRight), - offsetBottom: max(self.minOffset, offsetBottom)) - } - - prepareOffsetMatrix() - prepareValuePxMatrix() - } - - /// draws the grid background - internal func drawGridBackground(context: CGContext) - { - if drawGridBackgroundEnabled || drawBordersEnabled - { - context.saveGState() - } - - if drawGridBackgroundEnabled - { - // draw the grid background - context.setFillColor(gridBackgroundColor.cgColor) - context.fill(viewPortHandler.contentRect) - } - - if drawBordersEnabled - { - context.setLineWidth(borderLineWidth) - context.setStrokeColor(borderColor.cgColor) - context.stroke(viewPortHandler.contentRect) - } - - if drawGridBackgroundEnabled || drawBordersEnabled - { - context.restoreGState() - } - } - - // MARK: - Gestures - - private enum GestureScaleAxis - { - case both - case x - case y - } - - private var _isDragging = false - private var _isScaling = false - private var _gestureScaleAxis = GestureScaleAxis.both - private var _closestDataSetToTouch: ChartDataSetProtocol! - private var _panGestureReachedEdge: Bool = false - private weak var _outerScrollView: NSUIScrollView? - - private var _lastPanPoint = CGPoint() /// This is to prevent using setTranslation which resets velocity - - private var _decelerationLastTime: TimeInterval = 0.0 - private var _decelerationDisplayLink: NSUIDisplayLink! - private var _decelerationVelocity = CGPoint() - - @objc private func tapGestureRecognized(_ recognizer: NSUITapGestureRecognizer) - { - if data === nil - { - return - } - - if recognizer.state == NSUIGestureRecognizerState.ended - { - if !isHighLightPerTapEnabled { return } - - let h = getHighlightByTouchPoint(recognizer.location(in: self)) - - if h === nil || h == self.lastHighlighted - { - lastHighlighted = nil - highlightValue(nil, callDelegate: true) - } - else - { - lastHighlighted = h - highlightValue(h, callDelegate: true) - } - } - } - - @objc private func doubleTapGestureRecognized(_ recognizer: NSUITapGestureRecognizer) - { - if data === nil - { - return - } - - if recognizer.state == NSUIGestureRecognizerState.ended - { - if data !== nil && _doubleTapToZoomEnabled && (data?.entryCount ?? 0) > 0 - { - var location = recognizer.location(in: self) - location.x = location.x - viewPortHandler.offsetLeft - - if isTouchInverted() - { - location.y = -(location.y - viewPortHandler.offsetTop) - } - else - { - location.y = -(self.bounds.size.height - location.y - viewPortHandler.offsetBottom) - } - - self.zoom(scaleX: isScaleXEnabled ? 1.4 : 1.0, scaleY: isScaleYEnabled ? 1.4 : 1.0, x: location.x, y: location.y) - delegate?.chartScaled?(self, scaleX: scaleX, scaleY: scaleY) - } - } - } - - #if !os(tvOS) - @objc private func pinchGestureRecognized(_ recognizer: NSUIPinchGestureRecognizer) - { - if recognizer.state == NSUIGestureRecognizerState.began - { - stopDeceleration() - - if data !== nil && - (_pinchZoomEnabled || _scaleXEnabled || _scaleYEnabled) - { - _isScaling = true - - if _pinchZoomEnabled - { - _gestureScaleAxis = .both - } - else - { - let x = abs(recognizer.location(in: self).x - recognizer.nsuiLocationOfTouch(1, inView: self).x) - let y = abs(recognizer.location(in: self).y - recognizer.nsuiLocationOfTouch(1, inView: self).y) - - if _scaleXEnabled != _scaleYEnabled - { - _gestureScaleAxis = _scaleXEnabled ? .x : .y - } - else - { - _gestureScaleAxis = x > y ? .x : .y - } - } - } - } - else if recognizer.state == NSUIGestureRecognizerState.ended || - recognizer.state == NSUIGestureRecognizerState.cancelled - { - if _isScaling - { - _isScaling = false - - // Range might have changed, which means that Y-axis labels could have changed in size, affecting Y-axis size. So we need to recalculate offsets. - calculateOffsets() - setNeedsDisplay() - } - } - else if recognizer.state == NSUIGestureRecognizerState.changed - { - let isZoomingOut = (recognizer.nsuiScale < 1) - var canZoomMoreX = isZoomingOut ? viewPortHandler.canZoomOutMoreX : viewPortHandler.canZoomInMoreX - var canZoomMoreY = isZoomingOut ? viewPortHandler.canZoomOutMoreY : viewPortHandler.canZoomInMoreY - - if _isScaling - { - canZoomMoreX = canZoomMoreX && _scaleXEnabled && (_gestureScaleAxis == .both || _gestureScaleAxis == .x) - canZoomMoreY = canZoomMoreY && _scaleYEnabled && (_gestureScaleAxis == .both || _gestureScaleAxis == .y) - if canZoomMoreX || canZoomMoreY - { - var location = recognizer.location(in: self) - location.x = location.x - viewPortHandler.offsetLeft - - if isTouchInverted() - { - location.y = -(location.y - viewPortHandler.offsetTop) - } - else - { - location.y = -(viewPortHandler.chartHeight - location.y - viewPortHandler.offsetBottom) - } - - let scaleX = canZoomMoreX ? recognizer.nsuiScale : 1.0 - let scaleY = canZoomMoreY ? recognizer.nsuiScale : 1.0 - - var matrix = CGAffineTransform(translationX: location.x, y: location.y) - matrix = matrix.scaledBy(x: scaleX, y: scaleY) - matrix = matrix.translatedBy(x: -location.x, y: -location.y) - - matrix = viewPortHandler.touchMatrix.concatenating(matrix) - - viewPortHandler.refresh(newMatrix: matrix, chart: self, invalidate: true) - - if delegate !== nil - { - delegate?.chartScaled?(self, scaleX: scaleX, scaleY: scaleY) - } - } - - recognizer.nsuiScale = 1.0 - } - } - } - #endif - - @objc private func panGestureRecognized(_ recognizer: NSUIPanGestureRecognizer) - { - if recognizer.state == NSUIGestureRecognizerState.began && recognizer.nsuiNumberOfTouches() > 0 - { - stopDeceleration() - - if data === nil || !self.isDragEnabled - { // If we have no data, we have nothing to pan and no data to highlight - return - } - - // If drag is enabled and we are in a position where there's something to drag: - // * If we're zoomed in, then obviously we have something to drag. - // * If we have a drag offset - we always have something to drag - if !self.hasNoDragOffset || !self.isFullyZoomedOut - { - _isDragging = true - - _closestDataSetToTouch = getDataSetByTouchPoint(point: recognizer.nsuiLocationOfTouch(0, inView: self)) - - var translation = recognizer.translation(in: self) - if !self.dragXEnabled - { - translation.x = 0.0 - } - else if !self.dragYEnabled - { - translation.y = 0.0 - } - - let didUserDrag = translation.x != 0.0 || translation.y != 0.0 - - // Check to see if user dragged at all and if so, can the chart be dragged by the given amount - if didUserDrag && !performPanChange(translation: translation) - { - if _outerScrollView !== nil - { - // We can stop dragging right now, and let the scroll view take control - _outerScrollView = nil - _isDragging = false - } - } - else - { - if _outerScrollView !== nil - { - // Prevent the parent scroll view from scrolling - _outerScrollView?.nsuiIsScrollEnabled = false - } - } - - _lastPanPoint = recognizer.translation(in: self) - } - else if self.isHighlightPerDragEnabled - { - // We will only handle highlights on NSUIGestureRecognizerState.Changed - - _isDragging = false - - // Prevent the parent scroll view from scrolling - _outerScrollView?.nsuiIsScrollEnabled = false - - } - } - else if recognizer.state == NSUIGestureRecognizerState.changed - { - if _isDragging - { - let originalTranslation = recognizer.translation(in: self) - var translation = CGPoint(x: originalTranslation.x - _lastPanPoint.x, y: originalTranslation.y - _lastPanPoint.y) - - if !self.dragXEnabled - { - translation.x = 0.0 - } - else if !self.dragYEnabled - { - translation.y = 0.0 - } - - let _ = performPanChange(translation: translation) - - _lastPanPoint = originalTranslation - } - else if isHighlightPerDragEnabled - { - let h = getHighlightByTouchPoint(recognizer.location(in: self)) - - let lastHighlighted = self.lastHighlighted - - if h != lastHighlighted - { - self.lastHighlighted = h - self.highlightValue(h, callDelegate: true) - } - } - } - else if recognizer.state == NSUIGestureRecognizerState.ended || recognizer.state == NSUIGestureRecognizerState.cancelled - { - if _isDragging - { - if recognizer.state == NSUIGestureRecognizerState.ended && isDragDecelerationEnabled - { - stopDeceleration() - - _decelerationLastTime = CACurrentMediaTime() - _decelerationVelocity = recognizer.velocity(in: self) - - _decelerationDisplayLink = NSUIDisplayLink(target: self, selector: #selector(BarLineChartViewBase.decelerationLoop)) - _decelerationDisplayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common) - } - - _isDragging = false - } - - if _outerScrollView !== nil - { - _outerScrollView?.nsuiIsScrollEnabled = true - _outerScrollView = nil - } - - delegate?.chartViewDidEndPanning?(self) - } - } - - private func performPanChange(translation: CGPoint) -> Bool - { - var translation = translation - - if isTouchInverted() - { - if self is HorizontalBarChartView - { - translation.x = -translation.x - } - else - { - translation.y = -translation.y - } - } - - let originalMatrix = viewPortHandler.touchMatrix - - var matrix = CGAffineTransform(translationX: translation.x, y: translation.y) - matrix = originalMatrix.concatenating(matrix) - - matrix = viewPortHandler.refresh(newMatrix: matrix, chart: self, invalidate: true) - - if matrix != originalMatrix - { - delegate?.chartTranslated?(self, dX: translation.x, dY: translation.y) - } - - // Did we managed to actually drag or did we reach the edge? - return matrix.tx != originalMatrix.tx || matrix.ty != originalMatrix.ty - } - - private func isTouchInverted() -> Bool - { - return isAnyAxisInverted && - _closestDataSetToTouch !== nil && - getAxis(_closestDataSetToTouch.axisDependency).isInverted - } - - @objc open func stopDeceleration() - { - if _decelerationDisplayLink !== nil - { - _decelerationDisplayLink.remove(from: RunLoop.main, forMode: RunLoop.Mode.common) - _decelerationDisplayLink = nil - } - } - - @objc private func decelerationLoop() - { - let currentTime = CACurrentMediaTime() - - _decelerationVelocity.x *= self.dragDecelerationFrictionCoef - _decelerationVelocity.y *= self.dragDecelerationFrictionCoef - - let timeInterval = CGFloat(currentTime - _decelerationLastTime) - - let distance = CGPoint( - x: _decelerationVelocity.x * timeInterval, - y: _decelerationVelocity.y * timeInterval - ) - - if !performPanChange(translation: distance) - { - // We reached the edge, stop - _decelerationVelocity.x = 0.0 - _decelerationVelocity.y = 0.0 - } - - _decelerationLastTime = currentTime - - if abs(_decelerationVelocity.x) < 0.001 && abs(_decelerationVelocity.y) < 0.001 - { - stopDeceleration() - - // Range might have changed, which means that Y-axis labels could have changed in size, affecting Y-axis size. So we need to recalculate offsets. - calculateOffsets() - setNeedsDisplay() - } - } - - private func nsuiGestureRecognizerShouldBegin(_ gestureRecognizer: NSUIGestureRecognizer) -> Bool - { - if gestureRecognizer == _panGestureRecognizer - { - let velocity = _panGestureRecognizer.velocity(in: self) - if data === nil || !isDragEnabled || - (self.hasNoDragOffset && self.isFullyZoomedOut && !self.isHighlightPerDragEnabled) || - (!_dragYEnabled && abs(velocity.y) > abs(velocity.x)) || - (!_dragXEnabled && abs(velocity.y) < abs(velocity.x)) - { - return false - } - } - else - { - #if !os(tvOS) - if gestureRecognizer == _pinchGestureRecognizer - { - if data === nil || (!_pinchZoomEnabled && !_scaleXEnabled && !_scaleYEnabled) - { - return false - } - } - #endif - } - - return true - } - - #if !os(OSX) - open override func gestureRecognizerShouldBegin(_ gestureRecognizer: NSUIGestureRecognizer) -> Bool - { - if !super.gestureRecognizerShouldBegin(gestureRecognizer) - { - return false - } - - return nsuiGestureRecognizerShouldBegin(gestureRecognizer) - } - #endif - - #if os(OSX) - public func gestureRecognizerShouldBegin(gestureRecognizer: NSUIGestureRecognizer) -> Bool - { - return nsuiGestureRecognizerShouldBegin(gestureRecognizer) - } - #endif - - open func gestureRecognizer(_ gestureRecognizer: NSUIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: NSUIGestureRecognizer) -> Bool - { - #if !os(tvOS) - if ((gestureRecognizer is NSUIPinchGestureRecognizer && otherGestureRecognizer is NSUIPanGestureRecognizer) || - (gestureRecognizer is NSUIPanGestureRecognizer && otherGestureRecognizer is NSUIPinchGestureRecognizer)) - { - return true - } - #endif - - if gestureRecognizer is NSUIPanGestureRecognizer, - otherGestureRecognizer is NSUIPanGestureRecognizer, - gestureRecognizer == _panGestureRecognizer - { - var scrollView = self.superview - while scrollView != nil && !(scrollView is NSUIScrollView) - { - scrollView = scrollView?.superview - } - - // If there is two scrollview together, we pick the superview of the inner scrollview. - // In the case of UITableViewWrepperView, the superview will be UITableView - if let superViewOfScrollView = scrollView?.superview, - superViewOfScrollView is NSUIScrollView - { - scrollView = superViewOfScrollView - } - - var foundScrollView = scrollView as? NSUIScrollView - - if !(foundScrollView?.nsuiIsScrollEnabled ?? true) - { - foundScrollView = nil - } - - let scrollViewPanGestureRecognizer = foundScrollView?.nsuiGestureRecognizers?.first { - $0 is NSUIPanGestureRecognizer - } - - if otherGestureRecognizer === scrollViewPanGestureRecognizer - { - _outerScrollView = foundScrollView - - return true - } - } - - return false - } - - /// MARK: Viewport modifiers - - /// Zooms in by 1.4, into the charts center. - @objc open func zoomIn() - { - let center = viewPortHandler.contentCenter - - let matrix = viewPortHandler.zoomIn(x: center.x, y: -center.y) - viewPortHandler.refresh(newMatrix: matrix, chart: self, invalidate: false) - - // Range might have changed, which means that Y-axis labels could have changed in size, affecting Y-axis size. So we need to recalculate offsets. - calculateOffsets() - setNeedsDisplay() - } - - /// Zooms out by 0.7, from the charts center. - @objc open func zoomOut() - { - let center = viewPortHandler.contentCenter - - let matrix = viewPortHandler.zoomOut(x: center.x, y: -center.y) - viewPortHandler.refresh(newMatrix: matrix, chart: self, invalidate: false) - - // Range might have changed, which means that Y-axis labels could have changed in size, affecting Y-axis size. So we need to recalculate offsets. - calculateOffsets() - setNeedsDisplay() - } - - /// Zooms out to original size. - @objc open func resetZoom() - { - let matrix = viewPortHandler.resetZoom() - viewPortHandler.refresh(newMatrix: matrix, chart: self, invalidate: false) - - // Range might have changed, which means that Y-axis labels could have changed in size, affecting Y-axis size. So we need to recalculate offsets. - calculateOffsets() - setNeedsDisplay() - } - - /// Zooms in or out by the given scale factor. x and y are the coordinates - /// (in pixels) of the zoom center. - /// - /// - Parameters: - /// - scaleX: if < 1 --> zoom out, if > 1 --> zoom in - /// - scaleY: if < 1 --> zoom out, if > 1 --> zoom in - /// - x: - /// - y: - @objc open func zoom( - scaleX: CGFloat, - scaleY: CGFloat, - x: CGFloat, - y: CGFloat) - { - let matrix = viewPortHandler.zoom(scaleX: scaleX, scaleY: scaleY, x: x, y: -y) - viewPortHandler.refresh(newMatrix: matrix, chart: self, invalidate: false) - - // Range might have changed, which means that Y-axis labels could have changed in size, affecting Y-axis size. So we need to recalculate offsets. - calculateOffsets() - setNeedsDisplay() - } - - /// Zooms in or out by the given scale factor. - /// x and y are the values (**not pixels**) of the zoom center. - /// - /// - Parameters: - /// - scaleX: if < 1 --> zoom out, if > 1 --> zoom in - /// - scaleY: if < 1 --> zoom out, if > 1 --> zoom in - /// - xValue: - /// - yValue: - /// - axis: - @objc open func zoom( - scaleX: CGFloat, - scaleY: CGFloat, - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency) - { - let job = ZoomViewJob( - viewPortHandler: viewPortHandler, - scaleX: scaleX, - scaleY: scaleY, - xValue: xValue, - yValue: yValue, - transformer: getTransformer(forAxis: axis), - axis: axis, - view: self) - addViewportJob(job) - } - - /// Zooms to the center of the chart with the given scale factor. - /// - /// - Parameters: - /// - scaleX: if < 1 --> zoom out, if > 1 --> zoom in - /// - scaleY: if < 1 --> zoom out, if > 1 --> zoom in - /// - xValue: - /// - yValue: - /// - axis: - @objc open func zoomToCenter( - scaleX: CGFloat, - scaleY: CGFloat) - { - let center = centerOffsets - let matrix = viewPortHandler.zoom( - scaleX: scaleX, - scaleY: scaleY, - x: center.x, - y: -center.y) - viewPortHandler.refresh(newMatrix: matrix, chart: self, invalidate: false) - } - - /// Zooms by the specified scale factor to the specified values on the specified axis. - /// - /// - Parameters: - /// - scaleX: - /// - scaleY: - /// - xValue: - /// - yValue: - /// - axis: which axis should be used as a reference for the y-axis - /// - duration: the duration of the animation in seconds - /// - easing: - @objc open func zoomAndCenterViewAnimated( - scaleX: CGFloat, - scaleY: CGFloat, - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency, - duration: TimeInterval, - easing: ChartEasingFunctionBlock?) - { - let origin = valueForTouchPoint( - point: CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentTop), - axis: axis) - - let job = AnimatedZoomViewJob( - viewPortHandler: viewPortHandler, - transformer: getTransformer(forAxis: axis), - view: self, - yAxis: getAxis(axis), - xAxisRange: xAxis.axisRange, - scaleX: scaleX, - scaleY: scaleY, - xOrigin: viewPortHandler.scaleX, - yOrigin: viewPortHandler.scaleY, - zoomCenterX: CGFloat(xValue), - zoomCenterY: CGFloat(yValue), - zoomOriginX: origin.x, - zoomOriginY: origin.y, - duration: duration, - easing: easing) - - addViewportJob(job) - } - - /// Zooms by the specified scale factor to the specified values on the specified axis. - /// - /// - Parameters: - /// - scaleX: - /// - scaleY: - /// - xValue: - /// - yValue: - /// - axis: which axis should be used as a reference for the y-axis - /// - duration: the duration of the animation in seconds - /// - easing: - @objc open func zoomAndCenterViewAnimated( - scaleX: CGFloat, - scaleY: CGFloat, - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency, - duration: TimeInterval, - easingOption: ChartEasingOption) - { - zoomAndCenterViewAnimated(scaleX: scaleX, scaleY: scaleY, xValue: xValue, yValue: yValue, axis: axis, duration: duration, easing: easingFunctionFromOption(easingOption)) - } - - /// Zooms by the specified scale factor to the specified values on the specified axis. - /// - /// - Parameters: - /// - scaleX: - /// - scaleY: - /// - xValue: - /// - yValue: - /// - axis: which axis should be used as a reference for the y-axis - /// - duration: the duration of the animation in seconds - /// - easing: - @objc open func zoomAndCenterViewAnimated( - scaleX: CGFloat, - scaleY: CGFloat, - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency, - duration: TimeInterval) - { - zoomAndCenterViewAnimated(scaleX: scaleX, scaleY: scaleY, xValue: xValue, yValue: yValue, axis: axis, duration: duration, easingOption: .easeInOutSine) - } - - /// Resets all zooming and dragging and makes the chart fit exactly it's bounds. - @objc open func fitScreen() - { - let matrix = viewPortHandler.fitScreen() - viewPortHandler.refresh(newMatrix: matrix, chart: self, invalidate: false) - - calculateOffsets() - setNeedsDisplay() - } - - /// Sets the minimum scale value to which can be zoomed out. 1 = fitScreen - @objc open func setScaleMinima(_ scaleX: CGFloat, scaleY: CGFloat) - { - viewPortHandler.setMinimumScaleX(scaleX) - viewPortHandler.setMinimumScaleY(scaleY) - } - - @objc open var visibleXRange: Double - { - return abs(highestVisibleX - lowestVisibleX) - } - - /// Sets the size of the area (range on the x-axis) that should be maximum visible at once (no further zooming out allowed). - /// - /// If this is e.g. set to 10, no more than a range of 10 values on the x-axis can be viewed at once without scrolling. - /// - /// If you call this method, chart must have data or it has no effect. - @objc open func setVisibleXRangeMaximum(_ maxXRange: Double) - { - let xScale = xAxis.axisRange / maxXRange - viewPortHandler.setMinimumScaleX(CGFloat(xScale)) - } - - /// Sets the size of the area (range on the x-axis) that should be minimum visible at once (no further zooming in allowed). - /// - /// If this is e.g. set to 10, no less than a range of 10 values on the x-axis can be viewed at once without scrolling. - /// - /// If you call this method, chart must have data or it has no effect. - @objc open func setVisibleXRangeMinimum(_ minXRange: Double) - { - let xScale = xAxis.axisRange / minXRange - viewPortHandler.setMaximumScaleX(CGFloat(xScale)) - } - - /// Limits the maximum and minimum value count that can be visible by pinching and zooming. - /// - /// e.g. minRange=10, maxRange=100 no less than 10 values and no more that 100 values can be viewed - /// at once without scrolling. - /// - /// If you call this method, chart must have data or it has no effect. - @objc open func setVisibleXRange(minXRange: Double, maxXRange: Double) - { - let minScale = xAxis.axisRange / maxXRange - let maxScale = xAxis.axisRange / minXRange - viewPortHandler.setMinMaxScaleX( - minScaleX: CGFloat(minScale), - maxScaleX: CGFloat(maxScale)) - } - - /// Sets the size of the area (range on the y-axis) that should be maximum visible at once. - /// - /// - Parameters: - /// - yRange: - /// - axis: - the axis for which this limit should apply - @objc open func setVisibleYRangeMaximum(_ maxYRange: Double, axis: YAxis.AxisDependency) - { - let yScale = getAxisRange(axis: axis) / maxYRange - viewPortHandler.setMinimumScaleY(CGFloat(yScale)) - } - - /// Sets the size of the area (range on the y-axis) that should be minimum visible at once, no further zooming in possible. - /// - /// - Parameters: - /// - yRange: - /// - axis: - the axis for which this limit should apply - @objc open func setVisibleYRangeMinimum(_ minYRange: Double, axis: YAxis.AxisDependency) - { - let yScale = getAxisRange(axis: axis) / minYRange - viewPortHandler.setMaximumScaleY(CGFloat(yScale)) - } - - /// Limits the maximum and minimum y range that can be visible by pinching and zooming. - /// - /// - Parameters: - /// - minYRange: - /// - maxYRange: - /// - axis: - @objc open func setVisibleYRange(minYRange: Double, maxYRange: Double, axis: YAxis.AxisDependency) - { - let minScale = getAxisRange(axis: axis) / minYRange - let maxScale = getAxisRange(axis: axis) / maxYRange - viewPortHandler.setMinMaxScaleY(minScaleY: CGFloat(minScale), maxScaleY: CGFloat(maxScale)) - } - - /// Moves the left side of the current viewport to the specified x-value. - /// This also refreshes the chart by calling setNeedsDisplay(). - @objc open func moveViewToX(_ xValue: Double) - { - let job = MoveViewJob( - viewPortHandler: viewPortHandler, - xValue: xValue, - yValue: 0.0, - transformer: getTransformer(forAxis: .left), - view: self) - - addViewportJob(job) - } - - /// Centers the viewport to the specified y-value on the y-axis. - /// This also refreshes the chart by calling setNeedsDisplay(). - /// - /// - Parameters: - /// - yValue: - /// - axis: - which axis should be used as a reference for the y-axis - @objc open func moveViewToY(_ yValue: Double, axis: YAxis.AxisDependency) - { - let yInView = getAxisRange(axis: axis) / Double(viewPortHandler.scaleY) - - let job = MoveViewJob( - viewPortHandler: viewPortHandler, - xValue: 0.0, - yValue: yValue + yInView / 2.0, - transformer: getTransformer(forAxis: axis), - view: self) - - addViewportJob(job) - } - - /// This will move the left side of the current viewport to the specified x-value on the x-axis, and center the viewport to the specified y-value on the y-axis. - /// This also refreshes the chart by calling setNeedsDisplay(). - /// - /// - Parameters: - /// - xValue: - /// - yValue: - /// - axis: - which axis should be used as a reference for the y-axis - @objc open func moveViewTo(xValue: Double, yValue: Double, axis: YAxis.AxisDependency) - { - let yInView = getAxisRange(axis: axis) / Double(viewPortHandler.scaleY) - - let job = MoveViewJob( - viewPortHandler: viewPortHandler, - xValue: xValue, - yValue: yValue + yInView / 2.0, - transformer: getTransformer(forAxis: axis), - view: self) - - addViewportJob(job) - } - - /// This will move the left side of the current viewport to the specified x-position and center the viewport to the specified y-position animated. - /// This also refreshes the chart by calling setNeedsDisplay(). - /// - /// - Parameters: - /// - xValue: - /// - yValue: - /// - axis: which axis should be used as a reference for the y-axis - /// - duration: the duration of the animation in seconds - /// - easing: - @objc open func moveViewToAnimated( - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency, - duration: TimeInterval, - easing: ChartEasingFunctionBlock?) - { - let bounds = valueForTouchPoint( - point: CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentTop), - axis: axis) - - let yInView = getAxisRange(axis: axis) / Double(viewPortHandler.scaleY) - - let job = AnimatedMoveViewJob( - viewPortHandler: viewPortHandler, - xValue: xValue, - yValue: yValue + yInView / 2.0, - transformer: getTransformer(forAxis: axis), - view: self, - xOrigin: bounds.x, - yOrigin: bounds.y, - duration: duration, - easing: easing) - - addViewportJob(job) - } - - /// This will move the left side of the current viewport to the specified x-position and center the viewport to the specified y-position animated. - /// This also refreshes the chart by calling setNeedsDisplay(). - /// - /// - Parameters: - /// - xValue: - /// - yValue: - /// - axis: which axis should be used as a reference for the y-axis - /// - duration: the duration of the animation in seconds - /// - easing: - @objc open func moveViewToAnimated( - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency, - duration: TimeInterval, - easingOption: ChartEasingOption) - { - moveViewToAnimated(xValue: xValue, yValue: yValue, axis: axis, duration: duration, easing: easingFunctionFromOption(easingOption)) - } - - /// This will move the left side of the current viewport to the specified x-position and center the viewport to the specified y-position animated. - /// This also refreshes the chart by calling setNeedsDisplay(). - /// - /// - Parameters: - /// - xValue: - /// - yValue: - /// - axis: which axis should be used as a reference for the y-axis - /// - duration: the duration of the animation in seconds - /// - easing: - @objc open func moveViewToAnimated( - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency, - duration: TimeInterval) - { - moveViewToAnimated(xValue: xValue, yValue: yValue, axis: axis, duration: duration, easingOption: .easeInOutSine) - } - - /// This will move the center of the current viewport to the specified x-value and y-value. - /// This also refreshes the chart by calling setNeedsDisplay(). - /// - /// - Parameters: - /// - xValue: - /// - yValue: - /// - axis: - which axis should be used as a reference for the y-axis - @objc open func centerViewTo( - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency) - { - let yInView = getAxisRange(axis: axis) / Double(viewPortHandler.scaleY) - let xInView = xAxis.axisRange / Double(viewPortHandler.scaleX) - - let job = MoveViewJob( - viewPortHandler: viewPortHandler, - xValue: xValue - xInView / 2.0, - yValue: yValue + yInView / 2.0, - transformer: getTransformer(forAxis: axis), - view: self) - - addViewportJob(job) - } - - /// This will move the center of the current viewport to the specified x-value and y-value animated. - /// - /// - Parameters: - /// - xValue: - /// - yValue: - /// - axis: which axis should be used as a reference for the y-axis - /// - duration: the duration of the animation in seconds - /// - easing: - @objc open func centerViewToAnimated( - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency, - duration: TimeInterval, - easing: ChartEasingFunctionBlock?) - { - let bounds = valueForTouchPoint( - point: CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentTop), - axis: axis) - - let yInView = getAxisRange(axis: axis) / Double(viewPortHandler.scaleY) - let xInView = xAxis.axisRange / Double(viewPortHandler.scaleX) - - let job = AnimatedMoveViewJob( - viewPortHandler: viewPortHandler, - xValue: xValue - xInView / 2.0, - yValue: yValue + yInView / 2.0, - transformer: getTransformer(forAxis: axis), - view: self, - xOrigin: bounds.x, - yOrigin: bounds.y, - duration: duration, - easing: easing) - - addViewportJob(job) - } - - /// This will move the center of the current viewport to the specified x-value and y-value animated. - /// - /// - Parameters: - /// - xValue: - /// - yValue: - /// - axis: which axis should be used as a reference for the y-axis - /// - duration: the duration of the animation in seconds - /// - easing: - @objc open func centerViewToAnimated( - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency, - duration: TimeInterval, - easingOption: ChartEasingOption) - { - centerViewToAnimated(xValue: xValue, yValue: yValue, axis: axis, duration: duration, easing: easingFunctionFromOption(easingOption)) - } - - /// This will move the center of the current viewport to the specified x-value and y-value animated. - /// - /// - Parameters: - /// - xValue: - /// - yValue: - /// - axis: which axis should be used as a reference for the y-axis - /// - duration: the duration of the animation in seconds - /// - easing: - @objc open func centerViewToAnimated( - xValue: Double, - yValue: Double, - axis: YAxis.AxisDependency, - duration: TimeInterval) - { - centerViewToAnimated(xValue: xValue, yValue: yValue, axis: axis, duration: duration, easingOption: .easeInOutSine) - } - - /// Sets custom offsets for the current `ChartViewPort` (the offsets on the sides of the actual chart window). Setting this will prevent the chart from automatically calculating it's offsets. Use `resetViewPortOffsets()` to undo this. - /// ONLY USE THIS WHEN YOU KNOW WHAT YOU ARE DOING, else use `setExtraOffsets(...)`. - @objc open func setViewPortOffsets(left: CGFloat, top: CGFloat, right: CGFloat, bottom: CGFloat) - { - _customViewPortEnabled = true - - if Thread.isMainThread - { - self.viewPortHandler.restrainViewPort(offsetLeft: left, offsetTop: top, offsetRight: right, offsetBottom: bottom) - prepareOffsetMatrix() - prepareValuePxMatrix() - } - else - { - DispatchQueue.main.async(execute: { - self.setViewPortOffsets(left: left, top: top, right: right, bottom: bottom) - }) - } - } - - /// Resets all custom offsets set via `setViewPortOffsets(...)` method. Allows the chart to again calculate all offsets automatically. - @objc open func resetViewPortOffsets() - { - _customViewPortEnabled = false - calculateOffsets() - } - - // MARK: - Accessors - - /// - Returns: The range of the specified axis. - @objc open func getAxisRange(axis: YAxis.AxisDependency) -> Double - { - if axis == .left - { - return leftAxis.axisRange - } - else - { - return rightAxis.axisRange - } - } - - /// - Returns: The position (in pixels) the provided Entry has inside the chart view - @objc open func getPosition(entry e: ChartDataEntry, axis: YAxis.AxisDependency) -> CGPoint - { - var vals = CGPoint(x: CGFloat(e.x), y: CGFloat(e.y)) - - getTransformer(forAxis: axis).pointValueToPixel(&vals) - - return vals - } - - /// is dragging enabled? (moving the chart with the finger) for the chart (this does not affect scaling). - @objc open var dragEnabled: Bool - { - get - { - return _dragXEnabled || _dragYEnabled - } - set - { - _dragYEnabled = newValue - _dragXEnabled = newValue - } - } - - /// is dragging enabled? (moving the chart with the finger) for the chart (this does not affect scaling). - @objc open var isDragEnabled: Bool - { - return dragEnabled - } - - /// is dragging on the X axis enabled? - @objc open var dragXEnabled: Bool - { - get - { - return _dragXEnabled - } - set - { - _dragXEnabled = newValue - } - } - - /// is dragging on the Y axis enabled? - @objc open var dragYEnabled: Bool - { - get - { - return _dragYEnabled - } - set - { - _dragYEnabled = newValue - } - } - - /// is scaling enabled? (zooming in and out by gesture) for the chart (this does not affect dragging). - @objc open func setScaleEnabled(_ enabled: Bool) - { - if _scaleXEnabled != enabled || _scaleYEnabled != enabled - { - _scaleXEnabled = enabled - _scaleYEnabled = enabled - #if !os(tvOS) - _pinchGestureRecognizer.isEnabled = _pinchZoomEnabled || _scaleXEnabled || _scaleYEnabled - #endif - } - } - - @objc open var scaleXEnabled: Bool - { - get - { - return _scaleXEnabled - } - set - { - if _scaleXEnabled != newValue - { - _scaleXEnabled = newValue - #if !os(tvOS) - _pinchGestureRecognizer.isEnabled = _pinchZoomEnabled || _scaleXEnabled || _scaleYEnabled - #endif - } - } - } - - @objc open var scaleYEnabled: Bool - { - get - { - return _scaleYEnabled - } - set - { - if _scaleYEnabled != newValue - { - _scaleYEnabled = newValue - #if !os(tvOS) - _pinchGestureRecognizer.isEnabled = _pinchZoomEnabled || _scaleXEnabled || _scaleYEnabled - #endif - } - } - } - - @objc open var isScaleXEnabled: Bool { return scaleXEnabled } - @objc open var isScaleYEnabled: Bool { return scaleYEnabled } - - /// flag that indicates if double tap zoom is enabled or not - @objc open var doubleTapToZoomEnabled: Bool - { - get - { - return _doubleTapToZoomEnabled - } - set - { - if _doubleTapToZoomEnabled != newValue - { - _doubleTapToZoomEnabled = newValue - _doubleTapGestureRecognizer.isEnabled = _doubleTapToZoomEnabled - } - } - } - - /// **default**: true - /// `true` if zooming via double-tap is enabled `false` ifnot. - @objc open var isDoubleTapToZoomEnabled: Bool - { - return doubleTapToZoomEnabled - } - - /// flag that indicates if highlighting per dragging over a fully zoomed out chart is enabled - @objc open var highlightPerDragEnabled = true - - /// If set to true, highlighting per dragging over a fully zoomed out chart is enabled - /// You might want to disable this when using inside a `NSUIScrollView` - /// - /// **default**: true - @objc open var isHighlightPerDragEnabled: Bool - { - return highlightPerDragEnabled - } - - /// **default**: true - /// `true` if drawing the grid background is enabled, `false` ifnot. - @objc open var isDrawGridBackgroundEnabled: Bool - { - return drawGridBackgroundEnabled - } - - /// **default**: false - /// `true` if drawing the borders rectangle is enabled, `false` ifnot. - @objc open var isDrawBordersEnabled: Bool - { - return drawBordersEnabled - } - - /// - Returns: The x and y values in the chart at the given touch point - /// (encapsulated in a `CGPoint`). This method transforms pixel coordinates to - /// coordinates / values in the chart. This is the opposite method to - /// `getPixelsForValues(...)`. - @objc open func valueForTouchPoint(point pt: CGPoint, axis: YAxis.AxisDependency) -> CGPoint - { - return getTransformer(forAxis: axis).valueForTouchPoint(pt) - } - - /// Transforms the given chart values into pixels. This is the opposite - /// method to `valueForTouchPoint(...)`. - @objc open func pixelForValues(x: Double, y: Double, axis: YAxis.AxisDependency) -> CGPoint - { - return getTransformer(forAxis: axis).pixelForValues(x: x, y: y) - } - - /// - Returns: The Entry object displayed at the touched position of the chart - @objc open func getEntryByTouchPoint(point pt: CGPoint) -> ChartDataEntry! - { - if let h = getHighlightByTouchPoint(pt) - { - return data!.entry(for: h) - } - return nil - } - - /// - Returns: The DataSet object displayed at the touched position of the chart - @objc open func getDataSetByTouchPoint(point pt: CGPoint) -> BarLineScatterCandleBubbleChartDataSetProtocol? - { - guard let h = getHighlightByTouchPoint(pt) else { - return nil - } - - return data?[h.dataSetIndex] as? BarLineScatterCandleBubbleChartDataSetProtocol - } - - /// The current x-scale factor - @objc open var scaleX: CGFloat - { - return viewPortHandler.scaleX - } - - /// The current y-scale factor - @objc open var scaleY: CGFloat - { - return viewPortHandler.scaleY - } - - /// if the chart is fully zoomed out, return true - @objc open var isFullyZoomedOut: Bool { return viewPortHandler.isFullyZoomedOut } - - /// - Returns: The y-axis object to the corresponding AxisDependency. In the - /// horizontal bar-chart, LEFT == top, RIGHT == BOTTOM - @objc open func getAxis(_ axis: YAxis.AxisDependency) -> YAxis - { - if axis == .left - { - return leftAxis - } - else - { - return rightAxis - } - } - - /// flag that indicates if pinch-zoom is enabled. if true, both x and y axis can be scaled simultaneously with 2 fingers, if false, x and y axis can be scaled separately - @objc open var pinchZoomEnabled: Bool - { - get - { - return _pinchZoomEnabled - } - set - { - if _pinchZoomEnabled != newValue - { - _pinchZoomEnabled = newValue - #if !os(tvOS) - _pinchGestureRecognizer.isEnabled = _pinchZoomEnabled || _scaleXEnabled || _scaleYEnabled - #endif - } - } - } - - /// **default**: false - /// `true` if pinch-zoom is enabled, `false` ifnot - @objc open var isPinchZoomEnabled: Bool { return pinchZoomEnabled } - - /// Set an offset in dp that allows the user to drag the chart over it's - /// bounds on the x-axis. - @objc open func setDragOffsetX(_ offset: CGFloat) - { - viewPortHandler.setDragOffsetX(offset) - } - - /// Set an offset in dp that allows the user to drag the chart over it's - /// bounds on the y-axis. - @objc open func setDragOffsetY(_ offset: CGFloat) - { - viewPortHandler.setDragOffsetY(offset) - } - - /// `true` if both drag offsets (x and y) are zero or smaller. - @objc open var hasNoDragOffset: Bool { return viewPortHandler.hasNoDragOffset } - - open override var chartYMax: Double - { - return max(leftAxis._axisMaximum, rightAxis._axisMaximum) - } - - open override var chartYMin: Double - { - return min(leftAxis._axisMinimum, rightAxis._axisMinimum) - } - - /// `true` if either the left or the right or both axes are inverted. - @objc open var isAnyAxisInverted: Bool - { - return leftAxis.isInverted || rightAxis.isInverted - } - - /// flag that indicates if auto scaling on the y axis is enabled. - /// if yes, the y axis automatically adjusts to the min and max y values of the current x axis range whenever the viewport changes - @objc open var autoScaleMinMaxEnabled: Bool - { - get { return _autoScaleMinMaxEnabled } - set { _autoScaleMinMaxEnabled = newValue } - } - - /// **default**: false - /// `true` if auto scaling on the y axis is enabled. - @objc open var isAutoScaleMinMaxEnabled : Bool { return autoScaleMinMaxEnabled } - - /// Sets a minimum width to the specified y axis. - @objc open func setYAxisMinWidth(_ axis: YAxis.AxisDependency, width: CGFloat) - { - if axis == .left - { - leftAxis.minWidth = width - } - else - { - rightAxis.minWidth = width - } - } - - /// **default**: 0.0 - /// - /// - Returns: The (custom) minimum width of the specified Y axis. - @objc open func getYAxisMinWidth(_ axis: YAxis.AxisDependency) -> CGFloat - { - if axis == .left - { - return leftAxis.minWidth - } - else - { - return rightAxis.minWidth - } - } - /// Sets a maximum width to the specified y axis. - /// Zero (0.0) means there's no maximum width - @objc open func setYAxisMaxWidth(_ axis: YAxis.AxisDependency, width: CGFloat) - { - if axis == .left - { - leftAxis.maxWidth = width - } - else - { - rightAxis.maxWidth = width - } - } - - /// Zero (0.0) means there's no maximum width - /// - /// **default**: 0.0 (no maximum specified) - /// - /// - Returns: The (custom) maximum width of the specified Y axis. - @objc open func getYAxisMaxWidth(_ axis: YAxis.AxisDependency) -> CGFloat - { - if axis == .left - { - return leftAxis.maxWidth - } - else - { - return rightAxis.maxWidth - } - } - - /// - Returns the width of the specified y axis. - @objc open func getYAxisWidth(_ axis: YAxis.AxisDependency) -> CGFloat - { - if axis == .left - { - return leftAxis.requiredSize().width - } - else - { - return rightAxis.requiredSize().width - } - } - - // MARK: - BarLineScatterCandleBubbleChartDataProvider - - /// - Returns: The Transformer class that contains all matrices and is - /// responsible for transforming values into pixels on the screen and - /// backwards. - open func getTransformer(forAxis axis: YAxis.AxisDependency) -> Transformer - { - if axis == .left - { - return _leftAxisTransformer - } - else - { - return _rightAxisTransformer - } - } - - /// the number of maximum visible drawn values on the chart only active when `drawValuesEnabled` is enabled - open override var maxVisibleCount: Int - { - get - { - return _maxVisibleCount - } - set - { - _maxVisibleCount = newValue - } - } - - open func isInverted(axis: YAxis.AxisDependency) -> Bool - { - return getAxis(axis).isInverted - } - - /// The lowest x-index (value on the x-axis) that is still visible on he chart. - open var lowestVisibleX: Double - { - var pt = CGPoint( - x: viewPortHandler.contentLeft, - y: viewPortHandler.contentBottom) - - getTransformer(forAxis: .left).pixelToValues(&pt) - - return max(xAxis._axisMinimum, Double(pt.x)) - } - - /// The highest x-index (value on the x-axis) that is still visible on the chart. - open var highestVisibleX: Double - { - var pt = CGPoint( - x: viewPortHandler.contentRight, - y: viewPortHandler.contentBottom) - - getTransformer(forAxis: .left).pixelToValues(&pt) - - return min(xAxis._axisMaximum, Double(pt.x)) - } -} diff --git a/Pods/Charts/Source/Charts/Charts/BubbleChartView.swift b/Pods/Charts/Source/Charts/Charts/BubbleChartView.swift deleted file mode 100644 index 14e8b462a..000000000 --- a/Pods/Charts/Source/Charts/Charts/BubbleChartView.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// BubbleChartView.swift -// Charts -// -// Bubble chart implementation: -// Copyright 2015 Pierre-Marc Airoldi -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class BubbleChartView: BarLineChartViewBase, BubbleChartDataProvider -{ - open override func initialize() - { - super.initialize() - - renderer = BubbleChartRenderer(dataProvider: self, animator: chartAnimator, viewPortHandler: viewPortHandler) - } - - // MARK: - BubbleChartDataProvider - - open var bubbleData: BubbleChartData? { return data as? BubbleChartData } -} diff --git a/Pods/Charts/Source/Charts/Charts/CandleStickChartView.swift b/Pods/Charts/Source/Charts/Charts/CandleStickChartView.swift deleted file mode 100644 index 33c2174ae..000000000 --- a/Pods/Charts/Source/Charts/Charts/CandleStickChartView.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// CandleStickChartView.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// Financial chart type that draws candle-sticks. -open class CandleStickChartView: BarLineChartViewBase, CandleChartDataProvider -{ - internal override func initialize() - { - super.initialize() - - renderer = CandleStickChartRenderer(dataProvider: self, animator: chartAnimator, viewPortHandler: viewPortHandler) - - self.xAxis.spaceMin = 0.5 - self.xAxis.spaceMax = 0.5 - } - - // MARK: - CandleChartDataProvider - - open var candleData: CandleChartData? - { - return data as? CandleChartData - } -} diff --git a/Pods/Charts/Source/Charts/Charts/ChartViewBase.swift b/Pods/Charts/Source/Charts/Charts/ChartViewBase.swift deleted file mode 100644 index 02742d915..000000000 --- a/Pods/Charts/Source/Charts/Charts/ChartViewBase.swift +++ /dev/null @@ -1,890 +0,0 @@ -// -// ChartViewBase.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// -// Based on https://github.com/PhilJay/MPAndroidChart/commit/c42b880 - -import Foundation -import CoreGraphics - -#if !os(OSX) - import UIKit -#endif - -@objc -public protocol ChartViewDelegate -{ - /// Called when a value has been selected inside the chart. - /// - /// - Parameters: - /// - entry: The selected Entry. - /// - highlight: The corresponding highlight object that contains information about the highlighted position such as dataSetIndex etc. - @objc optional func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) - - /// Called when a user stops panning between values on the chart - @objc optional func chartViewDidEndPanning(_ chartView: ChartViewBase) - - // Called when nothing has been selected or an "un-select" has been made. - @objc optional func chartValueNothingSelected(_ chartView: ChartViewBase) - - // Callbacks when the chart is scaled / zoomed via pinch zoom gesture. - @objc optional func chartScaled(_ chartView: ChartViewBase, scaleX: CGFloat, scaleY: CGFloat) - - // Callbacks when the chart is moved / translated via drag gesture. - @objc optional func chartTranslated(_ chartView: ChartViewBase, dX: CGFloat, dY: CGFloat) - - // Callbacks when Animator stops animating - @objc optional func chartView(_ chartView: ChartViewBase, animatorDidStop animator: Animator) -} - -open class ChartViewBase: NSUIView, ChartDataProvider, AnimatorDelegate -{ - // MARK: - Properties - - /// The default IValueFormatter that has been determined by the chart considering the provided minimum and maximum values. - internal lazy var defaultValueFormatter: ValueFormatter = DefaultValueFormatter(decimals: 0) - - /// object that holds all data that was originally set for the chart, before it was modified or any filtering algorithms had been applied - @objc open var data: ChartData? - { - didSet - { - offsetsCalculated = false - - guard let data = data else { return } - - // calculate how many digits are needed - setupDefaultFormatter(min: data.yMin, max: data.yMax) - - for set in data where set.valueFormatter is DefaultValueFormatter - { - set.valueFormatter = defaultValueFormatter - } - - // let the chart know there is new data - notifyDataSetChanged() - } - } - - /// If set to true, chart continues to scroll after touch up - @objc open var dragDecelerationEnabled = true - - /// The object representing the labels on the x-axis - @objc open internal(set) lazy var xAxis = XAxis() - - /// The `Description` object of the chart. - @objc open lazy var chartDescription = Description() - - /// The legend object containing all data associated with the legend - @objc open internal(set) lazy var legend = Legend() - - /// delegate to receive chart events - @objc open weak var delegate: ChartViewDelegate? - - /// text that is displayed when the chart is empty - @objc open var noDataText = "No chart data available." - - /// Font to be used for the no data text. - @objc open var noDataFont = NSUIFont.systemFont(ofSize: 12) - - /// color of the no data text - @objc open var noDataTextColor: NSUIColor = .labelOrBlack - - /// alignment of the no data text - @objc open var noDataTextAlignment: TextAlignment = .left - - /// The renderer object responsible for rendering / drawing the Legend. - @objc open lazy var legendRenderer = LegendRenderer(viewPortHandler: viewPortHandler, legend: legend) - - /// object responsible for rendering the data - @objc open var renderer: DataRenderer? - - @objc open var highlighter: Highlighter? - - /// The ViewPortHandler of the chart that is responsible for the - /// content area of the chart and its offsets and dimensions. - @objc open internal(set) lazy var viewPortHandler = ViewPortHandler(width: bounds.size.width, height: bounds.size.height) - - /// The animator responsible for animating chart values. - @objc open internal(set) lazy var chartAnimator: Animator = { - let animator = Animator() - animator.delegate = self - return animator - }() - - /// flag that indicates if offsets calculation has already been done or not - private var offsetsCalculated = false - - /// The array of currently highlighted values. This might an empty if nothing is highlighted. - @objc open internal(set) var highlighted = [Highlight]() - - /// `true` if drawing the marker is enabled when tapping on values - /// (use the `marker` property to specify a marker) - @objc open var drawMarkers = true - - /// - Returns: `true` if drawing the marker is enabled when tapping on values - /// (use the `marker` property to specify a marker) - @objc open var isDrawMarkersEnabled: Bool { return drawMarkers } - - /// The marker that is displayed when a value is clicked on the chart - @objc open var marker: Marker? - - /// An extra offset to be appended to the viewport's top - @objc open var extraTopOffset: CGFloat = 0.0 - - /// An extra offset to be appended to the viewport's right - @objc open var extraRightOffset: CGFloat = 0.0 - - /// An extra offset to be appended to the viewport's bottom - @objc open var extraBottomOffset: CGFloat = 0.0 - - /// An extra offset to be appended to the viewport's left - @objc open var extraLeftOffset: CGFloat = 0.0 - - @objc open func setExtraOffsets(left: CGFloat, top: CGFloat, right: CGFloat, bottom: CGFloat) - { - extraLeftOffset = left - extraTopOffset = top - extraRightOffset = right - extraBottomOffset = bottom - } - - // MARK: - Initializers - - public override init(frame: CGRect) - { - super.init(frame: frame) - initialize() - } - - public required init?(coder aDecoder: NSCoder) - { - super.init(coder: aDecoder) - initialize() - } - - deinit - { - removeObserver(self, forKeyPath: "bounds") - removeObserver(self, forKeyPath: "frame") - } - - internal func initialize() - { - #if os(iOS) - self.backgroundColor = .clear - #endif - - addObserver(self, forKeyPath: "bounds", options: .new, context: nil) - addObserver(self, forKeyPath: "frame", options: .new, context: nil) - } - - // MARK: - ChartViewBase - - /// Clears the chart from all data (sets it to null) and refreshes it (by calling setNeedsDisplay()). - @objc open func clear() - { - data = nil - offsetsCalculated = false - highlighted.removeAll() - lastHighlighted = nil - - setNeedsDisplay() - } - - /// Removes all DataSets (and thereby Entries) from the chart. Does not set the data object to nil. Also refreshes the chart by calling setNeedsDisplay(). - @objc open func clearValues() - { - data?.clearValues() - setNeedsDisplay() - } - - /// - Returns: `true` if the chart is empty (meaning it's data object is either null or contains no entries). - @objc open func isEmpty() -> Bool - { - return data?.isEmpty ?? true - } - - /// Lets the chart know its underlying data has changed and should perform all necessary recalculations. - /// It is crucial that this method is called everytime data is changed dynamically. Not calling this method can lead to crashes or unexpected behaviour. - @objc open func notifyDataSetChanged() - { - fatalError("notifyDataSetChanged() cannot be called on ChartViewBase") - } - - /// Calculates the offsets of the chart to the border depending on the position of an eventual legend or depending on the length of the y-axis and x-axis labels and their position - internal func calculateOffsets() - { - fatalError("calculateOffsets() cannot be called on ChartViewBase") - } - - /// calcualtes the y-min and y-max value and the y-delta and x-delta value - internal func calcMinMax() - { - fatalError("calcMinMax() cannot be called on ChartViewBase") - } - - /// calculates the required number of digits for the values that might be drawn in the chart (if enabled), and creates the default value formatter - internal func setupDefaultFormatter(min: Double, max: Double) - { - // check if a custom formatter is set or not - var reference = 0.0 - - if let data = data , data.entryCount >= 2 - { - reference = abs(max - min) - } - else - { - reference = Swift.max(abs(min), abs(max)) - } - - - if let formatter = defaultValueFormatter as? DefaultValueFormatter - { - // setup the formatter with a new number of digits - let digits = reference.decimalPlaces - formatter.decimals = digits - } - } - - open override func draw(_ rect: CGRect) - { - guard let context = NSUIGraphicsGetCurrentContext() else { return } - - if data === nil && !noDataText.isEmpty - { - context.saveGState() - defer { context.restoreGState() } - - let paragraphStyle = MutableParagraphStyle.default.mutableCopy() as! MutableParagraphStyle - paragraphStyle.minimumLineHeight = noDataFont.lineHeight - paragraphStyle.lineBreakMode = .byWordWrapping - paragraphStyle.alignment = noDataTextAlignment - - context.drawMultilineText(noDataText, - at: CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0), - constrainedTo: bounds.size, - anchor: CGPoint(x: 0.5, y: 0.5), - angleRadians: 0.0, - attributes: [.font: noDataFont, - .foregroundColor: noDataTextColor, - .paragraphStyle: paragraphStyle]) - - return - } - - if !offsetsCalculated - { - calculateOffsets() - offsetsCalculated = true - } - } - - /// Draws the description text in the bottom right corner of the chart (per default) - internal func drawDescription(in context: CGContext) - { - let description = chartDescription - - // check if description should be drawn - guard - description.isEnabled, - let descriptionText = description.text, - !descriptionText.isEmpty - else { return } - - let position = description.position ?? CGPoint(x: bounds.width - viewPortHandler.offsetRight - description.xOffset, - y: bounds.height - viewPortHandler.offsetBottom - description.yOffset - description.font.lineHeight) - - let attrs: [NSAttributedString.Key : Any] = [ - .font: description.font, - .foregroundColor: description.textColor - ] - - context.drawText(descriptionText, - at: position, - align: description.textAlign, - attributes: attrs) - } - - // MARK: - Accessibility - - open override func accessibilityChildren() -> [Any]? { - return renderer?.accessibleChartElements - } - - // MARK: - Highlighting - - /// Set this to false to prevent values from being highlighted by tap gesture. - /// Values can still be highlighted via drag or programmatically. - /// **default**: true - @objc open var highlightPerTapEnabled: Bool = true - - /// `true` if values can be highlighted via tap gesture, `false` ifnot. - @objc open var isHighLightPerTapEnabled: Bool - { - return highlightPerTapEnabled - } - - /// Checks if the highlight array is null, has a length of zero or if the first object is null. - /// - /// - Returns: `true` if there are values to highlight, `false` ifthere are no values to highlight. - @objc open func valuesToHighlight() -> Bool - { - return !highlighted.isEmpty - } - - /// Highlights the values at the given indices in the given DataSets. Provide - /// null or an empty array to undo all highlighting. - /// This should be used to programmatically highlight values. - /// This method *will not* call the delegate. - @objc open func highlightValues(_ highs: [Highlight]?) - { - // set the indices to highlight - highlighted = highs ?? [] - - lastHighlighted = highlighted.first - - // redraw the chart - setNeedsDisplay() - } - - /// Highlights any y-value at the given x-value in the given DataSet. - /// Provide -1 as the dataSetIndex to undo all highlighting. - /// This method will call the delegate. - /// - /// - Parameters: - /// - x: The x-value to highlight - /// - dataSetIndex: The dataset index to search in - /// - dataIndex: The data index to search in (only used in CombinedChartView currently) - @objc open func highlightValue(x: Double, dataSetIndex: Int, dataIndex: Int = -1) - { - highlightValue(x: x, dataSetIndex: dataSetIndex, dataIndex: dataIndex, callDelegate: true) - } - - /// Highlights the value at the given x-value and y-value in the given DataSet. - /// Provide -1 as the dataSetIndex to undo all highlighting. - /// This method will call the delegate. - /// - /// - Parameters: - /// - x: The x-value to highlight - /// - y: The y-value to highlight. Supply `NaN` for "any" - /// - dataSetIndex: The dataset index to search in - /// - dataIndex: The data index to search in (only used in CombinedChartView currently) - @objc open func highlightValue(x: Double, y: Double, dataSetIndex: Int, dataIndex: Int = -1) - { - highlightValue(x: x, y: y, dataSetIndex: dataSetIndex, dataIndex: dataIndex, callDelegate: true) - } - - /// Highlights any y-value at the given x-value in the given DataSet. - /// Provide -1 as the dataSetIndex to undo all highlighting. - /// - /// - Parameters: - /// - x: The x-value to highlight - /// - dataSetIndex: The dataset index to search in - /// - dataIndex: The data index to search in (only used in CombinedChartView currently) - /// - callDelegate: Should the delegate be called for this change - @objc open func highlightValue(x: Double, dataSetIndex: Int, dataIndex: Int = -1, callDelegate: Bool) - { - highlightValue(x: x, y: .nan, dataSetIndex: dataSetIndex, dataIndex: dataIndex, callDelegate: callDelegate) - } - - /// Highlights the value at the given x-value and y-value in the given DataSet. - /// Provide -1 as the dataSetIndex to undo all highlighting. - /// - /// - Parameters: - /// - x: The x-value to highlight - /// - y: The y-value to highlight. Supply `NaN` for "any" - /// - dataSetIndex: The dataset index to search in - /// - dataIndex: The data index to search in (only used in CombinedChartView currently) - /// - callDelegate: Should the delegate be called for this change - @objc open func highlightValue(x: Double, y: Double, dataSetIndex: Int, dataIndex: Int = -1, callDelegate: Bool) - { - guard let data = data else - { - Swift.print("Value not highlighted because data is nil") - return - } - - if data.indices.contains(dataSetIndex) - { - highlightValue(Highlight(x: x, y: y, dataSetIndex: dataSetIndex, dataIndex: dataIndex), callDelegate: callDelegate) - } - else - { - highlightValue(nil, callDelegate: callDelegate) - } - } - - /// Highlights the values represented by the provided Highlight object - /// This method *will not* call the delegate. - /// - /// - Parameters: - /// - highlight: contains information about which entry should be highlighted - @objc open func highlightValue(_ highlight: Highlight?) - { - highlightValue(highlight, callDelegate: false) - } - - /// Highlights the value selected by touch gesture. - @objc open func highlightValue(_ highlight: Highlight?, callDelegate: Bool) - { - var high = highlight - guard - let h = high, - let entry = data?.entry(for: h) - else - { - high = nil - highlighted.removeAll(keepingCapacity: false) - if callDelegate - { - delegate?.chartValueNothingSelected?(self) - } - setNeedsDisplay() - return - } - - // set the indices to highlight - highlighted = [h] - - if callDelegate - { - // notify the listener - delegate?.chartValueSelected?(self, entry: entry, highlight: h) - } - - // redraw the chart - setNeedsDisplay() - } - - /// - Returns: The Highlight object (contains x-index and DataSet index) of the - /// selected value at the given touch point inside the Line-, Scatter-, or - /// CandleStick-Chart. - @objc open func getHighlightByTouchPoint(_ pt: CGPoint) -> Highlight? - { - guard data != nil else - { - Swift.print("Can't select by touch. No data set.") - return nil - } - - return self.highlighter?.getHighlight(x: pt.x, y: pt.y) - } - - /// The last value that was highlighted via touch. - @objc open var lastHighlighted: Highlight? - - // MARK: - Markers - - /// draws all MarkerViews on the highlighted positions - internal func drawMarkers(context: CGContext) - { - // if there is no marker view or drawing marker is disabled - guard - let marker = marker, - isDrawMarkersEnabled, - valuesToHighlight() - else { return } - - for highlight in highlighted - { - guard - let set = data?[highlight.dataSetIndex], - let e = data?.entry(for: highlight) - else { continue } - - let entryIndex = set.entryIndex(entry: e) - guard entryIndex <= Int(Double(set.entryCount) * chartAnimator.phaseX) else { continue } - - let pos = getMarkerPosition(highlight: highlight) - - // check bounds - guard viewPortHandler.isInBounds(x: pos.x, y: pos.y) else { continue } - - // callbacks to update the content - marker.refreshContent(entry: e, highlight: highlight) - - // draw the marker - marker.draw(context: context, point: pos) - } - } - - /// - Returns: The actual position in pixels of the MarkerView for the given Entry in the given DataSet. - @objc open func getMarkerPosition(highlight: Highlight) -> CGPoint - { - return CGPoint(x: highlight.drawX, y: highlight.drawY) - } - - // MARK: - Animation - - /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - yAxisDuration: duration for animating the y axis - /// - easingX: an easing function for the animation on the x axis - /// - easingY: an easing function for the animation on the y axis - @objc open func animate(xAxisDuration: TimeInterval, yAxisDuration: TimeInterval, easingX: ChartEasingFunctionBlock?, easingY: ChartEasingFunctionBlock?) - { - chartAnimator.animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easingX: easingX, easingY: easingY) - } - - /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - yAxisDuration: duration for animating the y axis - /// - easingOptionX: the easing function for the animation on the x axis - /// - easingOptionY: the easing function for the animation on the y axis - @objc open func animate(xAxisDuration: TimeInterval, yAxisDuration: TimeInterval, easingOptionX: ChartEasingOption, easingOptionY: ChartEasingOption) - { - chartAnimator.animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easingOptionX: easingOptionX, easingOptionY: easingOptionY) - } - - /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - yAxisDuration: duration for animating the y axis - /// - easing: an easing function for the animation - @objc open func animate(xAxisDuration: TimeInterval, yAxisDuration: TimeInterval, easing: ChartEasingFunctionBlock?) - { - chartAnimator.animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easing: easing) - } - - /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - yAxisDuration: duration for animating the y axis - /// - easingOption: the easing function for the animation - @objc open func animate(xAxisDuration: TimeInterval, yAxisDuration: TimeInterval, easingOption: ChartEasingOption) - { - chartAnimator.animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easingOption: easingOption) - } - - /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - yAxisDuration: duration for animating the y axis - @objc open func animate(xAxisDuration: TimeInterval, yAxisDuration: TimeInterval) - { - chartAnimator.animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration) - } - - /// Animates the drawing / rendering of the chart the x-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - easing: an easing function for the animation - @objc open func animate(xAxisDuration: TimeInterval, easing: ChartEasingFunctionBlock?) - { - chartAnimator.animate(xAxisDuration: xAxisDuration, easing: easing) - } - - /// Animates the drawing / rendering of the chart the x-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - /// - easingOption: the easing function for the animation - @objc open func animate(xAxisDuration: TimeInterval, easingOption: ChartEasingOption) - { - chartAnimator.animate(xAxisDuration: xAxisDuration, easingOption: easingOption) - } - - /// Animates the drawing / rendering of the chart the x-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - xAxisDuration: duration for animating the x axis - @objc open func animate(xAxisDuration: TimeInterval) - { - chartAnimator.animate(xAxisDuration: xAxisDuration) - } - - /// Animates the drawing / rendering of the chart the y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - yAxisDuration: duration for animating the y axis - /// - easing: an easing function for the animation - @objc open func animate(yAxisDuration: TimeInterval, easing: ChartEasingFunctionBlock?) - { - chartAnimator.animate(yAxisDuration: yAxisDuration, easing: easing) - } - - /// Animates the drawing / rendering of the chart the y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - yAxisDuration: duration for animating the y axis - /// - easingOption: the easing function for the animation - @objc open func animate(yAxisDuration: TimeInterval, easingOption: ChartEasingOption) - { - chartAnimator.animate(yAxisDuration: yAxisDuration, easingOption: easingOption) - } - - /// Animates the drawing / rendering of the chart the y-axis with the specified animation time. - /// If `animate(...)` is called, no further calling of `invalidate()` is necessary to refresh the chart. - /// - /// - Parameters: - /// - yAxisDuration: duration for animating the y axis - @objc open func animate(yAxisDuration: TimeInterval) - { - chartAnimator.animate(yAxisDuration: yAxisDuration) - } - - // MARK: - Accessors - - /// The current y-max value across all DataSets - open var chartYMax: Double - { - return data?.yMax ?? 0.0 - } - - /// The current y-min value across all DataSets - open var chartYMin: Double - { - return data?.yMin ?? 0.0 - } - - open var chartXMax: Double - { - return xAxis._axisMaximum - } - - open var chartXMin: Double - { - return xAxis._axisMinimum - } - - open var xRange: Double - { - return xAxis.axisRange - } - - /// - Note: (Equivalent of getCenter() in MPAndroidChart, as center is already a standard in iOS that returns the center point relative to superview, and MPAndroidChart returns relative to self)* - /// The center point of the chart (the whole View) in pixels. - @objc open var midPoint: CGPoint - { - return CGPoint(x: bounds.origin.x + bounds.size.width / 2.0, y: bounds.origin.y + bounds.size.height / 2.0) - } - - /// The center of the chart taking offsets under consideration. (returns the center of the content rectangle) - open var centerOffsets: CGPoint - { - return viewPortHandler.contentCenter - } - - /// The rectangle that defines the borders of the chart-value surface (into which the actual values are drawn). - @objc open var contentRect: CGRect - { - return viewPortHandler.contentRect - } - - /// - Returns: The bitmap that represents the chart. - @objc open func getChartImage(transparent: Bool) -> NSUIImage? - { - NSUIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque || !transparent, NSUIMainScreen()?.nsuiScale ?? 1.0) - - guard let context = NSUIGraphicsGetCurrentContext() - else { return nil } - - let rect = CGRect(origin: .zero, size: bounds.size) - - if isOpaque || !transparent - { - // Background color may be partially transparent, we must fill with white if we want to output an opaque image - context.setFillColor(NSUIColor.white.cgColor) - context.fill(rect) - - if let backgroundColor = self.backgroundColor - { - context.setFillColor(backgroundColor.cgColor) - context.fill(rect) - } - } - - nsuiLayer?.render(in: context) - - let image = NSUIGraphicsGetImageFromCurrentImageContext() - - NSUIGraphicsEndImageContext() - - return image - } - - public enum ImageFormat - { - case jpeg - case png - } - - /// Saves the current chart state with the given name to the given path on - /// the sdcard leaving the path empty "" will put the saved file directly on - /// the SD card chart is saved as a PNG image, example: - /// saveToPath("myfilename", "foldername1/foldername2") - /// - /// - Parameters: - /// - to: path to the image to save - /// - format: the format to save - /// - compressionQuality: compression quality for lossless formats (JPEG) - /// - Returns: `true` if the image was saved successfully - open func save(to path: String, format: ImageFormat, compressionQuality: Double) -> Bool - { - guard let image = getChartImage(transparent: format != .jpeg) else { return false } - - let imageData: Data? - switch (format) - { - case .png: imageData = NSUIImagePNGRepresentation(image) - case .jpeg: imageData = NSUIImageJPEGRepresentation(image, CGFloat(compressionQuality)) - } - - guard let data = imageData else { return false } - - do - { - try data.write(to: URL(fileURLWithPath: path), options: .atomic) - } - catch - { - return false - } - - return true - } - - internal var _viewportJobs = [ViewPortJob]() - - open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) - { - if keyPath == "bounds" || keyPath == "frame" - { - let bounds = self.bounds - - if ((bounds.size.width != viewPortHandler.chartWidth || - bounds.size.height != viewPortHandler.chartHeight)) - { - viewPortHandler.setChartDimens(width: bounds.size.width, height: bounds.size.height) - - // This may cause the chart view to mutate properties affecting the view port -- lets do this - // before we try to run any pending jobs on the view port itself - notifyDataSetChanged() - - // Finish any pending viewport changes - while (!_viewportJobs.isEmpty) - { - let job = _viewportJobs.remove(at: 0) - job.doJob() - } - } - } - } - - @objc open func removeViewportJob(_ job: ViewPortJob) - { - if let index = _viewportJobs.firstIndex(where: { $0 === job }) - { - _viewportJobs.remove(at: index) - } - } - - @objc open func clearAllViewportJobs() - { - _viewportJobs.removeAll(keepingCapacity: false) - } - - @objc open func addViewportJob(_ job: ViewPortJob) - { - if viewPortHandler.hasChartDimens - { - job.doJob() - } - else - { - _viewportJobs.append(job) - } - } - - /// **default**: true - /// `true` if chart continues to scroll after touch up, `false` ifnot. - @objc open var isDragDecelerationEnabled: Bool - { - return dragDecelerationEnabled - } - - /// Deceleration friction coefficient in [0 ; 1] interval, higher values indicate that speed will decrease slowly, for example if it set to 0, it will stop immediately. - /// 1 is an invalid value, and will be converted to 0.999 automatically. - @objc open var dragDecelerationFrictionCoef: CGFloat - { - get - { - return _dragDecelerationFrictionCoef - } - set - { - _dragDecelerationFrictionCoef = max(0, min(newValue, 0.999)) - } - } - private var _dragDecelerationFrictionCoef: CGFloat = 0.9 - - /// The maximum distance in screen pixels away from an entry causing it to highlight. - /// **default**: 500.0 - open var maxHighlightDistance: CGFloat = 500.0 - - /// the number of maximum visible drawn values on the chart only active when `drawValuesEnabled` is enabled - open var maxVisibleCount: Int - { - return .max - } - - // MARK: - AnimatorDelegate - - open func animatorUpdated(_ chartAnimator: Animator) - { - setNeedsDisplay() - } - - open func animatorStopped(_ chartAnimator: Animator) - { - delegate?.chartView?(self, animatorDidStop: chartAnimator) - } - - // MARK: - Touches - - open override func nsuiTouchesBegan(_ touches: Set, withEvent event: NSUIEvent?) - { - super.nsuiTouchesBegan(touches, withEvent: event) - } - - open override func nsuiTouchesMoved(_ touches: Set, withEvent event: NSUIEvent?) - { - super.nsuiTouchesMoved(touches, withEvent: event) - } - - open override func nsuiTouchesEnded(_ touches: Set, withEvent event: NSUIEvent?) - { - super.nsuiTouchesEnded(touches, withEvent: event) - } - - open override func nsuiTouchesCancelled(_ touches: Set?, withEvent event: NSUIEvent?) - { - super.nsuiTouchesCancelled(touches, withEvent: event) - } -} diff --git a/Pods/Charts/Source/Charts/Charts/CombinedChartView.swift b/Pods/Charts/Source/Charts/Charts/CombinedChartView.swift deleted file mode 100644 index 6b2ecbf10..000000000 --- a/Pods/Charts/Source/Charts/Charts/CombinedChartView.swift +++ /dev/null @@ -1,246 +0,0 @@ -// -// CombinedChartView.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// This chart class allows the combination of lines, bars, scatter and candle data all displayed in one chart area. -open class CombinedChartView: BarLineChartViewBase, CombinedChartDataProvider -{ - /// the fill-formatter used for determining the position of the fill-line - internal var _fillFormatter: FillFormatter! - - /// enum that allows to specify the order in which the different data objects for the combined-chart are drawn - @objc(CombinedChartDrawOrder) - public enum DrawOrder: Int - { - case bar - case bubble - case line - case candle - case scatter - } - - open override func initialize() - { - super.initialize() - - self.highlighter = CombinedHighlighter(chart: self, barDataProvider: self) - - // Old default behaviour - self.highlightFullBarEnabled = true - - _fillFormatter = DefaultFillFormatter() - - renderer = CombinedChartRenderer(chart: self, animator: chartAnimator, viewPortHandler: viewPortHandler) - } - - open override var data: ChartData? - { - get - { - return super.data - } - set - { - super.data = newValue - - self.highlighter = CombinedHighlighter(chart: self, barDataProvider: self) - - (renderer as? CombinedChartRenderer)?.createRenderers() - renderer?.initBuffers() - } - } - - @objc open var fillFormatter: FillFormatter - { - get - { - return _fillFormatter - } - set - { - _fillFormatter = newValue - if _fillFormatter == nil - { - _fillFormatter = DefaultFillFormatter() - } - } - } - - /// - Returns: The Highlight object (contains x-index and DataSet index) of the selected value at the given touch point inside the CombinedChart. - open override func getHighlightByTouchPoint(_ pt: CGPoint) -> Highlight? - { - if data === nil - { - Swift.print("Can't select by touch. No data set.") - return nil - } - - guard let h = self.highlighter?.getHighlight(x: pt.x, y: pt.y) - else { return nil } - - if !isHighlightFullBarEnabled { return h } - - // For isHighlightFullBarEnabled, remove stackIndex - return Highlight( - x: h.x, y: h.y, - xPx: h.xPx, yPx: h.yPx, - dataIndex: h.dataIndex, - dataSetIndex: h.dataSetIndex, - stackIndex: -1, - axis: h.axis) - } - - // MARK: - CombinedChartDataProvider - - open var combinedData: CombinedChartData? - { - get - { - return data as? CombinedChartData - } - } - - // MARK: - LineChartDataProvider - - open var lineData: LineChartData? - { - get - { - return combinedData?.lineData - } - } - - // MARK: - BarChartDataProvider - - open var barData: BarChartData? - { - get - { - return combinedData?.barData - } - } - - // MARK: - ScatterChartDataProvider - - open var scatterData: ScatterChartData? - { - get - { - return combinedData?.scatterData - } - } - - // MARK: - CandleChartDataProvider - - open var candleData: CandleChartData? - { - get - { - return combinedData?.candleData - } - } - - // MARK: - BubbleChartDataProvider - - open var bubbleData: BubbleChartData? - { - get - { - return combinedData?.bubbleData - } - } - - // MARK: - Accessors - - /// if set to true, all values are drawn above their bars, instead of below their top - @objc open var drawValueAboveBarEnabled: Bool - { - get { return (renderer as! CombinedChartRenderer).drawValueAboveBarEnabled } - set { (renderer as! CombinedChartRenderer).drawValueAboveBarEnabled = newValue } - } - - /// if set to true, a grey area is drawn behind each bar that indicates the maximum value - @objc open var drawBarShadowEnabled: Bool - { - get { return (renderer as! CombinedChartRenderer).drawBarShadowEnabled } - set { (renderer as! CombinedChartRenderer).drawBarShadowEnabled = newValue } - } - - /// `true` if drawing values above bars is enabled, `false` ifnot - open var isDrawValueAboveBarEnabled: Bool { return (renderer as! CombinedChartRenderer).drawValueAboveBarEnabled } - - /// `true` if drawing shadows (maxvalue) for each bar is enabled, `false` ifnot - open var isDrawBarShadowEnabled: Bool { return (renderer as! CombinedChartRenderer).drawBarShadowEnabled } - - /// the order in which the provided data objects should be drawn. - /// The earlier you place them in the provided array, the further they will be in the background. - /// e.g. if you provide [DrawOrder.Bar, DrawOrder.Line], the bars will be drawn behind the lines. - @objc open var drawOrder: [Int] - { - get - { - return (renderer as! CombinedChartRenderer).drawOrder.map { $0.rawValue } - } - set - { - (renderer as! CombinedChartRenderer).drawOrder = newValue.map { DrawOrder(rawValue: $0)! } - } - } - - /// Set this to `true` to make the highlight operation full-bar oriented, `false` to make it highlight single values - @objc open var highlightFullBarEnabled: Bool = false - - /// `true` the highlight is be full-bar oriented, `false` ifsingle-value - open var isHighlightFullBarEnabled: Bool { return highlightFullBarEnabled } - - // MARK: - ChartViewBase - - /// draws all MarkerViews on the highlighted positions - override func drawMarkers(context: CGContext) - { - guard - let marker = marker, - isDrawMarkersEnabled && valuesToHighlight() - else { return } - - for i in highlighted.indices - { - let highlight = highlighted[i] - - guard - let set = combinedData?.getDataSetByHighlight(highlight), - let e = data?.entry(for: highlight) - else { continue } - - let entryIndex = set.entryIndex(entry: e) - if entryIndex > Int(Double(set.entryCount) * chartAnimator.phaseX) - { - continue - } - - let pos = getMarkerPosition(highlight: highlight) - - // check bounds - if !viewPortHandler.isInBounds(x: pos.x, y: pos.y) - { - continue - } - - // callbacks to update the content - marker.refreshContent(entry: e, highlight: highlight) - - // draw the marker - marker.draw(context: context, point: pos) - } - } -} diff --git a/Pods/Charts/Source/Charts/Charts/HorizontalBarChartView.swift b/Pods/Charts/Source/Charts/Charts/HorizontalBarChartView.swift deleted file mode 100644 index 2da795212..000000000 --- a/Pods/Charts/Source/Charts/Charts/HorizontalBarChartView.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// HorizontalBarChartView.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// BarChart with horizontal bar orientation. In this implementation, x- and y-axis are switched. -open class HorizontalBarChartView: BarChartView -{ - internal override func initialize() - { - super.initialize() - - _leftAxisTransformer = TransformerHorizontalBarChart(viewPortHandler: viewPortHandler) - _rightAxisTransformer = TransformerHorizontalBarChart(viewPortHandler: viewPortHandler) - - renderer = HorizontalBarChartRenderer(dataProvider: self, animator: chartAnimator, viewPortHandler: viewPortHandler) - leftYAxisRenderer = YAxisRendererHorizontalBarChart(viewPortHandler: viewPortHandler, axis: leftAxis, transformer: _leftAxisTransformer) - rightYAxisRenderer = YAxisRendererHorizontalBarChart(viewPortHandler: viewPortHandler, axis: rightAxis, transformer: _rightAxisTransformer) - xAxisRenderer = XAxisRendererHorizontalBarChart(viewPortHandler: viewPortHandler, axis: xAxis, transformer: _leftAxisTransformer, chart: self) - - self.highlighter = HorizontalBarHighlighter(chart: self) - } - - internal override func calculateLegendOffsets(offsetLeft: inout CGFloat, offsetTop: inout CGFloat, offsetRight: inout CGFloat, offsetBottom: inout CGFloat) - { - guard - legend.isEnabled, - !legend.drawInside - else { return } - - // setup offsets for legend - switch legend.orientation - { - case .vertical: - switch legend.horizontalAlignment - { - case .left: - offsetLeft += min(legend.neededWidth, viewPortHandler.chartWidth * legend.maxSizePercent) + legend.xOffset - - case .right: - offsetRight += min(legend.neededWidth, viewPortHandler.chartWidth * legend.maxSizePercent) + legend.xOffset - - case .center: - - switch legend.verticalAlignment - { - case .top: - offsetTop += min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) + legend.yOffset - - case .bottom: - offsetBottom += min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) + legend.yOffset - - default: - break - } - } - - case .horizontal: - switch legend.verticalAlignment - { - case .top: - offsetTop += min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) + legend.yOffset - - // left axis equals the top x-axis in a horizontal chart - if leftAxis.isEnabled && leftAxis.isDrawLabelsEnabled - { - offsetTop += leftAxis.getRequiredHeightSpace() - } - - case .bottom: - offsetBottom += min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) + legend.yOffset - - // right axis equals the bottom x-axis in a horizontal chart - if rightAxis.isEnabled && rightAxis.isDrawLabelsEnabled - { - offsetBottom += rightAxis.getRequiredHeightSpace() - } - default: - break - } - } - } - - internal override func calculateOffsets() - { - var offsetLeft: CGFloat = 0.0, - offsetRight: CGFloat = 0.0, - offsetTop: CGFloat = 0.0, - offsetBottom: CGFloat = 0.0 - - calculateLegendOffsets(offsetLeft: &offsetLeft, - offsetTop: &offsetTop, - offsetRight: &offsetRight, - offsetBottom: &offsetBottom) - - // offsets for y-labels - if leftAxis.needsOffset - { - offsetTop += leftAxis.getRequiredHeightSpace() - } - - if rightAxis.needsOffset - { - offsetBottom += rightAxis.getRequiredHeightSpace() - } - - let xlabelwidth = xAxis.labelRotatedWidth - - if xAxis.isEnabled - { - // offsets for x-labels - if xAxis.labelPosition == .bottom - { - offsetLeft += xlabelwidth - } - else if xAxis.labelPosition == .top - { - offsetRight += xlabelwidth - } - else if xAxis.labelPosition == .bothSided - { - offsetLeft += xlabelwidth - offsetRight += xlabelwidth - } - } - - offsetTop += self.extraTopOffset - offsetRight += self.extraRightOffset - offsetBottom += self.extraBottomOffset - offsetLeft += self.extraLeftOffset - - viewPortHandler.restrainViewPort( - offsetLeft: max(self.minOffset, offsetLeft), - offsetTop: max(self.minOffset, offsetTop), - offsetRight: max(self.minOffset, offsetRight), - offsetBottom: max(self.minOffset, offsetBottom)) - - prepareOffsetMatrix() - prepareValuePxMatrix() - } - - internal override func prepareValuePxMatrix() - { - _rightAxisTransformer.prepareMatrixValuePx(chartXMin: rightAxis._axisMinimum, deltaX: CGFloat(rightAxis.axisRange), deltaY: CGFloat(xAxis.axisRange), chartYMin: xAxis._axisMinimum) - _leftAxisTransformer.prepareMatrixValuePx(chartXMin: leftAxis._axisMinimum, deltaX: CGFloat(leftAxis.axisRange), deltaY: CGFloat(xAxis.axisRange), chartYMin: xAxis._axisMinimum) - } - - open override func getMarkerPosition(highlight: Highlight) -> CGPoint - { - return CGPoint(x: highlight.drawY, y: highlight.drawX) - } - - open override func getBarBounds(entry e: BarChartDataEntry) -> CGRect - { - guard - let data = data as? BarChartData, - let set = data.getDataSetForEntry(e) as? BarChartDataSetProtocol - else { return .null } - - let y = e.y - let x = e.x - - let barWidth = data.barWidth - - let top = x - 0.5 + barWidth / 2.0 - let bottom = x + 0.5 - barWidth / 2.0 - let left = y >= 0.0 ? y : 0.0 - let right = y <= 0.0 ? y : 0.0 - - var bounds = CGRect(x: left, y: top, width: right - left, height: bottom - top) - - getTransformer(forAxis: set.axisDependency).rectValueToPixel(&bounds) - - return bounds - } - - open override func getPosition(entry e: ChartDataEntry, axis: YAxis.AxisDependency) -> CGPoint - { - var vals = CGPoint(x: CGFloat(e.y), y: CGFloat(e.x)) - - getTransformer(forAxis: axis).pointValueToPixel(&vals) - - return vals - } - - open override func getHighlightByTouchPoint(_ pt: CGPoint) -> Highlight? - { - if data === nil - { - Swift.print("Can't select by touch. No data set.", terminator: "\n") - return nil - } - - return self.highlighter?.getHighlight(x: pt.y, y: pt.x) - } - - /// The lowest x-index (value on the x-axis) that is still visible on he chart. - open override var lowestVisibleX: Double - { - var pt = CGPoint( - x: viewPortHandler.contentLeft, - y: viewPortHandler.contentBottom) - - getTransformer(forAxis: .left).pixelToValues(&pt) - - return max(xAxis._axisMinimum, Double(pt.y)) - } - - /// The highest x-index (value on the x-axis) that is still visible on the chart. - open override var highestVisibleX: Double - { - var pt = CGPoint( - x: viewPortHandler.contentLeft, - y: viewPortHandler.contentTop) - - getTransformer(forAxis: .left).pixelToValues(&pt) - - return min(xAxis._axisMaximum, Double(pt.y)) - } - - // MARK: - Viewport - - open override func setVisibleXRangeMaximum(_ maxXRange: Double) - { - let xScale = xAxis.axisRange / maxXRange - viewPortHandler.setMinimumScaleY(CGFloat(xScale)) - } - - open override func setVisibleXRangeMinimum(_ minXRange: Double) - { - let xScale = xAxis.axisRange / minXRange - viewPortHandler.setMaximumScaleY(CGFloat(xScale)) - } - - open override func setVisibleXRange(minXRange: Double, maxXRange: Double) - { - let minScale = xAxis.axisRange / minXRange - let maxScale = xAxis.axisRange / maxXRange - viewPortHandler.setMinMaxScaleY(minScaleY: CGFloat(minScale), maxScaleY: CGFloat(maxScale)) - } - - open override func setVisibleYRangeMaximum(_ maxYRange: Double, axis: YAxis.AxisDependency) - { - let yScale = getAxisRange(axis: axis) / maxYRange - viewPortHandler.setMinimumScaleX(CGFloat(yScale)) - } - - open override func setVisibleYRangeMinimum(_ minYRange: Double, axis: YAxis.AxisDependency) - { - let yScale = getAxisRange(axis: axis) / minYRange - viewPortHandler.setMaximumScaleX(CGFloat(yScale)) - } - - open override func setVisibleYRange(minYRange: Double, maxYRange: Double, axis: YAxis.AxisDependency) - { - let minScale = getAxisRange(axis: axis) / minYRange - let maxScale = getAxisRange(axis: axis) / maxYRange - viewPortHandler.setMinMaxScaleX(minScaleX: CGFloat(minScale), maxScaleX: CGFloat(maxScale)) - } -} diff --git a/Pods/Charts/Source/Charts/Charts/LineChartView.swift b/Pods/Charts/Source/Charts/Charts/LineChartView.swift deleted file mode 100644 index e7918266b..000000000 --- a/Pods/Charts/Source/Charts/Charts/LineChartView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// LineChartView.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// Chart that draws lines, surfaces, circles, ... -open class LineChartView: BarLineChartViewBase, LineChartDataProvider -{ - internal override func initialize() - { - super.initialize() - - renderer = LineChartRenderer(dataProvider: self, animator: chartAnimator, viewPortHandler: viewPortHandler) - } - - // MARK: - LineChartDataProvider - - open var lineData: LineChartData? { return data as? LineChartData } -} diff --git a/Pods/Charts/Source/Charts/Charts/PieChartView.swift b/Pods/Charts/Source/Charts/Charts/PieChartView.swift deleted file mode 100644 index 704c066bd..000000000 --- a/Pods/Charts/Source/Charts/Charts/PieChartView.swift +++ /dev/null @@ -1,675 +0,0 @@ -// -// PieChartView.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -#if !os(OSX) - import UIKit -#endif - -/// View that represents a pie chart. Draws cake like slices. -open class PieChartView: PieRadarChartViewBase -{ - /// rect object that represents the bounds of the piechart, needed for drawing the circle - private var _circleBox = CGRect() - - /// flag indicating if entry labels should be drawn or not - private var _drawEntryLabelsEnabled = true - - /// array that holds the width of each pie-slice in degrees - private var _drawAngles = [CGFloat]() - - /// array that holds the absolute angle in degrees of each slice - private var _absoluteAngles = [CGFloat]() - - /// if true, the hole inside the chart will be drawn - private var _drawHoleEnabled = true - - private var _holeColor: NSUIColor? = NSUIColor.white - - /// Sets the color the entry labels are drawn with. - private var _entryLabelColor: NSUIColor? = NSUIColor.white - - /// Sets the font the entry labels are drawn with. - private var _entryLabelFont: NSUIFont? = NSUIFont(name: "HelveticaNeue", size: 13.0) - - /// if true, the hole will see-through to the inner tips of the slices - private var _drawSlicesUnderHoleEnabled = false - - /// if true, the values inside the piechart are drawn as percent values - private var _usePercentValuesEnabled = false - - /// variable for the text that is drawn in the center of the pie-chart - private var _centerAttributedText: NSAttributedString? - - /// the offset on the x- and y-axis the center text has in dp. - private var _centerTextOffset: CGPoint = CGPoint() - - /// indicates the size of the hole in the center of the piechart - /// - /// **default**: `0.5` - private var _holeRadiusPercent = CGFloat(0.5) - - private var _transparentCircleColor: NSUIColor? = NSUIColor(white: 1.0, alpha: 105.0/255.0) - - /// the radius of the transparent circle next to the chart-hole in the center - private var _transparentCircleRadiusPercent = CGFloat(0.55) - - /// if enabled, centertext is drawn - private var _drawCenterTextEnabled = true - - private var _centerTextRadiusPercent: CGFloat = 1.0 - - /// maximum angle for this pie - private var _maxAngle: CGFloat = 360.0 - - public override init(frame: CGRect) - { - super.init(frame: frame) - } - - public required init?(coder aDecoder: NSCoder) - { - super.init(coder: aDecoder) - } - - internal override func initialize() - { - super.initialize() - - renderer = PieChartRenderer(chart: self, animator: chartAnimator, viewPortHandler: viewPortHandler) - - self.highlighter = PieHighlighter(chart: self) - } - - open override func draw(_ rect: CGRect) - { - super.draw(rect) - - if data === nil - { - return - } - - let optionalContext = NSUIGraphicsGetCurrentContext() - guard let context = optionalContext, let renderer = renderer else - { - return - } - - renderer.drawData(context: context) - - if (valuesToHighlight()) - { - renderer.drawHighlighted(context: context, indices: highlighted) - } - - renderer.drawExtras(context: context) - - renderer.drawValues(context: context) - - legendRenderer.renderLegend(context: context) - - drawDescription(in: context) - - drawMarkers(context: context) - } - - /// if width is larger than height - private var widthLarger: Bool - { - return viewPortHandler.contentRect.orientation == .landscape - } - - /// adjusted radius. Use diameter when it's half pie and width is larger - private var adjustedRadius: CGFloat - { - return maxAngle <= 180 && widthLarger ? diameter : diameter / 2.0 - } - - /// true centerOffsets considering half pie & width is larger - private func adjustedCenterOffsets() -> CGPoint - { - var c = self.centerOffsets - c.y = maxAngle <= 180 && widthLarger ? c.y + adjustedRadius / 2 : c.y - return c - } - - internal override func calculateOffsets() - { - super.calculateOffsets() - - // prevent nullpointer when no data set - if data === nil - { - return - } - - let radius = adjustedRadius - - let c = adjustedCenterOffsets() - - let shift = (data as? PieChartData)?.dataSet?.selectionShift ?? 0.0 - - // create the circle box that will contain the pie-chart (the bounds of the pie-chart) - _circleBox.origin.x = (c.x - radius) + shift - _circleBox.origin.y = (c.y - radius) + shift - _circleBox.size.width = radius * 2 - shift * 2.0 - _circleBox.size.height = radius * 2 - shift * 2.0 - - } - - internal override func calcMinMax() - { - calcAngles() - } - - @objc open override func angleForPoint(x: CGFloat, y: CGFloat) -> CGFloat - { - let c = adjustedCenterOffsets() - - let tx = Double(x - c.x) - let ty = Double(y - c.y) - let length = sqrt(tx * tx + ty * ty) - let r = acos(ty / length) - - var angle = r.RAD2DEG - - if x > c.x - { - angle = 360.0 - angle - } - - // add 90° because chart starts EAST - angle = angle + 90.0 - - // neutralize overflow - if angle > 360.0 - { - angle = angle - 360.0 - } - - return CGFloat(angle) - } - - /// - Returns: The distance of a certain point on the chart to the center of the chart. - @objc open override func distanceToCenter(x: CGFloat, y: CGFloat) -> CGFloat - { - let c = adjustedCenterOffsets() - var dist = CGFloat(0.0) - - let xDist = x > c.x ? x - c.x : c.x - x - let yDist = y > c.y ? y - c.y : c.y - y - - // pythagoras - dist = sqrt(pow(xDist, 2.0) + pow(yDist, 2.0)) - - return dist - } - - open override func getMarkerPosition(highlight: Highlight) -> CGPoint - { - let center = self.centerCircleBox - var r = self.radius - - var off = r / 10.0 * 3.6 - - if self.isDrawHoleEnabled - { - off = (r - (r * self.holeRadiusPercent)) / 2.0 - } - - r -= off // offset to keep things inside the chart - - let rotationAngle = self.rotationAngle - - let entryIndex = Int(highlight.x) - - // offset needed to center the drawn text in the slice - let offset = drawAngles[entryIndex] / 2.0 - - // calculate the text position - let x = (r * cos(((rotationAngle + absoluteAngles[entryIndex] - offset) * CGFloat(chartAnimator.phaseY)).DEG2RAD) + center.x) - let y = (r * sin(((rotationAngle + absoluteAngles[entryIndex] - offset) * CGFloat(chartAnimator.phaseY)).DEG2RAD) + center.y) - - return CGPoint(x: x, y: y) - } - - /// calculates the needed angles for the chart slices - private func calcAngles() - { - _drawAngles = [CGFloat]() - _absoluteAngles = [CGFloat]() - - guard let data = data else { return } - - let entryCount = data.entryCount - - _drawAngles.reserveCapacity(entryCount) - _absoluteAngles.reserveCapacity(entryCount) - - let yValueSum = (data as! PieChartData).yValueSum - - var cnt = 0 - - for set in data - { - for j in 0 ..< set.entryCount - { - guard let e = set.entryForIndex(j) else { continue } - - _drawAngles.append(calcAngle(value: abs(e.y), yValueSum: yValueSum)) - - if cnt == 0 - { - _absoluteAngles.append(_drawAngles[cnt]) - } - else - { - _absoluteAngles.append(_absoluteAngles[cnt - 1] + _drawAngles[cnt]) - } - - cnt += 1 - } - } - } - - /// Checks if the given index is set to be highlighted. - @objc open func needsHighlight(index: Int) -> Bool - { - return highlighted.contains { Int($0.x) == index } - } - - /// calculates the needed angle for a given value - private func calcAngle(_ value: Double) -> CGFloat - { - return calcAngle(value: value, yValueSum: (data as! PieChartData).yValueSum) - } - - /// calculates the needed angle for a given value - private func calcAngle(value: Double, yValueSum: Double) -> CGFloat - { - return CGFloat(value) / CGFloat(yValueSum) * _maxAngle - } - - /// This will throw an exception, PieChart has no XAxis object. - open override var xAxis: XAxis - { - get { fatalError("PieChart has no XAxis") } - set { fatalError("PieChart has no XAxis") } - } - - open override func indexForAngle(_ angle: CGFloat) -> Int - { - // TODO: Return nil instead of -1 - // take the current angle of the chart into consideration - let a = (angle - self.rotationAngle).normalizedAngle - return _absoluteAngles.firstIndex { $0 > a } ?? -1 - } - - /// - Returns: The index of the DataSet this x-index belongs to. - @objc open func dataSetIndexForIndex(_ xValue: Double) -> Int - { - // TODO: Return nil instead of -1 - return data?.firstIndex { - $0.entryForXValue(xValue, closestToY: .nan) != nil - } ?? -1 - } - - /// - Returns: An integer array of all the different angles the chart slices - /// have the angles in the returned array determine how much space (of 360°) - /// each slice takes - @objc open var drawAngles: [CGFloat] - { - return _drawAngles - } - - /// - Returns: The absolute angles of the different chart slices (where the - /// slices end) - @objc open var absoluteAngles: [CGFloat] - { - return _absoluteAngles - } - - /// The color for the hole that is drawn in the center of the PieChart (if enabled). - /// - /// - Note: Use holeTransparent with holeColor = nil to make the hole transparent.* - @objc open var holeColor: NSUIColor? - { - get - { - return _holeColor - } - set - { - _holeColor = newValue - setNeedsDisplay() - } - } - - /// if true, the hole will see-through to the inner tips of the slices - /// - /// **default**: `false` - @objc open var drawSlicesUnderHoleEnabled: Bool - { - get - { - return _drawSlicesUnderHoleEnabled - } - set - { - _drawSlicesUnderHoleEnabled = newValue - setNeedsDisplay() - } - } - - /// `true` if the inner tips of the slices are visible behind the hole, `false` if not. - @objc open var isDrawSlicesUnderHoleEnabled: Bool - { - return drawSlicesUnderHoleEnabled - } - - /// `true` if the hole in the center of the pie-chart is set to be visible, `false` ifnot - @objc open var drawHoleEnabled: Bool - { - get - { - return _drawHoleEnabled - } - set - { - _drawHoleEnabled = newValue - setNeedsDisplay() - } - } - - /// `true` if the hole in the center of the pie-chart is set to be visible, `false` ifnot - @objc open var isDrawHoleEnabled: Bool - { - get - { - return drawHoleEnabled - } - } - - /// the text that is displayed in the center of the pie-chart - @objc open var centerText: String? - { - get - { - return self.centerAttributedText?.string - } - set - { - var attrString: NSMutableAttributedString? - if newValue == nil - { - attrString = nil - } - else - { - let paragraphStyle = ParagraphStyle.default.mutableCopy() as! MutableParagraphStyle - paragraphStyle.lineBreakMode = .byTruncatingTail - paragraphStyle.alignment = .center - - attrString = NSMutableAttributedString(string: newValue!) - attrString?.setAttributes([ - .foregroundColor: NSUIColor.labelOrBlack, - .font: NSUIFont.systemFont(ofSize: 12.0), - .paragraphStyle: paragraphStyle - ], range: NSMakeRange(0, attrString!.length)) - } - self.centerAttributedText = attrString - } - } - - /// the text that is displayed in the center of the pie-chart - @objc open var centerAttributedText: NSAttributedString? - { - get - { - return _centerAttributedText - } - set - { - _centerAttributedText = newValue - setNeedsDisplay() - } - } - - /// Sets the offset the center text should have from it's original position in dp. Default x = 0, y = 0 - @objc open var centerTextOffset: CGPoint - { - get - { - return _centerTextOffset - } - set - { - _centerTextOffset = newValue - setNeedsDisplay() - } - } - - /// `true` if drawing the center text is enabled - @objc open var drawCenterTextEnabled: Bool - { - get - { - return _drawCenterTextEnabled - } - set - { - _drawCenterTextEnabled = newValue - setNeedsDisplay() - } - } - - /// `true` if drawing the center text is enabled - @objc open var isDrawCenterTextEnabled: Bool - { - get - { - return drawCenterTextEnabled - } - } - - internal override var requiredLegendOffset: CGFloat - { - return legend.font.pointSize * 2.0 - } - - internal override var requiredBaseOffset: CGFloat - { - return 0.0 - } - - open override var radius: CGFloat - { - return _circleBox.width / 2.0 - } - - /// The circlebox, the boundingbox of the pie-chart slices - @objc open var circleBox: CGRect - { - return _circleBox - } - - /// The center of the circlebox - @objc open var centerCircleBox: CGPoint - { - return CGPoint(x: _circleBox.midX, y: _circleBox.midY) - } - - /// the radius of the hole in the center of the piechart in percent of the maximum radius (max = the radius of the whole chart) - /// - /// **default**: 0.5 (50%) (half the pie) - @objc open var holeRadiusPercent: CGFloat - { - get - { - return _holeRadiusPercent - } - set - { - _holeRadiusPercent = newValue - setNeedsDisplay() - } - } - - /// The color that the transparent-circle should have. - /// - /// **default**: `nil` - @objc open var transparentCircleColor: NSUIColor? - { - get - { - return _transparentCircleColor - } - set - { - _transparentCircleColor = newValue - setNeedsDisplay() - } - } - - /// the radius of the transparent circle that is drawn next to the hole in the piechart in percent of the maximum radius (max = the radius of the whole chart) - /// - /// **default**: 0.55 (55%) -> means 5% larger than the center-hole by default - @objc open var transparentCircleRadiusPercent: CGFloat - { - get - { - return _transparentCircleRadiusPercent - } - set - { - _transparentCircleRadiusPercent = newValue - setNeedsDisplay() - } - } - - /// The color the entry labels are drawn with. - @objc open var entryLabelColor: NSUIColor? - { - get { return _entryLabelColor } - set - { - _entryLabelColor = newValue - setNeedsDisplay() - } - } - - /// The font the entry labels are drawn with. - @objc open var entryLabelFont: NSUIFont? - { - get { return _entryLabelFont } - set - { - _entryLabelFont = newValue - setNeedsDisplay() - } - } - - /// Set this to true to draw the enrty labels into the pie slices - @objc open var drawEntryLabelsEnabled: Bool - { - get - { - return _drawEntryLabelsEnabled - } - set - { - _drawEntryLabelsEnabled = newValue - setNeedsDisplay() - } - } - - /// `true` if drawing entry labels is enabled, `false` ifnot - @objc open var isDrawEntryLabelsEnabled: Bool - { - get - { - return drawEntryLabelsEnabled - } - } - - /// If this is enabled, values inside the PieChart are drawn in percent and not with their original value. Values provided for the ValueFormatter to format are then provided in percent. - @objc open var usePercentValuesEnabled: Bool - { - get - { - return _usePercentValuesEnabled - } - set - { - _usePercentValuesEnabled = newValue - setNeedsDisplay() - } - } - - /// `true` if drawing x-values is enabled, `false` ifnot - @objc open var isUsePercentValuesEnabled: Bool - { - get - { - return usePercentValuesEnabled - } - } - - /// the rectangular radius of the bounding box for the center text, as a percentage of the pie hole - @objc open var centerTextRadiusPercent: CGFloat - { - get - { - return _centerTextRadiusPercent - } - set - { - _centerTextRadiusPercent = newValue - setNeedsDisplay() - } - } - - /// The max angle that is used for calculating the pie-circle. - /// 360 means it's a full pie-chart, 180 results in a half-pie-chart. - /// **default**: 360.0 - @objc open var maxAngle: CGFloat - { - get - { - return _maxAngle - } - set - { - _maxAngle = newValue - - if _maxAngle > 360.0 - { - _maxAngle = 360.0 - } - - if _maxAngle < 90.0 - { - _maxAngle = 90.0 - } - } - } - - /// smallest pie slice angle that will have a label drawn in degrees, 0 by default - @objc open var sliceTextDrawingThreshold: CGFloat = 0.0 - { - didSet { - setNeedsDisplay() - } - } -} diff --git a/Pods/Charts/Source/Charts/Charts/PieRadarChartViewBase.swift b/Pods/Charts/Source/Charts/Charts/PieRadarChartViewBase.swift deleted file mode 100644 index 45b8e0f1d..000000000 --- a/Pods/Charts/Source/Charts/Charts/PieRadarChartViewBase.swift +++ /dev/null @@ -1,854 +0,0 @@ -// -// PieRadarChartViewBase.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics -import QuartzCore - -#if canImport(AppKit) -import AppKit -#endif - - -/// Base class of PieChartView and RadarChartView. -open class PieRadarChartViewBase: ChartViewBase -{ - /// holds the normalized version of the current rotation angle of the chart - private var _rotationAngle = CGFloat(270.0) - - /// holds the raw version of the current rotation angle of the chart - private var _rawRotationAngle = CGFloat(270.0) - - /// flag that indicates if rotation is enabled or not - @objc open var rotationEnabled = true - - /// Sets the minimum offset (padding) around the chart, defaults to 0.0 - @objc open var minOffset = CGFloat(0.0) - - /// iOS && OSX only: Enabled multi-touch rotation using two fingers. - private var _rotationWithTwoFingers = false - - private var _tapGestureRecognizer: NSUITapGestureRecognizer! - #if !os(tvOS) - private var _rotationGestureRecognizer: NSUIRotationGestureRecognizer! - #endif - - public override init(frame: CGRect) - { - super.init(frame: frame) - } - - public required init?(coder aDecoder: NSCoder) - { - super.init(coder: aDecoder) - } - - deinit - { - stopDeceleration() - } - - internal override func initialize() - { - super.initialize() - - _tapGestureRecognizer = NSUITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))) - - self.addGestureRecognizer(_tapGestureRecognizer) - - #if !os(tvOS) - _rotationGestureRecognizer = NSUIRotationGestureRecognizer(target: self, action: #selector(rotationGestureRecognized(_:))) - self.addGestureRecognizer(_rotationGestureRecognizer) - _rotationGestureRecognizer.isEnabled = rotationWithTwoFingers - #endif - } - - internal override func calcMinMax() - { - /*_xAxis.axisRange = Double((data?.xVals.count ?? 0) - 1)*/ - } - - open override var maxVisibleCount: Int - { - get - { - return data?.entryCount ?? 0 - } - } - - open override func notifyDataSetChanged() - { - calcMinMax() - - if let data = data - { - legendRenderer.computeLegend(data: data) - } - - calculateOffsets() - - setNeedsDisplay() - } - - internal override func calculateOffsets() - { - var legendLeft = CGFloat(0.0) - var legendRight = CGFloat(0.0) - var legendBottom = CGFloat(0.0) - var legendTop = CGFloat(0.0) - - if legend.enabled && !legend.drawInside - { - let fullLegendWidth = min(legend.neededWidth, viewPortHandler.chartWidth * legend.maxSizePercent) - - switch legend.orientation - { - case .vertical: - - var xLegendOffset: CGFloat = 0.0 - - if legend.horizontalAlignment == .left - || legend.horizontalAlignment == .right - { - if legend.verticalAlignment == .center - { - // this is the space between the legend and the chart - let spacing = CGFloat(13.0) - - xLegendOffset = fullLegendWidth + spacing - } - else - { - // this is the space between the legend and the chart - let spacing = CGFloat(8.0) - - let legendWidth = fullLegendWidth + spacing - let legendHeight = legend.neededHeight + legend.textHeightMax - - let c = self.midPoint - - let bottomX = legend.horizontalAlignment == .right - ? self.bounds.width - legendWidth + 15.0 - : legendWidth - 15.0 - let bottomY = legendHeight + 15 - let distLegend = distanceToCenter(x: bottomX, y: bottomY) - - let reference = getPosition(center: c, dist: self.radius, - angle: angleForPoint(x: bottomX, y: bottomY)) - - let distReference = distanceToCenter(x: reference.x, y: reference.y) - let minOffset = CGFloat(5.0) - - if bottomY >= c.y - && self.bounds.height - legendWidth > self.bounds.width - { - xLegendOffset = legendWidth - } - else if distLegend < distReference - { - let diff = distReference - distLegend - xLegendOffset = minOffset + diff - } - } - } - - switch legend.horizontalAlignment - { - case .left: - legendLeft = xLegendOffset - - case .right: - legendRight = xLegendOffset - - case .center: - - switch legend.verticalAlignment - { - case .top: - legendTop = min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) - - case .bottom: - legendBottom = min(legend.neededHeight, viewPortHandler.chartHeight * legend.maxSizePercent) - - default: - break - } - } - - case .horizontal: - - var yLegendOffset: CGFloat = 0.0 - - if legend.verticalAlignment == .top - || legend.verticalAlignment == .bottom - { - // It's possible that we do not need this offset anymore as it - // is available through the extraOffsets, but changing it can mean - // changing default visibility for existing apps. - let yOffset = self.requiredLegendOffset - - yLegendOffset = min( - legend.neededHeight + yOffset, - viewPortHandler.chartHeight * legend.maxSizePercent) - } - - switch legend.verticalAlignment - { - case .top: - - legendTop = yLegendOffset - - case .bottom: - - legendBottom = yLegendOffset - - default: - break - } - } - - legendLeft += self.requiredBaseOffset - legendRight += self.requiredBaseOffset - legendTop += self.requiredBaseOffset - legendBottom += self.requiredBaseOffset - } - - legendTop += self.extraTopOffset - legendRight += self.extraRightOffset - legendBottom += self.extraBottomOffset - legendLeft += self.extraLeftOffset - - var minOffset = self.minOffset - - if self is RadarChartView - { - let x = self.xAxis - - if x.isEnabled && x.drawLabelsEnabled - { - minOffset = max(minOffset, x.labelRotatedWidth) - } - } - - let offsetLeft = max(minOffset, legendLeft) - let offsetTop = max(minOffset, legendTop) - let offsetRight = max(minOffset, legendRight) - let offsetBottom = max(minOffset, max(self.requiredBaseOffset, legendBottom)) - - viewPortHandler.restrainViewPort(offsetLeft: offsetLeft, offsetTop: offsetTop, offsetRight: offsetRight, offsetBottom: offsetBottom) - } - - /// - Returns: The angle relative to the chart center for the given point on the chart in degrees. - /// The angle is always between 0 and 360°, 0° is NORTH, 90° is EAST, ... - @objc open func angleForPoint(x: CGFloat, y: CGFloat) -> CGFloat - { - let c = centerOffsets - - let tx = Double(x - c.x) - let ty = Double(y - c.y) - let length = sqrt(tx * tx + ty * ty) - let r = acos(ty / length) - - var angle = r.RAD2DEG - - if x > c.x - { - angle = 360.0 - angle - } - - // add 90° because chart starts EAST - angle = angle + 90.0 - - // neutralize overflow - if angle > 360.0 - { - angle = angle - 360.0 - } - - return CGFloat(angle) - } - - /// Calculates the position around a center point, depending on the distance - /// from the center, and the angle of the position around the center. - @objc open func getPosition(center: CGPoint, dist: CGFloat, angle: CGFloat) -> CGPoint - { - return CGPoint(x: center.x + dist * cos(angle.DEG2RAD), - y: center.y + dist * sin(angle.DEG2RAD)) - } - - /// - Returns: The distance of a certain point on the chart to the center of the chart. - @objc open func distanceToCenter(x: CGFloat, y: CGFloat) -> CGFloat - { - let c = self.centerOffsets - - var dist = CGFloat(0.0) - - var xDist = CGFloat(0.0) - var yDist = CGFloat(0.0) - - if x > c.x - { - xDist = x - c.x - } - else - { - xDist = c.x - x - } - - if y > c.y - { - yDist = y - c.y - } - else - { - yDist = c.y - y - } - - // pythagoras - dist = sqrt(pow(xDist, 2.0) + pow(yDist, 2.0)) - - return dist - } - - /// - Returns: The xIndex for the given angle around the center of the chart. - /// -1 if not found / outofbounds. - @objc open func indexForAngle(_ angle: CGFloat) -> Int - { - fatalError("indexForAngle() cannot be called on PieRadarChartViewBase") - } - - /// current rotation angle of the pie chart - /// - /// **default**: 270 --> top (NORTH) - /// Will always return a normalized value, which will be between 0.0 < 360.0 - @objc open var rotationAngle: CGFloat - { - get - { - return _rotationAngle - } - set - { - _rawRotationAngle = newValue - _rotationAngle = newValue.normalizedAngle - setNeedsDisplay() - } - } - - /// gets the raw version of the current rotation angle of the pie chart the returned value could be any value, negative or positive, outside of the 360 degrees. - /// this is used when working with rotation direction, mainly by gestures and animations. - @objc open var rawRotationAngle: CGFloat - { - return _rawRotationAngle - } - - /// The diameter of the pie- or radar-chart - @objc open var diameter: CGFloat - { - var content = viewPortHandler.contentRect - content.origin.x += extraLeftOffset - content.origin.y += extraTopOffset - content.size.width -= extraLeftOffset + extraRightOffset - content.size.height -= extraTopOffset + extraBottomOffset - return min(content.width, content.height) - } - - /// The radius of the chart in pixels. - @objc open var radius: CGFloat - { - fatalError("radius cannot be called on PieRadarChartViewBase") - } - - /// The required offset for the chart legend. - internal var requiredLegendOffset: CGFloat - { - fatalError("requiredLegendOffset cannot be called on PieRadarChartViewBase") - } - - /// - Returns: The base offset needed for the chart without calculating the - /// legend size. - internal var requiredBaseOffset: CGFloat - { - fatalError("requiredBaseOffset cannot be called on PieRadarChartViewBase") - } - - open override var chartYMax: Double - { - return 0.0 - } - - open override var chartYMin: Double - { - return 0.0 - } - - @objc open var isRotationEnabled: Bool { return rotationEnabled } - - /// flag that indicates if rotation is done with two fingers or one. - /// when the chart is inside a scrollview, you need a two-finger rotation because a one-finger rotation eats up all touch events. - /// - /// On iOS this will disable one-finger rotation. - /// On OSX this will keep two-finger multitouch rotation, and one-pointer mouse rotation. - /// - /// **default**: false - @objc open var rotationWithTwoFingers: Bool - { - get - { - return _rotationWithTwoFingers - } - set - { - _rotationWithTwoFingers = newValue - #if !os(tvOS) - _rotationGestureRecognizer.isEnabled = _rotationWithTwoFingers - #endif - } - } - - /// flag that indicates if rotation is done with two fingers or one. - /// when the chart is inside a scrollview, you need a two-finger rotation because a one-finger rotation eats up all touch events. - /// - /// On iOS this will disable one-finger rotation. - /// On OSX this will keep two-finger multitouch rotation, and one-pointer mouse rotation. - /// - /// **default**: false - @objc open var isRotationWithTwoFingers: Bool - { - return _rotationWithTwoFingers - } - - // MARK: - Animation - - private var _spinAnimator: Animator! - - /// Applys a spin animation to the Chart. - @objc open func spin(duration: TimeInterval, fromAngle: CGFloat, toAngle: CGFloat, easing: ChartEasingFunctionBlock?) - { - if _spinAnimator != nil - { - _spinAnimator.stop() - } - - _spinAnimator = Animator() - _spinAnimator.updateBlock = { - self.rotationAngle = (toAngle - fromAngle) * CGFloat(self._spinAnimator.phaseX) + fromAngle - } - _spinAnimator.stopBlock = { self._spinAnimator = nil } - - _spinAnimator.animate(xAxisDuration: duration, easing: easing) - } - - @objc open func spin(duration: TimeInterval, fromAngle: CGFloat, toAngle: CGFloat, easingOption: ChartEasingOption) - { - spin(duration: duration, fromAngle: fromAngle, toAngle: toAngle, easing: easingFunctionFromOption(easingOption)) - } - - @objc open func spin(duration: TimeInterval, fromAngle: CGFloat, toAngle: CGFloat) - { - spin(duration: duration, fromAngle: fromAngle, toAngle: toAngle, easing: nil) - } - - @objc open func stopSpinAnimation() - { - if _spinAnimator != nil - { - _spinAnimator.stop() - } - } - - // MARK: - Gestures - - private var _rotationGestureStartPoint: CGPoint! - private var _isRotating = false - private var _startAngle = CGFloat(0.0) - - private struct AngularVelocitySample - { - var time: TimeInterval - var angle: CGFloat - } - - private var velocitySamples = [AngularVelocitySample]() - - private var _decelerationLastTime: TimeInterval = 0.0 - private var _decelerationDisplayLink: NSUIDisplayLink! - private var _decelerationAngularVelocity: CGFloat = 0.0 - - internal final func processRotationGestureBegan(location: CGPoint) - { - self.resetVelocity() - - if rotationEnabled - { - self.sampleVelocity(touchLocation: location) - } - - self.setGestureStartAngle(x: location.x, y: location.y) - - _rotationGestureStartPoint = location - } - - internal final func processRotationGestureMoved(location: CGPoint) - { - if isDragDecelerationEnabled - { - sampleVelocity(touchLocation: location) - } - - if !_isRotating && - distance( - eventX: location.x, - startX: _rotationGestureStartPoint.x, - eventY: location.y, - startY: _rotationGestureStartPoint.y) > CGFloat(8.0) - { - _isRotating = true - } - else - { - self.updateGestureRotation(x: location.x, y: location.y) - setNeedsDisplay() - } - } - - internal final func processRotationGestureEnded(location: CGPoint) - { - if isDragDecelerationEnabled - { - stopDeceleration() - - sampleVelocity(touchLocation: location) - - _decelerationAngularVelocity = calculateVelocity() - - if _decelerationAngularVelocity != 0.0 - { - _decelerationLastTime = CACurrentMediaTime() - _decelerationDisplayLink = NSUIDisplayLink(target: self, selector: #selector(PieRadarChartViewBase.decelerationLoop)) - _decelerationDisplayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common) - } - } - } - - internal final func processRotationGestureCancelled() - { - if _isRotating - { - _isRotating = false - } - } - - #if !os(OSX) - open override func nsuiTouchesBegan(_ touches: Set, withEvent event: NSUIEvent?) - { - // if rotation by touch is enabled - if rotationEnabled - { - stopDeceleration() - - if !rotationWithTwoFingers, let touchLocation = touches.first?.location(in: self) - { - processRotationGestureBegan(location: touchLocation) - } - } - - if !_isRotating - { - super.nsuiTouchesBegan(touches, withEvent: event) - } - } - - open override func nsuiTouchesMoved(_ touches: Set, withEvent event: NSUIEvent?) - { - if rotationEnabled && !rotationWithTwoFingers, let touch = touches.first - { - let touchLocation = touch.location(in: self) - processRotationGestureMoved(location: touchLocation) - } - - if !_isRotating - { - super.nsuiTouchesMoved(touches, withEvent: event) - } - } - - open override func nsuiTouchesEnded(_ touches: Set, withEvent event: NSUIEvent?) - { - if !_isRotating - { - super.nsuiTouchesEnded(touches, withEvent: event) - } - - if rotationEnabled && !rotationWithTwoFingers, let touch = touches.first - { - let touchLocation = touch.location(in: self) - processRotationGestureEnded(location: touchLocation) - } - - if _isRotating - { - _isRotating = false - } - } - - open override func nsuiTouchesCancelled(_ touches: Set?, withEvent event: NSUIEvent?) - { - super.nsuiTouchesCancelled(touches, withEvent: event) - - processRotationGestureCancelled() - } - #endif - - #if os(OSX) - open override func mouseDown(with theEvent: NSEvent) - { - // if rotation by touch is enabled - if rotationEnabled - { - stopDeceleration() - - let location = self.convert(theEvent.locationInWindow, from: nil) - - processRotationGestureBegan(location: location) - } - - if !_isRotating - { - super.mouseDown(with: theEvent) - } - } - - open override func mouseDragged(with theEvent: NSEvent) - { - if rotationEnabled - { - let location = self.convert(theEvent.locationInWindow, from: nil) - - processRotationGestureMoved(location: location) - } - - if !_isRotating - { - super.mouseDragged(with: theEvent) - } - } - - open override func mouseUp(with theEvent: NSEvent) - { - if !_isRotating - { - super.mouseUp(with: theEvent) - } - - if rotationEnabled - { - let location = self.convert(theEvent.locationInWindow, from: nil) - - processRotationGestureEnded(location: location) - } - - if _isRotating - { - _isRotating = false - } - } - #endif - - private func resetVelocity() - { - velocitySamples.removeAll(keepingCapacity: false) - } - - private func sampleVelocity(touchLocation: CGPoint) - { - let currentSample: AngularVelocitySample = { - let time = CACurrentMediaTime() - let angle = angleForPoint(x: touchLocation.x, y: touchLocation.y) - return AngularVelocitySample(time: time, angle: angle) - }() - - // Remove samples older than our sample time - 1 seconds - // while keeping at least one sample - var i = 0, count = velocitySamples.count - while (i < count - 2) - { - if currentSample.time - velocitySamples[i].time > 1.0 - { - velocitySamples.remove(at: 0) - i -= 1 - count -= 1 - } - else - { - break - } - - i += 1 - } - - velocitySamples.append(currentSample) - } - - private func calculateVelocity() -> CGFloat - { - guard var firstSample = velocitySamples.first, - var lastSample = velocitySamples.last - else { return 0 } - - // Look for a sample that's closest to the latest sample, but not the same, so we can deduce the direction - let beforeLastSample = velocitySamples.last { $0.angle != lastSample.angle } - ?? firstSample - - // Calculate the sampling time - let timeDelta: CGFloat = { - let delta = CGFloat(lastSample.time - firstSample.time) - return delta == 0 ? 0.1 : delta - }() - - // Calculate clockwise/ccw by choosing two values that should be closest to each other, - // so if the angles are two far from each other we know they are inverted "for sure" - let isClockwise: Bool = { - let isClockwise = lastSample.angle >= beforeLastSample.angle - let isInverted = abs(lastSample.angle - beforeLastSample.angle) > 270.0 - return isInverted ? !isClockwise : isClockwise - }() - - // Now if the "gesture" is over a too big of an angle - then we know the angles are inverted, and we need to move them closer to each other from both sides of the 360.0 wrapping point - if lastSample.angle - firstSample.angle > 180.0 - { - firstSample.angle += 360.0 - } - else if firstSample.angle - lastSample.angle > 180.0 - { - lastSample.angle += 360.0 - } - - // The velocity - let velocity = abs((lastSample.angle - firstSample.angle) / timeDelta) - return isClockwise ? velocity : -velocity - } - - /// sets the starting angle of the rotation, this is only used by the touch listener, x and y is the touch position - private func setGestureStartAngle(x: CGFloat, y: CGFloat) - { - _startAngle = angleForPoint(x: x, y: y) - - // take the current angle into consideration when starting a new drag - _startAngle -= _rotationAngle - } - - /// updates the view rotation depending on the given touch position, also takes the starting angle into consideration - private func updateGestureRotation(x: CGFloat, y: CGFloat) - { - self.rotationAngle = angleForPoint(x: x, y: y) - _startAngle - } - - @objc open func stopDeceleration() - { - if _decelerationDisplayLink !== nil - { - _decelerationDisplayLink.remove(from: RunLoop.main, forMode: RunLoop.Mode.common) - _decelerationDisplayLink = nil - } - } - - @objc private func decelerationLoop() - { - let currentTime = CACurrentMediaTime() - - _decelerationAngularVelocity *= self.dragDecelerationFrictionCoef - - let timeInterval = CGFloat(currentTime - _decelerationLastTime) - - self.rotationAngle += _decelerationAngularVelocity * timeInterval - - _decelerationLastTime = currentTime - - if(abs(_decelerationAngularVelocity) < 0.001) - { - stopDeceleration() - } - } - - /// - Returns: The distance between two points - private func distance(eventX: CGFloat, startX: CGFloat, eventY: CGFloat, startY: CGFloat) -> CGFloat - { - let dx = eventX - startX - let dy = eventY - startY - return sqrt(dx * dx + dy * dy) - } - - /// - Returns: The distance between two points - private func distance(from: CGPoint, to: CGPoint) -> CGFloat - { - let dx = from.x - to.x - let dy = from.y - to.y - return sqrt(dx * dx + dy * dy) - } - - /// reference to the last highlighted object - private var _lastHighlight: Highlight! - - @objc private func tapGestureRecognized(_ recognizer: NSUITapGestureRecognizer) - { - if recognizer.state == NSUIGestureRecognizerState.ended - { - if !self.isHighLightPerTapEnabled { return } - - let location = recognizer.location(in: self) - - let high = self.getHighlightByTouchPoint(location) - self.highlightValue(high, callDelegate: true) - } - } - - #if !os(tvOS) - @objc private func rotationGestureRecognized(_ recognizer: NSUIRotationGestureRecognizer) - { - if recognizer.state == NSUIGestureRecognizerState.began - { - stopDeceleration() - - _startAngle = self.rawRotationAngle - } - - if recognizer.state == NSUIGestureRecognizerState.began || recognizer.state == NSUIGestureRecognizerState.changed - { - let angle = recognizer.nsuiRotation.RAD2DEG - - self.rotationAngle = _startAngle + angle - setNeedsDisplay() - } - else if recognizer.state == NSUIGestureRecognizerState.ended - { - let angle = recognizer.nsuiRotation.RAD2DEG - - self.rotationAngle = _startAngle + angle - setNeedsDisplay() - - if isDragDecelerationEnabled - { - stopDeceleration() - - _decelerationAngularVelocity = recognizer.velocity.RAD2DEG - - if _decelerationAngularVelocity != 0.0 - { - _decelerationLastTime = CACurrentMediaTime() - _decelerationDisplayLink = NSUIDisplayLink(target: self, selector: #selector(PieRadarChartViewBase.decelerationLoop)) - _decelerationDisplayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common) - } - } - } - } - #endif -} diff --git a/Pods/Charts/Source/Charts/Charts/RadarChartView.swift b/Pods/Charts/Source/Charts/Charts/RadarChartView.swift deleted file mode 100644 index 3b21cedd3..000000000 --- a/Pods/Charts/Source/Charts/Charts/RadarChartView.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// RadarChartView.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -/// Implementation of the RadarChart, a "spidernet"-like chart. It works best -/// when displaying 5-10 entries per DataSet. -open class RadarChartView: PieRadarChartViewBase -{ - /// width of the web lines that come from the center. - @objc open var webLineWidth = CGFloat(1.5) - - /// width of the web lines that are in between the lines coming from the center - @objc open var innerWebLineWidth = CGFloat(0.75) - - /// color for the web lines that come from the center - @objc open var webColor = NSUIColor(red: 122/255.0, green: 122/255.0, blue: 122.0/255.0, alpha: 1.0) - - /// color for the web lines in between the lines that come from the center. - @objc open var innerWebColor = NSUIColor(red: 122/255.0, green: 122/255.0, blue: 122.0/255.0, alpha: 1.0) - - /// transparency the grid is drawn with (0.0 - 1.0) - @objc open var webAlpha: CGFloat = 150.0 / 255.0 - - /// flag indicating if the web lines should be drawn or not - @objc open var drawWeb = true - - /// modulus that determines how many labels and web-lines are skipped before the next is drawn - private var _skipWebLineCount = 0 - - /// the object reprsenting the y-axis labels - private var _yAxis: YAxis! - - internal var _yAxisRenderer: YAxisRendererRadarChart! - internal var _xAxisRenderer: XAxisRendererRadarChart! - - public override init(frame: CGRect) - { - super.init(frame: frame) - } - - public required init?(coder aDecoder: NSCoder) - { - super.init(coder: aDecoder) - } - - internal override func initialize() - { - super.initialize() - - _yAxis = YAxis(position: .left) - _yAxis.labelXOffset = 10.0 - - renderer = RadarChartRenderer(chart: self, animator: chartAnimator, viewPortHandler: viewPortHandler) - - _yAxisRenderer = YAxisRendererRadarChart(viewPortHandler: viewPortHandler, axis: _yAxis, chart: self) - _xAxisRenderer = XAxisRendererRadarChart(viewPortHandler: viewPortHandler, axis: xAxis, chart: self) - - self.highlighter = RadarHighlighter(chart: self) - } - - internal override func calcMinMax() - { - super.calcMinMax() - - guard let data = data else { return } - - _yAxis.calculate(min: data.getYMin(axis: .left), max: data.getYMax(axis: .left)) - xAxis.calculate(min: 0.0, max: Double(data.maxEntryCountSet?.entryCount ?? 0)) - } - - open override func notifyDataSetChanged() - { - calcMinMax() - - _yAxisRenderer?.computeAxis(min: _yAxis._axisMinimum, max: _yAxis._axisMaximum, inverted: _yAxis.isInverted) - _xAxisRenderer?.computeAxis(min: xAxis._axisMinimum, max: xAxis._axisMaximum, inverted: false) - - if let data = data, - !legend.isLegendCustom - { - legendRenderer.computeLegend(data: data) - } - - calculateOffsets() - - setNeedsDisplay() - } - - open override func draw(_ rect: CGRect) - { - super.draw(rect) - - guard data != nil, let renderer = renderer else { return } - - let optionalContext = NSUIGraphicsGetCurrentContext() - guard let context = optionalContext else { return } - - if xAxis.isEnabled - { - _xAxisRenderer.computeAxis(min: xAxis._axisMinimum, max: xAxis._axisMaximum, inverted: false) - } - - _xAxisRenderer?.renderAxisLabels(context: context) - - if drawWeb - { - renderer.drawExtras(context: context) - } - - if _yAxis.isEnabled && _yAxis.isDrawLimitLinesBehindDataEnabled - { - _yAxisRenderer.renderLimitLines(context: context) - } - - renderer.drawData(context: context) - - if valuesToHighlight() - { - renderer.drawHighlighted(context: context, indices: highlighted) - } - - if _yAxis.isEnabled && !_yAxis.isDrawLimitLinesBehindDataEnabled - { - _yAxisRenderer.renderLimitLines(context: context) - } - - _yAxisRenderer.renderAxisLabels(context: context) - - renderer.drawValues(context: context) - - legendRenderer.renderLegend(context: context) - - drawDescription(in: context) - - drawMarkers(context: context) - } - - /// The factor that is needed to transform values into pixels. - @objc open var factor: CGFloat - { - let content = viewPortHandler.contentRect - return min(content.width / 2.0, content.height / 2.0) - / CGFloat(_yAxis.axisRange) - } - - /// The angle that each slice in the radar chart occupies. - @objc open var sliceAngle: CGFloat - { - return 360.0 / CGFloat(data?.maxEntryCountSet?.entryCount ?? 0) - } - - open override func indexForAngle(_ angle: CGFloat) -> Int - { - // take the current angle of the chart into consideration - let a = (angle - self.rotationAngle).normalizedAngle - - let sliceAngle = self.sliceAngle - - let max = data?.maxEntryCountSet?.entryCount ?? 0 - return (0.. a - } ?? 0 - } - - /// The object that represents all y-labels of the RadarChart. - @objc open var yAxis: YAxis - { - return _yAxis - } - - /// Sets the number of web-lines that should be skipped on chart web before the next one is drawn. This targets the lines that come from the center of the RadarChart. - /// if count = 1 -> 1 line is skipped in between - @objc open var skipWebLineCount: Int - { - get - { - return _skipWebLineCount - } - set - { - _skipWebLineCount = max(0, newValue) - } - } - - internal override var requiredLegendOffset: CGFloat - { - return legend.font.pointSize * 4.0 - } - - internal override var requiredBaseOffset: CGFloat - { - return xAxis.isEnabled && xAxis.isDrawLabelsEnabled ? xAxis.labelRotatedWidth : 10.0 - } - - open override var radius: CGFloat - { - let content = viewPortHandler.contentRect - return min(content.width / 2.0, content.height / 2.0) - } - - /// The maximum value this chart can display on it's y-axis. - open override var chartYMax: Double { return _yAxis._axisMaximum } - - /// The minimum value this chart can display on it's y-axis. - open override var chartYMin: Double { return _yAxis._axisMinimum } - - /// The range of y-values this chart can display. - @objc open var yRange: Double { return _yAxis.axisRange } -} diff --git a/Pods/Charts/Source/Charts/Charts/ScatterChartView.swift b/Pods/Charts/Source/Charts/Charts/ScatterChartView.swift deleted file mode 100644 index 070acc68e..000000000 --- a/Pods/Charts/Source/Charts/Charts/ScatterChartView.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ScatterChartView.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// The ScatterChart. Draws dots, triangles, squares and custom shapes into the chartview. -open class ScatterChartView: BarLineChartViewBase, ScatterChartDataProvider -{ - open override func initialize() - { - super.initialize() - - renderer = ScatterChartRenderer(dataProvider: self, animator: chartAnimator, viewPortHandler: viewPortHandler) - - xAxis.spaceMin = 0.5 - xAxis.spaceMax = 0.5 - } - - // MARK: - ScatterChartDataProvider - - open var scatterData: ScatterChartData? { return data as? ScatterChartData } -} diff --git a/Pods/Charts/Source/Charts/Components/AxisBase.swift b/Pods/Charts/Source/Charts/Components/AxisBase.swift deleted file mode 100644 index 83db26b62..000000000 --- a/Pods/Charts/Source/Charts/Components/AxisBase.swift +++ /dev/null @@ -1,358 +0,0 @@ -// -// AxisBase.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// Base class for all axes -@objc(ChartAxisBase) -open class AxisBase: ComponentBase -{ - public override init() - { - super.init() - } - - /// Custom formatter that is used instead of the auto-formatter if set - private lazy var _axisValueFormatter: AxisValueFormatter = DefaultAxisValueFormatter(decimals: decimals) - - @objc open var labelFont = NSUIFont.systemFont(ofSize: 10.0) - @objc open var labelTextColor = NSUIColor.labelOrBlack - - @objc open var axisLineColor = NSUIColor.gray - @objc open var axisLineWidth = CGFloat(0.5) - @objc open var axisLineDashPhase = CGFloat(0.0) - @objc open var axisLineDashLengths: [CGFloat]! - - @objc open var gridColor = NSUIColor.gray.withAlphaComponent(0.9) - @objc open var gridLineWidth = CGFloat(0.5) - @objc open var gridLineDashPhase = CGFloat(0.0) - @objc open var gridLineDashLengths: [CGFloat]! - @objc open var gridLineCap = CGLineCap.butt - - @objc open var drawGridLinesEnabled = true - @objc open var drawAxisLineEnabled = true - - /// flag that indicates of the labels of this axis should be drawn or not - @objc open var drawLabelsEnabled = true - - private var _centerAxisLabelsEnabled = false - - /// Centers the axis labels instead of drawing them at their original position. - /// This is useful especially for grouped BarChart. - @objc open var centerAxisLabelsEnabled: Bool - { - get { return _centerAxisLabelsEnabled && entryCount > 0 } - set { _centerAxisLabelsEnabled = newValue } - } - - @objc open var isCenterAxisLabelsEnabled: Bool - { - get { return centerAxisLabelsEnabled } - } - - /// array of limitlines that can be set for the axis - private var _limitLines = [ChartLimitLine]() - - /// Are the LimitLines drawn behind the data or in front of the data? - /// - /// **default**: false - @objc open var drawLimitLinesBehindDataEnabled = false - - /// Are the grid lines drawn behind the data or in front of the data? - /// - /// **default**: true - @objc open var drawGridLinesBehindDataEnabled = true - - /// the flag can be used to turn off the antialias for grid lines - @objc open var gridAntialiasEnabled = true - - /// the actual array of entries - @objc open var entries = [Double]() - - /// axis label entries only used for centered labels - @objc open var centeredEntries = [Double]() - - /// the number of entries the legend contains - @objc open var entryCount: Int { return entries.count } - - /// the number of label entries the axis should have - /// - /// **default**: 6 - private var _labelCount = Int(6) - - /// the number of decimal digits to use (for the default formatter - @objc open var decimals: Int = 0 - - /// When true, axis labels are controlled by the `granularity` property. - /// When false, axis values could possibly be repeated. - /// This could happen if two adjacent axis values are rounded to same value. - /// If using granularity this could be avoided by having fewer axis values visible. - @objc open var granularityEnabled = false - - private var _granularity = Double(1.0) - - /// The minimum interval between axis values. - /// This can be used to avoid label duplicating when zooming in. - /// - /// **default**: 1.0 - @objc open var granularity: Double - { - get - { - return _granularity - } - set - { - _granularity = newValue - - // set this to `true` if it was disabled, as it makes no sense to set this property with granularity disabled - granularityEnabled = true - } - } - - /// The minimum interval between axis values. - @objc open var isGranularityEnabled: Bool - { - get - { - return granularityEnabled - } - } - - /// if true, the set number of y-labels will be forced - @objc open var forceLabelsEnabled = false - - @objc open func getLongestLabel() -> String - { - let longest = entries.indices - .lazy - .map(getFormattedLabel(_:)) - .max(by: \.count) - - return longest ?? "" - } - - /// - Returns: The formatted label at the specified index. This will either use the auto-formatter or the custom formatter (if one is set). - @objc open func getFormattedLabel(_ index: Int) -> String - { - guard entries.indices.contains(index) else { return "" } - - return valueFormatter?.stringForValue(entries[index], axis: self) ?? "" - } - - /// Sets the formatter to be used for formatting the axis labels. - /// If no formatter is set, the chart will automatically determine a reasonable formatting (concerning decimals) for all the values that are drawn inside the chart. - /// Use `nil` to use the formatter calculated by the chart. - @objc open var valueFormatter: AxisValueFormatter? - { - get - { - if _axisValueFormatter is DefaultAxisValueFormatter && - (_axisValueFormatter as! DefaultAxisValueFormatter).hasAutoDecimals && - (_axisValueFormatter as! DefaultAxisValueFormatter).decimals != decimals - { - (self._axisValueFormatter as! DefaultAxisValueFormatter).decimals = self.decimals - } - - return _axisValueFormatter - } - set - { - _axisValueFormatter = newValue ?? DefaultAxisValueFormatter(decimals: decimals) - } - } - - @objc open var isDrawGridLinesEnabled: Bool { return drawGridLinesEnabled } - - @objc open var isDrawAxisLineEnabled: Bool { return drawAxisLineEnabled } - - @objc open var isDrawLabelsEnabled: Bool { return drawLabelsEnabled } - - /// Are the LimitLines drawn behind the data or in front of the data? - /// - /// **default**: false - @objc open var isDrawLimitLinesBehindDataEnabled: Bool { return drawLimitLinesBehindDataEnabled } - - /// Are the grid lines drawn behind the data or in front of the data? - /// - /// **default**: true - @objc open var isDrawGridLinesBehindDataEnabled: Bool { return drawGridLinesBehindDataEnabled } - - /// Extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` - @objc open var spaceMin: Double = 0.0 - - /// Extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` - @objc open var spaceMax: Double = 0.0 - - /// Flag indicating that the axis-min value has been customized - internal var _customAxisMin: Bool = false - - /// Flag indicating that the axis-max value has been customized - internal var _customAxisMax: Bool = false - - /// Do not touch this directly, instead, use axisMinimum. - /// This is automatically calculated to represent the real min value, - /// and is used when calculating the effective minimum. - internal var _axisMinimum = Double(0) - - /// Do not touch this directly, instead, use axisMaximum. - /// This is automatically calculated to represent the real max value, - /// and is used when calculating the effective maximum. - internal var _axisMaximum = Double(0) - - /// the total range of values this axis covers - @objc open var axisRange = Double(0) - - /// The minumum number of labels on the axis - @objc open var axisMinLabels = Int(2) { - didSet { axisMinLabels = axisMinLabels > 0 ? axisMinLabels : oldValue } - } - - /// The maximum number of labels on the axis - @objc open var axisMaxLabels = Int(25) { - didSet { axisMaxLabels = axisMaxLabels > 0 ? axisMaxLabels : oldValue } - } - - /// the number of label entries the axis should have - /// max = 25, - /// min = 2, - /// default = 6, - /// be aware that this number is not fixed and can only be approximated - @objc open var labelCount: Int - { - get - { - return _labelCount - } - set - { - let range = axisMinLabels...axisMaxLabels as ClosedRange - _labelCount = newValue.clamped(to: range) - - forceLabelsEnabled = false - } - } - - @objc open func setLabelCount(_ count: Int, force: Bool) - { - self.labelCount = count - forceLabelsEnabled = force - } - - /// `true` if focing the y-label count is enabled. Default: false - @objc open var isForceLabelsEnabled: Bool { return forceLabelsEnabled } - - /// Adds a new ChartLimitLine to this axis. - @objc open func addLimitLine(_ line: ChartLimitLine) - { - _limitLines.append(line) - } - - /// Removes the specified ChartLimitLine from the axis. - @objc open func removeLimitLine(_ line: ChartLimitLine) - { - guard let i = _limitLines.firstIndex(of: line) else { return } - _limitLines.remove(at: i) - } - - /// Removes all LimitLines from the axis. - @objc open func removeAllLimitLines() - { - _limitLines.removeAll(keepingCapacity: false) - } - - /// The LimitLines of this axis. - @objc open var limitLines : [ChartLimitLine] - { - return _limitLines - } - - // MARK: Custom axis ranges - - /// By calling this method, any custom minimum value that has been previously set is reseted, and the calculation is done automatically. - @objc open func resetCustomAxisMin() - { - _customAxisMin = false - } - - @objc open var isAxisMinCustom: Bool { return _customAxisMin } - - /// By calling this method, any custom maximum value that has been previously set is reseted, and the calculation is done automatically. - @objc open func resetCustomAxisMax() - { - _customAxisMax = false - } - - @objc open var isAxisMaxCustom: Bool { return _customAxisMax } - - /// The minimum value for this axis. - /// If set, this value will not be calculated automatically depending on the provided data. - /// Use `resetCustomAxisMin()` to undo this. - @objc open var axisMinimum: Double - { - get - { - return _axisMinimum - } - set - { - _customAxisMin = true - _axisMinimum = newValue - axisRange = abs(_axisMaximum - newValue) - } - } - - /// The maximum value for this axis. - /// If set, this value will not be calculated automatically depending on the provided data. - /// Use `resetCustomAxisMax()` to undo this. - @objc open var axisMaximum: Double - { - get - { - return _axisMaximum - } - set - { - _customAxisMax = true - _axisMaximum = newValue - axisRange = abs(newValue - _axisMinimum) - } - } - - /// Calculates the minimum, maximum and range values of the YAxis with the given minimum and maximum values from the chart data. - /// - /// - Parameters: - /// - dataMin: the y-min value according to chart data - /// - dataMax: the y-max value according to chart - @objc open func calculate(min dataMin: Double, max dataMax: Double) - { - // if custom, use value as is, else use data value - var min = _customAxisMin ? _axisMinimum : (dataMin - spaceMin) - var max = _customAxisMax ? _axisMaximum : (dataMax + spaceMax) - - // temporary range (before calculations) - let range = abs(max - min) - - // in case all values are equal - if range == 0.0 - { - max = max + 1.0 - min = min - 1.0 - } - - _axisMinimum = min - _axisMaximum = max - - // actual range - axisRange = abs(max - min) - } -} diff --git a/Pods/Charts/Source/Charts/Components/ChartLimitLine.swift b/Pods/Charts/Source/Charts/Components/ChartLimitLine.swift deleted file mode 100644 index fc876b4d8..000000000 --- a/Pods/Charts/Source/Charts/Components/ChartLimitLine.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// ChartLimitLine.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -/// The limit line is an additional feature for all Line, Bar and ScatterCharts. -/// It allows the displaying of an additional line in the chart that marks a certain maximum / limit on the specified axis (x- or y-axis). -open class ChartLimitLine: ComponentBase -{ - @objc(ChartLimitLabelPosition) - public enum LabelPosition: Int - { - case leftTop - case leftBottom - case rightTop - case rightBottom - } - - /// limit / maximum (the y-value or xIndex) - @objc open var limit = Double(0.0) - - private var _lineWidth = CGFloat(2.0) - @objc open var lineColor = NSUIColor(red: 237.0/255.0, green: 91.0/255.0, blue: 91.0/255.0, alpha: 1.0) - @objc open var lineDashPhase = CGFloat(0.0) - @objc open var lineDashLengths: [CGFloat]? - - @objc open var valueTextColor = NSUIColor.labelOrBlack - @objc open var valueFont = NSUIFont.systemFont(ofSize: 13.0) - - @objc open var drawLabelEnabled = true - @objc open var label = "" - @objc open var labelPosition = LabelPosition.rightTop - - public override init() - { - super.init() - } - - @objc public init(limit: Double) - { - super.init() - self.limit = limit - } - - @objc public init(limit: Double, label: String) - { - super.init() - self.limit = limit - self.label = label - } - - /// set the line width of the chart (min = 0.2, max = 12); default 2 - @objc open var lineWidth: CGFloat - { - get - { - return _lineWidth - } - set - { - _lineWidth = newValue.clamped(to: 0.2...12) - } - } -} diff --git a/Pods/Charts/Source/Charts/Components/ComponentBase.swift b/Pods/Charts/Source/Charts/Components/ComponentBase.swift deleted file mode 100644 index 6014a1756..000000000 --- a/Pods/Charts/Source/Charts/Components/ComponentBase.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// ComponentBase.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -/// This class encapsulates everything both Axis, Legend and LimitLines have in common -@objc(ChartComponentBase) -open class ComponentBase: NSObject -{ - /// flag that indicates if this component is enabled or not - @objc open var enabled = true - - /// The offset this component has on the x-axis - /// **default**: 5.0 - @objc open var xOffset = CGFloat(5.0) - - /// The offset this component has on the x-axis - /// **default**: 5.0 (or 0.0 on ChartYAxis) - @objc open var yOffset = CGFloat(5.0) - - public override init() - { - super.init() - } - - @objc open var isEnabled: Bool { return enabled } -} diff --git a/Pods/Charts/Source/Charts/Components/Description.swift b/Pods/Charts/Source/Charts/Components/Description.swift deleted file mode 100644 index ba455dc05..000000000 --- a/Pods/Charts/Source/Charts/Components/Description.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Description.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -#if canImport(UIKit) - import UIKit -#endif - -#if canImport(Cocoa) -import Cocoa -#endif - -@objc(ChartDescription) -open class Description: ComponentBase -{ - public override init() - { - #if os(tvOS) - // 23 is the smallest recommended font size on the TV - font = .systemFont(ofSize: 23) - #elseif os(OSX) - font = .systemFont(ofSize: NSUIFont.systemFontSize) - #else - font = .systemFont(ofSize: 8.0) - #endif - - super.init() - } - - /// The text to be shown as the description. - @objc open var text: String? - - /// Custom position for the description text in pixels on the screen. - open var position: CGPoint? = nil - - /// The text alignment of the description text. Default RIGHT. - @objc open var textAlign: TextAlignment = TextAlignment.right - - /// Font object used for drawing the description text. - @objc open var font: NSUIFont - - /// Text color used for drawing the description text - @objc open var textColor = NSUIColor.labelOrBlack -} diff --git a/Pods/Charts/Source/Charts/Components/Legend.swift b/Pods/Charts/Source/Charts/Components/Legend.swift deleted file mode 100644 index e86d59ca5..000000000 --- a/Pods/Charts/Source/Charts/Components/Legend.swift +++ /dev/null @@ -1,418 +0,0 @@ -// -// Legend.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartLegend) -open class Legend: ComponentBase -{ - @objc(ChartLegendForm) - public enum Form: Int - { - /// Avoid drawing a form - case none - - /// Do not draw the a form, but leave space for it - case empty - - /// Use default (default dataset's form to the legend's form) - case `default` - - /// Draw a square - case square - - /// Draw a circle - case circle - - /// Draw a horizontal line - case line - } - - @objc(ChartLegendHorizontalAlignment) - public enum HorizontalAlignment: Int - { - case left - case center - case right - } - - @objc(ChartLegendVerticalAlignment) - public enum VerticalAlignment: Int - { - case top - case center - case bottom - } - - @objc(ChartLegendOrientation) - public enum Orientation: Int - { - case horizontal - case vertical - } - - @objc(ChartLegendDirection) - public enum Direction: Int - { - case leftToRight - case rightToLeft - } - - /// The legend entries array - @objc open var entries = [LegendEntry]() - - /// Entries that will be appended to the end of the auto calculated entries after calculating the legend. - /// (if the legend has already been calculated, you will need to call notifyDataSetChanged() to let the changes take effect) - @objc open var extraEntries = [LegendEntry]() - - /// Are the legend labels/colors a custom value or auto calculated? If false, then it's auto, if true, then custom. - /// - /// **default**: false (automatic legend) - private var _isLegendCustom = false - - /// The horizontal alignment of the legend - @objc open var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.left - - /// The vertical alignment of the legend - @objc open var verticalAlignment: VerticalAlignment = VerticalAlignment.bottom - - /// The orientation of the legend - @objc open var orientation: Orientation = Orientation.horizontal - - /// Flag indicating whether the legend will draw inside the chart or outside - @objc open var drawInside: Bool = false - - /// Flag indicating whether the legend will draw inside the chart or outside - @objc open var isDrawInsideEnabled: Bool { return drawInside } - - /// The text direction of the legend - @objc open var direction: Direction = Direction.leftToRight - - @objc open var font: NSUIFont = NSUIFont.systemFont(ofSize: 10.0) - @objc open var textColor = NSUIColor.labelOrBlack - - /// The form/shape of the legend forms - @objc open var form = Form.square - - /// The size of the legend forms - @objc open var formSize = CGFloat(8.0) - - /// The line width for forms that consist of lines - @objc open var formLineWidth = CGFloat(3.0) - - /// Line dash configuration for shapes that consist of lines. - /// - /// This is how much (in pixels) into the dash pattern are we starting from. - @objc open var formLineDashPhase: CGFloat = 0.0 - - /// Line dash configuration for shapes that consist of lines. - /// - /// This is the actual dash pattern. - /// I.e. [2, 3] will paint [-- -- ] - /// [1, 3, 4, 2] will paint [- ---- - ---- ] - @objc open var formLineDashLengths: [CGFloat]? - - @objc open var xEntrySpace = CGFloat(6.0) - @objc open var yEntrySpace = CGFloat(0.0) - @objc open var formToTextSpace = CGFloat(5.0) - @objc open var stackSpace = CGFloat(3.0) - - @objc open var calculatedLabelSizes = [CGSize]() - @objc open var calculatedLabelBreakPoints = [Bool]() - @objc open var calculatedLineSizes = [CGSize]() - - public override init() - { - super.init() - - self.xOffset = 5.0 - self.yOffset = 3.0 - } - - @objc public init(entries: [LegendEntry]) - { - super.init() - - self.entries = entries - } - - @objc open func getMaximumEntrySize(withFont font: NSUIFont) -> CGSize - { - var maxW = CGFloat(0.0) - var maxH = CGFloat(0.0) - - var maxFormSize: CGFloat = 0.0 - - for entry in entries - { - let formSize = entry.formSize.isNaN ? self.formSize : entry.formSize - if formSize > maxFormSize - { - maxFormSize = formSize - } - - guard let label = entry.label - else { continue } - - let size = (label as NSString).size(withAttributes: [.font: font]) - - if size.width > maxW - { - maxW = size.width - } - if size.height > maxH - { - maxH = size.height - } - } - - return CGSize( - width: maxW + maxFormSize + formToTextSpace, - height: maxH - ) - } - - @objc open var neededWidth = CGFloat(0.0) - @objc open var neededHeight = CGFloat(0.0) - @objc open var textWidthMax = CGFloat(0.0) - @objc open var textHeightMax = CGFloat(0.0) - - /// flag that indicates if word wrapping is enabled - /// this is currently supported only for `orientation == Horizontal`. - /// you may want to set maxSizePercent when word wrapping, to set the point where the text wraps. - /// - /// **default**: true - @objc open var wordWrapEnabled = true - - /// if this is set, then word wrapping the legend is enabled. - @objc open var isWordWrapEnabled: Bool { return wordWrapEnabled } - - /// The maximum relative size out of the whole chart view in percent. - /// If the legend is to the right/left of the chart, then this affects the width of the legend. - /// If the legend is to the top/bottom of the chart, then this affects the height of the legend. - /// - /// **default**: 0.95 (95%) - @objc open var maxSizePercent: CGFloat = 0.95 - - @objc open func calculateDimensions(labelFont: NSUIFont, viewPortHandler: ViewPortHandler) - { - let maxEntrySize = getMaximumEntrySize(withFont: labelFont) - let defaultFormSize = self.formSize - let stackSpace = self.stackSpace - let formToTextSpace = self.formToTextSpace - let xEntrySpace = self.xEntrySpace - let yEntrySpace = self.yEntrySpace - let wordWrapEnabled = self.wordWrapEnabled - let entries = self.entries - let entryCount = entries.count - - textWidthMax = maxEntrySize.width - textHeightMax = maxEntrySize.height - - switch orientation - { - case .vertical: - - var maxWidth = CGFloat(0.0) - var width = CGFloat(0.0) - var maxHeight = CGFloat(0.0) - let labelLineHeight = labelFont.lineHeight - - var wasStacked = false - - for i in entries.indices - { - let e = entries[i] - let drawingForm = e.form != .none - let formSize = e.formSize.isNaN ? defaultFormSize : e.formSize - - if !wasStacked - { - width = 0.0 - } - - if drawingForm - { - if wasStacked - { - width += stackSpace - } - width += formSize - } - - if let label = e.label - { - let size = (label as NSString).size(withAttributes: [.font: labelFont]) - - if drawingForm && !wasStacked - { - width += formToTextSpace - } - else if wasStacked - { - maxWidth = max(maxWidth, width) - maxHeight += labelLineHeight + yEntrySpace - width = 0.0 - wasStacked = false - } - - width += size.width - maxHeight += labelLineHeight + yEntrySpace - } - else - { - wasStacked = true - width += formSize - - if i < entryCount - 1 - { - width += stackSpace - } - } - - maxWidth = max(maxWidth, width) - } - - neededWidth = maxWidth - neededHeight = maxHeight - - case .horizontal: - - let labelLineHeight = labelFont.lineHeight - - let contentWidth: CGFloat = viewPortHandler.contentWidth * maxSizePercent - - // Prepare arrays for calculated layout - if calculatedLabelSizes.count != entryCount - { - calculatedLabelSizes = [CGSize](repeating: CGSize(), count: entryCount) - } - - if calculatedLabelBreakPoints.count != entryCount - { - calculatedLabelBreakPoints = [Bool](repeating: false, count: entryCount) - } - - calculatedLineSizes.removeAll(keepingCapacity: true) - - // Start calculating layout - - var maxLineWidth: CGFloat = 0.0 - var currentLineWidth: CGFloat = 0.0 - var requiredWidth: CGFloat = 0.0 - var stackedStartIndex: Int = -1 - - for i in entries.indices - { - let e = entries[i] - let drawingForm = e.form != .none - let label = e.label - - calculatedLabelBreakPoints[i] = false - - if stackedStartIndex == -1 - { - // we are not stacking, so required width is for this label only - requiredWidth = 0.0 - } - else - { - // add the spacing appropriate for stacked labels/forms - requiredWidth += stackSpace - } - - // grouped forms have null labels - if let label = label - { - calculatedLabelSizes[i] = (label as NSString).size(withAttributes: [.font: labelFont]) - requiredWidth += drawingForm ? formToTextSpace + formSize : 0.0 - requiredWidth += calculatedLabelSizes[i].width - } - else - { - calculatedLabelSizes[i] = CGSize() - requiredWidth += drawingForm ? formSize : 0.0 - - if stackedStartIndex == -1 - { - // mark this index as we might want to break here later - stackedStartIndex = i - } - } - - if label != nil || i == entryCount - 1 - { - let requiredSpacing = currentLineWidth == 0.0 ? 0.0 : xEntrySpace - - if (!wordWrapEnabled || // No word wrapping, it must fit. - currentLineWidth == 0.0 || // The line is empty, it must fit. - (contentWidth - currentLineWidth >= requiredSpacing + requiredWidth)) // It simply fits - { - // Expand current line - currentLineWidth += requiredSpacing + requiredWidth - } - else - { // It doesn't fit, we need to wrap a line - - // Add current line size to array - calculatedLineSizes.append(CGSize(width: currentLineWidth, height: labelLineHeight)) - maxLineWidth = max(maxLineWidth, currentLineWidth) - - // Start a new line - calculatedLabelBreakPoints[stackedStartIndex > -1 ? stackedStartIndex : i] = true - currentLineWidth = requiredWidth - } - - if i == entryCount - 1 - { // Add last line size to array - calculatedLineSizes.append(CGSize(width: currentLineWidth, height: labelLineHeight)) - maxLineWidth = max(maxLineWidth, currentLineWidth) - } - } - - stackedStartIndex = label != nil ? -1 : stackedStartIndex - } - - neededWidth = maxLineWidth - neededHeight = labelLineHeight * CGFloat(calculatedLineSizes.count) + - yEntrySpace * CGFloat(calculatedLineSizes.isEmpty ? 0 : (calculatedLineSizes.count - 1)) - } - - neededWidth += xOffset - neededHeight += yOffset - } - - /// MARK: - Custom legend - - /// Sets a custom legend's entries array. - /// * A nil label will start a group. - /// This will disable the feature that automatically calculates the legend entries from the datasets. - /// Call `resetCustom(...)` to re-enable automatic calculation (and then `notifyDataSetChanged()` is needed). - @objc open func setCustom(entries: [LegendEntry]) - { - self.entries = entries - _isLegendCustom = true - } - - /// Calling this will disable the custom legend entries (set by `setLegend(...)`). Instead, the entries will again be calculated automatically (after `notifyDataSetChanged()` is called). - @objc open func resetCustom() - { - _isLegendCustom = false - } - - /// **default**: false (automatic legend) - /// `true` if a custom legend entries has been set - @objc open var isLegendCustom: Bool - { - return _isLegendCustom - } -} diff --git a/Pods/Charts/Source/Charts/Components/LegendEntry.swift b/Pods/Charts/Source/Charts/Components/LegendEntry.swift deleted file mode 100644 index 9d2ae5eaf..000000000 --- a/Pods/Charts/Source/Charts/Components/LegendEntry.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// LegendEntry.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartLegendEntry) -open class LegendEntry: NSObject -{ - public override init() - { - super.init() - } - - /// - Parameters: - /// - label: The legend entry text. - /// A `nil` label will start a group. - @objc public init(label: String?) - { - self.label = label - } - - /// The legend entry text. - /// A `nil` label will start a group. - @objc open var label: String? - - /// The color for drawing the label - @objc open var labelColor: NSUIColor? - - /// The form to draw for this entry. - /// - /// `None` will avoid drawing a form, and any related space. - /// `Empty` will avoid drawing a form, but keep its space. - /// `Default` will use the Legend's default. - @objc open var form: Legend.Form = .default - - /// Form size will be considered except for when .None is used - /// - /// Set as NaN to use the legend's default - @objc open var formSize: CGFloat = CGFloat.nan - - /// Line width used for shapes that consist of lines. - /// - /// Set to NaN to use the legend's default. - @objc open var formLineWidth: CGFloat = CGFloat.nan - - /// Line dash configuration for shapes that consist of lines. - /// - /// This is how much (in pixels) into the dash pattern are we starting from. - /// - /// Set to NaN to use the legend's default. - @objc open var formLineDashPhase: CGFloat = 0.0 - - /// Line dash configuration for shapes that consist of lines. - /// - /// This is the actual dash pattern. - /// I.e. [2, 3] will paint [-- -- ] - /// [1, 3, 4, 2] will paint [- ---- - ---- ] - /// - /// Set to nil to use the legend's default. - @objc open var formLineDashLengths: [CGFloat]? - - /// The color for drawing the form - @objc open var formColor: NSUIColor? -} diff --git a/Pods/Charts/Source/Charts/Components/Marker.swift b/Pods/Charts/Source/Charts/Components/Marker.swift deleted file mode 100644 index a9220067d..000000000 --- a/Pods/Charts/Source/Charts/Components/Marker.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ChartMarker.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartMarker) -public protocol Marker: AnyObject -{ - /// - Returns: The desired (general) offset you wish the IMarker to have on the x-axis. - /// By returning x: -(width / 2) you will center the IMarker horizontally. - /// By returning y: -(height / 2) you will center the IMarker vertically. - var offset: CGPoint { get } - - /// - Parameters: - /// - point: This is the point at which the marker wants to be drawn. You can adjust the offset conditionally based on this argument. - /// - Returns: The offset for drawing at the specific `point`. - /// This allows conditional adjusting of the Marker position. - /// If you have no adjustments to make, return self.offset(). - func offsetForDrawing(atPoint: CGPoint) -> CGPoint - - /// This method enables a custom Marker to update it's content every time the Marker is redrawn according to the data entry it points to. - /// - /// - Parameters: - /// - entry: The Entry the IMarker belongs to. This can also be any subclass of Entry, like BarEntry or CandleEntry, simply cast it at runtime. - /// - highlight: The highlight object contains information about the highlighted value such as it's dataset-index, the selected range or stack-index (only stacked bar entries). - func refreshContent(entry: ChartDataEntry, highlight: Highlight) - - /// Draws the Marker on the given position on the given context - func draw(context: CGContext, point: CGPoint) -} diff --git a/Pods/Charts/Source/Charts/Components/MarkerImage.swift b/Pods/Charts/Source/Charts/Components/MarkerImage.swift deleted file mode 100644 index eed33422e..000000000 --- a/Pods/Charts/Source/Charts/Components/MarkerImage.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// ChartMarkerImage.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartMarkerImage) -open class MarkerImage: NSObject, Marker -{ - /// The marker image to render - @objc open var image: NSUIImage? - - open var offset: CGPoint = CGPoint() - - @objc open weak var chartView: ChartViewBase? - - /// As long as size is 0.0/0.0 - it will default to the image's size - @objc open var size: CGSize = CGSize() - - public override init() - { - super.init() - } - - open func offsetForDrawing(atPoint point: CGPoint) -> CGPoint - { - var offset = self.offset - - let chart = self.chartView - - var size = self.size - - if size.width == 0.0 && image != nil - { - size.width = image?.size.width ?? 0.0 - } - if size.height == 0.0 && image != nil - { - size.height = image?.size.height ?? 0.0 - } - - let width = size.width - let height = size.height - - if point.x + offset.x < 0.0 - { - offset.x = -point.x - } - else if chart != nil && point.x + width + offset.x > chart!.bounds.size.width - { - offset.x = chart!.bounds.size.width - point.x - width - } - - if point.y + offset.y < 0 - { - offset.y = -point.y - } - else if chart != nil && point.y + height + offset.y > chart!.bounds.size.height - { - offset.y = chart!.bounds.size.height - point.y - height - } - - return offset - } - - open func refreshContent(entry: ChartDataEntry, highlight: Highlight) - { - // Do nothing here... - } - - open func draw(context: CGContext, point: CGPoint) - { - guard let image = image else { return } - - let offset = offsetForDrawing(atPoint: point) - - var size = self.size - - if size.width == 0.0 - { - size.width = image.size.width - } - if size.height == 0.0 - { - size.height = image.size.height - } - - let rect = CGRect( - x: point.x + offset.x, - y: point.y + offset.y, - width: size.width, - height: size.height) - - NSUIGraphicsPushContext(context) - image.draw(in: rect) - NSUIGraphicsPopContext() - } -} diff --git a/Pods/Charts/Source/Charts/Components/MarkerView.swift b/Pods/Charts/Source/Charts/Components/MarkerView.swift deleted file mode 100644 index d80ad535b..000000000 --- a/Pods/Charts/Source/Charts/Components/MarkerView.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// ChartMarkerView.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -#if canImport(AppKit) -import AppKit -#endif - -@objc(ChartMarkerView) -open class MarkerView: NSUIView, Marker -{ - open var offset: CGPoint = CGPoint() - - @objc open weak var chartView: ChartViewBase? - - open func offsetForDrawing(atPoint point: CGPoint) -> CGPoint - { - guard let chart = chartView else { return self.offset } - - var offset = self.offset - - let width = self.bounds.size.width - let height = self.bounds.size.height - - if point.x + offset.x < 0.0 - { - offset.x = -point.x - } - else if point.x + width + offset.x > chart.bounds.size.width - { - offset.x = chart.bounds.size.width - point.x - width - } - - if point.y + offset.y < 0 - { - offset.y = -point.y - } - else if point.y + height + offset.y > chart.bounds.size.height - { - offset.y = chart.bounds.size.height - point.y - height - } - - return offset - } - - open func refreshContent(entry: ChartDataEntry, highlight: Highlight) - { - // Do nothing here... - } - - open func draw(context: CGContext, point: CGPoint) - { - let offset = self.offsetForDrawing(atPoint: point) - - context.saveGState() - context.translateBy(x: point.x + offset.x, - y: point.y + offset.y) - NSUIGraphicsPushContext(context) - self.nsuiLayer?.render(in: context) - NSUIGraphicsPopContext() - context.restoreGState() - } - - @objc - open class func viewFromXib(in bundle: Bundle = .main) -> MarkerView? - { - #if !os(OSX) - - return bundle.loadNibNamed( - String(describing: self), - owner: nil, - options: nil)?[0] as? MarkerView - #else - - var loadedObjects: NSArray? = NSArray() - - if bundle.loadNibNamed( - NSNib.Name(String(describing: self)), - owner: nil, - topLevelObjects: &loadedObjects) - { - return loadedObjects?[0] as? MarkerView - } - - return nil - #endif - } - -} diff --git a/Pods/Charts/Source/Charts/Components/XAxis.swift b/Pods/Charts/Source/Charts/Components/XAxis.swift deleted file mode 100644 index 77dbbd697..000000000 --- a/Pods/Charts/Source/Charts/Components/XAxis.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// XAxis.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartXAxis) -open class XAxis: AxisBase -{ - @objc(XAxisLabelPosition) - public enum LabelPosition: Int - { - case top - case bottom - case bothSided - case topInside - case bottomInside - } - - /// width of the x-axis labels in pixels - this is automatically calculated by the `computeSize()` methods in the renderers - @objc open var labelWidth = CGFloat(1.0) - - /// height of the x-axis labels in pixels - this is automatically calculated by the `computeSize()` methods in the renderers - @objc open var labelHeight = CGFloat(1.0) - - /// width of the (rotated) x-axis labels in pixels - this is automatically calculated by the `computeSize()` methods in the renderers - @objc open var labelRotatedWidth = CGFloat(1.0) - - /// height of the (rotated) x-axis labels in pixels - this is automatically calculated by the `computeSize()` methods in the renderers - @objc open var labelRotatedHeight = CGFloat(1.0) - - /// This is the angle for drawing the X axis labels (in degrees) - @objc open var labelRotationAngle = CGFloat(0.0) - - /// if set to true, the chart will avoid that the first and last label entry in the chart "clip" off the edge of the chart - @objc open var avoidFirstLastClippingEnabled = false - - /// the position of the x-labels relative to the chart - @objc open var labelPosition = LabelPosition.top - - /// if set to true, word wrapping the labels will be enabled. - /// word wrapping is done using `(value width * labelRotatedWidth)` - /// - /// - Note: currently supports all charts except pie/radar/horizontal-bar* - @objc open var wordWrapEnabled = false - - /// `true` if word wrapping the labels is enabled - @objc open var isWordWrapEnabled: Bool { return wordWrapEnabled } - - /// the width for wrapping the labels, as percentage out of one value width. - /// used only when isWordWrapEnabled = true. - /// - /// **default**: 1.0 - @objc open var wordWrapWidthPercent: CGFloat = 1.0 - - public override init() - { - super.init() - - self.yOffset = 4.0 - } - - @objc open var isAvoidFirstLastClippingEnabled: Bool - { - return avoidFirstLastClippingEnabled - } -} diff --git a/Pods/Charts/Source/Charts/Components/YAxis.swift b/Pods/Charts/Source/Charts/Components/YAxis.swift deleted file mode 100644 index bf0beef6d..000000000 --- a/Pods/Charts/Source/Charts/Components/YAxis.swift +++ /dev/null @@ -1,206 +0,0 @@ -// -// YAxis.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -#if canImport(UIKit) - import UIKit -#endif - -#if canImport(Cocoa) -import Cocoa -#endif - - -/// Class representing the y-axis labels settings and its entries. -/// Be aware that not all features the YLabels class provides are suitable for the RadarChart. -/// Customizations that affect the value range of the axis need to be applied before setting data for the chart. -@objc(ChartYAxis) -open class YAxis: AxisBase -{ - @objc(YAxisLabelPosition) - public enum LabelPosition: Int - { - case outsideChart - case insideChart - } - - /// Enum that specifies the axis a DataSet should be plotted against, either Left or Right. - @objc - public enum AxisDependency: Int - { - case left - case right - } - - /// indicates if the bottom y-label entry is drawn or not - @objc open var drawBottomYLabelEntryEnabled = true - - /// indicates if the top y-label entry is drawn or not - @objc open var drawTopYLabelEntryEnabled = true - - /// flag that indicates if the axis is inverted or not - @objc open var inverted = false - - /// flag that indicates if the zero-line should be drawn regardless of other grid lines - @objc open var drawZeroLineEnabled = false - - /// Color of the zero line - @objc open var zeroLineColor: NSUIColor? = NSUIColor.gray - - /// Width of the zero line - @objc open var zeroLineWidth: CGFloat = 1.0 - - /// This is how much (in pixels) into the dash pattern are we starting from. - @objc open var zeroLineDashPhase = CGFloat(0.0) - - /// This is the actual dash pattern. - /// I.e. [2, 3] will paint [-- -- ] - /// [1, 3, 4, 2] will paint [- ---- - ---- ] - @objc open var zeroLineDashLengths: [CGFloat]? - - /// axis space from the largest value to the top in percent of the total axis range - @objc open var spaceTop = CGFloat(0.1) - - /// axis space from the smallest value to the bottom in percent of the total axis range - @objc open var spaceBottom = CGFloat(0.1) - - /// the position of the y-labels relative to the chart - @objc open var labelPosition = LabelPosition.outsideChart - - /// the alignment of the text in the y-label - @objc open var labelAlignment: TextAlignment = .left - - /// the horizontal offset of the y-label - @objc open var labelXOffset: CGFloat = 0.0 - - /// the side this axis object represents - private var _axisDependency = AxisDependency.left - - /// the minimum width that the axis should take - /// - /// **default**: 0.0 - @objc open var minWidth = CGFloat(0) - - /// the maximum width that the axis can take. - /// use Infinity for disabling the maximum. - /// - /// **default**: CGFloat.infinity - @objc open var maxWidth = CGFloat(CGFloat.infinity) - - public override init() - { - super.init() - - self.yOffset = 0.0 - } - - @objc public init(position: AxisDependency) - { - super.init() - - _axisDependency = position - - self.yOffset = 0.0 - } - - @objc open var axisDependency: AxisDependency - { - return _axisDependency - } - - @objc open func requiredSize() -> CGSize - { - let label = getLongestLabel() as NSString - var size = label.size(withAttributes: [.font: labelFont]) - size.width += xOffset * 2.0 - size.height += yOffset * 2.0 - size.width = max(minWidth, min(size.width, maxWidth > 0.0 ? maxWidth : size.width)) - return size - } - - @objc open func getRequiredHeightSpace() -> CGFloat - { - return requiredSize().height - } - - /// `true` if this axis needs horizontal offset, `false` ifno offset is needed. - @objc open var needsOffset: Bool - { - if isEnabled && isDrawLabelsEnabled && labelPosition == .outsideChart - { - return true - } - else - { - return false - } - } - - @objc open var isInverted: Bool { return inverted } - - open override func calculate(min dataMin: Double, max dataMax: Double) - { - // if custom, use value as is, else use data value - var min = _customAxisMin ? _axisMinimum : dataMin - var max = _customAxisMax ? _axisMaximum : dataMax - - // Make sure max is greater than min - // Discussion: https://github.com/danielgindi/Charts/pull/3650#discussion_r221409991 - if min > max - { - switch(_customAxisMax, _customAxisMin) - { - case(true, true): - (min, max) = (max, min) - case(true, false): - min = max < 0 ? max * 1.5 : max * 0.5 - case(false, true): - max = min < 0 ? min * 0.5 : min * 1.5 - case(false, false): - break - } - } - - // temporary range (before calculations) - let range = abs(max - min) - - // in case all values are equal - if range == 0.0 - { - max = max + 1.0 - min = min - 1.0 - } - - // bottom-space only effects non-custom min - if !_customAxisMin - { - let bottomSpace = range * Double(spaceBottom) - _axisMinimum = (min - bottomSpace) - } - - // top-space only effects non-custom max - if !_customAxisMax - { - let topSpace = range * Double(spaceTop) - _axisMaximum = (max + topSpace) - } - - // calc actual range - axisRange = abs(_axisMaximum - _axisMinimum) - } - - @objc open var isDrawBottomYLabelEntryEnabled: Bool { return drawBottomYLabelEntryEnabled } - - @objc open var isDrawTopYLabelEntryEnabled: Bool { return drawTopYLabelEntryEnabled } - -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/ChartBaseDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/ChartBaseDataSet.swift deleted file mode 100644 index ccb82de6b..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/ChartBaseDataSet.swift +++ /dev/null @@ -1,413 +0,0 @@ -// -// BaseDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class ChartBaseDataSet: NSObject, ChartDataSetProtocol, NSCopying -{ - public required override init() - { - super.init() - - // default color - colors.append(NSUIColor(red: 140.0/255.0, green: 234.0/255.0, blue: 255.0/255.0, alpha: 1.0)) - valueColors.append(.labelOrBlack) - } - - @objc public init(label: String) - { - super.init() - - // default color - colors.append(NSUIColor(red: 140.0/255.0, green: 234.0/255.0, blue: 255.0/255.0, alpha: 1.0)) - valueColors.append(.labelOrBlack) - - self.label = label - } - - // MARK: - Data functions and accessors - - /// Use this method to tell the data set that the underlying data has changed - open func notifyDataSetChanged() - { - calcMinMax() - } - - open func calcMinMax() - { - fatalError("calcMinMax is not implemented in ChartBaseDataSet") - } - - open func calcMinMaxY(fromX: Double, toX: Double) - { - fatalError("calcMinMaxY(fromX:, toX:) is not implemented in ChartBaseDataSet") - } - - open var yMin: Double - { - fatalError("yMin is not implemented in ChartBaseDataSet") - } - - open var yMax: Double - { - fatalError("yMax is not implemented in ChartBaseDataSet") - } - - open var xMin: Double - { - fatalError("xMin is not implemented in ChartBaseDataSet") - } - - open var xMax: Double - { - fatalError("xMax is not implemented in ChartBaseDataSet") - } - - open var entryCount: Int - { - fatalError("entryCount is not implemented in ChartBaseDataSet") - } - - open func entryForIndex(_ i: Int) -> ChartDataEntry? - { - fatalError("entryForIndex is not implemented in ChartBaseDataSet") - } - - open func entryForXValue( - _ x: Double, - closestToY y: Double, - rounding: ChartDataSetRounding) -> ChartDataEntry? - { - fatalError("entryForXValue(x, closestToY, rounding) is not implemented in ChartBaseDataSet") - } - - open func entryForXValue( - _ x: Double, - closestToY y: Double) -> ChartDataEntry? - { - fatalError("entryForXValue(x, closestToY) is not implemented in ChartBaseDataSet") - } - - open func entriesForXValue(_ x: Double) -> [ChartDataEntry] - { - fatalError("entriesForXValue is not implemented in ChartBaseDataSet") - } - - open func entryIndex( - x xValue: Double, - closestToY y: Double, - rounding: ChartDataSetRounding) -> Int - { - fatalError("entryIndex(x, closestToY, rounding) is not implemented in ChartBaseDataSet") - } - - open func entryIndex(entry e: ChartDataEntry) -> Int - { - fatalError("entryIndex(entry) is not implemented in ChartBaseDataSet") - } - - open func addEntry(_ e: ChartDataEntry) -> Bool - { - fatalError("addEntry is not implemented in ChartBaseDataSet") - } - - open func addEntryOrdered(_ e: ChartDataEntry) -> Bool - { - fatalError("addEntryOrdered is not implemented in ChartBaseDataSet") - } - - @discardableResult open func removeEntry(_ entry: ChartDataEntry) -> Bool - { - fatalError("removeEntry is not implemented in ChartBaseDataSet") - } - - @discardableResult open func removeEntry(index: Int) -> Bool - { - if let entry = entryForIndex(index) - { - return removeEntry(entry) - } - return false - } - - @discardableResult open func removeEntry(x: Double) -> Bool - { - if let entry = entryForXValue(x, closestToY: Double.nan) - { - return removeEntry(entry) - } - return false - } - - @discardableResult open func removeFirst() -> Bool - { - if entryCount > 0 - { - if let entry = entryForIndex(0) - { - return removeEntry(entry) - } - } - return false - } - - @discardableResult open func removeLast() -> Bool - { - if entryCount > 0 - { - if let entry = entryForIndex(entryCount - 1) - { - return removeEntry(entry) - } - } - return false - } - - open func contains(_ e: ChartDataEntry) -> Bool - { - fatalError("removeEntry is not implemented in ChartBaseDataSet") - } - - open func clear() - { - fatalError("clear is not implemented in ChartBaseDataSet") - } - - // MARK: - Styling functions and accessors - - /// All the colors that are used for this DataSet. - /// Colors are reused as soon as the number of Entries the DataSet represents is higher than the size of the colors array. - open var colors = [NSUIColor]() - - /// List representing all colors that are used for drawing the actual values for this DataSet - open var valueColors = [NSUIColor]() - - /// The label string that describes the DataSet. - open var label: String? = "DataSet" - - /// The axis this DataSet should be plotted against. - open var axisDependency = YAxis.AxisDependency.left - - /// - Returns: The color at the given index of the DataSet's color array. - /// This prevents out-of-bounds by performing a modulus on the color index, so colours will repeat themselves. - open func color(atIndex index: Int) -> NSUIColor - { - var index = index - if index < 0 - { - index = 0 - } - return colors[index % colors.count] - } - - /// Resets all colors of this DataSet and recreates the colors array. - open func resetColors() - { - colors.removeAll(keepingCapacity: false) - } - - /// Adds a new color to the colors array of the DataSet. - /// - /// - Parameters: - /// - color: the color to add - open func addColor(_ color: NSUIColor) - { - colors.append(color) - } - - /// Sets the one and **only** color that should be used for this DataSet. - /// Internally, this recreates the colors array and adds the specified color. - /// - /// - Parameters: - /// - color: the color to set - open func setColor(_ color: NSUIColor) - { - colors.removeAll(keepingCapacity: false) - colors.append(color) - } - - /// Sets colors to a single color a specific alpha value. - /// - /// - Parameters: - /// - color: the color to set - /// - alpha: alpha to apply to the set `color` - @objc open func setColor(_ color: NSUIColor, alpha: CGFloat) - { - setColor(color.withAlphaComponent(alpha)) - } - - /// Sets colors with a specific alpha value. - /// - /// - Parameters: - /// - colors: the colors to set - /// - alpha: alpha to apply to the set `colors` - @objc open func setColors(_ colors: [NSUIColor], alpha: CGFloat) - { - self.colors = colors.map { $0.withAlphaComponent(alpha) } - } - - /// Sets colors with a specific alpha value. - /// - /// - Parameters: - /// - colors: the colors to set - /// - alpha: alpha to apply to the set `colors` - open func setColors(_ colors: NSUIColor...) - { - self.colors = colors - } - - /// if true, value highlighting is enabled - open var highlightEnabled = true - - /// `true` if value highlighting is enabled for this dataset - open var isHighlightEnabled: Bool { return highlightEnabled } - - /// Custom formatter that is used instead of the auto-formatter if set - open lazy var valueFormatter: ValueFormatter = DefaultValueFormatter() - - /// Sets/get a single color for value text. - /// Setting the color clears the colors array and adds a single color. - /// Getting will return the first color in the array. - open var valueTextColor: NSUIColor - { - get - { - return valueColors[0] - } - set - { - valueColors.removeAll(keepingCapacity: false) - valueColors.append(newValue) - } - } - - /// - Returns: The color at the specified index that is used for drawing the values inside the chart. Uses modulus internally. - open func valueTextColorAt(_ index: Int) -> NSUIColor - { - var index = index - if index < 0 - { - index = 0 - } - return valueColors[index % valueColors.count] - } - - /// the font for the value-text labels - open var valueFont: NSUIFont = NSUIFont.systemFont(ofSize: 7.0) - - /// The rotation angle (in degrees) for value-text labels - open var valueLabelAngle: CGFloat = CGFloat(0.0) - - /// The form to draw for this dataset in the legend. - open var form = Legend.Form.default - - /// The form size to draw for this dataset in the legend. - /// - /// Return `NaN` to use the default legend form size. - open var formSize: CGFloat = CGFloat.nan - - /// The line width for drawing the form of this dataset in the legend - /// - /// Return `NaN` to use the default legend form line width. - open var formLineWidth: CGFloat = CGFloat.nan - - /// Line dash configuration for legend shapes that consist of lines. - /// - /// This is how much (in pixels) into the dash pattern are we starting from. - open var formLineDashPhase: CGFloat = 0.0 - - /// Line dash configuration for legend shapes that consist of lines. - /// - /// This is the actual dash pattern. - /// I.e. [2, 3] will paint [-- -- ] - /// [1, 3, 4, 2] will paint [- ---- - ---- ] - open var formLineDashLengths: [CGFloat]? = nil - - /// Set this to true to draw y-values on the chart. - /// - /// - Note: For bar and line charts: if `maxVisibleCount` is reached, no values will be drawn even if this is enabled. - open var drawValuesEnabled = true - - /// `true` if y-value drawing is enabled, `false` ifnot - open var isDrawValuesEnabled: Bool - { - return drawValuesEnabled - } - - /// Set this to true to draw y-icons on the chart. - /// - /// - Note: For bar and line charts: if `maxVisibleCount` is reached, no icons will be drawn even if this is enabled. - open var drawIconsEnabled = true - - /// Returns true if y-icon drawing is enabled, false if not - open var isDrawIconsEnabled: Bool - { - return drawIconsEnabled - } - - /// Offset of icons drawn on the chart. - /// - /// For all charts except Pie and Radar it will be ordinary (x offset, y offset). - /// - /// For Pie and Radar chart it will be (y offset, distance from center offset); so if you want icon to be rendered under value, you should increase X component of CGPoint, and if you want icon to be rendered closet to center, you should decrease height component of CGPoint. - open var iconsOffset = CGPoint(x: 0, y: 0) - - /// Set the visibility of this DataSet. If not visible, the DataSet will not be drawn to the chart upon refreshing it. - open var visible = true - - /// `true` if this DataSet is visible inside the chart, or `false` ifit is currently hidden. - open var isVisible: Bool - { - return visible - } - - // MARK: - NSObject - - open override var description: String - { - return String(format: "%@, label: %@, %i entries", arguments: [NSStringFromClass(type(of: self)), self.label ?? "", self.entryCount]) - } - - open override var debugDescription: String - { - return (0.. Any - { - let copy = type(of: self).init() - - copy.colors = colors - copy.valueColors = valueColors - copy.label = label - copy.axisDependency = axisDependency - copy.highlightEnabled = highlightEnabled - copy.valueFormatter = valueFormatter - copy.valueFont = valueFont - copy.form = form - copy.formSize = formSize - copy.formLineWidth = formLineWidth - copy.formLineDashPhase = formLineDashPhase - copy.formLineDashLengths = formLineDashLengths - copy.drawValuesEnabled = drawValuesEnabled - copy.drawIconsEnabled = drawIconsEnabled - copy.iconsOffset = iconsOffset - copy.visible = visible - - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarChartData.swift deleted file mode 100644 index 0403c76b7..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarChartData.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// BarChartData.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class BarChartData: BarLineScatterCandleBubbleChartData -{ - public required init() - { - super.init() - } - - public override init(dataSets: [ChartDataSetProtocol]) - { - super.init(dataSets: dataSets) - } - - public required init(arrayLiteral elements: ChartDataSetProtocol...) - { - super.init(dataSets: elements) - } - - /// The width of the bars on the x-axis, in values (not pixels) - /// - /// **default**: 0.85 - @objc open var barWidth = Double(0.85) - - /// Groups all BarDataSet objects this data object holds together by modifying the x-value of their entries. - /// Previously set x-values of entries will be overwritten. Leaves space between bars and groups as specified by the parameters. - /// Do not forget to call notifyDataSetChanged() on your BarChart object after calling this method. - /// - /// - Parameters: - /// - fromX: the starting point on the x-axis where the grouping should begin - /// - groupSpace: The space between groups of bars in values (not pixels) e.g. 0.8f for bar width 1f - /// - barSpace: The space between individual bars in values (not pixels) e.g. 0.1f for bar width 1f - @objc open func groupBars(fromX: Double, groupSpace: Double, barSpace: Double) - { - guard !isEmpty else { - print("BarData needs to hold at least 2 BarDataSets to allow grouping.", terminator: "\n") - return - } - - let max = maxEntryCountSet - let maxEntryCount = max?.entryCount ?? 0 - - let groupSpaceWidthHalf = groupSpace / 2.0 - let barSpaceHalf = barSpace / 2.0 - let barWidthHalf = self.barWidth / 2.0 - - var fromX = fromX - - let interval = groupWidth(groupSpace: groupSpace, barSpace: barSpace) - - for i in 0.. 0 || diff < 0 - { - fromX += diff - } - } - - notifyDataChanged() - } - - /// In case of grouped bars, this method returns the space an individual group of bar needs on the x-axis. - /// - /// - Parameters: - /// - groupSpace: - /// - barSpace: - @objc open func groupWidth(groupSpace: Double, barSpace: Double) -> Double - { - return Double(count) * (self.barWidth + barSpace) + groupSpace - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarChartDataEntry.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarChartDataEntry.swift deleted file mode 100644 index 86a8fa264..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarChartDataEntry.swift +++ /dev/null @@ -1,214 +0,0 @@ -// -// BarChartDataEntry.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -open class BarChartDataEntry: ChartDataEntry -{ - /// the values the stacked barchart holds - private var _yVals: [Double]? - - /// the ranges for the individual stack values - automatically calculated - private var _ranges: [Range]? - - /// the sum of all negative values this entry (if stacked) contains - private var _negativeSum: Double = 0.0 - - /// the sum of all positive values this entry (if stacked) contains - private var _positiveSum: Double = 0.0 - - public required init() - { - super.init() - } - - /// Constructor for normal bars (not stacked). - public override init(x: Double, y: Double) - { - super.init(x: x, y: y) - } - - /// Constructor for normal bars (not stacked). - public convenience init(x: Double, y: Double, data: Any?) - { - self.init(x: x, y: y) - self.data = data - } - - /// Constructor for normal bars (not stacked). - public convenience init(x: Double, y: Double, icon: NSUIImage?) - { - self.init(x: x, y: y) - self.icon = icon - } - - /// Constructor for normal bars (not stacked). - public convenience init(x: Double, y: Double, icon: NSUIImage?, data: Any?) - { - self.init(x: x, y: y) - self.icon = icon - self.data = data - } - - /// Constructor for stacked bar entries. - @objc public init(x: Double, yValues: [Double]) - { - super.init(x: x, y: BarChartDataEntry.calcSum(values: yValues)) - self._yVals = yValues - calcPosNegSum() - calcRanges() - } - - /// Constructor for stacked bar entries. One data object for whole stack - @objc public convenience init(x: Double, yValues: [Double], icon: NSUIImage?) - { - self.init(x: x, yValues: yValues) - self.icon = icon - } - - /// Constructor for stacked bar entries. One data object for whole stack - @objc public convenience init(x: Double, yValues: [Double], data: Any?) - { - self.init(x: x, yValues: yValues) - self.data = data - } - - /// Constructor for stacked bar entries. One data object for whole stack - @objc public convenience init(x: Double, yValues: [Double], icon: NSUIImage?, data: Any?) - { - self.init(x: x, yValues: yValues) - self.icon = icon - self.data = data - } - - @objc open func sumBelow(stackIndex: Int) -> Double - { - guard let yVals = _yVals, yVals.indices.contains(stackIndex) else - { - return 0 - } - - let remainder = yVals[stackIndex...].reduce(into: 0.0) { $0 += $1 } - return remainder - } - - /// The sum of all negative values this entry (if stacked) contains. (this is a positive number) - @objc open var negativeSum: Double - { - return _negativeSum - } - - /// The sum of all positive values this entry (if stacked) contains. - @objc open var positiveSum: Double - { - return _positiveSum - } - - var stackSize: Int { yValues?.count ?? 1} - - @objc open func calcPosNegSum() - { - (_negativeSum, _positiveSum) = _yVals?.reduce(into: (0,0)) { (result, y) in - if y < 0 - { - result.0 += -y - } - else - { - result.1 += y - } - } ?? (0,0) - } - - /// Splits up the stack-values of the given bar-entry into Range objects. - /// - /// - Parameters: - /// - entry: - /// - Returns: - @objc open func calcRanges() - { - guard let values = yValues, !values.isEmpty else { return } - - if _ranges == nil - { - _ranges = [Range]() - } - else - { - _ranges!.removeAll() - } - - _ranges!.reserveCapacity(values.count) - - var negRemain = -negativeSum - var posRemain: Double = 0.0 - - for value in values - { - if value < 0 - { - _ranges!.append(Range(from: negRemain, to: negRemain - value)) - negRemain -= value - } - else - { - _ranges!.append(Range(from: posRemain, to: posRemain + value)) - posRemain += value - } - } - } - - // MARK: Accessors - - /// the values the stacked barchart holds - @objc open var isStacked: Bool { return _yVals != nil } - - /// the values the stacked barchart holds - @objc open var yValues: [Double]? - { - get { return self._yVals } - set - { - self.y = BarChartDataEntry.calcSum(values: newValue ?? []) - self._yVals = newValue - calcPosNegSum() - calcRanges() - } - } - - /// The ranges of the individual stack-entries. Will return null if this entry is not stacked. - @objc open var ranges: [Range]? - { - return _ranges - } - - // MARK: NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! BarChartDataEntry - copy._yVals = _yVals - copy.y = y - copy._negativeSum = _negativeSum - copy._positiveSum = _positiveSum - return copy - } - - /// Calculates the sum across all values of the given stack. - /// - /// - Parameters: - /// - vals: - /// - Returns: - private static func calcSum(values: [Double]) -> Double - { - values.reduce(into: 0, +=) - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift deleted file mode 100644 index aae7ae30a..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// BarChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class BarChartDataSet: BarLineScatterCandleBubbleChartDataSet, BarChartDataSetProtocol -{ - private func initialize() - { - self.highlightColor = NSUIColor.black - - self.calcStackSize(entries: entries as! [BarChartDataEntry]) - self.calcEntryCountIncludingStacks(entries: entries as! [BarChartDataEntry]) - } - - public required init() - { - super.init() - initialize() - } - - public override init(entries: [ChartDataEntry], label: String) - { - super.init(entries: entries, label: label) - initialize() - } - - // MARK: - Data functions and accessors - - /// the maximum number of bars that are stacked upon each other, this value - /// is calculated from the Entries that are added to the DataSet - private var _stackSize = 1 - - /// the overall entry count, including counting each stack-value individually - private var _entryCountStacks = 0 - - /// Calculates the total number of entries this DataSet represents, including - /// stacks. All values belonging to a stack are calculated separately. - private func calcEntryCountIncludingStacks(entries: [BarChartDataEntry]) - { - _entryCountStacks = entries.lazy - .map(\.stackSize) - .reduce(into: 0, +=) - } - - /// calculates the maximum stacksize that occurs in the Entries array of this DataSet - private func calcStackSize(entries: [BarChartDataEntry]) - { - _stackSize = entries.lazy - .map(\.stackSize) - .max() ?? 1 - } - - open override func calcMinMax(entry e: ChartDataEntry) - { - guard let e = e as? BarChartDataEntry, - !e.y.isNaN - else { return } - - if e.yValues == nil - { - _yMin = Swift.min(e.y, _yMin) - _yMax = Swift.max(e.y, _yMax) - } - else - { - _yMin = Swift.min(-e.negativeSum, _yMin) - _yMax = Swift.max(e.positiveSum, _yMax) - } - - calcMinMaxX(entry: e) - } - - /// The maximum number of bars that can be stacked upon another in this DataSet. - open var stackSize: Int - { - return _stackSize - } - - /// `true` if this DataSet is stacked (stacksize > 1) or not. - open var isStacked: Bool - { - return _stackSize > 1 - } - - /// The overall entry count, including counting each stack-value individually - @objc open var entryCountStacks: Int - { - return _entryCountStacks - } - - /// array of labels used to describe the different values of the stacked bars - open var stackLabels: [String] = [] - - // MARK: - Styling functions and accessors - - /// the color used for drawing the bar-shadows. The bar shadows is a surface behind the bar that indicates the maximum value - open var barShadowColor = NSUIColor(red: 215.0/255.0, green: 215.0/255.0, blue: 215.0/255.0, alpha: 1.0) - - /// the width used for drawing borders around the bars. If borderWidth == 0, no border will be drawn. - open var barBorderWidth : CGFloat = 0.0 - - /// the color drawing borders around the bars. - open var barBorderColor = NSUIColor.black - - /// the alpha value (transparency) that is used for drawing the highlight indicator bar. min = 0.0 (fully transparent), max = 1.0 (fully opaque) - open var highlightAlpha = CGFloat(120.0 / 255.0) - - // MARK: - NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! BarChartDataSet - copy._stackSize = _stackSize - copy._entryCountStacks = _entryCountStacks - copy.stackLabels = stackLabels - - copy.barShadowColor = barShadowColor - copy.barBorderWidth = barBorderWidth - copy.barBorderColor = barBorderColor - copy.highlightAlpha = highlightAlpha - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartData.swift deleted file mode 100644 index 75eb8a278..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartData.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// BarLineScatterCandleBubbleChartData.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -open class BarLineScatterCandleBubbleChartData: ChartData -{ - public required init() - { - super.init() - } - - public override init(dataSets: [ChartDataSetProtocol]) - { - super.init(dataSets: dataSets) - } - - public required init(arrayLiteral elements: ChartDataSetProtocol...) - { - super.init(dataSets: elements) - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartDataSet.swift deleted file mode 100644 index 6d233db51..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartDataSet.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// BarLineScatterCandleBubbleChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class BarLineScatterCandleBubbleChartDataSet: ChartDataSet, BarLineScatterCandleBubbleChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - open var highlightColor = NSUIColor(red: 255.0/255.0, green: 187.0/255.0, blue: 115.0/255.0, alpha: 1.0) - open var highlightLineWidth = CGFloat(0.5) - open var highlightLineDashPhase = CGFloat(0.0) - open var highlightLineDashLengths: [CGFloat]? - - // MARK: - NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! BarLineScatterCandleBubbleChartDataSet - copy.highlightColor = highlightColor - copy.highlightLineWidth = highlightLineWidth - copy.highlightLineDashPhase = highlightLineDashPhase - copy.highlightLineDashLengths = highlightLineDashLengths - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BubbleChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/BubbleChartData.swift deleted file mode 100644 index 5a0a85563..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BubbleChartData.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// BubbleChartData.swift -// Charts -// -// Bubble chart implementation: -// Copyright 2015 Pierre-Marc Airoldi -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class BubbleChartData: BarLineScatterCandleBubbleChartData -{ - public required init() - { - super.init() - } - - public override init(dataSets: [ChartDataSetProtocol]) - { - super.init(dataSets: dataSets) - } - - public required init(arrayLiteral elements: ChartDataSetProtocol...) - { - super.init(dataSets: elements) - } - - /// Sets the width of the circle that surrounds the bubble when highlighted for all DataSet objects this data object contains - @objc open func setHighlightCircleWidth(_ width: CGFloat) - { - (_dataSets as? [BubbleChartDataSetProtocol])?.forEach { $0.highlightCircleWidth = width } - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BubbleChartDataEntry.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/BubbleChartDataEntry.swift deleted file mode 100644 index 01f9fc968..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BubbleChartDataEntry.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// BubbleDataEntry.swift -// Charts -// -// Bubble chart implementation: -// Copyright 2015 Pierre-Marc Airoldi -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class BubbleChartDataEntry: ChartDataEntry -{ - /// The size of the bubble. - @objc open var size = CGFloat(0.0) - - public required init() - { - super.init() - } - - /// - Parameters: - /// - x: The index on the x-axis. - /// - y: The value on the y-axis. - /// - size: The size of the bubble. - @objc public init(x: Double, y: Double, size: CGFloat) - { - super.init(x: x, y: y) - - self.size = size - } - - /// - Parameters: - /// - x: The index on the x-axis. - /// - y: The value on the y-axis. - /// - size: The size of the bubble. - /// - data: Spot for additional data this Entry represents. - @objc public convenience init(x: Double, y: Double, size: CGFloat, data: Any?) - { - self.init(x: x, y: y, size: size) - self.data = data - } - - /// - Parameters: - /// - x: The index on the x-axis. - /// - y: The value on the y-axis. - /// - size: The size of the bubble. - /// - icon: icon image - @objc public convenience init(x: Double, y: Double, size: CGFloat, icon: NSUIImage?) - { - self.init(x: x, y: y, size: size) - self.icon = icon - } - - /// - Parameters: - /// - x: The index on the x-axis. - /// - y: The value on the y-axis. - /// - size: The size of the bubble. - /// - icon: icon image - /// - data: Spot for additional data this Entry represents. - @objc public convenience init(x: Double, y: Double, size: CGFloat, icon: NSUIImage?, data: Any?) - { - self.init(x: x, y: y, size: size) - self.icon = icon - self.data = data - } - - // MARK: NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! BubbleChartDataEntry - copy.size = size - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BubbleChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/BubbleChartDataSet.swift deleted file mode 100644 index 02bdc399d..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/BubbleChartDataSet.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// BubbleChartDataSet.swift -// Charts -// -// Bubble chart implementation: -// Copyright 2015 Pierre-Marc Airoldi -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class BubbleChartDataSet: BarLineScatterCandleBubbleChartDataSet, BubbleChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - internal var _maxSize = CGFloat(0.0) - - open var maxSize: CGFloat { return _maxSize } - @objc open var normalizeSizeEnabled: Bool = true - open var isNormalizeSizeEnabled: Bool { return normalizeSizeEnabled } - - open override func calcMinMax(entry e: ChartDataEntry) - { - guard let e = e as? BubbleChartDataEntry - else { return } - - super.calcMinMax(entry: e) - - _maxSize = Swift.max(e.size, maxSize) - } - - // MARK: - Styling functions and accessors - - /// Sets/gets the width of the circle that surrounds the bubble when highlighted - open var highlightCircleWidth: CGFloat = 2.5 - - // MARK: - NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! BubbleChartDataSet - copy._xMin = _xMin - copy._xMax = _xMax - copy._maxSize = _maxSize - copy.normalizeSizeEnabled = normalizeSizeEnabled - copy.highlightCircleWidth = highlightCircleWidth - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/CandleChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/CandleChartData.swift deleted file mode 100644 index 3cfe5bac3..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/CandleChartData.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// CandleChartData.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -open class CandleChartData: BarLineScatterCandleBubbleChartData -{ - public required init() - { - super.init() - } - - public override init(dataSets: [ChartDataSetProtocol]) - { - super.init(dataSets: dataSets) - } - - public required init(arrayLiteral elements: ChartDataSetProtocol...) - { - super.init(dataSets: elements) - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/CandleChartDataEntry.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/CandleChartDataEntry.swift deleted file mode 100644 index 467bc168d..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/CandleChartDataEntry.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// CandleChartDataEntry.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -open class CandleChartDataEntry: ChartDataEntry -{ - /// shadow-high value - @objc open var high = Double(0.0) - - /// shadow-low value - @objc open var low = Double(0.0) - - /// close value - @objc open var close = Double(0.0) - - /// open value - @objc open var open = Double(0.0) - - public required init() - { - super.init() - } - - @objc public init(x: Double, shadowH: Double, shadowL: Double, open: Double, close: Double) - { - super.init(x: x, y: (shadowH + shadowL) / 2.0) - - self.high = shadowH - self.low = shadowL - self.open = open - self.close = close - } - - @objc public convenience init(x: Double, shadowH: Double, shadowL: Double, open: Double, close: Double, icon: NSUIImage?) - { - self.init(x: x, shadowH: shadowH, shadowL: shadowL, open: open, close: close) - self.icon = icon - } - - @objc public convenience init(x: Double, shadowH: Double, shadowL: Double, open: Double, close: Double, data: Any?) - { - self.init(x: x, shadowH: shadowH, shadowL: shadowL, open: open, close: close) - self.data = data - } - - @objc public convenience init(x: Double, shadowH: Double, shadowL: Double, open: Double, close: Double, icon: NSUIImage?, data: Any?) - { - self.init(x: x, shadowH: shadowH, shadowL: shadowL, open: open, close: close) - self.icon = icon - self.data = data - } - - /// The overall range (difference) between shadow-high and shadow-low. - @objc open var shadowRange: Double - { - return abs(high - low) - } - - /// The body size (difference between open and close). - @objc open var bodyRange: Double - { - return abs(open - close) - } - - /// the center value of the candle. (Middle value between high and low) - open override var y: Double - { - get - { - return super.y - } - set - { - super.y = (high + low) / 2.0 - } - } - - // MARK: NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! CandleChartDataEntry - copy.high = high - copy.low = low - copy.open = open - copy.close = close - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/CandleChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/CandleChartDataSet.swift deleted file mode 100644 index a860a3b91..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/CandleChartDataSet.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// CandleChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class CandleChartDataSet: LineScatterCandleRadarChartDataSet, CandleChartDataSetProtocol -{ - - public required init() - { - super.init() - } - - public override init(entries: [ChartDataEntry], label: String) - { - super.init(entries: entries, label: label) - } - - // MARK: - Data functions and accessors - - open override func calcMinMax(entry e: ChartDataEntry) - { - guard let e = e as? CandleChartDataEntry - else { return } - - _yMin = Swift.min(e.low, _yMin) - _yMax = Swift.max(e.high, _yMax) - - calcMinMaxX(entry: e) - } - - open override func calcMinMaxY(entry e: ChartDataEntry) - { - guard let e = e as? CandleChartDataEntry - else { return } - - _yMin = Swift.min(e.low, _yMin) - _yMax = Swift.max(e.high, _yMin) - - _yMin = Swift.min(e.low, _yMax) - _yMax = Swift.max(e.high, _yMax) - } - - // MARK: - Styling functions and accessors - - /// the space between the candle entries - /// - /// **default**: 0.1 (10%) - private var _barSpace: CGFloat = 0.1 - - /// the space that is left out on the left and right side of each candle, - /// **default**: 0.1 (10%), max 0.45, min 0.0 - open var barSpace: CGFloat - { - get - { - return _barSpace - } - set - { - _barSpace = newValue.clamped(to: 0...0.45) - } - } - - /// should the candle bars show? - /// when false, only "ticks" will show - /// - /// **default**: true - open var showCandleBar: Bool = true - - /// the width of the candle-shadow-line in pixels. - /// - /// **default**: 1.5 - open var shadowWidth = CGFloat(1.5) - - /// the color of the shadow line - open var shadowColor: NSUIColor? - - /// use candle color for the shadow - open var shadowColorSameAsCandle = false - - /// Is the shadow color same as the candle color? - open var isShadowColorSameAsCandle: Bool { return shadowColorSameAsCandle } - - /// color for open == close - open var neutralColor: NSUIColor? - - /// color for open > close - open var increasingColor: NSUIColor? - - /// color for open < close - open var decreasingColor: NSUIColor? - - /// Are increasing values drawn as filled? - /// increasing candlesticks are traditionally hollow - open var increasingFilled = false - - /// Are increasing values drawn as filled? - open var isIncreasingFilled: Bool { return increasingFilled } - - /// Are decreasing values drawn as filled? - /// descreasing candlesticks are traditionally filled - open var decreasingFilled = true - - /// Are decreasing values drawn as filled? - open var isDecreasingFilled: Bool { return decreasingFilled } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartData.swift deleted file mode 100644 index 81fc37b3a..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartData.swift +++ /dev/null @@ -1,574 +0,0 @@ -// -// ChartData.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -open class ChartData: NSObject, ExpressibleByArrayLiteral -{ - - @objc public internal(set) var xMax = -Double.greatestFiniteMagnitude - @objc public internal(set) var xMin = Double.greatestFiniteMagnitude - @objc public internal(set) var yMax = -Double.greatestFiniteMagnitude - @objc public internal(set) var yMin = Double.greatestFiniteMagnitude - var leftAxisMax = -Double.greatestFiniteMagnitude - var leftAxisMin = Double.greatestFiniteMagnitude - var rightAxisMax = -Double.greatestFiniteMagnitude - var rightAxisMin = Double.greatestFiniteMagnitude - - // MARK: - Accessibility - - /// When the data entry labels are generated identifiers, set this property to prepend a string before each identifier - /// - /// For example, if a label is "#3", settings this property to "Item" allows it to be spoken as "Item #3" - @objc open var accessibilityEntryLabelPrefix: String? - - /// When the data entry value requires a unit, use this property to append the string representation of the unit to the value - /// - /// For example, if a value is "44.1", setting this property to "m" allows it to be spoken as "44.1 m" - @objc open var accessibilityEntryLabelSuffix: String? - - /// If the data entry value is a count, set this to true to allow plurals and other grammatical changes - /// **default**: false - @objc open var accessibilityEntryLabelSuffixIsCount: Bool = false - - var _dataSets = [Element]() - - public override required init() - { - super.init() - } - - public required init(arrayLiteral elements: Element...) - { - super.init() - self.dataSets = elements - } - - @objc public init(dataSets: [Element]) - { - super.init() - self.dataSets = dataSets - } - - @objc public convenience init(dataSet: Element) - { - self.init(dataSets: [dataSet]) - } - - /// Call this method to let the ChartData know that the underlying data has changed. - /// Calling this performs all necessary recalculations needed when the contained data has changed. - @objc open func notifyDataChanged() - { - calcMinMax() - } - - @objc open func calcMinMaxY(fromX: Double, toX: Double) - { - forEach { $0.calcMinMaxY(fromX: fromX, toX: toX) } - - // apply the new data - calcMinMax() - } - - /// calc minimum and maximum y value over all datasets - @objc open func calcMinMax() - { - leftAxisMax = -.greatestFiniteMagnitude - leftAxisMin = .greatestFiniteMagnitude - rightAxisMax = -.greatestFiniteMagnitude - rightAxisMin = .greatestFiniteMagnitude - yMax = -.greatestFiniteMagnitude - yMin = .greatestFiniteMagnitude - xMax = -.greatestFiniteMagnitude - xMin = .greatestFiniteMagnitude - - forEach { calcMinMax(dataSet: $0) } - - // left axis - let firstLeft = getFirstLeft(dataSets: dataSets) - - if firstLeft !== nil - { - leftAxisMax = firstLeft!.yMax - leftAxisMin = firstLeft!.yMin - - for dataSet in _dataSets where dataSet.axisDependency == .left - { - if dataSet.yMin < leftAxisMin - { - leftAxisMin = dataSet.yMin - } - - if dataSet.yMax > leftAxisMax - { - leftAxisMax = dataSet.yMax - } - } - } - - // right axis - let firstRight = getFirstRight(dataSets: dataSets) - - if firstRight !== nil - { - rightAxisMax = firstRight!.yMax - rightAxisMin = firstRight!.yMin - - for dataSet in _dataSets where dataSet.axisDependency == .right - { - if dataSet.yMin < rightAxisMin - { - rightAxisMin = dataSet.yMin - } - - if dataSet.yMax > rightAxisMax - { - rightAxisMax = dataSet.yMax - } - } - } - } - - /// Adjusts the current minimum and maximum values based on the provided Entry object. - @objc open func calcMinMax(entry e: ChartDataEntry, axis: YAxis.AxisDependency) - { - xMax = Swift.max(xMax, e.x) - xMin = Swift.min(xMin, e.x) - yMax = Swift.max(yMax, e.y) - yMin = Swift.min(yMin, e.y) - - switch axis - { - case .left: - leftAxisMax = Swift.max(leftAxisMax, e.y) - leftAxisMin = Swift.min(leftAxisMin, e.y) - - case .right: - rightAxisMax = Swift.max(rightAxisMax, e.y) - rightAxisMin = Swift.min(rightAxisMin, e.y) - } - } - - /// Adjusts the minimum and maximum values based on the given DataSet. - @objc open func calcMinMax(dataSet d: Element) - { - xMax = Swift.max(xMax, d.xMax) - xMin = Swift.min(xMin, d.xMin) - yMax = Swift.max(yMax, d.yMax) - yMin = Swift.min(yMin, d.yMin) - - switch d.axisDependency - { - case .left: - leftAxisMax = Swift.max(leftAxisMax, d.yMax) - leftAxisMin = Swift.min(leftAxisMin, d.yMin) - - case .right: - rightAxisMax = Swift.max(rightAxisMax, d.yMax) - rightAxisMin = Swift.min(rightAxisMin, d.yMin) - } - } - - /// The number of LineDataSets this object contains - // exists only for objc compatibility - @objc open var dataSetCount: Int - { - return dataSets.count - } - - @objc open func getYMin(axis: YAxis.AxisDependency) -> Double - { - // TODO: Why does it make sense to return the other axisMin if there is none for the one requested? - switch axis - { - case .left: - if leftAxisMin == .greatestFiniteMagnitude - { - return rightAxisMin - } - else - { - return leftAxisMin - } - - case .right: - if rightAxisMin == .greatestFiniteMagnitude - { - return leftAxisMin - } - else - { - return rightAxisMin - } - } - } - - @objc open func getYMax(axis: YAxis.AxisDependency) -> Double - { - if axis == .left - { - if leftAxisMax == -.greatestFiniteMagnitude - { - return rightAxisMax - } - else - { - return leftAxisMax - } - } - else - { - if rightAxisMax == -.greatestFiniteMagnitude - { - return leftAxisMax - } - else - { - return rightAxisMax - } - } - } - - /// All DataSet objects this ChartData object holds. - @objc open var dataSets: [Element] - { - get - { - return _dataSets - } - set - { - _dataSets = newValue - notifyDataChanged() - } - } - - /// Get the Entry for a corresponding highlight object - /// - /// - Parameters: - /// - highlight: - /// - Returns: The entry that is highlighted - @objc open func entry(for highlight: Highlight) -> ChartDataEntry? - { - guard highlight.dataSetIndex < dataSets.endIndex else { return nil } - return self[highlight.dataSetIndex].entryForXValue(highlight.x, closestToY: highlight.y) - } - - /// **IMPORTANT: This method does calculations at runtime. Use with care in performance critical situations.** - /// - /// - Parameters: - /// - label: - /// - ignorecase: - /// - Returns: The DataSet Object with the given label. Sensitive or not. - @objc open func dataSet(forLabel label: String, ignorecase: Bool) -> Element? - { - guard let index = index(forLabel: label, ignoreCase: ignorecase) else { return nil } - return self[index] - } - - @objc(dataSetAtIndex:) - open func dataSet(at index: Index) -> Element? - { - guard dataSets.indices.contains(index) else { return nil } - return self[index] - } - - /// Removes the given DataSet from this data object. - /// Also recalculates all minimum and maximum values. - /// - /// - Returns: `true` if a DataSet was removed, `false` ifno DataSet could be removed. - @objc @discardableResult open func removeDataSet(_ dataSet: Element) -> Element? - { - guard let index = firstIndex(where: { $0 === dataSet }) else { return nil } - return remove(at: index) - } - - /// Adds an Entry to the DataSet at the specified index. Entries are added to the end of the list. - @objc(addEntry:dataSetIndex:) - open func appendEntry(_ e: ChartDataEntry, toDataSet dataSetIndex: Index) - { - guard dataSets.indices.contains(dataSetIndex) else { - return print("ChartData.addEntry() - Cannot add Entry because dataSetIndex too high or too low.", terminator: "\n") - } - - let set = self[dataSetIndex] - if !set.addEntry(e) { return } - calcMinMax(entry: e, axis: set.axisDependency) - } - - /// Removes the given Entry object from the DataSet at the specified index. - @objc @discardableResult open func removeEntry(_ entry: ChartDataEntry, dataSetIndex: Index) -> Bool - { - guard dataSets.indices.contains(dataSetIndex) else { return false } - - // remove the entry from the dataset - let removed = self[dataSetIndex].removeEntry(entry) - - if removed - { - calcMinMax() - } - - return removed - } - - /// Removes the Entry object closest to the given xIndex from the ChartDataSet at the - /// specified index. - /// - /// - Returns: `true` if an entry was removed, `false` ifno Entry was found that meets the specified requirements. - @objc @discardableResult open func removeEntry(xValue: Double, dataSetIndex: Index) -> Bool - { - guard - dataSets.indices.contains(dataSetIndex), - let entry = self[dataSetIndex].entryForXValue(xValue, closestToY: .nan) - else { return false } - - return removeEntry(entry, dataSetIndex: dataSetIndex) - } - - /// - Returns: The DataSet that contains the provided Entry, or null, if no DataSet contains this entry. - @objc open func getDataSetForEntry(_ e: ChartDataEntry) -> Element? - { - return first { $0.entryForXValue(e.x, closestToY: e.y) === e } - } - - /// - Returns: The index of the provided DataSet in the DataSet array of this data object, or -1 if it does not exist. - @objc open func index(of dataSet: Element) -> Index - { - // TODO: Return nil instead of -1 - return firstIndex(where: { $0 === dataSet }) ?? -1 - } - - /// - Returns: The first DataSet from the datasets-array that has it's dependency on the left axis. Returns null if no DataSet with left dependency could be found. - @objc open func getFirstLeft(dataSets: [Element]) -> Element? - { - return first { $0.axisDependency == .left } - } - - /// - Returns: The first DataSet from the datasets-array that has it's dependency on the right axis. Returns null if no DataSet with right dependency could be found. - @objc open func getFirstRight(dataSets: [Element]) -> Element? - { - return first { $0.axisDependency == .right } - } - - /// - Returns: All colors used across all DataSet objects this object represents. - @objc open var colors: [NSUIColor] - { - // TODO: Don't return nil - return reduce(into: []) { $0 += $1.colors } - } - - /// Sets a custom ValueFormatter for all DataSets this data object contains. - @objc open func setValueFormatter(_ formatter: ValueFormatter) - { - forEach { $0.valueFormatter = formatter } - } - - /// Sets the color of the value-text (color in which the value-labels are drawn) for all DataSets this data object contains. - @objc open func setValueTextColor(_ color: NSUIColor) - { - forEach { $0.valueTextColor = color } - } - - /// Sets the font for all value-labels for all DataSets this data object contains. - @objc open func setValueFont(_ font: NSUIFont) - { - forEach { $0.valueFont = font } - } - - /// Enables / disables drawing values (value-text) for all DataSets this data object contains. - @objc open func setDrawValues(_ enabled: Bool) - { - forEach { $0.drawValuesEnabled = enabled } - } - - /// Enables / disables highlighting values for all DataSets this data object contains. - /// If set to true, this means that values can be highlighted programmatically or by touch gesture. - @objc open var isHighlightEnabled: Bool - { - get { return allSatisfy { $0.isHighlightEnabled } } - set { forEach { $0.highlightEnabled = newValue } } - } - - /// Clears this data object from all DataSets and removes all Entries. - /// Don't forget to invalidate the chart after this. - @objc open func clearValues() - { - removeAll(keepingCapacity: false) - } - - /// Checks if this data object contains the specified DataSet. - /// - /// - Returns: `true` if so, `false` ifnot. - @objc open func contains(dataSet: Element) -> Bool - { - return contains { $0 === dataSet } - } - - /// The total entry count across all DataSet objects this data object contains. - @objc open var entryCount: Int - { - return reduce(0) { return $0 + $1.entryCount } - } - - /// The DataSet object with the maximum number of entries or null if there are no DataSets. - @objc open var maxEntryCountSet: Element? - { - return self.max { $0.entryCount < $1.entryCount } - } -} - -// MARK: MutableCollection -extension ChartData: MutableCollection -{ - public typealias Index = Int - public typealias Element = ChartDataSetProtocol - - public var startIndex: Index - { - return dataSets.startIndex - } - - public var endIndex: Index - { - return dataSets.endIndex - } - - public func index(after: Index) -> Index - { - return dataSets.index(after: after) - } - - public subscript(position: Index) -> Element - { - get { return dataSets[position] } - set { self._dataSets[position] = newValue } - } -} - -// MARK: RandomAccessCollection -extension ChartData: RandomAccessCollection -{ - public func index(before: Index) -> Index - { - return dataSets.index(before: before) - } -} - -// TODO: Conform when dropping Objective-C support -// MARK: RangeReplaceableCollection -extension ChartData//: RangeReplaceableCollection -{ - @objc(addDataSet:) - public func append(_ newElement: Element) - { - _dataSets.append(newElement) - calcMinMax(dataSet: newElement) - } - - @objc(removeDataSetByIndex:) - public func remove(at position: Index) -> Element - { - let element = _dataSets.remove(at: position) - calcMinMax() - return element - } - - public func removeFirst() -> Element - { - assert(!(self is CombinedChartData), "\(#function) not supported for CombinedData") - - let element = _dataSets.removeFirst() - notifyDataChanged() - return element - } - - public func removeFirst(_ n: Int) - { - assert(!(self is CombinedChartData), "\(#function) not supported for CombinedData") - - _dataSets.removeFirst(n) - notifyDataChanged() - } - - public func removeLast() -> Element - { - assert(!(self is CombinedChartData), "\(#function) not supported for CombinedData") - - let element = _dataSets.removeLast() - notifyDataChanged() - return element - } - - public func removeLast(_ n: Int) - { - assert(!(self is CombinedChartData), "\(#function) not supported for CombinedData") - - _dataSets.removeLast(n) - notifyDataChanged() - } - - public func removeSubrange(_ bounds: R) where R : RangeExpression, Index == R.Bound - { - assert(!(self is CombinedChartData), "\(#function) not supported for CombinedData") - - _dataSets.removeSubrange(bounds) - notifyDataChanged() - } - - public func removeAll(keepingCapacity keepCapacity: Bool) - { - assert(!(self is CombinedChartData), "\(#function) not supported for CombinedData") - - _dataSets.removeAll(keepingCapacity: keepCapacity) - notifyDataChanged() - } - - public func replaceSubrange(_ subrange: Swift.Range, with newElements: C) where C : Collection, Element == C.Element - { - assert(!(self is CombinedChartData), "\(#function) not supported for CombinedData") - - _dataSets.replaceSubrange(subrange, with: newElements) - newElements.forEach { self.calcMinMax(dataSet: $0) } - } -} - -// MARK: Swift Accessors -extension ChartData -{ - /// Retrieve the index of a ChartDataSet with a specific label from the ChartData. Search can be case sensitive or not. - /// **IMPORTANT: This method does calculations at runtime, do not over-use in performance critical situations.** - /// - /// - Parameters: - /// - label: The label to search for - /// - ignoreCase: if true, the search is not case-sensitive - /// - Returns: The index of the DataSet Object with the given label. `nil` if not found - public func index(forLabel label: String, ignoreCase: Bool) -> Index? - { - return ignoreCase - ? firstIndex { $0.label?.caseInsensitiveCompare(label) == .orderedSame } - : firstIndex { $0.label == label } - } - - public subscript(label label: String, ignoreCase ignoreCase: Bool) -> Element? - { - guard let index = index(forLabel: label, ignoreCase: ignoreCase) else { return nil } - return self[index] - } - - public subscript(entry entry: ChartDataEntry) -> Element? - { - assert(!(self is CombinedChartData), "\(#function) not supported for CombinedData") - - guard let index = firstIndex(where: { $0.entryForXValue(entry.x, closestToY: entry.y) === entry }) else { return nil } - return self[index] - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartDataEntry.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartDataEntry.swift deleted file mode 100644 index 3f29f964f..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartDataEntry.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// ChartDataEntry.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -open class ChartDataEntry: ChartDataEntryBase, NSCopying -{ - /// the x value - @objc open var x = 0.0 - - public required init() - { - super.init() - } - - /// An Entry represents one single entry in the chart. - /// - /// - Parameters: - /// - x: the x value - /// - y: the y value (the actual value of the entry) - @objc public init(x: Double, y: Double) - { - super.init(y: y) - self.x = x - } - - /// An Entry represents one single entry in the chart. - /// - /// - Parameters: - /// - x: the x value - /// - y: the y value (the actual value of the entry) - /// - data: Space for additional data this Entry represents. - - @objc public convenience init(x: Double, y: Double, data: Any?) - { - self.init(x: x, y: y) - self.data = data - } - - /// An Entry represents one single entry in the chart. - /// - /// - Parameters: - /// - x: the x value - /// - y: the y value (the actual value of the entry) - /// - icon: icon image - - @objc public convenience init(x: Double, y: Double, icon: NSUIImage?) - { - self.init(x: x, y: y) - self.icon = icon - } - - /// An Entry represents one single entry in the chart. - /// - /// - Parameters: - /// - x: the x value - /// - y: the y value (the actual value of the entry) - /// - icon: icon image - /// - data: Space for additional data this Entry represents. - - @objc public convenience init(x: Double, y: Double, icon: NSUIImage?, data: Any?) - { - self.init(x: x, y: y) - self.icon = icon - self.data = data - } - - // MARK: NSObject - - open override var description: String - { - return "ChartDataEntry, x: \(x), y \(y)" - } - - // MARK: NSCopying - - open func copy(with zone: NSZone? = nil) -> Any - { - let copy = type(of: self).init() - - copy.x = x - copy.y = y - copy.data = data - - return copy - } -} - -// MARK: Equatable -extension ChartDataEntry/*: Equatable*/ { - open override func isEqual(_ object: Any?) -> Bool { - guard let object = object as? ChartDataEntry else { return false } - - if self === object - { - return true - } - - return y == object.y - && x == object.x - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartDataEntryBase.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartDataEntryBase.swift deleted file mode 100644 index 2c9d0d574..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartDataEntryBase.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// ChartDataEntryBase.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -open class ChartDataEntryBase: NSObject -{ - /// the y value - @objc open var y = 0.0 - - /// optional spot for additional data this Entry represents - @objc open var data: Any? - - /// optional icon image - @objc open var icon: NSUIImage? - - public override required init() - { - super.init() - } - - /// An Entry represents one single entry in the chart. - /// - /// - Parameters: - /// - y: the y value (the actual value of the entry) - @objc public init(y: Double) - { - super.init() - - self.y = y - } - - /// - Parameters: - /// - y: the y value (the actual value of the entry) - /// - data: Space for additional data this Entry represents. - - @objc public convenience init(y: Double, data: Any?) - { - self.init(y: y) - - self.data = data - } - - /// - Parameters: - /// - y: the y value (the actual value of the entry) - /// - icon: icon image - - @objc public convenience init(y: Double, icon: NSUIImage?) - { - self.init(y: y) - - self.icon = icon - } - - /// - Parameters: - /// - y: the y value (the actual value of the entry) - /// - icon: icon image - /// - data: Space for additional data this Entry represents. - - @objc public convenience init(y: Double, icon: NSUIImage?, data: Any?) - { - self.init(y: y) - - self.icon = icon - self.data = data - } - - // MARK: NSObject - - open override var description: String - { - return "ChartDataEntryBase, y \(y)" - } -} - -// MARK: Equatable -extension ChartDataEntryBase/*: Equatable*/ { - open override func isEqual(_ object: Any?) -> Bool { - guard let object = object as? ChartDataEntryBase else { return false } - - if self === object - { - return true - } - - return y == object.y - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartDataSet.swift deleted file mode 100644 index 344f0f658..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ChartDataSet.swift +++ /dev/null @@ -1,489 +0,0 @@ -// -// ChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Algorithms -import Foundation - -/// Determines how to round DataSet index values for `ChartDataSet.entryIndex(x, rounding)` when an exact x-value is not found. -@objc -public enum ChartDataSetRounding: Int -{ - case up = 0 - case down = 1 - case closest = 2 -} - -/// The DataSet class represents one group or type of entries (Entry) in the Chart that belong together. -/// It is designed to logically separate different groups of values inside the Chart (e.g. the values for a specific line in the LineChart, or the values of a specific group of bars in the BarChart). -open class ChartDataSet: ChartBaseDataSet -{ - public required init() - { - entries = [] - - super.init() - } - - public override convenience init(label: String) - { - self.init(entries: [], label: label) - } - - @objc public init(entries: [ChartDataEntry], label: String) - { - self.entries = entries - - super.init(label: label) - - self.calcMinMax() - } - - @objc public convenience init(entries: [ChartDataEntry]) - { - self.init(entries: entries, label: "DataSet") - } - - // MARK: - Data functions and accessors - - /// - Note: Calls `notifyDataSetChanged()` after setting a new value. - /// - Returns: The array of y-values that this DataSet represents. - /// the entries that this dataset represents / holds together - @objc - open private(set) var entries: [ChartDataEntry] - - /// Used to replace all entries of a data set while retaining styling properties. - /// This is a separate method from a setter on `entries` to encourage usage - /// of `Collection` conformances. - /// - /// - Parameter entries: new entries to replace existing entries in the dataset - @objc - public func replaceEntries(_ entries: [ChartDataEntry]) { - self.entries = entries - notifyDataSetChanged() - } - - /// maximum y-value in the value array - internal var _yMax: Double = -Double.greatestFiniteMagnitude - - /// minimum y-value in the value array - internal var _yMin: Double = Double.greatestFiniteMagnitude - - /// maximum x-value in the value array - internal var _xMax: Double = -Double.greatestFiniteMagnitude - - /// minimum x-value in the value array - internal var _xMin: Double = Double.greatestFiniteMagnitude - - open override func calcMinMax() - { - _yMax = -Double.greatestFiniteMagnitude - _yMin = Double.greatestFiniteMagnitude - _xMax = -Double.greatestFiniteMagnitude - _xMin = Double.greatestFiniteMagnitude - - guard !isEmpty else { return } - - forEach(calcMinMax) - } - - open override func calcMinMaxY(fromX: Double, toX: Double) - { - _yMax = -Double.greatestFiniteMagnitude - _yMin = Double.greatestFiniteMagnitude - - guard !isEmpty else { return } - - let indexFrom = entryIndex(x: fromX, closestToY: .nan, rounding: .closest) - var indexTo = entryIndex(x: toX, closestToY: .nan, rounding: .up) - if indexTo == -1 { indexTo = entryIndex(x: toX, closestToY: .nan, rounding: .closest) } - - guard indexTo >= indexFrom else { return } - // only recalculate y - self[indexFrom...indexTo].forEach(calcMinMaxY) - } - - @objc open func calcMinMaxX(entry e: ChartDataEntry) - { - _xMin = Swift.min(e.x, _xMin) - _xMax = Swift.max(e.x, _xMax) - } - - @objc open func calcMinMaxY(entry e: ChartDataEntry) - { - _yMin = Swift.min(e.y, _yMin) - _yMax = Swift.max(e.y, _yMax) - } - - /// Updates the min and max x and y value of this DataSet based on the given Entry. - /// - /// - Parameters: - /// - e: - internal func calcMinMax(entry e: ChartDataEntry) - { - calcMinMaxX(entry: e) - calcMinMaxY(entry: e) - } - - /// The minimum y-value this DataSet holds - @objc open override var yMin: Double { return _yMin } - - /// The maximum y-value this DataSet holds - @objc open override var yMax: Double { return _yMax } - - /// The minimum x-value this DataSet holds - @objc open override var xMin: Double { return _xMin } - - /// The maximum x-value this DataSet holds - @objc open override var xMax: Double { return _xMax } - - /// The number of y-values this DataSet represents - @available(*, deprecated, message: "Use `count` instead") - open override var entryCount: Int { return count } - - /// - Throws: out of bounds - /// if `i` is out of bounds, it may throw an out-of-bounds exception - /// - Returns: The entry object found at the given index (not x-value!) - @available(*, deprecated, message: "Use `subscript(index:)` instead.") - open override func entryForIndex(_ i: Int) -> ChartDataEntry? - { - guard indices.contains(i) else { - return nil - } - return self[i] - } - - /// - Parameters: - /// - xValue: the x-value - /// - closestToY: If there are multiple y-values for the specified x-value, - /// - rounding: determine whether to round up/down/closest if there is no Entry matching the provided x-value - /// - Returns: The first Entry object found at the given x-value with binary search. - /// If the no Entry at the specified x-value is found, this method returns the Entry at the closest x-value according to the rounding. - /// nil if no Entry object at that x-value. - open override func entryForXValue( - _ xValue: Double, - closestToY yValue: Double, - rounding: ChartDataSetRounding) -> ChartDataEntry? - { - let index = entryIndex(x: xValue, closestToY: yValue, rounding: rounding) - if index > -1 - { - return self[index] - } - return nil - } - - /// - Parameters: - /// - xValue: the x-value - /// - closestToY: If there are multiple y-values for the specified x-value, - /// - Returns: The first Entry object found at the given x-value with binary search. - /// If the no Entry at the specified x-value is found, this method returns the Entry at the closest x-value. - /// nil if no Entry object at that x-value. - open override func entryForXValue( - _ xValue: Double, - closestToY yValue: Double) -> ChartDataEntry? - { - return entryForXValue(xValue, closestToY: yValue, rounding: .closest) - } - - /// - Returns: All Entry objects found at the given xIndex with binary search. - /// An empty array if no Entry object at that index. - open override func entriesForXValue(_ xValue: Double) -> [ChartDataEntry] - { - let match: (ChartDataEntry) -> Bool = { $0.x == xValue } - var partitioned = self.entries - _ = partitioned.partition(by: match) - let i = partitioned.partitioningIndex(where: match) - guard i < endIndex else { return [] } - return partitioned[i...].prefix(while: match) - } - - /// - Parameters: - /// - xValue: x-value of the entry to search for - /// - closestToY: If there are multiple y-values for the specified x-value, - /// - rounding: Rounding method if exact value was not found - /// - Returns: The array-index of the specified entry. - /// If the no Entry at the specified x-value is found, this method returns the index of the Entry at the closest x-value according to the rounding. - open override func entryIndex( - x xValue: Double, - closestToY yValue: Double, - rounding: ChartDataSetRounding) -> Int - { - var closest = partitioningIndex { $0.x >= xValue } - guard closest < endIndex else { return index(before: endIndex) } - - var closestXValue = self[closest].x - - switch rounding { - case .up: - // If rounding up, and found x-value is lower than specified x, and we can go upper... - if closestXValue < xValue && closest < index(before: endIndex) - { - formIndex(after: &closest) - } - - case .down: - // If rounding down, and found x-value is upper than specified x, and we can go lower... - if closestXValue > xValue && closest > startIndex - { - formIndex(before: &closest) - } - - case .closest: - // The closest value in the beginning of this function - // `var closest = partitioningIndex { $0.x >= xValue }` - // doesn't guarantee closest rounding method - if closest > startIndex { - let distanceAfter = abs(self[closest].x - xValue) - let distanceBefore = abs(self[index(before: closest)].x - xValue) - if distanceBefore < distanceAfter - { - closest = index(before: closest) - } - closestXValue = self[closest].x - } - } - - // Search by closest to y-value - if !yValue.isNaN - { - while closest > startIndex && self[index(before: closest)].x == closestXValue - { - formIndex(before: &closest) - } - - var closestYValue = self[closest].y - var closestYIndex = closest - - while closest < index(before: endIndex) - { - formIndex(after: &closest) - let value = self[closest] - - if value.x != closestXValue { break } - if abs(value.y - yValue) <= abs(closestYValue - yValue) - { - closestYValue = yValue - closestYIndex = closest - } - } - - closest = closestYIndex - } - - return closest - } - - /// - Parameters: - /// - e: the entry to search for - /// - Returns: The array-index of the specified entry - // TODO: Should be returning `nil` to follow Swift convention - @available(*, deprecated, message: "Use `firstIndex(of:)` or `lastIndex(of:)`") - open override func entryIndex(entry e: ChartDataEntry) -> Int - { - return firstIndex(of: e) ?? -1 - } - - /// Adds an Entry to the DataSet dynamically. - /// Entries are added to the end of the list. - /// This will also recalculate the current minimum and maximum values of the DataSet and the value-sum. - /// - /// - Parameters: - /// - e: the entry to add - /// - Returns: True - // TODO: This should return `Void` to follow Swift convention - @available(*, deprecated, message: "Use `append(_:)` instead", renamed: "append(_:)") - open override func addEntry(_ e: ChartDataEntry) -> Bool - { - append(e) - return true - } - - /// Adds an Entry to the DataSet dynamically. - /// Entries are added to their appropriate index respective to it's x-index. - /// This will also recalculate the current minimum and maximum values of the DataSet and the value-sum. - /// - /// - Parameters: - /// - e: the entry to add - /// - Returns: True - // TODO: This should return `Void` to follow Swift convention - open override func addEntryOrdered(_ e: ChartDataEntry) -> Bool - { - if let last = last, last.x > e.x - { - let startIndex = entryIndex(x: e.x, closestToY: e.y, rounding: .up) - let closestIndex = self[startIndex...].lastIndex { $0.x < e.x } - ?? startIndex - calcMinMax(entry: e) - entries.insert(e, at: closestIndex) - } - else - { - append(e) - } - - return true - } - - @available(*, renamed: "remove(_:)") - open override func removeEntry(_ entry: ChartDataEntry) -> Bool - { - remove(entry) - } - - /// Removes an Entry from the DataSet dynamically. - /// This will also recalculate the current minimum and maximum values of the DataSet and the value-sum. - /// - /// - Parameters: - /// - entry: the entry to remove - /// - Returns: `true` if the entry was removed successfully, else if the entry does not exist - open func remove(_ entry: ChartDataEntry) -> Bool - { - guard let index = firstIndex(of: entry) else { return false } - _ = remove(at: index) - return true - } - - /// Removes the first Entry (at index 0) of this DataSet from the entries array. - /// - /// - Returns: `true` if successful, `false` if not. - // TODO: This should return the removed entry to follow Swift convention. - @available(*, deprecated, message: "Use `func removeFirst() -> ChartDataEntry` instead.") - open override func removeFirst() -> Bool - { - let entry: ChartDataEntry? = isEmpty ? nil : removeFirst() - return entry != nil - } - - /// Removes the last Entry (at index size-1) of this DataSet from the entries array. - /// - /// - Returns: `true` if successful, `false` if not. - // TODO: This should return the removed entry to follow Swift convention. - @available(*, deprecated, message: "Use `func removeLast() -> ChartDataEntry` instead.") - open override func removeLast() -> Bool - { - let entry: ChartDataEntry? = isEmpty ? nil : removeLast() - return entry != nil - } - - /// Removes all values from this DataSet and recalculates min and max value. - @available(*, deprecated, message: "Use `removeAll(keepingCapacity:)` instead.") - open override func clear() - { - removeAll(keepingCapacity: true) - } - - // MARK: - Data functions and accessors - - // MARK: - NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! ChartDataSet - - copy.entries = entries - copy._yMax = _yMax - copy._yMin = _yMin - copy._xMax = _xMax - copy._xMin = _xMin - - return copy - } -} - -// MARK: MutableCollection -extension ChartDataSet: MutableCollection { - public typealias Index = Int - public typealias Element = ChartDataEntry - - public var startIndex: Index { - return entries.startIndex - } - - public var endIndex: Index { - return entries.endIndex - } - - public func index(after: Index) -> Index { - return entries.index(after: after) - } - - @objc - public subscript(position: Index) -> Element { - get { - // This is intentionally not a safe subscript to mirror - // the behaviour of the built in Swift Collection Types - return entries[position] - } - set { - calcMinMax(entry: newValue) - entries[position] = newValue - } - } -} - -// MARK: RandomAccessCollection -extension ChartDataSet: RandomAccessCollection { - public func index(before: Index) -> Index { - return entries.index(before: before) - } -} - -// MARK: RangeReplaceableCollection -extension ChartDataSet: RangeReplaceableCollection { - public func replaceSubrange(_ subrange: Swift.Range, with newElements: C) where C : Collection, Element == C.Element { - entries.replaceSubrange(subrange, with: newElements) - notifyDataSetChanged() - } - - public func append(_ newElement: Element) { - calcMinMax(entry: newElement) - entries.append(newElement) - } - - public func remove(at position: Index) -> Element { - let element = entries.remove(at: position) - notifyDataSetChanged() - return element - } - - public func removeFirst() -> Element { - let element = entries.removeFirst() - notifyDataSetChanged() - return element - } - - public func removeFirst(_ n: Int) { - entries.removeFirst(n) - notifyDataSetChanged() - } - - public func removeLast() -> Element { - let element = entries.removeLast() - notifyDataSetChanged() - return element - } - - public func removeLast(_ n: Int) { - entries.removeLast(n) - notifyDataSetChanged() - } - - public func removeSubrange(_ bounds: R) where R : RangeExpression, Index == R.Bound { - entries.removeSubrange(bounds) - notifyDataSetChanged() - } - - @objc - public func removeAll(keepingCapacity keepCapacity: Bool) { - entries.removeAll(keepingCapacity: keepCapacity) - notifyDataSetChanged() - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/CombinedChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/CombinedChartData.swift deleted file mode 100644 index 9659055a4..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/CombinedChartData.swift +++ /dev/null @@ -1,292 +0,0 @@ -// -// CombinedChartData.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -open class CombinedChartData: BarLineScatterCandleBubbleChartData -{ - private var _lineData: LineChartData! - private var _barData: BarChartData! - private var _scatterData: ScatterChartData! - private var _candleData: CandleChartData! - private var _bubbleData: BubbleChartData! - - public required init() - { - super.init() - } - - public override init(dataSets: [ChartDataSetProtocol]) - { - super.init(dataSets: dataSets) - } - - public required init(arrayLiteral elements: ChartDataSetProtocol...) - { - super.init(dataSets: elements) - } - - @objc open var lineData: LineChartData! - { - get - { - return _lineData - } - set - { - _lineData = newValue - notifyDataChanged() - } - } - - @objc open var barData: BarChartData! - { - get - { - return _barData - } - set - { - _barData = newValue - notifyDataChanged() - } - } - - @objc open var scatterData: ScatterChartData! - { - get - { - return _scatterData - } - set - { - _scatterData = newValue - notifyDataChanged() - } - } - - @objc open var candleData: CandleChartData! - { - get - { - return _candleData - } - set - { - _candleData = newValue - notifyDataChanged() - } - } - - @objc open var bubbleData: BubbleChartData! - { - get - { - return _bubbleData - } - set - { - _bubbleData = newValue - notifyDataChanged() - } - } - - open override func calcMinMax() - { - _dataSets.removeAll() - - yMax = -Double.greatestFiniteMagnitude - yMin = Double.greatestFiniteMagnitude - xMax = -Double.greatestFiniteMagnitude - xMin = Double.greatestFiniteMagnitude - - leftAxisMax = -Double.greatestFiniteMagnitude - leftAxisMin = Double.greatestFiniteMagnitude - rightAxisMax = -Double.greatestFiniteMagnitude - rightAxisMin = Double.greatestFiniteMagnitude - - let allData = self.allData - - for data in allData - { - data.calcMinMax() - - _dataSets.append(contentsOf: data) - - if data.yMax > yMax - { - yMax = data.yMax - } - - if data.yMin < yMin - { - yMin = data.yMin - } - - if data.xMax > xMax - { - xMax = data.xMax - } - - if data.xMin < xMin - { - xMin = data.xMin - } - - for set in data - { - if set.axisDependency == .left - { - if set.yMax > leftAxisMax - { - leftAxisMax = set.yMax - } - if set.yMin < leftAxisMin - { - leftAxisMin = set.yMin - } - } - else - { - if set.yMax > rightAxisMax - { - rightAxisMax = set.yMax - } - if set.yMin < rightAxisMin - { - rightAxisMin = set.yMin - } - } - } - } - } - - /// All data objects in row: line-bar-scatter-candle-bubble if not null. - @objc open var allData: [ChartData] - { - var data = [ChartData]() - - if lineData !== nil - { - data.append(lineData) - } - if barData !== nil - { - data.append(barData) - } - if scatterData !== nil - { - data.append(scatterData) - } - if candleData !== nil - { - data.append(candleData) - } - if bubbleData !== nil - { - data.append(bubbleData) - } - - return data - } - - @objc open func dataByIndex(_ index: Int) -> ChartData - { - return allData[index] - } - - open func dataIndex(_ data: ChartData) -> Int? - { - return allData.firstIndex(of: data) - } - - open override func removeDataSet(_ dataSet: ChartDataSetProtocol) -> Element? - { - for data in allData - { - if let e = data.removeDataSet(dataSet) - { - return e - } - } - - return nil - } - - open override func removeEntry(_ entry: ChartDataEntry, dataSetIndex: Int) -> Bool - { - print("removeEntry(entry, dataSetIndex) not supported for CombinedData", terminator: "\n") - return false - } - - open override func removeEntry(xValue: Double, dataSetIndex: Int) -> Bool - { - print("removeEntry(xValue, dataSetIndex) not supported for CombinedData", terminator: "\n") - return false - } - - open override func notifyDataChanged() - { - _lineData?.notifyDataChanged() - _barData?.notifyDataChanged() - _scatterData?.notifyDataChanged() - _candleData?.notifyDataChanged() - _bubbleData?.notifyDataChanged() - - super.notifyDataChanged() // recalculate everything - } - - /// Get the Entry for a corresponding highlight object - /// - /// - Parameters: - /// - highlight: - /// - Returns: The entry that is highlighted - @objc override open func entry(for highlight: Highlight) -> ChartDataEntry? - { - // The value of the highlighted entry could be NaN - if we are not interested in highlighting a specific value. - getDataSetByHighlight(highlight)? - .entriesForXValue(highlight.x) - .first { $0.y == highlight.y || highlight.y.isNaN } - } - - /// Get dataset for highlight - /// - /// - Parameters: - /// - highlight: current highlight - /// - Returns: dataset related to highlight - @objc open func getDataSetByHighlight(_ highlight: Highlight) -> ChartDataSetProtocol! - { - guard allData.indices.contains(highlight.dataIndex) else - { - return nil - } - - let data = dataByIndex(highlight.dataIndex) - - guard data.indices.contains(highlight.dataSetIndex) else - { - return nil - } - - // The value of the highlighted entry could be NaN - if we are not interested in highlighting a specific value. - return data[highlight.dataSetIndex] - } - - // MARK: Unsupported Collection Methods - - public override func append(_ newElement: ChartData.Element) { - fatalError("append(_:) not supported for CombinedData") - } - - public override func remove(at i: Int) -> ChartDataSetProtocol { - fatalError("remove(at:) not supported for CombinedData") - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineChartData.swift deleted file mode 100644 index fe3eb9286..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineChartData.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// LineChartData.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -/// Data object that encapsulates all data associated with a LineChart. -open class LineChartData: ChartData -{ - public required init() - { - super.init() - } - - public override init(dataSets: [ChartDataSetProtocol]) - { - super.init(dataSets: dataSets) - } - - public required init(arrayLiteral elements: ChartDataSetProtocol...) - { - super.init(dataSets: elements) - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineChartDataSet.swift deleted file mode 100644 index 423ac54c7..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineChartDataSet.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// LineChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class LineChartDataSet: LineRadarChartDataSet, LineChartDataSetProtocol -{ - @objc(LineChartMode) - public enum Mode: Int - { - case linear - case stepped - case cubicBezier - case horizontalBezier - } - - private func initialize() - { - // default color - circleColors.append(NSUIColor(red: 140.0/255.0, green: 234.0/255.0, blue: 255.0/255.0, alpha: 1.0)) - } - - public required init() - { - super.init() - initialize() - } - - public override init(entries: [ChartDataEntry], label: String) - { - super.init(entries: entries, label: label) - initialize() - } - - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// The drawing mode for this line dataset - /// - /// **default**: Linear - open var mode: Mode = Mode.linear - - private var _cubicIntensity = CGFloat(0.2) - - /// Intensity for cubic lines (min = 0.05, max = 1) - /// - /// **default**: 0.2 - open var cubicIntensity: CGFloat - { - get - { - return _cubicIntensity - } - set - { - _cubicIntensity = newValue.clamped(to: 0.05...1) - } - } - - open var isDrawLineWithGradientEnabled = false - - open var gradientPositions: [CGFloat]? - - /// The radius of the drawn circles. - open var circleRadius = CGFloat(8.0) - - /// The hole radius of the drawn circles - open var circleHoleRadius = CGFloat(4.0) - - open var circleColors = [NSUIColor]() - - /// - Returns: The color at the given index of the DataSet's circle-color array. - /// Performs a IndexOutOfBounds check by modulus. - open func getCircleColor(atIndex index: Int) -> NSUIColor? - { - let size = circleColors.count - let index = index % size - if index >= size - { - return nil - } - return circleColors[index] - } - - /// Sets the one and ONLY color that should be used for this DataSet. - /// Internally, this recreates the colors array and adds the specified color. - open func setCircleColor(_ color: NSUIColor) - { - circleColors.removeAll(keepingCapacity: false) - circleColors.append(color) - } - - open func setCircleColors(_ colors: NSUIColor...) - { - circleColors.removeAll(keepingCapacity: false) - circleColors.append(contentsOf: colors) - } - - /// Resets the circle-colors array and creates a new one - open func resetCircleColors(_ index: Int) - { - circleColors.removeAll(keepingCapacity: false) - } - - /// If true, drawing circles is enabled - open var drawCirclesEnabled = true - - /// `true` if drawing circles for this DataSet is enabled, `false` ifnot - open var isDrawCirclesEnabled: Bool { return drawCirclesEnabled } - - /// The color of the inner circle (the circle-hole). - open var circleHoleColor: NSUIColor? = NSUIColor.white - - /// `true` if drawing circles for this DataSet is enabled, `false` ifnot - open var drawCircleHoleEnabled = true - - /// `true` if drawing the circle-holes is enabled, `false` ifnot. - open var isDrawCircleHoleEnabled: Bool { return drawCircleHoleEnabled } - - /// This is how much (in pixels) into the dash pattern are we starting from. - open var lineDashPhase = CGFloat(0.0) - - /// This is the actual dash pattern. - /// I.e. [2, 3] will paint [-- -- ] - /// [1, 3, 4, 2] will paint [- ---- - ---- ] - open var lineDashLengths: [CGFloat]? - - /// Line cap type, default is CGLineCap.Butt - open var lineCapType = CGLineCap.butt - - /// formatter for customizing the position of the fill-line - private var _fillFormatter: FillFormatter = DefaultFillFormatter() - - /// Sets a custom FillFormatterProtocol to the chart that handles the position of the filled-line for each DataSet. Set this to null to use the default logic. - open var fillFormatter: FillFormatter? - { - get - { - return _fillFormatter - } - set - { - _fillFormatter = newValue ?? DefaultFillFormatter() - } - } - - // MARK: NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! LineChartDataSet - copy.circleColors = circleColors - copy.circleHoleColor = circleHoleColor - copy.circleRadius = circleRadius - copy.circleHoleRadius = circleHoleRadius - copy.cubicIntensity = cubicIntensity - copy.lineDashPhase = lineDashPhase - copy.lineDashLengths = lineDashLengths - copy.lineCapType = lineCapType - copy.drawCirclesEnabled = drawCirclesEnabled - copy.drawCircleHoleEnabled = drawCircleHoleEnabled - copy.mode = mode - copy._fillFormatter = _fillFormatter - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineRadarChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineRadarChartDataSet.swift deleted file mode 100644 index 91ad13e4b..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineRadarChartDataSet.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// LineRadarChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class LineRadarChartDataSet: LineScatterCandleRadarChartDataSet, LineRadarChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// The color that is used for filling the line surface area. - private var _fillColor = NSUIColor(red: 140.0/255.0, green: 234.0/255.0, blue: 255.0/255.0, alpha: 1.0) - - /// The color that is used for filling the line surface area. - open var fillColor: NSUIColor - { - get { return _fillColor } - set - { - _fillColor = newValue - fill = nil - } - } - - /// The object that is used for filling the area below the line. - /// **default**: nil - open var fill: Fill? - - /// The alpha value that is used for filling the line surface, - /// **default**: 0.33 - open var fillAlpha = CGFloat(0.33) - - private var _lineWidth = CGFloat(1.0) - - /// line width of the chart (min = 0.0, max = 10) - /// - /// **default**: 1 - open var lineWidth: CGFloat - { - get - { - return _lineWidth - } - set - { - _lineWidth = newValue.clamped(to: 0...10) - } - } - - /// Set to `true` if the DataSet should be drawn filled (surface), and not just as a line. - /// Disabling this will give great performance boost. - /// Please note that this method uses the path clipping for drawing the filled area (with images, gradients and layers). - open var drawFilledEnabled = false - - /// `true` if filled drawing is enabled, `false` ifnot - open var isDrawFilledEnabled: Bool - { - return drawFilledEnabled - } - - // MARK: NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! LineRadarChartDataSet - copy.fill = fill - copy.fillAlpha = fillAlpha - copy._fillColor = _fillColor - copy._lineWidth = _lineWidth - copy.drawFilledEnabled = drawFilledEnabled - return copy - } - -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineScatterCandleRadarChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineScatterCandleRadarChartDataSet.swift deleted file mode 100644 index 1392cd246..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/LineScatterCandleRadarChartDataSet.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// LineScatterCandleRadarChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - - -open class LineScatterCandleRadarChartDataSet: BarLineScatterCandleBubbleChartDataSet, LineScatterCandleRadarChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// Enables / disables the horizontal highlight-indicator. If disabled, the indicator is not drawn. - open var drawHorizontalHighlightIndicatorEnabled = true - - /// Enables / disables the vertical highlight-indicator. If disabled, the indicator is not drawn. - open var drawVerticalHighlightIndicatorEnabled = true - - /// `true` if horizontal highlight indicator lines are enabled (drawn) - open var isHorizontalHighlightIndicatorEnabled: Bool { return drawHorizontalHighlightIndicatorEnabled } - - /// `true` if vertical highlight indicator lines are enabled (drawn) - open var isVerticalHighlightIndicatorEnabled: Bool { return drawVerticalHighlightIndicatorEnabled } - - /// Enables / disables both vertical and horizontal highlight-indicators. - /// :param: enabled - open func setDrawHighlightIndicators(_ enabled: Bool) - { - drawHorizontalHighlightIndicatorEnabled = enabled - drawVerticalHighlightIndicatorEnabled = enabled - } - - // MARK: NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! LineScatterCandleRadarChartDataSet - copy.drawHorizontalHighlightIndicatorEnabled = drawHorizontalHighlightIndicatorEnabled - copy.drawVerticalHighlightIndicatorEnabled = drawVerticalHighlightIndicatorEnabled - return copy - } - -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/PieChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/PieChartData.swift deleted file mode 100644 index accda2ed8..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/PieChartData.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// PieData.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -open class PieChartData: ChartData -{ - public required init() - { - super.init() - } - - public override init(dataSets: [ChartDataSetProtocol]) - { - super.init(dataSets: dataSets) - } - - public required init(arrayLiteral elements: ChartDataSetProtocol...) - { - super.init(dataSets: elements) - } - - @objc public var dataSet: PieChartDataSetProtocol? - { - get - { - return dataSets.first as? PieChartDataSetProtocol - } - set - { - if let set = newValue - { - dataSets = [set] - } - else - { - dataSets = [] - } - } - } - - /// - returns: All up to one dataSet object this ChartData object holds. - @objc open override var dataSets: [ChartDataSetProtocol] - { - get - { - assert(super.dataSets.count <= 1, "Found multiple data sets while pie chart only allows one") - return super.dataSets - } - set - { - super.dataSets = newValue - } - } - - open override func dataSet(at index: ChartData.Index) -> ChartData.Element? - { - guard index == 0 else { return nil } - return self[index] - } - - open override func dataSet(forLabel label: String, ignorecase: Bool) -> ChartDataSetProtocol? - { - if dataSets.first?.label == nil - { - return nil - } - - if ignorecase - { - if let label = dataSets[0].label, label.caseInsensitiveCompare(label) == .orderedSame - { - return dataSets[0] - } - } - else - { - if label == dataSets[0].label - { - return dataSets[0] - } - } - return nil - } - - @objc override open func entry(for highlight: Highlight) -> ChartDataEntry? - { - return dataSet?.entryForIndex(Int(highlight.x)) - } - - /// The total y-value sum across all DataSet objects the this object represents. - @objc open var yValueSum: Double - { - guard let dataSet = dataSet else { return 0.0 } - return (0.. Any - { - let copy = super.copy(with: zone) as! PieChartDataEntry - copy.label = label - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/PieChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/PieChartDataSet.swift deleted file mode 100644 index 07287ad06..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/PieChartDataSet.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// PieChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class PieChartDataSet: ChartDataSet, PieChartDataSetProtocol -{ - @objc(PieChartValuePosition) - public enum ValuePosition: Int - { - case insideSlice - case outsideSlice - } - - private func initialize() - { - self.valueTextColor = NSUIColor.white - self.valueFont = NSUIFont.systemFont(ofSize: 13.0) - } - - public required init() - { - super.init() - initialize() - } - - public override init(entries: [ChartDataEntry], label: String) - { - super.init(entries: entries, label: label) - initialize() - } - - internal override func calcMinMax(entry e: ChartDataEntry) - { - calcMinMaxY(entry: e) - } - - // MARK: - Styling functions and accessors - - private var _sliceSpace = CGFloat(0.0) - - /// the space in pixels between the pie-slices - /// **default**: 0 - /// **maximum**: 20 - open var sliceSpace: CGFloat - { - get - { - return _sliceSpace - } - set - { - switch newValue { - case ..<0.0: _sliceSpace = 0.0 - case 20.0...: _sliceSpace = 20.0 - default: _sliceSpace = newValue - } - } - } - - /// When enabled, slice spacing will be 0.0 when the smallest value is going to be smaller than the slice spacing itself. - open var automaticallyDisableSliceSpacing: Bool = false - - /// indicates the selection distance of a pie slice - open var selectionShift = CGFloat(18.0) - - open var xValuePosition: ValuePosition = .insideSlice - open var yValuePosition: ValuePosition = .insideSlice - - /// When valuePosition is OutsideSlice, indicates line color - open var valueLineColor: NSUIColor? = NSUIColor.black - - /// When valuePosition is OutsideSlice and enabled, line will have the same color as the slice - open var useValueColorForLine: Bool = false - - /// When valuePosition is OutsideSlice, indicates line width - open var valueLineWidth: CGFloat = 1.0 - - /// When valuePosition is OutsideSlice, indicates offset as percentage out of the slice size - open var valueLinePart1OffsetPercentage: CGFloat = 0.75 - - /// When valuePosition is OutsideSlice, indicates length of first half of the line - open var valueLinePart1Length: CGFloat = 0.3 - - /// When valuePosition is OutsideSlice, indicates length of second half of the line - open var valueLinePart2Length: CGFloat = 0.4 - - /// When valuePosition is OutsideSlice, this allows variable line length - open var valueLineVariableLength: Bool = true - - /// the font for the slice-text labels - open var entryLabelFont: NSUIFont? = nil - - /// the color for the slice-text labels - open var entryLabelColor: NSUIColor? = nil - - /// the color for the highlighted sector - open var highlightColor: NSUIColor? = nil - - // MARK: - NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! PieChartDataSet - copy._sliceSpace = _sliceSpace - copy.automaticallyDisableSliceSpacing = automaticallyDisableSliceSpacing - copy.selectionShift = selectionShift - copy.xValuePosition = xValuePosition - copy.yValuePosition = yValuePosition - copy.valueLineColor = valueLineColor - copy.valueLineWidth = valueLineWidth - copy.valueLinePart1OffsetPercentage = valueLinePart1OffsetPercentage - copy.valueLinePart1Length = valueLinePart1Length - copy.valueLinePart2Length = valueLinePart2Length - copy.valueLineVariableLength = valueLineVariableLength - copy.entryLabelFont = entryLabelFont - copy.entryLabelColor = entryLabelColor - copy.highlightColor = highlightColor - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/RadarChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/RadarChartData.swift deleted file mode 100644 index 2f8a2db7a..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/RadarChartData.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// RadarChartData.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class RadarChartData: ChartData -{ - @objc open var highlightColor = NSUIColor(red: 255.0/255.0, green: 187.0/255.0, blue: 115.0/255.0, alpha: 1.0) - @objc open var highlightLineWidth = CGFloat(1.0) - @objc open var highlightLineDashPhase = CGFloat(0.0) - @objc open var highlightLineDashLengths: [CGFloat]? - - /// Sets labels that should be drawn around the RadarChart at the end of each web line. - @objc open var labels = [String]() - - /// Sets the labels that should be drawn around the RadarChart at the end of each web line. - open func setLabels(_ labels: String...) - { - self.labels = labels - } - - public required init() - { - super.init() - } - - public override init(dataSets: [ChartDataSetProtocol]) - { - super.init(dataSets: dataSets) - } - - public required init(arrayLiteral elements: ChartDataSetProtocol...) - { - super.init(dataSets: elements) - } - - @objc open override func entry(for highlight: Highlight) -> ChartDataEntry? - { - return self[highlight.dataSetIndex].entryForIndex(Int(highlight.x)) - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/RadarChartDataEntry.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/RadarChartDataEntry.swift deleted file mode 100644 index 576ad24c2..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/RadarChartDataEntry.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// RadarChartDataEntry.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class RadarChartDataEntry: ChartDataEntry -{ - public required init() - { - super.init() - } - - /// - Parameters: - /// - value: The value on the y-axis. - @objc public init(value: Double) - { - super.init(x: .nan, y: value) - } - - /// - Parameters: - /// - value: The value on the y-axis. - /// - data: Spot for additional data this Entry represents. - @objc public convenience init(value: Double, data: Any?) - { - self.init(value: value) - self.data = data - } - - // MARK: Data property accessors - - @objc open var value: Double - { - get { return y } - set { y = newValue } - } - - // MARK: NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! RadarChartDataEntry - - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/RadarChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/RadarChartDataSet.swift deleted file mode 100644 index 9b859d144..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/RadarChartDataSet.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// RadarChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class RadarChartDataSet: LineRadarChartDataSet, RadarChartDataSetProtocol -{ - private func initialize() - { - self.valueFont = NSUIFont.systemFont(ofSize: 13.0) - } - - public required init() - { - super.init() - initialize() - } - - public required override init(entries: [ChartDataEntry], label: String) - { - super.init(entries: entries, label: label) - initialize() - } - - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// flag indicating whether highlight circle should be drawn or not - /// **default**: false - open var drawHighlightCircleEnabled: Bool = false - - /// `true` if highlight circle should be drawn, `false` ifnot - open var isDrawHighlightCircleEnabled: Bool { return drawHighlightCircleEnabled } - - open var highlightCircleFillColor: NSUIColor? = NSUIColor.white - - /// The stroke color for highlight circle. - /// If `nil`, the color of the dataset is taken. - open var highlightCircleStrokeColor: NSUIColor? - - open var highlightCircleStrokeAlpha: CGFloat = 0.3 - - open var highlightCircleInnerRadius: CGFloat = 3.0 - - open var highlightCircleOuterRadius: CGFloat = 4.0 - - open var highlightCircleStrokeWidth: CGFloat = 2.0 -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ScatterChartData.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/ScatterChartData.swift deleted file mode 100644 index 49b1e89bb..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ScatterChartData.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ScatterChartData.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class ScatterChartData: BarLineScatterCandleBubbleChartData -{ - public required init() - { - super.init() - } - - public override init(dataSets: [ChartDataSetProtocol]) - { - super.init(dataSets: dataSets) - } - - public required init(arrayLiteral elements: ChartDataSetProtocol...) - { - super.init(dataSets: elements) - } - - /// - Returns: The maximum shape-size across all DataSets. - @objc open func getGreatestShapeSize() -> CGFloat - { - return (_dataSets as? [ScatterChartDataSetProtocol])? - .max { $0.scatterShapeSize < $1.scatterShapeSize }? - .scatterShapeSize ?? 0 - } -} diff --git a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ScatterChartDataSet.swift b/Pods/Charts/Source/Charts/Data/Implementations/Standard/ScatterChartDataSet.swift deleted file mode 100644 index 161deb2f9..000000000 --- a/Pods/Charts/Source/Charts/Data/Implementations/Standard/ScatterChartDataSet.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// ScatterChartDataSet.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class ScatterChartDataSet: LineScatterCandleRadarChartDataSet, ScatterChartDataSetProtocol -{ - - @objc(ScatterShape) - public enum Shape: Int - { - case square - case circle - case triangle - case cross - case x - case chevronUp - case chevronDown - } - - /// The size the scatter shape will have - open var scatterShapeSize = CGFloat(10.0) - - /// The radius of the hole in the shape (applies to Square, Circle and Triangle) - /// **default**: 0.0 - open var scatterShapeHoleRadius: CGFloat = 0.0 - - /// Color for the hole in the shape. Setting to `nil` will behave as transparent. - /// **default**: nil - open var scatterShapeHoleColor: NSUIColor? = nil - - /// Sets the ScatterShape this DataSet should be drawn with. - /// This will search for an available ShapeRenderer and set this renderer for the DataSet - @objc open func setScatterShape(_ shape: Shape) - { - self.shapeRenderer = ScatterChartDataSet.renderer(forShape: shape) - } - - /// The IShapeRenderer responsible for rendering this DataSet. - /// This can also be used to set a custom IShapeRenderer aside from the default ones. - /// **default**: `SquareShapeRenderer` - open var shapeRenderer: ShapeRenderer? = SquareShapeRenderer() - - @objc open class func renderer(forShape shape: Shape) -> ShapeRenderer - { - switch shape - { - case .square: return SquareShapeRenderer() - case .circle: return CircleShapeRenderer() - case .triangle: return TriangleShapeRenderer() - case .cross: return CrossShapeRenderer() - case .x: return XShapeRenderer() - case .chevronUp: return ChevronUpShapeRenderer() - case .chevronDown: return ChevronDownShapeRenderer() - } - } - - // MARK: NSCopying - - open override func copy(with zone: NSZone? = nil) -> Any - { - let copy = super.copy(with: zone) as! ScatterChartDataSet - copy.scatterShapeSize = scatterShapeSize - copy.scatterShapeHoleRadius = scatterShapeHoleRadius - copy.scatterShapeHoleColor = scatterShapeHoleColor - copy.shapeRenderer = shapeRenderer - return copy - } -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift deleted file mode 100644 index 3ea0903cc..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// BarChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol BarChartDataSetProtocol: BarLineScatterCandleBubbleChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// `true` if this DataSet is stacked (stacksize > 1) or not. - var isStacked: Bool { get } - - /// The maximum number of bars that can be stacked upon another in this DataSet. - var stackSize: Int { get } - - /// the color used for drawing the bar-shadows. The bar shadows is a surface behind the bar that indicates the maximum value - var barShadowColor: NSUIColor { get set } - - /// the width used for drawing borders around the bars. If borderWidth == 0, no border will be drawn. - var barBorderWidth : CGFloat { get set } - - /// the color drawing borders around the bars. - var barBorderColor: NSUIColor { get set } - - /// the alpha value (transparency) that is used for drawing the highlight indicator bar. min = 0.0 (fully transparent), max = 1.0 (fully opaque) - var highlightAlpha: CGFloat { get set } - - /// array of labels used to describe the different values of the stacked bars - var stackLabels: [String] { get set } -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/BarLineScatterCandleBubbleChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/BarLineScatterCandleBubbleChartDataSetProtocol.swift deleted file mode 100644 index cbc90147f..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/BarLineScatterCandleBubbleChartDataSetProtocol.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// BarLineScatterCandleBubbleChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol BarLineScatterCandleBubbleChartDataSetProtocol: ChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - var highlightColor: NSUIColor { get set } - var highlightLineWidth: CGFloat { get set } - var highlightLineDashPhase: CGFloat { get set } - var highlightLineDashLengths: [CGFloat]? { get set } -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/BubbleChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/BubbleChartDataSetProtocol.swift deleted file mode 100644 index 5cc945795..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/BubbleChartDataSetProtocol.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// BubbleChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol BubbleChartDataSetProtocol: BarLineScatterCandleBubbleChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - var maxSize: CGFloat { get } - var isNormalizeSizeEnabled: Bool { get } - - // MARK: - Styling functions and accessors - - /// Sets/gets the width of the circle that surrounds the bubble when highlighted - var highlightCircleWidth: CGFloat { get set } -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/CandleChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/CandleChartDataSetProtocol.swift deleted file mode 100644 index d47b5a8c9..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/CandleChartDataSetProtocol.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// CandleChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol CandleChartDataSetProtocol: LineScatterCandleRadarChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// the space that is left out on the left and right side of each candle, - /// **default**: 0.1 (10%), max 0.45, min 0.0 - var barSpace: CGFloat { get set } - - /// should the candle bars show? - /// when false, only "ticks" will show - /// - /// **default**: true - var showCandleBar: Bool { get set } - - /// the width of the candle-shadow-line in pixels. - /// - /// **default**: 3.0 - var shadowWidth: CGFloat { get set } - - /// the color of the shadow line - var shadowColor: NSUIColor? { get set } - - /// use candle color for the shadow - var shadowColorSameAsCandle: Bool { get set } - - /// Is the shadow color same as the candle color? - var isShadowColorSameAsCandle: Bool { get } - - /// color for open == close - var neutralColor: NSUIColor? { get set } - - /// color for open > close - var increasingColor: NSUIColor? { get set } - - /// color for open < close - var decreasingColor: NSUIColor? { get set } - - /// Are increasing values drawn as filled? - var increasingFilled: Bool { get set } - - /// Are increasing values drawn as filled? - var isIncreasingFilled: Bool { get } - - /// Are decreasing values drawn as filled? - var decreasingFilled: Bool { get set } - - /// Are decreasing values drawn as filled? - var isDecreasingFilled: Bool { get } -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/ChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/ChartDataSetProtocol.swift deleted file mode 100644 index 9cf80d336..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/ChartDataSetProtocol.swift +++ /dev/null @@ -1,270 +0,0 @@ -// -// ChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol ChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - /// Use this method to tell the data set that the underlying data has changed - func notifyDataSetChanged() - - /// Calculates the minimum and maximum x and y values (xMin, xMax, yMin, yMax). - func calcMinMax() - - /// Calculates the min and max y-values from the Entry closest to the given fromX to the Entry closest to the given toX value. - /// This is only needed for the autoScaleMinMax feature. - func calcMinMaxY(fromX: Double, toX: Double) - - /// The minimum y-value this DataSet holds - var yMin: Double { get } - - /// The maximum y-value this DataSet holds - var yMax: Double { get } - - /// The minimum x-value this DataSet holds - var xMin: Double { get } - - /// The maximum x-value this DataSet holds - var xMax: Double { get } - - /// The number of y-values this DataSet represents - var entryCount: Int { get } - - /// - Throws: out of bounds - /// if `i` is out of bounds, it may throw an out-of-bounds exception - /// - Returns: The entry object found at the given index (not x-value!) - func entryForIndex(_ i: Int) -> ChartDataEntry? - - /// - Parameters: - /// - xValue: the x-value - /// - closestToY: If there are multiple y-values for the specified x-value, - /// - rounding: determine whether to round up/down/closest if there is no Entry matching the provided x-value - /// - Returns: The first Entry object found at the given x-value with binary search. - /// If the no Entry at the specified x-value is found, this method returns the Entry at the closest x-value according to the rounding. - /// nil if no Entry object at that x-value. - func entryForXValue( - _ xValue: Double, - closestToY yValue: Double, - rounding: ChartDataSetRounding) -> ChartDataEntry? - - /// - Parameters: - /// - xValue: the x-value - /// - closestToY: If there are multiple y-values for the specified x-value, - /// - Returns: The first Entry object found at the given x-value with binary search. - /// If the no Entry at the specified x-value is found, this method returns the Entry at the closest x-value. - /// nil if no Entry object at that x-value. - func entryForXValue( - _ xValue: Double, - closestToY yValue: Double) -> ChartDataEntry? - - /// - Returns: All Entry objects found at the given x-value with binary search. - /// An empty array if no Entry object at that x-value. - func entriesForXValue(_ xValue: Double) -> [ChartDataEntry] - - /// - Parameters: - /// - xValue: x-value of the entry to search for - /// - closestToY: If there are multiple y-values for the specified x-value, - /// - rounding: Rounding method if exact value was not found - /// - Returns: The array-index of the specified entry. - /// If the no Entry at the specified x-value is found, this method returns the index of the Entry at the closest x-value according to the rounding. - func entryIndex( - x xValue: Double, - closestToY yValue: Double, - rounding: ChartDataSetRounding) -> Int - - /// - Parameters: - /// - e: the entry to search for - /// - Returns: The array-index of the specified entry - func entryIndex(entry e: ChartDataEntry) -> Int - - /// Adds an Entry to the DataSet dynamically. - /// - /// *optional feature, can return `false` ifnot implemented* - /// - /// Entries are added to the end of the list. - /// - /// - Parameters: - /// - e: the entry to add - /// - Returns: `true` if the entry was added successfully, `false` ifthis feature is not supported - func addEntry(_ e: ChartDataEntry) -> Bool - - /// Adds an Entry to the DataSet dynamically. - /// Entries are added to their appropriate index in the values array respective to their x-position. - /// This will also recalculate the current minimum and maximum values of the DataSet and the value-sum. - /// - /// *optional feature, can return `false` ifnot implemented* - /// - /// Entries are added to the end of the list. - /// - /// - Parameters: - /// - e: the entry to add - /// - Returns: `true` if the entry was added successfully, `false` ifthis feature is not supported - func addEntryOrdered(_ e: ChartDataEntry) -> Bool - - /// Removes an Entry from the DataSet dynamically. - /// - /// *optional feature, can return `false` ifnot implemented* - /// - /// - Parameters: - /// - entry: the entry to remove - /// - Returns: `true` if the entry was removed successfully, `false` ifthe entry does not exist or if this feature is not supported - func removeEntry(_ entry: ChartDataEntry) -> Bool - - /// Removes the Entry object at the given index in the values array from the DataSet. - /// - /// *optional feature, can return `false` ifnot implemented* - /// - /// - Parameters: - /// - index: the index of the entry to remove - /// - Returns: `true` if the entry was removed successfully, `false` ifthe entry does not exist or if this feature is not supported - func removeEntry(index: Int) -> Bool - - /// Removes the Entry object closest to the given x-value from the DataSet. - /// - /// *optional feature, can return `false` ifnot implemented* - /// - /// - Parameters: - /// - x: the x-value to remove - /// - Returns: `true` if the entry was removed successfully, `false` ifthe entry does not exist or if this feature is not supported - func removeEntry(x: Double) -> Bool - - /// Removes the first Entry (at index 0) of this DataSet from the entries array. - /// - /// *optional feature, can return `false` ifnot implemented* - /// - /// - Returns: `true` if the entry was removed successfully, `false` ifthe entry does not exist or if this feature is not supported - func removeFirst() -> Bool - - /// Removes the last Entry (at index 0) of this DataSet from the entries array. - /// - /// *optional feature, can return `false` ifnot implemented* - /// - /// - Returns: `true` if the entry was removed successfully, `false` ifthe entry does not exist or if this feature is not supported - func removeLast() -> Bool - - /// Checks if this DataSet contains the specified Entry. - /// - /// - Returns: `true` if contains the entry, `false` ifnot. - func contains(_ e: ChartDataEntry) -> Bool - - /// Removes all values from this DataSet and does all necessary recalculations. - /// - /// *optional feature, could throw if not implemented* - func clear() - - // MARK: - Styling functions and accessors - - /// The label string that describes the DataSet. - var label: String? { get } - - /// The axis this DataSet should be plotted against. - var axisDependency: YAxis.AxisDependency { get } - - /// List representing all colors that are used for drawing the actual values for this DataSet - var valueColors: [NSUIColor] { get } - - /// All the colors that are used for this DataSet. - /// Colors are reused as soon as the number of Entries the DataSet represents is higher than the size of the colors array. - var colors: [NSUIColor] { get } - - /// - Returns: The color at the given index of the DataSet's color array. - /// This prevents out-of-bounds by performing a modulus on the color index, so colours will repeat themselves. - func color(atIndex: Int) -> NSUIColor - - func resetColors() - - func addColor(_ color: NSUIColor) - - func setColor(_ color: NSUIColor) - - /// if true, value highlighting is enabled - var highlightEnabled: Bool { get set } - - /// `true` if value highlighting is enabled for this dataset - var isHighlightEnabled: Bool { get } - - /// Custom formatter that is used instead of the auto-formatter if set - var valueFormatter: ValueFormatter { get set } - - /// Sets/get a single color for value text. - /// Setting the color clears the colors array and adds a single color. - /// Getting will return the first color in the array. - var valueTextColor: NSUIColor { get set } - - /// - Returns: The color at the specified index that is used for drawing the values inside the chart. Uses modulus internally. - func valueTextColorAt(_ index: Int) -> NSUIColor - - /// the font for the value-text labels - var valueFont: NSUIFont { get set } - - /// The rotation angle (in degrees) for value-text labels - var valueLabelAngle: CGFloat { get set } - - /// The form to draw for this dataset in the legend. - /// - /// Return `.Default` to use the default legend form. - var form: Legend.Form { get } - - /// The form size to draw for this dataset in the legend. - /// - /// Return `NaN` to use the default legend form size. - var formSize: CGFloat { get } - - /// The line width for drawing the form of this dataset in the legend - /// - /// Return `NaN` to use the default legend form line width. - var formLineWidth: CGFloat { get } - - /// Line dash configuration for legend shapes that consist of lines. - /// - /// This is how much (in pixels) into the dash pattern are we starting from. - var formLineDashPhase: CGFloat { get } - - /// Line dash configuration for legend shapes that consist of lines. - /// - /// This is the actual dash pattern. - /// I.e. [2, 3] will paint [-- -- ] - /// [1, 3, 4, 2] will paint [- ---- - ---- ] - var formLineDashLengths: [CGFloat]? { get } - - /// Set this to true to draw y-values on the chart. - /// - /// - Note: For bar and line charts: if `maxVisibleCount` is reached, no values will be drawn even if this is enabled. - var drawValuesEnabled: Bool { get set } - - /// `true` if y-value drawing is enabled, `false` ifnot - var isDrawValuesEnabled: Bool { get } - - /// Set this to true to draw y-icons on the chart - /// - /// - Note: For bar and line charts: if `maxVisibleCount` is reached, no icons will be drawn even if this is enabled. - var drawIconsEnabled: Bool { get set } - - /// Returns true if y-icon drawing is enabled, false if not - var isDrawIconsEnabled: Bool { get } - - /// Offset of icons drawn on the chart. - /// - /// For all charts except Pie and Radar it will be ordinary (x offset, y offset). - /// - /// For Pie and Radar chart it will be (y offset, distance from center offset); so if you want icon to be rendered under value, you should increase X component of CGPoint, and if you want icon to be rendered closet to center, you should decrease height component of CGPoint. - var iconsOffset: CGPoint { get set } - - /// Set the visibility of this DataSet. If not visible, the DataSet will not be drawn to the chart upon refreshing it. - var visible: Bool { get set } - - /// `true` if this DataSet is visible inside the chart, or `false` ifit is currently hidden. - var isVisible: Bool { get } -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/LineChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/LineChartDataSetProtocol.swift deleted file mode 100644 index 7cfc3ab2b..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/LineChartDataSetProtocol.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// LineChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -@objc -public protocol LineChartDataSetProtocol: LineRadarChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// The drawing mode for this line dataset - /// - /// **default**: Linear - var mode: LineChartDataSet.Mode { get set } - - /// Intensity for cubic lines (min = 0.05, max = 1) - /// - /// **default**: 0.2 - var cubicIntensity: CGFloat { get set } - - /// If true, gradient lines are drawn instead of solid - var isDrawLineWithGradientEnabled: Bool { get set } - - /// The points where gradient should change color - var gradientPositions: [CGFloat]? { get set } - - /// The radius of the drawn circles. - var circleRadius: CGFloat { get set } - - /// The hole radius of the drawn circles. - var circleHoleRadius: CGFloat { get set } - - var circleColors: [NSUIColor] { get set } - - /// - Returns: The color at the given index of the DataSet's circle-color array. - /// Performs a IndexOutOfBounds check by modulus. - func getCircleColor(atIndex: Int) -> NSUIColor? - - /// Sets the one and ONLY color that should be used for this DataSet. - /// Internally, this recreates the colors array and adds the specified color. - func setCircleColor(_ color: NSUIColor) - - /// Resets the circle-colors array and creates a new one - func resetCircleColors(_ index: Int) - - /// If true, drawing circles is enabled - var drawCirclesEnabled: Bool { get set } - - /// `true` if drawing circles for this DataSet is enabled, `false` ifnot - var isDrawCirclesEnabled: Bool { get } - - /// The color of the inner circle (the circle-hole). - var circleHoleColor: NSUIColor? { get set } - - /// `true` if drawing circles for this DataSet is enabled, `false` ifnot - var drawCircleHoleEnabled: Bool { get set } - - /// `true` if drawing the circle-holes is enabled, `false` ifnot. - var isDrawCircleHoleEnabled: Bool { get } - - /// This is how much (in pixels) into the dash pattern are we starting from. - var lineDashPhase: CGFloat { get } - - /// This is the actual dash pattern. - /// I.e. [2, 3] will paint [-- -- ] - /// [1, 3, 4, 2] will paint [- ---- - ---- ] - var lineDashLengths: [CGFloat]? { get set } - - /// Line cap type, default is CGLineCap.Butt - var lineCapType: CGLineCap { get set } - - /// Sets a custom FillFormatterProtocol to the chart that handles the position of the filled-line for each DataSet. Set this to null to use the default logic. - var fillFormatter: FillFormatter? { get set } -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/LineRadarChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/LineRadarChartDataSetProtocol.swift deleted file mode 100644 index aadcb28b9..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/LineRadarChartDataSetProtocol.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// LineRadarChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol LineRadarChartDataSetProtocol: LineScatterCandleRadarChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// The color that is used for filling the line surface area. - var fillColor: NSUIColor { get set } - - /// - Returns: The object that is used for filling the area below the line. - /// **default**: nil - var fill: Fill? { get set } - - /// The alpha value that is used for filling the line surface. - /// **default**: 0.33 - var fillAlpha: CGFloat { get set } - - /// line width of the chart (min = 0.0, max = 10) - /// - /// **default**: 1 - var lineWidth: CGFloat { get set } - - /// Set to `true` if the DataSet should be drawn filled (surface), and not just as a line. - /// Disabling this will give great performance boost. - /// Please note that this method uses the path clipping for drawing the filled area (with images, gradients and layers). - var drawFilledEnabled: Bool { get set } - - /// `true` if filled drawing is enabled, `false` if not - var isDrawFilledEnabled: Bool { get } -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/LineScatterCandleRadarChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/LineScatterCandleRadarChartDataSetProtocol.swift deleted file mode 100644 index 25325e036..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/LineScatterCandleRadarChartDataSetProtocol.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// LineScatterCandleRadarChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -@objc -public protocol LineScatterCandleRadarChartDataSetProtocol: BarLineScatterCandleBubbleChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// Enables / disables the horizontal highlight-indicator. If disabled, the indicator is not drawn. - var drawHorizontalHighlightIndicatorEnabled: Bool { get set } - - /// Enables / disables the vertical highlight-indicator. If disabled, the indicator is not drawn. - var drawVerticalHighlightIndicatorEnabled: Bool { get set } - - /// `true` if horizontal highlight indicator lines are enabled (drawn) - var isHorizontalHighlightIndicatorEnabled: Bool { get } - - /// `true` if vertical highlight indicator lines are enabled (drawn) - var isVerticalHighlightIndicatorEnabled: Bool { get } - - /// Enables / disables both vertical and horizontal highlight-indicators. - /// :param: enabled - func setDrawHighlightIndicators(_ enabled: Bool) -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/PieChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/PieChartDataSetProtocol.swift deleted file mode 100644 index 1b8ea8785..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/PieChartDataSetProtocol.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// PieChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol PieChartDataSetProtocol: ChartDataSetProtocol -{ - // MARK: - Styling functions and accessors - - /// the space in pixels between the pie-slices - /// **default**: 0 - /// **maximum**: 20 - var sliceSpace: CGFloat { get set } - - /// When enabled, slice spacing will be 0.0 when the smallest value is going to be smaller than the slice spacing itself. - var automaticallyDisableSliceSpacing: Bool { get set } - - /// indicates the selection distance of a pie slice - var selectionShift: CGFloat { get set } - - var xValuePosition: PieChartDataSet.ValuePosition { get set } - var yValuePosition: PieChartDataSet.ValuePosition { get set } - - /// When valuePosition is OutsideSlice, indicates line color - var valueLineColor: NSUIColor? { get set } - - /// When valuePosition is OutsideSlice and enabled, line will have the same color as the slice - var useValueColorForLine: Bool { get set } - - /// When valuePosition is OutsideSlice, indicates line width - var valueLineWidth: CGFloat { get set } - - /// When valuePosition is OutsideSlice, indicates offset as percentage out of the slice size - var valueLinePart1OffsetPercentage: CGFloat { get set } - - /// When valuePosition is OutsideSlice, indicates length of first half of the line - var valueLinePart1Length: CGFloat { get set } - - /// When valuePosition is OutsideSlice, indicates length of second half of the line - var valueLinePart2Length: CGFloat { get set } - - /// When valuePosition is OutsideSlice, this allows variable line length - var valueLineVariableLength: Bool { get set } - - /// the font for the slice-text labels - var entryLabelFont: NSUIFont? { get set } - - /// the color for the slice-text labels - var entryLabelColor: NSUIColor? { get set } - - /// get/sets the color for the highlighted sector - var highlightColor: NSUIColor? { get set } - -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/RadarChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/RadarChartDataSetProtocol.swift deleted file mode 100644 index 98c12d77c..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/RadarChartDataSetProtocol.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// RadarChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol RadarChartDataSetProtocol: LineRadarChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// flag indicating whether highlight circle should be drawn or not - var drawHighlightCircleEnabled: Bool { get set } - - var isDrawHighlightCircleEnabled: Bool { get } - - var highlightCircleFillColor: NSUIColor? { get set } - - /// The stroke color for highlight circle. - /// If `nil`, the color of the dataset is taken. - var highlightCircleStrokeColor: NSUIColor? { get set } - - var highlightCircleStrokeAlpha: CGFloat { get set } - - var highlightCircleInnerRadius: CGFloat { get set } - - var highlightCircleOuterRadius: CGFloat { get set } - - var highlightCircleStrokeWidth: CGFloat { get set } -} diff --git a/Pods/Charts/Source/Charts/Data/Interfaces/ScatterChartDataSetProtocol.swift b/Pods/Charts/Source/Charts/Data/Interfaces/ScatterChartDataSetProtocol.swift deleted file mode 100644 index 4d160fcd0..000000000 --- a/Pods/Charts/Source/Charts/Data/Interfaces/ScatterChartDataSetProtocol.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ScatterChartDataSetProtocol.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol ScatterChartDataSetProtocol: LineScatterCandleRadarChartDataSetProtocol -{ - // MARK: - Data functions and accessors - - // MARK: - Styling functions and accessors - - /// - Returns: The size the scatter shape will have - var scatterShapeSize: CGFloat { get } - - /// - Returns: The radius of the hole in the shape (applies to Square, Circle and Triangle) - /// Set this to <= 0 to remove holes. - /// **default**: 0.0 - var scatterShapeHoleRadius: CGFloat { get } - - /// - Returns: Color for the hole in the shape. Setting to `nil` will behave as transparent. - /// **default**: nil - var scatterShapeHoleColor: NSUIColor? { get } - - /// - Returns: The ShapeRenderer responsible for rendering this DataSet. - var shapeRenderer: ShapeRenderer? { get } -} diff --git a/Pods/Charts/Source/Charts/Filters/DataApproximator+N.swift b/Pods/Charts/Source/Charts/Filters/DataApproximator+N.swift deleted file mode 100644 index e291d84a7..000000000 --- a/Pods/Charts/Source/Charts/Filters/DataApproximator+N.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// DataApproximator+N.swift -// Charts -// -// Created by M Ivaniushchenko on 9/6/17. -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -extension CGPoint { - fileprivate func distanceToLine(from linePoint1: CGPoint, to linePoint2: CGPoint) -> CGFloat { - let dx = linePoint2.x - linePoint1.x - let dy = linePoint2.y - linePoint1.y - - let dividend = abs(dy * self.x - dx * self.y - linePoint1.x * linePoint2.y + linePoint2.x * linePoint1.y) - let divisor = sqrt(dx * dx + dy * dy) - - return dividend / divisor - } -} - -private struct LineAlt { - let start: Int - let end: Int - - var distance: CGFloat = 0 - var index: Int = 0 - - init(start: Int, end: Int, points: [CGPoint]) { - self.start = start - self.end = end - - let startPoint = points[start] - let endPoint = points[end] - - guard (end > start + 1) else { - return - } - - for i in start + 1 ..< end { - let currentPoint = points[i] - - let distance = currentPoint.distanceToLine(from: startPoint, to: endPoint) - - if distance > self.distance { - self.index = i - self.distance = distance - } - } - } -} - -extension LineAlt: Comparable { - static func ==(lhs: LineAlt, rhs: LineAlt) -> Bool { - return (lhs.start == rhs.start) && (lhs.end == rhs.end) && (lhs.index == rhs.index) - } - - static func <(lhs: LineAlt, rhs: LineAlt) -> Bool { - return lhs.distance < rhs.distance - } -} - - -extension DataApproximator { - /// uses the douglas peuker algorithm to reduce the given arraylist of entries to given number of points - /// More algorithm details here - http://psimpl.sourceforge.net/douglas-peucker.html - @objc open class func reduceWithDouglasPeukerN(_ points: [CGPoint], resultCount: Int) -> [CGPoint] - { - // if a shape has 2 or less points it cannot be reduced - if resultCount <= 2 || resultCount >= points.count - { - return points - } - var keep = [Bool](repeating: false, count: points.count) - - // first and last always stay - keep[points.startIndex] = true - keep[points.endIndex - 1] = true - var currentStoredPoints = 2 - - var queue = [LineAlt]() - let line = LineAlt(start: points.startIndex, end: points.endIndex - 1, points: points) - queue.append(line) - - repeat { - let line = queue.popLast()! - - // store the key - keep[line.index] = true - - // check point count tolerance - currentStoredPoints += 1 - - if (currentStoredPoints == resultCount) { - break; - } - - // split the polyline at the key and recurse - let left = LineAlt(start: line.start, end: line.index, points: points) - if (left.index > 0) { - self.insertLine(left, into: &queue) - } - - let right = LineAlt(start: line.index, end: line.end, points: points) - if (right.index > 0) { - self.insertLine(right, into: &queue) - } - - } while !queue.isEmpty - - // create a new array with series, only take the kept ones - let reducedEntries = points.enumerated().compactMap { (index: Int, point: CGPoint) -> CGPoint? in - return keep[index] ? point : nil - } - - return reducedEntries - } - - // Keeps array sorted - private static func insertLine(_ line: LineAlt, into array: inout [LineAlt]) { - let insertionIndex = self.insertionIndex(for: line, into: &array) - array.insert(line, at: insertionIndex) - } - - private static func insertionIndex(for line: LineAlt, into array: inout [LineAlt]) -> Int { - var indices = array.indices - - while !indices.isEmpty { - let midIndex = indices.lowerBound.advanced(by: indices.count / 2) - let midLine = array[midIndex] - - if midLine == line { - return midIndex - } - else if (line < midLine) { - // perform search in left half - indices = indices.lowerBound.. [CGPoint] - { - // if a shape has 2 or less points it cannot be reduced - if tolerance <= 0 || points.count < 3 - { - return points - } - - var keep = [Bool](repeating: false, count: points.count) - - // first and last always stay - keep[points.startIndex] = true - keep[points.endIndex - 1] = true - - // first and last entry are entry point to recursion - reduceWithDouglasPeuker(points: points, - tolerance: tolerance, - start: points.startIndex, - end: points.endIndex - 1, - keep: &keep) - - // create a new array with series, only take the kept ones - return zip(keep, points).compactMap { $0 ? nil : $1 } - } - - /// apply the Douglas-Peucker-Reduction to an array of `CGPoint`s with a given tolerance - /// - /// - Parameters: - /// - points: - /// - tolerance: - /// - start: - /// - end: - open class func reduceWithDouglasPeuker( - points: [CGPoint], - tolerance: CGFloat, - start: Int, - end: Int, - keep: inout [Bool]) - { - if end <= start + 1 - { - // recursion finished - return - } - - var greatestIndex = Int(0) - var greatestDistance = CGFloat(0.0) - - let line = Line(pt1: points[start], pt2: points[end]) - - for i in start + 1 ..< end - { - let distance = line.distance(toPoint: points[i]) - - if distance > greatestDistance - { - greatestDistance = distance - greatestIndex = i - } - } - - if greatestDistance > tolerance - { - // keep max dist point - keep[greatestIndex] = true - - // recursive call - reduceWithDouglasPeuker(points: points, tolerance: tolerance, start: start, end: greatestIndex, keep: &keep) - reduceWithDouglasPeuker(points: points, tolerance: tolerance, start: greatestIndex, end: end, keep: &keep) - } // else don't keep the point... - } - - private class Line - { - var sxey: CGFloat - var exsy: CGFloat - - var dx: CGFloat - var dy: CGFloat - - var length: CGFloat - - init(pt1: CGPoint, pt2: CGPoint) - { - dx = pt1.x - pt2.x - dy = pt1.y - pt2.y - sxey = pt1.x * pt2.y - exsy = pt2.x * pt1.y - length = sqrt(dx * dx + dy * dy) - } - - func distance(toPoint pt: CGPoint) -> CGFloat - { - return abs(dy * pt.x - dx * pt.y + sxey - exsy) / length - } - } -} diff --git a/Pods/Charts/Source/Charts/Formatters/AxisValueFormatter.swift b/Pods/Charts/Source/Charts/Formatters/AxisValueFormatter.swift deleted file mode 100644 index 5a3061384..000000000 --- a/Pods/Charts/Source/Charts/Formatters/AxisValueFormatter.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// AxisValueFormatter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -/// An interface for providing custom axis Strings. -@objc(ChartAxisValueFormatter) -public protocol AxisValueFormatter: AnyObject -{ - - /// Called when a value from an axis is formatted before being drawn. - /// - /// For performance reasons, avoid excessive calculations and memory allocations inside this method. - /// - /// - Parameters: - /// - value: the value that is currently being drawn - /// - axis: the axis that the value belongs to - /// - Returns: The customized label that is drawn on the x-axis. - func stringForValue(_ value: Double, - axis: AxisBase?) -> String - -} diff --git a/Pods/Charts/Source/Charts/Formatters/DefaultAxisValueFormatter.swift b/Pods/Charts/Source/Charts/Formatters/DefaultAxisValueFormatter.swift deleted file mode 100644 index b698c5a31..000000000 --- a/Pods/Charts/Source/Charts/Formatters/DefaultAxisValueFormatter.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// DefaultAxisValueFormatter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -@objc(ChartDefaultAxisValueFormatter) -open class DefaultAxisValueFormatter: NSObject, AxisValueFormatter -{ - public typealias Block = ( - _ value: Double, - _ axis: AxisBase?) -> String - - @objc open var block: Block? - - @objc open var hasAutoDecimals: Bool = false - - private var _formatter: NumberFormatter? - @objc open var formatter: NumberFormatter? - { - get { return _formatter } - set - { - hasAutoDecimals = false - _formatter = newValue - } - } - - // TODO: Documentation. Especially the nil case - private var _decimals: Int? - open var decimals: Int? - { - get { return _decimals } - set - { - _decimals = newValue - - if let digits = newValue - { - self.formatter?.minimumFractionDigits = digits - self.formatter?.maximumFractionDigits = digits - self.formatter?.usesGroupingSeparator = true - } - } - } - - public override init() - { - super.init() - - self.formatter = NumberFormatter() - hasAutoDecimals = true - } - - @objc public init(formatter: NumberFormatter) - { - super.init() - - self.formatter = formatter - } - - @objc public init(decimals: Int) - { - super.init() - - self.formatter = NumberFormatter() - self.formatter?.usesGroupingSeparator = true - self.decimals = decimals - hasAutoDecimals = true - } - - @objc public init(block: @escaping Block) - { - super.init() - - self.block = block - } - - @objc public static func with(block: @escaping Block) -> DefaultAxisValueFormatter? - { - return DefaultAxisValueFormatter(block: block) - } - - open func stringForValue(_ value: Double, - axis: AxisBase?) -> String - { - if let block = block { - return block(value, axis) - } else { - return formatter?.string(from: NSNumber(floatLiteral: value)) ?? "" - } - } -} diff --git a/Pods/Charts/Source/Charts/Formatters/DefaultFillFormatter.swift b/Pods/Charts/Source/Charts/Formatters/DefaultFillFormatter.swift deleted file mode 100644 index 07a6c3444..000000000 --- a/Pods/Charts/Source/Charts/Formatters/DefaultFillFormatter.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// DefaultFillFormatter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// Default formatter that calculates the position of the filled line. -@objc(ChartDefaultFillFormatter) -open class DefaultFillFormatter: NSObject, FillFormatter -{ - public typealias Block = ( - _ dataSet: LineChartDataSetProtocol, - _ dataProvider: LineChartDataProvider) -> CGFloat - - @objc open var block: Block? - - public override init() { } - - @objc public init(block: @escaping Block) - { - self.block = block - } - - @objc public static func with(block: @escaping Block) -> DefaultFillFormatter? - { - return DefaultFillFormatter(block: block) - } - - open func getFillLinePosition( - dataSet: LineChartDataSetProtocol, - dataProvider: LineChartDataProvider) -> CGFloat - { - guard block == nil else { return block!(dataSet, dataProvider) } - var fillMin: CGFloat = 0.0 - - if dataSet.yMax > 0.0 && dataSet.yMin < 0.0 - { - fillMin = 0.0 - } - else if let data = dataProvider.data - { - let max = data.yMax > 0.0 ? 0.0 : dataProvider.chartYMax - let min = data.yMin < 0.0 ? 0.0 : dataProvider.chartYMin - - fillMin = CGFloat(dataSet.yMin >= 0.0 ? min : max) - } - - return fillMin - } -} diff --git a/Pods/Charts/Source/Charts/Formatters/DefaultValueFormatter.swift b/Pods/Charts/Source/Charts/Formatters/DefaultValueFormatter.swift deleted file mode 100644 index ee86dc79a..000000000 --- a/Pods/Charts/Source/Charts/Formatters/DefaultValueFormatter.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// DefaultValueFormatter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -/// The default value formatter used for all chart components that needs a default -@objc(ChartDefaultValueFormatter) -open class DefaultValueFormatter: NSObject, ValueFormatter -{ - public typealias Block = ( - _ value: Double, - _ entry: ChartDataEntry, - _ dataSetIndex: Int, - _ viewPortHandler: ViewPortHandler?) -> String - - @objc open var block: Block? - - @objc open var hasAutoDecimals: Bool - - @objc open var formatter: NumberFormatter? - { - willSet - { - hasAutoDecimals = false - } - } - - open var decimals: Int? - { - didSet - { - setupDecimals(decimals: decimals) - } - } - - private func setupDecimals(decimals: Int?) - { - if let digits = decimals - { - formatter?.minimumFractionDigits = digits - formatter?.maximumFractionDigits = digits - formatter?.usesGroupingSeparator = true - } - } - - public override init() - { - formatter = NumberFormatter() - formatter?.usesGroupingSeparator = true - decimals = 1 - hasAutoDecimals = true - - super.init() - setupDecimals(decimals: decimals) - } - - @objc public init(formatter: NumberFormatter) - { - self.formatter = formatter - hasAutoDecimals = false - - super.init() - } - - @objc public init(decimals: Int) - { - formatter = NumberFormatter() - formatter?.usesGroupingSeparator = true - self.decimals = decimals - hasAutoDecimals = true - - super.init() - setupDecimals(decimals: decimals) - } - - @objc public init(block: @escaping Block) - { - self.block = block - hasAutoDecimals = false - - super.init() - } - - /// This function is deprecated - Use `init(block:)` instead. - // DEC 11, 2017 - @available(*, deprecated, message: "Use `init(block:)` instead.") - @objc public static func with(block: @escaping Block) -> DefaultValueFormatter - { - return DefaultValueFormatter(block: block) - } - - open func stringForValue(_ value: Double, - entry: ChartDataEntry, - dataSetIndex: Int, - viewPortHandler: ViewPortHandler?) -> String - { - if let block = block { - return block(value, entry, dataSetIndex, viewPortHandler) - } else { - return formatter?.string(from: NSNumber(floatLiteral: value)) ?? "" - } - } -} diff --git a/Pods/Charts/Source/Charts/Formatters/FillFormatter.swift b/Pods/Charts/Source/Charts/Formatters/FillFormatter.swift deleted file mode 100644 index bd0598110..000000000 --- a/Pods/Charts/Source/Charts/Formatters/FillFormatter.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// FillFormatter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// Protocol for providing a custom logic to where the filling line of a LineDataSet should end. This of course only works if setFillEnabled(...) is set to true. -@objc(ChartFillFormatter) -public protocol FillFormatter -{ - /// - Returns: The vertical (y-axis) position where the filled-line of the LineDataSet should end. - func getFillLinePosition(dataSet: LineChartDataSetProtocol, dataProvider: LineChartDataProvider) -> CGFloat -} diff --git a/Pods/Charts/Source/Charts/Formatters/IndexAxisValueFormatter.swift b/Pods/Charts/Source/Charts/Formatters/IndexAxisValueFormatter.swift deleted file mode 100644 index 6f339eb36..000000000 --- a/Pods/Charts/Source/Charts/Formatters/IndexAxisValueFormatter.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// IndexAxisValueFormatter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -/// This formatter is used for passing an array of x-axis labels, on whole x steps. -@objc(ChartIndexAxisValueFormatter) -open class IndexAxisValueFormatter: NSObject, AxisValueFormatter -{ - @objc public var values: [String] = [String]() - - public override init() - { - super.init() - - } - - @objc public init(values: [String]) - { - super.init() - - self.values = values - } - - @objc public static func with(values: [String]) -> IndexAxisValueFormatter? - { - return IndexAxisValueFormatter(values: values) - } - - open func stringForValue(_ value: Double, - axis: AxisBase?) -> String - { - let index = Int(value.rounded()) - guard values.indices.contains(index), index == Int(value) else { return "" } - return values[index] - } -} diff --git a/Pods/Charts/Source/Charts/Formatters/ValueFormatter.swift b/Pods/Charts/Source/Charts/Formatters/ValueFormatter.swift deleted file mode 100644 index e8c5e1127..000000000 --- a/Pods/Charts/Source/Charts/Formatters/ValueFormatter.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ValueFormatter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -/// Interface that allows custom formatting of all values inside the chart before they are drawn to the screen. -/// -/// Simply create your own formatting class and let it implement ValueFormatter. Then override the stringForValue() -/// method and return whatever you want. - -@objc(ChartValueFormatter) -public protocol ValueFormatter: AnyObject -{ - - /// Called when a value (from labels inside the chart) is formatted before being drawn. - /// - /// For performance reasons, avoid excessive calculations and memory allocations inside this method. - /// - /// - Parameters: - /// - value: The value to be formatted - /// - dataSetIndex: The index of the DataSet the entry in focus belongs to - /// - viewPortHandler: provides information about the current chart state (scale, translation, ...) - /// - Returns: The formatted label ready to be drawn - func stringForValue(_ value: Double, - entry: ChartDataEntry, - dataSetIndex: Int, - viewPortHandler: ViewPortHandler?) -> String -} diff --git a/Pods/Charts/Source/Charts/Highlight/BarHighlighter.swift b/Pods/Charts/Source/Charts/Highlight/BarHighlighter.swift deleted file mode 100644 index 904302cf1..000000000 --- a/Pods/Charts/Source/Charts/Highlight/BarHighlighter.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// BarHighlighter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(BarChartHighlighter) -open class BarHighlighter: ChartHighlighter -{ - open override func getHighlight(x: CGFloat, y: CGFloat) -> Highlight? - { - guard - let barData = (self.chart as? BarChartDataProvider)?.barData, - let high = super.getHighlight(x: x, y: y) - else { return nil } - - let pos = getValsForTouch(x: x, y: y) - - if let set = barData[high.dataSetIndex] as? BarChartDataSetProtocol, - set.isStacked - { - return getStackedHighlight(high: high, - set: set, - xValue: Double(pos.x), - yValue: Double(pos.y)) - } - else - { - return high - } - } - - internal override func getDistance(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) -> CGFloat - { - return abs(x1 - x2) - } - - internal override var data: ChartData? - { - return (chart as? BarChartDataProvider)?.barData - } - - /// This method creates the Highlight object that also indicates which value of a stacked BarEntry has been selected. - /// - /// - Parameters: - /// - high: the Highlight to work with looking for stacked values - /// - set: - /// - xIndex: - /// - yValue: - /// - Returns: - @objc open func getStackedHighlight(high: Highlight, - set: BarChartDataSetProtocol, - xValue: Double, - yValue: Double) -> Highlight? - { - guard - let chart = self.chart as? BarLineScatterCandleBubbleChartDataProvider, - let entry = set.entryForXValue(xValue, closestToY: yValue) as? BarChartDataEntry - else { return nil } - - // Not stacked - if entry.yValues == nil - { - return high - } - - guard - let ranges = entry.ranges, - !ranges.isEmpty - else { return nil } - - let stackIndex = getClosestStackIndex(ranges: ranges, value: yValue) - let pixel = chart - .getTransformer(forAxis: set.axisDependency) - .pixelForValues(x: high.x, y: ranges[stackIndex].to) - - return Highlight(x: entry.x, - y: entry.y, - xPx: pixel.x, - yPx: pixel.y, - dataSetIndex: high.dataSetIndex, - stackIndex: stackIndex, - axis: high.axis) - } - - /// - Parameters: - /// - entry: - /// - value: - /// - Returns: The index of the closest value inside the values array / ranges (stacked barchart) to the value given as a parameter. - @objc open func getClosestStackIndex(ranges: [Range]?, value: Double) -> Int - { - guard let ranges = ranges else { return 0 } - - if let stackIndex = ranges.firstIndex(where: { $0.contains(value) }) { - return stackIndex - } else { - let length = max(ranges.endIndex - 1, 0) - return (value > ranges[length].to) ? length : 0 - } - } -} diff --git a/Pods/Charts/Source/Charts/Highlight/ChartHighlighter.swift b/Pods/Charts/Source/Charts/Highlight/ChartHighlighter.swift deleted file mode 100644 index f1f187338..000000000 --- a/Pods/Charts/Source/Charts/Highlight/ChartHighlighter.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// ChartHighlighter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Algorithms -import Foundation -import CoreGraphics - -open class ChartHighlighter : NSObject, Highlighter -{ - /// instance of the data-provider - @objc open weak var chart: ChartDataProvider? - - @objc public init(chart: ChartDataProvider) - { - self.chart = chart - } - - open func getHighlight(x: CGFloat, y: CGFloat) -> Highlight? - { - let xVal = Double(getValsForTouch(x: x, y: y).x) - return getHighlight(xValue: xVal, x: x, y: y) - } - - /// - Parameters: - /// - x: - /// - Returns: The corresponding x-pos for a given touch-position in pixels. - @objc open func getValsForTouch(x: CGFloat, y: CGFloat) -> CGPoint - { - guard let chart = self.chart as? BarLineScatterCandleBubbleChartDataProvider else { return .zero } - - // take any transformer to determine the values - return chart.getTransformer(forAxis: .left).valueForTouchPoint(x: x, y: y) - } - - /// - Parameters: - /// - xValue: - /// - x: - /// - y: - /// - Returns: The corresponding ChartHighlight for a given x-value and xy-touch position in pixels. - @objc open func getHighlight(xValue xVal: Double, x: CGFloat, y: CGFloat) -> Highlight? - { - guard let chart = chart else { return nil } - - let closestValues = getHighlights(xValue: xVal, x: x, y: y) - guard !closestValues.isEmpty else { return nil } - - let leftAxisMinDist = getMinimumDistance(closestValues: closestValues, y: y, axis: .left) - let rightAxisMinDist = getMinimumDistance(closestValues: closestValues, y: y, axis: .right) - - let axis: YAxis.AxisDependency = leftAxisMinDist < rightAxisMinDist ? .left : .right - - let detail = closestSelectionDetailByPixel(closestValues: closestValues, x: x, y: y, axis: axis, minSelectionDistance: chart.maxHighlightDistance) - - return detail - } - - /// - Parameters: - /// - xValue: the transformed x-value of the x-touch position - /// - x: touch position - /// - y: touch position - /// - Returns: A list of Highlight objects representing the entries closest to the given xVal. - /// The returned list contains two objects per DataSet (closest rounding up, closest rounding down). - @objc open func getHighlights(xValue: Double, x: CGFloat, y: CGFloat) -> [Highlight] - { - var vals = [Highlight]() - - guard let data = self.data else { return vals } - - for (i, set) in data.indexed() where set.isHighlightEnabled - { - // extract all y-values from all DataSets at the given x-value. - // some datasets (i.e bubble charts) make sense to have multiple values for an x-value. We'll have to find a way to handle that later on. It's more complicated now when x-indices are floating point. - vals.append(contentsOf: buildHighlights(dataSet: set, dataSetIndex: i, xValue: xValue, rounding: .closest)) - } - - return vals - } - - /// - Returns: An array of `Highlight` objects corresponding to the selected xValue and dataSetIndex. - internal func buildHighlights( - dataSet set: ChartDataSetProtocol, - dataSetIndex: Int, - xValue: Double, - rounding: ChartDataSetRounding) -> [Highlight] - { - guard let chart = self.chart as? BarLineScatterCandleBubbleChartDataProvider else { return [] } - - var entries = set.entriesForXValue(xValue) - if entries.isEmpty, let closest = set.entryForXValue(xValue, closestToY: .nan, rounding: rounding) - { - // Try to find closest x-value and take all entries for that x-value - entries = set.entriesForXValue(closest.x) - } - - return entries.map { e in - let px = chart.getTransformer(forAxis: set.axisDependency) - .pixelForValues(x: e.x, y: e.y) - - return Highlight(x: e.x, y: e.y, xPx: px.x, yPx: px.y, dataSetIndex: dataSetIndex, axis: set.axisDependency) - } - } - - // - MARK: - Utilities - - /// - Returns: The `ChartHighlight` of the closest value on the x-y cartesian axes - internal func closestSelectionDetailByPixel( - closestValues: [Highlight], - x: CGFloat, - y: CGFloat, - axis: YAxis.AxisDependency?, - minSelectionDistance: CGFloat) -> Highlight? - { - var distance = minSelectionDistance - var closest: Highlight? - - for high in closestValues - { - if axis == nil || high.axis == axis - { - let cDistance = getDistance(x1: x, y1: y, x2: high.xPx, y2: high.yPx) - - if cDistance < distance - { - closest = high - distance = cDistance - } - } - } - - return closest - } - - /// - Returns: The minimum distance from a touch-y-value (in pixels) to the closest y-value (in pixels) that is displayed in the chart. - internal func getMinimumDistance( - closestValues: [Highlight], - y: CGFloat, - axis: YAxis.AxisDependency - ) -> CGFloat { - var distance = CGFloat.greatestFiniteMagnitude - - for high in closestValues where high.axis == axis - { - let tempDistance = abs(getHighlightPos(high: high) - y) - if tempDistance < distance - { - distance = tempDistance - } - } - - return distance - } - - internal func getHighlightPos(high: Highlight) -> CGFloat - { - return high.yPx - } - - internal func getDistance(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) -> CGFloat - { - return hypot(x1 - x2, y1 - y2) - } - - internal var data: ChartData? - { - return chart?.data - } -} diff --git a/Pods/Charts/Source/Charts/Highlight/CombinedHighlighter.swift b/Pods/Charts/Source/Charts/Highlight/CombinedHighlighter.swift deleted file mode 100644 index 0a03db230..000000000 --- a/Pods/Charts/Source/Charts/Highlight/CombinedHighlighter.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// CombinedHighlighter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Algorithms -import Foundation -import CoreGraphics - -@objc(CombinedChartHighlighter) -open class CombinedHighlighter: ChartHighlighter -{ - /// bar highlighter for supporting stacked highlighting - private var barHighlighter: BarHighlighter? - - @objc public init(chart: CombinedChartDataProvider, barDataProvider: BarChartDataProvider) - { - super.init(chart: chart) - - // if there is BarData, create a BarHighlighter - self.barHighlighter = barDataProvider.barData == nil ? nil : BarHighlighter(chart: barDataProvider) - } - - open override func getHighlights(xValue: Double, x: CGFloat, y: CGFloat) -> [Highlight] - { - var vals = [Highlight]() - - guard - let chart = self.chart as? CombinedChartDataProvider, - let dataObjects = chart.combinedData?.allData - else { return vals } - - for (i, dataObject) in dataObjects.indexed() - { - // in case of BarData, let the BarHighlighter take over - if barHighlighter != nil && dataObject is BarChartData, - let high = barHighlighter?.getHighlight(x: x, y: y) - { - high.dataIndex = i - vals.append(high) - } - else - { - for (j, set) in dataObject.indexed() where set.isHighlightEnabled - { - let highs = buildHighlights(dataSet: set, dataSetIndex: j, xValue: xValue, rounding: .closest) - - for high in highs - { - high.dataIndex = i - vals.append(high) - } - } - } - } - - return vals - } -} diff --git a/Pods/Charts/Source/Charts/Highlight/Highlight.swift b/Pods/Charts/Source/Charts/Highlight/Highlight.swift deleted file mode 100644 index d1692d3fb..000000000 --- a/Pods/Charts/Source/Charts/Highlight/Highlight.swift +++ /dev/null @@ -1,203 +0,0 @@ -// -// Highlight.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartHighlight) -open class Highlight: NSObject -{ - /// the x-value of the highlighted value - fileprivate var _x = Double.nan - - /// the y-value of the highlighted value - fileprivate var _y = Double.nan - - /// the x-pixel of the highlight - private var _xPx = CGFloat.nan - - /// the y-pixel of the highlight - private var _yPx = CGFloat.nan - - /// the index of the data object - in case it refers to more than one - @objc open var dataIndex = Int(-1) - - /// the index of the dataset the highlighted value is in - fileprivate var _dataSetIndex = Int(0) - - /// index which value of a stacked bar entry is highlighted - /// - /// **default**: -1 - fileprivate var _stackIndex = Int(-1) - - /// the axis the highlighted value belongs to - private var _axis: YAxis.AxisDependency = YAxis.AxisDependency.left - - /// the x-position (pixels) on which this highlight object was last drawn - @objc open var drawX: CGFloat = 0.0 - - /// the y-position (pixels) on which this highlight object was last drawn - @objc open var drawY: CGFloat = 0.0 - - public override init() - { - super.init() - } - - /// - Parameters: - /// - x: the x-value of the highlighted value - /// - y: the y-value of the highlighted value - /// - xPx: the x-pixel of the highlighted value - /// - yPx: the y-pixel of the highlighted value - /// - dataIndex: the index of the Data the highlighted value belongs to - /// - dataSetIndex: the index of the DataSet the highlighted value belongs to - /// - stackIndex: references which value of a stacked-bar entry has been selected - /// - axis: the axis the highlighted value belongs to - @objc public init( - x: Double, y: Double, - xPx: CGFloat, yPx: CGFloat, - dataIndex: Int, - dataSetIndex: Int, - stackIndex: Int, - axis: YAxis.AxisDependency) - { - super.init() - - _x = x - _y = y - _xPx = xPx - _yPx = yPx - self.dataIndex = dataIndex - _dataSetIndex = dataSetIndex - _stackIndex = stackIndex - _axis = axis - } - - /// - Parameters: - /// - x: the x-value of the highlighted value - /// - y: the y-value of the highlighted value - /// - xPx: the x-pixel of the highlighted value - /// - yPx: the y-pixel of the highlighted value - /// - dataSetIndex: the index of the DataSet the highlighted value belongs to - /// - stackIndex: references which value of a stacked-bar entry has been selected - /// - axis: the axis the highlighted value belongs to - @objc public convenience init( - x: Double, y: Double, - xPx: CGFloat, yPx: CGFloat, - dataSetIndex: Int, - stackIndex: Int, - axis: YAxis.AxisDependency) - { - self.init(x: x, y: y, xPx: xPx, yPx: yPx, - dataIndex: 0, - dataSetIndex: dataSetIndex, - stackIndex: stackIndex, - axis: axis) - } - - /// - Parameters: - /// - x: the x-value of the highlighted value - /// - y: the y-value of the highlighted value - /// - xPx: the x-pixel of the highlighted value - /// - yPx: the y-pixel of the highlighted value - /// - dataIndex: the index of the Data the highlighted value belongs to - /// - dataSetIndex: the index of the DataSet the highlighted value belongs to - /// - stackIndex: references which value of a stacked-bar entry has been selected - /// - axis: the axis the highlighted value belongs to - @objc public init( - x: Double, y: Double, - xPx: CGFloat, yPx: CGFloat, - dataSetIndex: Int, - axis: YAxis.AxisDependency) - { - super.init() - - _x = x - _y = y - _xPx = xPx - _yPx = yPx - _dataSetIndex = dataSetIndex - _axis = axis - } - - /// - Parameters: - /// - x: the x-value of the highlighted value - /// - y: the y-value of the highlighted value - /// - dataSetIndex: the index of the DataSet the highlighted value belongs to - /// - dataIndex: The data index to search in (only used in CombinedChartView currently) - @objc public init(x: Double, y: Double, dataSetIndex: Int, dataIndex: Int = -1) - { - _x = x - _y = y - _dataSetIndex = dataSetIndex - self.dataIndex = dataIndex - } - - /// - Parameters: - /// - x: the x-value of the highlighted value - /// - dataSetIndex: the index of the DataSet the highlighted value belongs to - /// - stackIndex: references which value of a stacked-bar entry has been selected - @objc public convenience init(x: Double, dataSetIndex: Int, stackIndex: Int) - { - self.init(x: x, y: Double.nan, dataSetIndex: dataSetIndex) - _stackIndex = stackIndex - } - - @objc open var x: Double { return _x } - @objc open var y: Double { return _y } - @objc open var xPx: CGFloat { return _xPx } - @objc open var yPx: CGFloat { return _yPx } - @objc open var dataSetIndex: Int { return _dataSetIndex } - @objc open var stackIndex: Int { return _stackIndex } - @objc open var axis: YAxis.AxisDependency { return _axis } - - @objc open var isStacked: Bool { return _stackIndex >= 0 } - - /// Sets the x- and y-position (pixels) where this highlight was last drawn. - @objc open func setDraw(x: CGFloat, y: CGFloat) - { - self.drawX = x - self.drawY = y - } - - /// Sets the x- and y-position (pixels) where this highlight was last drawn. - @objc open func setDraw(pt: CGPoint) - { - self.drawX = pt.x - self.drawY = pt.y - } - - // MARK: NSObject - - open override var description: String - { - return "Highlight, x: \(_x), y: \(_y), dataIndex (combined charts): \(dataIndex), dataSetIndex: \(_dataSetIndex), stackIndex (only stacked barentry): \(_stackIndex)" - } -} - - -// MARK: Equatable -extension Highlight /*: Equatable*/ { - open override func isEqual(_ object: Any?) -> Bool { - guard let object = object as? Highlight else { return false } - - if self === object - { - return true - } - - return _x == object._x - && _y == object._y - && dataIndex == object.dataIndex - && _dataSetIndex == object._dataSetIndex - && _stackIndex == object._stackIndex - } -} diff --git a/Pods/Charts/Source/Charts/Highlight/Highlighter.swift b/Pods/Charts/Source/Charts/Highlight/Highlighter.swift deleted file mode 100644 index 025763571..000000000 --- a/Pods/Charts/Source/Charts/Highlight/Highlighter.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Highlighter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartHighlighter) -public protocol Highlighter: AnyObject -{ - /// - Parameters: - /// - x: - /// - y: - /// - Returns: A Highlight object corresponding to the given x- and y- touch positions in pixels. - func getHighlight(x: CGFloat, y: CGFloat) -> Highlight? -} diff --git a/Pods/Charts/Source/Charts/Highlight/HorizontalBarHighlighter.swift b/Pods/Charts/Source/Charts/Highlight/HorizontalBarHighlighter.swift deleted file mode 100644 index fffffef6b..000000000 --- a/Pods/Charts/Source/Charts/Highlight/HorizontalBarHighlighter.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// HorizontalBarHighlighter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(HorizontalBarChartHighlighter) -open class HorizontalBarHighlighter: BarHighlighter -{ - open override func getHighlight(x: CGFloat, y: CGFloat) -> Highlight? - { - guard let barData = self.chart?.data as? BarChartData else { return nil } - - let pos = getValsForTouch(x: y, y: x) - guard let high = getHighlight(xValue: Double(pos.y), x: y, y: x) else { return nil } - - if let set = barData[high.dataSetIndex] as? BarChartDataSetProtocol, - set.isStacked - { - return getStackedHighlight(high: high, - set: set, - xValue: Double(pos.y), - yValue: Double(pos.x)) - } - - return high - } - - internal override func buildHighlights( - dataSet set: ChartDataSetProtocol, - dataSetIndex: Int, - xValue: Double, - rounding: ChartDataSetRounding) -> [Highlight] - { - guard let chart = self.chart as? BarLineScatterCandleBubbleChartDataProvider else { return [] } - - var entries = set.entriesForXValue(xValue) - if entries.isEmpty, let closest = set.entryForXValue(xValue, closestToY: .nan, rounding: rounding) - { - // Try to find closest x-value and take all entries for that x-value - entries = set.entriesForXValue(closest.x) - } - - return entries.map { e in - let px = chart.getTransformer(forAxis: set.axisDependency) - .pixelForValues(x: e.y, y: e.x) - return Highlight(x: e.x, y: e.y, xPx: px.x, yPx: px.y, dataSetIndex: dataSetIndex, axis: set.axisDependency) - } - } - - internal override func getDistance(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) -> CGFloat - { - return abs(y1 - y2) - } -} diff --git a/Pods/Charts/Source/Charts/Highlight/PieHighlighter.swift b/Pods/Charts/Source/Charts/Highlight/PieHighlighter.swift deleted file mode 100644 index 42b30a2b8..000000000 --- a/Pods/Charts/Source/Charts/Highlight/PieHighlighter.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// PieHighlighter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(PieChartHighlighter) -open class PieHighlighter: PieRadarHighlighter -{ - open override func closestHighlight(index: Int, x: CGFloat, y: CGFloat) -> Highlight? - { - guard - let set = chart?.data?[0], - let entry = set.entryForIndex(index) - else { return nil } - - return Highlight(x: Double(index), y: entry.y, xPx: x, yPx: y, dataSetIndex: 0, axis: set.axisDependency) - } -} diff --git a/Pods/Charts/Source/Charts/Highlight/PieRadarHighlighter.swift b/Pods/Charts/Source/Charts/Highlight/PieRadarHighlighter.swift deleted file mode 100644 index c55988ab3..000000000 --- a/Pods/Charts/Source/Charts/Highlight/PieRadarHighlighter.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// PieRadarHighlighter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(PieRadarChartHighlighter) -open class PieRadarHighlighter: ChartHighlighter -{ - open override func getHighlight(x: CGFloat, y: CGFloat) -> Highlight? - { - guard let chart = self.chart as? PieRadarChartViewBase else { return nil } - - let touchDistanceToCenter = chart.distanceToCenter(x: x, y: y) - - // check if a slice was touched - guard touchDistanceToCenter <= chart.radius else - { - // if no slice was touched, highlight nothing - return nil - } - - var angle = chart.angleForPoint(x: x ,y: y) - - if chart is PieChartView - { - angle /= CGFloat(chart.chartAnimator.phaseY) - } - - let index = chart.indexForAngle(angle) - - // check if the index could be found - if index < 0 || index >= chart.data?.maxEntryCountSet?.entryCount ?? 0 - { - return nil - } - else - { - return closestHighlight(index: index, x: x, y: y) - } - - } - - /// - Parameters: - /// - index: - /// - x: - /// - y: - /// - Returns: The closest Highlight object of the given objects based on the touch position inside the chart. - @objc open func closestHighlight(index: Int, x: CGFloat, y: CGFloat) -> Highlight? - { - fatalError("closestHighlight(index, x, y) cannot be called on PieRadarChartHighlighter") - } -} diff --git a/Pods/Charts/Source/Charts/Highlight/RadarHighlighter.swift b/Pods/Charts/Source/Charts/Highlight/RadarHighlighter.swift deleted file mode 100644 index d01cb4fd6..000000000 --- a/Pods/Charts/Source/Charts/Highlight/RadarHighlighter.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// RadarHighlighter.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(RadarChartHighlighter) -open class RadarHighlighter: PieRadarHighlighter -{ - open override func closestHighlight(index: Int, x: CGFloat, y: CGFloat) -> Highlight? - { - guard let chart = self.chart as? RadarChartView else { return nil } - - let highlights = getHighlights(forIndex: index) - - let distanceToCenter = Double(chart.distanceToCenter(x: x, y: y) / chart.factor) - - func closestToCenter(lhs: Highlight, rhs: Highlight) -> Bool { - abs(lhs.y - distanceToCenter) < abs(rhs.y - distanceToCenter) - } - - let closest = highlights.min(by: closestToCenter(lhs:rhs:)) - return closest - } - - /// - Parameters: - /// - index: - /// - Returns: An array of Highlight objects for the given index. - /// The Highlight objects give information about the value at the selected index and DataSet it belongs to. - internal func getHighlights(forIndex index: Int) -> [Highlight] - { - var vals = [Highlight]() - - guard - let chart = self.chart as? RadarChartView, - let chartData = chart.data - else { return vals } - - let phaseX = chart.chartAnimator.phaseX - let phaseY = chart.chartAnimator.phaseY - let sliceangle = chart.sliceAngle - let factor = chart.factor - - for (i, dataSet) in chartData.indexed() - { - guard let entry = dataSet.entryForIndex(index) else { continue } - - let y = (entry.y - chart.chartYMin) - - let p = chart.centerOffsets.moving(distance: CGFloat(y) * factor * CGFloat(phaseY), - atAngle: sliceangle * CGFloat(index) * CGFloat(phaseX) + chart.rotationAngle) - - let highlight = Highlight(x: Double(index), y: entry.y, xPx: p.x, yPx: p.y, dataSetIndex: i, axis: dataSet.axisDependency) - vals.append(highlight) - } - - return vals - } -} diff --git a/Pods/Charts/Source/Charts/Highlight/Range.swift b/Pods/Charts/Source/Charts/Highlight/Range.swift deleted file mode 100644 index 4b7ead10f..000000000 --- a/Pods/Charts/Source/Charts/Highlight/Range.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// Range.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -@objc(ChartRange) -open class Range: NSObject -{ - @objc open var from: Double - @objc open var to: Double - - @objc public init(from: Double, to: Double) - { - self.from = from - self.to = to - - super.init() - } - - /// - Parameters: - /// - value: - /// - Returns: `true` if this range contains (if the value is in between) the given value, `false` ifnot. - @objc open func contains(_ value: Double) -> Bool - { - if value > from && value <= to - { - return true - } - else - { - return false - } - } - - @objc open func isLarger(_ value: Double) -> Bool - { - return value > to - } - - @objc open func isSmaller(_ value: Double) -> Bool - { - return value < from - } -} diff --git a/Pods/Charts/Source/Charts/Interfaces/BarChartDataProvider.swift b/Pods/Charts/Source/Charts/Interfaces/BarChartDataProvider.swift deleted file mode 100644 index e1d0a8d0b..000000000 --- a/Pods/Charts/Source/Charts/Interfaces/BarChartDataProvider.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// BarChartDataProvider.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol BarChartDataProvider: BarLineScatterCandleBubbleChartDataProvider -{ - var barData: BarChartData? { get } - - var isDrawBarShadowEnabled: Bool { get } - var isDrawValueAboveBarEnabled: Bool { get } - var isHighlightFullBarEnabled: Bool { get } -} \ No newline at end of file diff --git a/Pods/Charts/Source/Charts/Interfaces/BarLineScatterCandleBubbleChartDataProvider.swift b/Pods/Charts/Source/Charts/Interfaces/BarLineScatterCandleBubbleChartDataProvider.swift deleted file mode 100644 index fd5c06529..000000000 --- a/Pods/Charts/Source/Charts/Interfaces/BarLineScatterCandleBubbleChartDataProvider.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// BarLineScatterCandleBubbleChartDataProvider.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol BarLineScatterCandleBubbleChartDataProvider: ChartDataProvider -{ - func getTransformer(forAxis: YAxis.AxisDependency) -> Transformer - func isInverted(axis: YAxis.AxisDependency) -> Bool - - var lowestVisibleX: Double { get } - var highestVisibleX: Double { get } -} diff --git a/Pods/Charts/Source/Charts/Interfaces/BubbleChartDataProvider.swift b/Pods/Charts/Source/Charts/Interfaces/BubbleChartDataProvider.swift deleted file mode 100644 index d9fc9e81b..000000000 --- a/Pods/Charts/Source/Charts/Interfaces/BubbleChartDataProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// BubbleChartDataProvider.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol BubbleChartDataProvider: BarLineScatterCandleBubbleChartDataProvider -{ - var bubbleData: BubbleChartData? { get } -} \ No newline at end of file diff --git a/Pods/Charts/Source/Charts/Interfaces/CandleChartDataProvider.swift b/Pods/Charts/Source/Charts/Interfaces/CandleChartDataProvider.swift deleted file mode 100644 index 6e729ff13..000000000 --- a/Pods/Charts/Source/Charts/Interfaces/CandleChartDataProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// CandleChartDataProvider.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol CandleChartDataProvider: BarLineScatterCandleBubbleChartDataProvider -{ - var candleData: CandleChartData? { get } -} \ No newline at end of file diff --git a/Pods/Charts/Source/Charts/Interfaces/ChartDataProvider.swift b/Pods/Charts/Source/Charts/Interfaces/ChartDataProvider.swift deleted file mode 100644 index 531f04b75..000000000 --- a/Pods/Charts/Source/Charts/Interfaces/ChartDataProvider.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ChartDataProvider.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol ChartDataProvider -{ - /// The minimum x-value of the chart, regardless of zoom or translation. - var chartXMin: Double { get } - - /// The maximum x-value of the chart, regardless of zoom or translation. - var chartXMax: Double { get } - - /// The minimum y-value of the chart, regardless of zoom or translation. - var chartYMin: Double { get } - - /// The maximum y-value of the chart, regardless of zoom or translation. - var chartYMax: Double { get } - - var maxHighlightDistance: CGFloat { get } - - var xRange: Double { get } - - var centerOffsets: CGPoint { get } - - var data: ChartData? { get } - - var maxVisibleCount: Int { get } -} diff --git a/Pods/Charts/Source/Charts/Interfaces/CombinedChartDataProvider.swift b/Pods/Charts/Source/Charts/Interfaces/CombinedChartDataProvider.swift deleted file mode 100644 index e360eedd6..000000000 --- a/Pods/Charts/Source/Charts/Interfaces/CombinedChartDataProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// CombinedChartDataProvider.swoft -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol CombinedChartDataProvider: LineChartDataProvider, BarChartDataProvider, BubbleChartDataProvider, CandleChartDataProvider, ScatterChartDataProvider -{ - var combinedData: CombinedChartData? { get } -} \ No newline at end of file diff --git a/Pods/Charts/Source/Charts/Interfaces/LineChartDataProvider.swift b/Pods/Charts/Source/Charts/Interfaces/LineChartDataProvider.swift deleted file mode 100644 index e63548216..000000000 --- a/Pods/Charts/Source/Charts/Interfaces/LineChartDataProvider.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// LineChartDataProvider.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol LineChartDataProvider: BarLineScatterCandleBubbleChartDataProvider -{ - var lineData: LineChartData? { get } - - func getAxis(_ axis: YAxis.AxisDependency) -> YAxis -} diff --git a/Pods/Charts/Source/Charts/Interfaces/ScatterChartDataProvider.swift b/Pods/Charts/Source/Charts/Interfaces/ScatterChartDataProvider.swift deleted file mode 100644 index 050bde740..000000000 --- a/Pods/Charts/Source/Charts/Interfaces/ScatterChartDataProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ScatterChartDataProvider.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol ScatterChartDataProvider: BarLineScatterCandleBubbleChartDataProvider -{ - var scatterData: ScatterChartData? { get } -} \ No newline at end of file diff --git a/Pods/Charts/Source/Charts/Jobs/AnimatedMoveViewJob.swift b/Pods/Charts/Source/Charts/Jobs/AnimatedMoveViewJob.swift deleted file mode 100644 index 408883a92..000000000 --- a/Pods/Charts/Source/Charts/Jobs/AnimatedMoveViewJob.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// AnimatedMoveViewJob.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class AnimatedMoveViewJob: AnimatedViewPortJob -{ - internal override func animationUpdate() - { - var pt = CGPoint( - x: xOrigin + (CGFloat(xValue) - xOrigin) * phase, - y: yOrigin + (CGFloat(yValue) - yOrigin) * phase - ) - - transformer.pointValueToPixel(&pt) - viewPortHandler.centerViewPort(pt: pt, chart: view) - } -} diff --git a/Pods/Charts/Source/Charts/Jobs/AnimatedViewPortJob.swift b/Pods/Charts/Source/Charts/Jobs/AnimatedViewPortJob.swift deleted file mode 100644 index 363bb0085..000000000 --- a/Pods/Charts/Source/Charts/Jobs/AnimatedViewPortJob.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// AnimatedViewPortJob.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics -import QuartzCore - -open class AnimatedViewPortJob: ViewPortJob -{ - internal var phase: CGFloat = 1.0 - internal var xOrigin: CGFloat = 0.0 - internal var yOrigin: CGFloat = 0.0 - - private var _startTime: TimeInterval = 0.0 - private var _displayLink: NSUIDisplayLink! - private var _duration: TimeInterval = 0.0 - private var _endTime: TimeInterval = 0.0 - - private var _easing: ChartEasingFunctionBlock? - - @objc public init( - viewPortHandler: ViewPortHandler, - xValue: Double, - yValue: Double, - transformer: Transformer, - view: ChartViewBase, - xOrigin: CGFloat, - yOrigin: CGFloat, - duration: TimeInterval, - easing: ChartEasingFunctionBlock?) - { - self.xOrigin = xOrigin - self.yOrigin = yOrigin - self._duration = duration - self._easing = easing - - super.init(viewPortHandler: viewPortHandler, - xValue: xValue, - yValue: yValue, - transformer: transformer, - view: view) - } - - deinit - { - stop(finish: false) - } - - open override func doJob() - { - start() - } - - @objc open func start() - { - _startTime = CACurrentMediaTime() - _endTime = _startTime + _duration - _endTime = _endTime > _endTime ? _endTime : _endTime - - updateAnimationPhase(_startTime) - - _displayLink = NSUIDisplayLink(target: self, selector: #selector(animationLoop)) - _displayLink.add(to: .main, forMode: RunLoop.Mode.common) - } - - @objc open func stop(finish: Bool) - { - guard _displayLink != nil else { return } - - _displayLink.remove(from: .main, forMode: RunLoop.Mode.common) - _displayLink = nil - - if finish - { - if phase != 1.0 - { - phase = 1.0 - animationUpdate() - } - - animationEnd() - } - } - - private func updateAnimationPhase(_ currentTime: TimeInterval) - { - let elapsedTime = currentTime - _startTime - let duration = _duration - var elapsed = elapsedTime - - elapsed = min(elapsed, duration) - - phase = CGFloat(_easing?(elapsed, duration) ?? elapsed / duration) - } - - @objc private func animationLoop() - { - let currentTime: TimeInterval = CACurrentMediaTime() - - updateAnimationPhase(currentTime) - - animationUpdate() - - if currentTime >= _endTime - { - stop(finish: true) - } - } - - internal func animationUpdate() - { - // Override this - } - - internal func animationEnd() - { - // Override this - } -} diff --git a/Pods/Charts/Source/Charts/Jobs/AnimatedZoomViewJob.swift b/Pods/Charts/Source/Charts/Jobs/AnimatedZoomViewJob.swift deleted file mode 100644 index 4f2391128..000000000 --- a/Pods/Charts/Source/Charts/Jobs/AnimatedZoomViewJob.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// AnimatedZoomViewJob.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class AnimatedZoomViewJob: AnimatedViewPortJob -{ - internal var yAxis: YAxis - internal var xAxisRange: Double = 0.0 - internal var scaleX: CGFloat = 0.0 - internal var scaleY: CGFloat = 0.0 - internal var zoomOriginX: CGFloat = 0.0 - internal var zoomOriginY: CGFloat = 0.0 - internal var zoomCenterX: CGFloat = 0.0 - internal var zoomCenterY: CGFloat = 0.0 - - @objc public init( - viewPortHandler: ViewPortHandler, - transformer: Transformer, - view: ChartViewBase, - yAxis: YAxis, - xAxisRange: Double, - scaleX: CGFloat, - scaleY: CGFloat, - xOrigin: CGFloat, - yOrigin: CGFloat, - zoomCenterX: CGFloat, - zoomCenterY: CGFloat, - zoomOriginX: CGFloat, - zoomOriginY: CGFloat, - duration: TimeInterval, - easing: ChartEasingFunctionBlock?) - { - self.yAxis = yAxis - self.xAxisRange = xAxisRange - self.scaleX = scaleX - self.scaleY = scaleY - self.zoomCenterX = zoomCenterX - self.zoomCenterY = zoomCenterY - self.zoomOriginX = zoomOriginX - self.zoomOriginY = zoomOriginY - - super.init(viewPortHandler: viewPortHandler, - xValue: 0.0, - yValue: 0.0, - transformer: transformer, - view: view, - xOrigin: xOrigin, - yOrigin: yOrigin, - duration: duration, - easing: easing) - } - - internal override func animationUpdate() - { - let scaleX = xOrigin + (self.scaleX - xOrigin) * phase - let scaleY = yOrigin + (self.scaleY - yOrigin) * phase - - var matrix = viewPortHandler.setZoom(scaleX: scaleX, scaleY: scaleY) - viewPortHandler.refresh(newMatrix: matrix, chart: view, invalidate: false) - - let valsInView = CGFloat(yAxis.axisRange) / viewPortHandler.scaleY - let xsInView = CGFloat(xAxisRange) / viewPortHandler.scaleX - - var pt = CGPoint( - x: zoomOriginX + ((zoomCenterX - xsInView / 2.0) - zoomOriginX) * phase, - y: zoomOriginY + ((zoomCenterY + valsInView / 2.0) - zoomOriginY) * phase - ) - - transformer.pointValueToPixel(&pt) - - matrix = viewPortHandler.translate(pt: pt) - viewPortHandler.refresh(newMatrix: matrix, chart: view, invalidate: true) - } - - internal override func animationEnd() - { - (view as? BarLineChartViewBase)?.calculateOffsets() - view.setNeedsDisplay() - } -} diff --git a/Pods/Charts/Source/Charts/Jobs/MoveViewJob.swift b/Pods/Charts/Source/Charts/Jobs/MoveViewJob.swift deleted file mode 100644 index a9a64feab..000000000 --- a/Pods/Charts/Source/Charts/Jobs/MoveViewJob.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// MoveViewJob.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(MoveChartViewJob) -open class MoveViewJob: ViewPortJob -{ - open override func doJob() - { - var pt = CGPoint( - x: xValue, - y: yValue - ) - - transformer.pointValueToPixel(&pt) - viewPortHandler.centerViewPort(pt: pt, chart: view) - } -} diff --git a/Pods/Charts/Source/Charts/Jobs/ViewPortJob.swift b/Pods/Charts/Source/Charts/Jobs/ViewPortJob.swift deleted file mode 100644 index c3adcca5c..000000000 --- a/Pods/Charts/Source/Charts/Jobs/ViewPortJob.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ViewPortJob.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -// This defines a viewport modification job, used for delaying or animating viewport changes -@objc(ChartViewPortJob) -open class ViewPortJob: NSObject -{ - internal var point: CGPoint = .zero - internal unowned var viewPortHandler: ViewPortHandler - internal var xValue = 0.0 - internal var yValue = 0.0 - internal unowned var transformer: Transformer - internal unowned var view: ChartViewBase - - @objc public init( - viewPortHandler: ViewPortHandler, - xValue: Double, - yValue: Double, - transformer: Transformer, - view: ChartViewBase) - { - self.viewPortHandler = viewPortHandler - self.xValue = xValue - self.yValue = yValue - self.transformer = transformer - self.view = view - - super.init() - } - - @objc open func doJob() - { - fatalError("`doJob()` must be overridden by subclasses") - } -} diff --git a/Pods/Charts/Source/Charts/Jobs/ZoomViewJob.swift b/Pods/Charts/Source/Charts/Jobs/ZoomViewJob.swift deleted file mode 100644 index 79ce41c76..000000000 --- a/Pods/Charts/Source/Charts/Jobs/ZoomViewJob.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// ZoomViewJob.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ZoomChartViewJob) -open class ZoomViewJob: ViewPortJob -{ - internal var scaleX: CGFloat = 0.0 - internal var scaleY: CGFloat = 0.0 - internal var axisDependency: YAxis.AxisDependency = .left - - @objc public init( - viewPortHandler: ViewPortHandler, - scaleX: CGFloat, - scaleY: CGFloat, - xValue: Double, - yValue: Double, - transformer: Transformer, - axis: YAxis.AxisDependency, - view: ChartViewBase) - { - self.scaleX = scaleX - self.scaleY = scaleY - self.axisDependency = axis - - super.init( - viewPortHandler: viewPortHandler, - xValue: xValue, - yValue: yValue, - transformer: transformer, - view: view) - - } - - open override func doJob() - { - var matrix = viewPortHandler.setZoom(scaleX: scaleX, scaleY: scaleY) - viewPortHandler.refresh(newMatrix: matrix, chart: view, invalidate: false) - - let yValsInView = (view as! BarLineChartViewBase).getAxis(axisDependency).axisRange / Double(viewPortHandler.scaleY) - let xValsInView = (view as! BarLineChartViewBase).xAxis.axisRange / Double(viewPortHandler.scaleX) - - var pt = CGPoint( - x: CGFloat(xValue - xValsInView / 2.0), - y: CGFloat(yValue + yValsInView / 2.0) - ) - - transformer.pointValueToPixel(&pt) - - matrix = viewPortHandler.translate(pt: pt) - viewPortHandler.refresh(newMatrix: matrix, chart: view, invalidate: false) - - (view as! BarLineChartViewBase).calculateOffsets() - view.setNeedsDisplay() - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/AxisRenderer.swift b/Pods/Charts/Source/Charts/Renderers/AxisRenderer.swift deleted file mode 100644 index 35b1ddf72..000000000 --- a/Pods/Charts/Source/Charts/Renderers/AxisRenderer.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// AxisRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -public protocol AxisRenderer: Renderer { - - associatedtype Axis: AxisBase - - /// base axis this axis renderer works with - var axis: Axis { get } - - /// transformer to transform values to screen pixels and return - var transformer: Transformer? { get } - - /// Draws the axis labels on the specified context - func renderAxisLabels(context: CGContext) - - /// Draws the grid lines belonging to the axis. - func renderGridLines(context: CGContext) - - /// Draws the line that goes alongside the axis. - func renderAxisLine(context: CGContext) - - /// Draws the LimitLines associated with this axis to the screen. - func renderLimitLines(context: CGContext) - - /// Computes the axis values. - /// - parameter min: the minimum value in the data object for this axis - /// - parameter max: the maximum value in the data object for this axis - func computeAxis(min: Double, max: Double, inverted: Bool) - - /// Sets up the axis values. Computes the desired number of labels between the two given extremes. - func computeAxisValues(min: Double, max: Double) -} diff --git a/Pods/Charts/Source/Charts/Renderers/BarChartRenderer.swift b/Pods/Charts/Source/Charts/Renderers/BarChartRenderer.swift deleted file mode 100644 index 2aa69a828..000000000 --- a/Pods/Charts/Source/Charts/Renderers/BarChartRenderer.swift +++ /dev/null @@ -1,835 +0,0 @@ -// -// BarChartRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -#if !os(OSX) - import UIKit -#endif - -open class BarChartRenderer: BarLineScatterCandleBubbleRenderer -{ - /// A nested array of elements ordered logically (i.e not in visual/drawing order) for use with VoiceOver - /// - /// Its use is apparent when there are multiple data sets, since we want to read bars in left to right order, - /// irrespective of dataset. However, drawing is done per dataset, so using this array and then flattening it prevents us from needing to - /// re-render for the sake of accessibility. - /// - /// In practise, its structure is: - /// - /// ```` - /// [ - /// [dataset1 element1, dataset2 element1], - /// [dataset1 element2, dataset2 element2], - /// [dataset1 element3, dataset2 element3] - /// ... - /// ] - /// ```` - /// This is done to provide numerical inference across datasets to a screenreader user, in the same way that a sighted individual - /// uses a multi-dataset bar chart. - /// - /// The ````internal```` specifier is to allow subclasses (HorizontalBar) to populate the same array - internal lazy var accessibilityOrderedElements: [[NSUIAccessibilityElement]] = accessibilityCreateEmptyOrderedElements() - - private typealias Buffer = [CGRect] - - @objc open weak var dataProvider: BarChartDataProvider? - - @objc public init(dataProvider: BarChartDataProvider, animator: Animator, viewPortHandler: ViewPortHandler) - { - super.init(animator: animator, viewPortHandler: viewPortHandler) - - self.dataProvider = dataProvider - } - - // [CGRect] per dataset - private var _buffers = [Buffer]() - - open override func initBuffers() - { - guard let barData = dataProvider?.barData else { return _buffers.removeAll() } - - // Match buffers count to dataset count - if _buffers.count != barData.count - { - while _buffers.count < barData.count - { - _buffers.append(Buffer()) - } - while _buffers.count > barData.count - { - _buffers.removeLast() - } - } - - _buffers = zip(_buffers, barData).map { buffer, set -> Buffer in - let set = set as! BarChartDataSetProtocol - let size = set.entryCount * (set.isStacked ? set.stackSize : 1) - return buffer.count == size - ? buffer - : Buffer(repeating: .zero, count: size) - } - } - - private func prepareBuffer(dataSet: BarChartDataSetProtocol, index: Int) - { - guard - let dataProvider = dataProvider, - let barData = dataProvider.barData - else { return } - - let barWidthHalf = CGFloat(barData.barWidth / 2.0) - - var bufferIndex = 0 - let containsStacks = dataSet.isStacked - - let isInverted = dataProvider.isInverted(axis: dataSet.axisDependency) - let phaseY = CGFloat(animator.phaseY) - - for i in (0..= 0.0 - { - y = posY - yStart = posY + value - posY = yStart - } - else - { - y = negY - yStart = negY + abs(value) - negY += abs(value) - } - - var top = isInverted - ? (y <= yStart ? CGFloat(y) : CGFloat(yStart)) - : (y >= yStart ? CGFloat(y) : CGFloat(yStart)) - var bottom = isInverted - ? (y >= yStart ? CGFloat(y) : CGFloat(yStart)) - : (y <= yStart ? CGFloat(y) : CGFloat(yStart)) - - // multiply the height of the rect with the phase - top *= phaseY - bottom *= phaseY - - let barRect = CGRect(x: left, y: top, - width: right - left, - height: bottom - top) - _buffers[index][bufferIndex] = barRect - bufferIndex += 1 - } - } - else - { - var top = isInverted - ? (y <= 0.0 ? CGFloat(y) : 0) - : (y >= 0.0 ? CGFloat(y) : 0) - var bottom = isInverted - ? (y >= 0.0 ? CGFloat(y) : 0) - : (y <= 0.0 ? CGFloat(y) : 0) - - /* When drawing each bar, the renderer actually draws each bar from 0 to the required value. - * This drawn bar is then clipped to the visible chart rect in BarLineChartViewBase's draw(rect:) using clipDataToContent. - * While this works fine when calculating the bar rects for drawing, it causes the accessibilityFrames to be oversized in some cases. - * This offset attempts to undo that unnecessary drawing when calculating barRects - * - * +---------------------------------------------------------------+---------------------------------------------------------------+ - * | Situation 1: (!inverted && y >= 0) | Situation 3: (inverted && y >= 0) | - * | | | - * | y -> +--+ <- top | 0 -> ---+--+---+--+------ <- top | - * | |//| } topOffset = y - max | | | |//| } topOffset = min | - * | max -> +---------+--+----+ <- top - topOffset | min -> +--+--+---+--+----+ <- top + topOffset | - * | | +--+ |//| | | | | | |//| | | - * | | | | |//| | | | +--+ |//| | | - * | | | | |//| | | | |//| | | - * | min -> +--+--+---+--+----+ <- bottom + bottomOffset | max -> +---------+--+----+ <- bottom - bottomOffset | - * | | | |//| } bottomOffset = min | |//| } bottomOffset = y - max | - * | 0 -> ---+--+---+--+----- <- bottom | y -> +--+ <- bottom | - * | | | - * +---------------------------------------------------------------+---------------------------------------------------------------+ - * | Situation 2: (!inverted && y < 0) | Situation 4: (inverted && y < 0) | - * | | | - * | 0 -> ---+--+---+--+----- <- top | y -> +--+ <- top | - * | | | |//| } topOffset = -max | |//| } topOffset = min - y | - * | max -> +--+--+---+--+----+ <- top - topOffset | min -> +---------+--+----+ <- top + topOffset | - * | | | | |//| | | | +--+ |//| | | - * | | +--+ |//| | | | | | |//| | | - * | | |//| | | | | | |//| | | - * | min -> +---------+--+----+ <- bottom + bottomOffset | max -> +--+--+---+--+----+ <- bottom - bottomOffset | - * | |//| } bottomOffset = min - y | | | |//| } bottomOffset = -max | - * | y -> +--+ <- bottom | 0 -> ---+--+---+--+------- <- bottom | - * | | | - * +---------------------------------------------------------------+---------------------------------------------------------------+ - */ - var topOffset: CGFloat = 0.0 - var bottomOffset: CGFloat = 0.0 - if let offsetView = dataProvider as? BarChartView - { - let offsetAxis = offsetView.getAxis(dataSet.axisDependency) - if y >= 0 - { - // situation 1 - if offsetAxis.axisMaximum < y - { - topOffset = CGFloat(y - offsetAxis.axisMaximum) - } - if offsetAxis.axisMinimum > 0 - { - bottomOffset = CGFloat(offsetAxis.axisMinimum) - } - } - else // y < 0 - { - //situation 2 - if offsetAxis.axisMaximum < 0 - { - topOffset = CGFloat(offsetAxis.axisMaximum * -1) - } - if offsetAxis.axisMinimum > y - { - bottomOffset = CGFloat(offsetAxis.axisMinimum - y) - } - } - if isInverted - { - // situation 3 and 4 - // exchange topOffset/bottomOffset based on 1 and 2 - // see diagram above - (topOffset, bottomOffset) = (bottomOffset, topOffset) - } - } - //apply offset - top = isInverted ? top + topOffset : top - topOffset - bottom = isInverted ? bottom - bottomOffset : bottom + bottomOffset - - // multiply the height of the rect with the phase - // explicitly add 0 + topOffset to indicate this is changed after adding accessibility support (#3650, #3520) - if top > 0 + topOffset - { - top *= phaseY - } - else - { - bottom *= phaseY - } - - let barRect = CGRect(x: left, y: top, - width: right - left, - height: bottom - top) - _buffers[index][bufferIndex] = barRect - bufferIndex += 1 - } - } - } - - open override func drawData(context: CGContext) - { - guard - let dataProvider = dataProvider, - let barData = dataProvider.barData - else { return } - - // If we redraw the data, remove and repopulate accessible elements to update label values and frames - accessibleChartElements.removeAll() - accessibilityOrderedElements = accessibilityCreateEmptyOrderedElements() - - // Make the chart header the first element in the accessible elements array - if let chart = dataProvider as? BarChartView { - let element = createAccessibleHeader(usingChart: chart, - andData: barData, - withDefaultDescription: "Bar Chart") - accessibleChartElements.append(element) - } - - // Populate logically ordered nested elements into accessibilityOrderedElements in drawDataSet() - for i in barData.indices - { - guard let set = barData[i] as? BarChartDataSetProtocol else { - fatalError("Datasets for BarChartRenderer must conform to IBarChartDataset") - } - - guard set.isVisible else { continue } - - drawDataSet(context: context, dataSet: set, index: i) - } - - // Merge nested ordered arrays into the single accessibleChartElements. - accessibleChartElements.append(contentsOf: accessibilityOrderedElements.flatMap { $0 } ) - accessibilityPostLayoutChangedNotification() - } - - private var _barShadowRectBuffer: CGRect = CGRect() - - @objc open func drawDataSet(context: CGContext, dataSet: BarChartDataSetProtocol, index: Int) - { - guard let dataProvider = dataProvider else { return } - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - - prepareBuffer(dataSet: dataSet, index: index) - trans.rectValuesToPixel(&_buffers[index]) - - let borderWidth = dataSet.barBorderWidth - let borderColor = dataSet.barBorderColor - let drawBorder = borderWidth > 0.0 - - context.saveGState() - defer { context.restoreGState() } - - // draw the bar shadow before the values - if dataProvider.isDrawBarShadowEnabled - { - guard let barData = dataProvider.barData else { return } - - let barWidth = barData.barWidth - let barWidthHalf = barWidth / 2.0 - var x: Double = 0.0 - - let range = (0..= 0.0 - ? (rect.origin.y + posOffset) - : (rect.origin.y + rect.size.height + negOffset), - font: valueFont, - align: .center, - color: dataSet.valueTextColorAt(j), - anchor: CGPoint(x: 0.5, y: 0.5), - angleRadians: angleRadians) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - var px = x - var py = val >= 0.0 - ? (rect.origin.y + posOffset) - : (rect.origin.y + rect.size.height + negOffset) - - px += iconsOffset.x - py += iconsOffset.y - - context.drawImage(icon, - atCenter: CGPoint(x: px, y: py), - size: icon.size) - } - - } - } - else - { - // if we have stacks - - var bufferIndex = 0 - let lastIndex = ceil(Double(dataSet.entryCount) * animator.phaseX) - - for index in 0 ..< Int(lastIndex) - { - guard let e = dataSet.entryForIndex(index) as? BarChartDataEntry else { continue } - - let vals = e.yValues - - let rect = buffer[bufferIndex] - - let x = rect.origin.x + rect.size.width / 2.0 - - // we still draw stacked bars, but there is one non-stacked in between - if let values = vals - { - // draw stack values - var transformed = [CGPoint]() - - var posY = 0.0 - var negY = -e.negativeSum - - for value in values - { - let y: Double - - if value == 0.0 && (posY == 0.0 || negY == 0.0) - { - // Take care of the situation of a 0.0 value, which overlaps a non-zero bar - y = value - } - else if value >= 0.0 - { - posY += value - y = posY - } - else - { - y = negY - negY -= value - } - - transformed.append(CGPoint(x: 0.0, y: CGFloat(y * phaseY))) - } - - trans.pointValuesToPixel(&transformed) - - for (value, transformed) in zip(values, transformed) - { - let drawBelow = (value == 0.0 && negY == 0.0 && posY > 0.0) || value < 0.0 - let y = transformed.y + (drawBelow ? negOffset : posOffset) - - guard viewPortHandler.isInBoundsRight(x) else { break } - guard viewPortHandler.isInBoundsY(y), - viewPortHandler.isInBoundsLeft(x) - else { continue } - - if dataSet.isDrawValuesEnabled - { - drawValue( - context: context, - value: formatter.stringForValue( - value, - entry: e, - dataSetIndex: dataSetIndex, - viewPortHandler: viewPortHandler), - xPos: x, - yPos: y, - font: valueFont, - align: .center, - color: dataSet.valueTextColorAt(index), - anchor: CGPoint(x: 0.5, y: 0.5), - angleRadians: angleRadians) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - context.drawImage(icon, - atCenter: CGPoint(x: x + iconsOffset.x, - y: y + iconsOffset.y), - size: icon.size) - } - } - } - else - { - guard viewPortHandler.isInBoundsRight(x) else { break } - guard viewPortHandler.isInBoundsY(rect.origin.y), - viewPortHandler.isInBoundsLeft(x) else { continue } - - if dataSet.isDrawValuesEnabled - { - drawValue( - context: context, - value: formatter.stringForValue( - e.y, - entry: e, - dataSetIndex: dataSetIndex, - viewPortHandler: viewPortHandler), - xPos: x, - yPos: rect.origin.y + - (e.y >= 0 ? posOffset : negOffset), - font: valueFont, - align: .center, - color: dataSet.valueTextColorAt(index), - anchor: CGPoint(x: 0.5, y: 0.5), - angleRadians: angleRadians) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - var px = x - var py = rect.origin.y + - (e.y >= 0 ? posOffset : negOffset) - - px += iconsOffset.x - py += iconsOffset.y - - context.drawImage(icon, - atCenter: CGPoint(x: px, y: py), - size: icon.size) - } - } - - bufferIndex += vals?.count ?? 1 - } - } - } - } - } - - /// Draws a value at the specified x and y position. - @objc open func drawValue(context: CGContext, value: String, xPos: CGFloat, yPos: CGFloat, font: NSUIFont, align: TextAlignment, color: NSUIColor, anchor: CGPoint, angleRadians: CGFloat) - { - if (angleRadians == 0.0) - { - context.drawText(value, at: CGPoint(x: xPos, y: yPos), align: align, attributes: [.font: font, .foregroundColor: color]) - } - else - { - // align left to center text with rotation - context.drawText(value, at: CGPoint(x: xPos, y: yPos), align: align, anchor: anchor, angleRadians: angleRadians, attributes: [.font: font, .foregroundColor: color]) - } - } - - - open override func drawExtras(context: CGContext) - { - - } - - open override func drawHighlighted(context: CGContext, indices: [Highlight]) - { - guard - let dataProvider = dataProvider, - let barData = dataProvider.barData - else { return } - - context.saveGState() - defer { context.restoreGState() } - var barRect = CGRect() - - for high in indices - { - guard - let set = barData[high.dataSetIndex] as? BarChartDataSetProtocol, - set.isHighlightEnabled - else { continue } - - if let e = set.entryForXValue(high.x, closestToY: high.y) as? BarChartDataEntry - { - guard isInBoundsX(entry: e, dataSet: set) else { continue } - - let trans = dataProvider.getTransformer(forAxis: set.axisDependency) - - context.setFillColor(set.highlightColor.cgColor) - context.setAlpha(set.highlightAlpha) - - let isStack = high.stackIndex >= 0 && e.isStacked - - let y1: Double - let y2: Double - - if isStack - { - if dataProvider.isHighlightFullBarEnabled - { - y1 = e.positiveSum - y2 = -e.negativeSum - } - else - { - let range = e.ranges?[high.stackIndex] - - y1 = range?.from ?? 0.0 - y2 = range?.to ?? 0.0 - } - } - else - { - y1 = e.y - y2 = 0.0 - } - - prepareBarHighlight(x: e.x, y1: y1, y2: y2, barWidthHalf: barData.barWidth / 2.0, trans: trans, rect: &barRect) - - setHighlightDrawPos(highlight: high, barRect: barRect) - - context.fill(barRect) - } - } - } - - /// Sets the drawing position of the highlight object based on the given bar-rect. - internal func setHighlightDrawPos(highlight high: Highlight, barRect: CGRect) - { - high.setDraw(x: barRect.midX, y: barRect.origin.y) - } - - /// Creates a nested array of empty subarrays each of which will be populated with NSUIAccessibilityElements. - /// This is marked internal to support HorizontalBarChartRenderer as well. - internal func accessibilityCreateEmptyOrderedElements() -> [[NSUIAccessibilityElement]] - { - guard let chart = dataProvider as? BarChartView else { return [] } - - // Unlike Bubble & Line charts, here we use the maximum entry count to account for stacked bars - let maxEntryCount = chart.data?.maxEntryCountSet?.entryCount ?? 0 - - return Array(repeating: [NSUIAccessibilityElement](), - count: maxEntryCount) - } - - /// Creates an NSUIAccessibleElement representing the smallest meaningful bar of the chart - /// i.e. in case of a stacked chart, this returns each stack, not the combined bar. - /// Note that it is marked internal to support subclass modification in the HorizontalBarChart. - internal func createAccessibleElement(withIndex idx: Int, - container: BarChartView, - dataSet: BarChartDataSetProtocol, - dataSetIndex: Int, - stackSize: Int, - modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement - { - let element = NSUIAccessibilityElement(accessibilityContainer: container) - let xAxis = container.xAxis - - guard let e = dataSet.entryForIndex(idx/stackSize) as? BarChartDataEntry else { return element } - guard let dataProvider = dataProvider else { return element } - - // NOTE: The formatter can cause issues when the x-axis labels are consecutive ints. - // i.e. due to the Double conversion, if there are more than one data set that are grouped, - // there is the possibility of some labels being rounded up. A floor() might fix this, but seems to be a brute force solution. - let label = xAxis.valueFormatter?.stringForValue(e.x, axis: xAxis) ?? "\(e.x)" - - var elementValueText = dataSet.valueFormatter.stringForValue( - e.y, - entry: e, - dataSetIndex: dataSetIndex, - viewPortHandler: viewPortHandler) - - if dataSet.isStacked, let vals = e.yValues - { - let labelCount = min(dataSet.colors.count, stackSize) - - let stackLabel: String? - if (!dataSet.stackLabels.isEmpty && labelCount > 0) { - let labelIndex = idx % labelCount - stackLabel = dataSet.stackLabels.indices.contains(labelIndex) ? dataSet.stackLabels[labelIndex] : nil - } else { - stackLabel = nil - } - - //Handles empty array of yValues - let yValue = vals.isEmpty ? 0.0 : vals[idx % vals.count] - - elementValueText = dataSet.valueFormatter.stringForValue( - yValue, - entry: e, - dataSetIndex: dataSetIndex, - viewPortHandler: viewPortHandler) - - if let stackLabel = stackLabel { - elementValueText = stackLabel + " \(elementValueText)" - } else { - elementValueText = "\(elementValueText)" - } - } - - let dataSetCount = dataProvider.barData?.dataSetCount ?? -1 - let doesContainMultipleDataSets = dataSetCount > 1 - - element.accessibilityLabel = "\(doesContainMultipleDataSets ? (dataSet.label ?? "") + ", " : "") \(label): \(elementValueText)" - - modifier(element) - - return element - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/BarLineScatterCandleBubbleRenderer.swift b/Pods/Charts/Source/Charts/Renderers/BarLineScatterCandleBubbleRenderer.swift deleted file mode 100644 index 2f4751d1a..000000000 --- a/Pods/Charts/Source/Charts/Renderers/BarLineScatterCandleBubbleRenderer.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// BarLineScatterCandleBubbleRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(BarLineScatterCandleBubbleChartRenderer) -open class BarLineScatterCandleBubbleRenderer: NSObject, DataRenderer -{ - public let viewPortHandler: ViewPortHandler - - public final var accessibleChartElements: [NSUIAccessibilityElement] = [] - - public let animator: Animator - - internal var _xBounds = XBounds() // Reusable XBounds object - - public init(animator: Animator, viewPortHandler: ViewPortHandler) - { - self.viewPortHandler = viewPortHandler - self.animator = animator - - super.init() - } - - open func drawData(context: CGContext) { } - - open func drawValues(context: CGContext) { } - - open func drawExtras(context: CGContext) { } - - open func drawHighlighted(context: CGContext, indices: [Highlight]) { } - - /// Checks if the provided entry object is in bounds for drawing considering the current animation phase. - internal func isInBoundsX(entry e: ChartDataEntry, dataSet: BarLineScatterCandleBubbleChartDataSetProtocol) -> Bool - { - let entryIndex = dataSet.entryIndex(entry: e) - return Double(entryIndex) < Double(dataSet.entryCount) * animator.phaseX - } - - /// Calculates and returns the x-bounds for the given DataSet in terms of index in their values array. - /// This includes minimum and maximum visible x, as well as range. - internal func xBounds(chart: BarLineScatterCandleBubbleChartDataProvider, - dataSet: BarLineScatterCandleBubbleChartDataSetProtocol, - animator: Animator?) -> XBounds - { - return XBounds(chart: chart, dataSet: dataSet, animator: animator) - } - - /// - Returns: `true` if the DataSet values should be drawn, `false` if not. - internal func shouldDrawValues(forDataSet set: ChartDataSetProtocol) -> Bool - { - return set.isVisible && (set.isDrawValuesEnabled || set.isDrawIconsEnabled) - } - - open func initBuffers() { } - - open func isDrawingValuesAllowed(dataProvider: ChartDataProvider?) -> Bool - { - guard let data = dataProvider?.data else { return false } - return data.entryCount < Int(CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleX) - } - - /// Class representing the bounds of the current viewport in terms of indices in the values array of a DataSet. - open class XBounds - { - /// minimum visible entry index - open var min: Int = 0 - - /// maximum visible entry index - open var max: Int = 0 - - /// range of visible entry indices - open var range: Int = 0 - - public init() - { - - } - - public init(chart: BarLineScatterCandleBubbleChartDataProvider, - dataSet: BarLineScatterCandleBubbleChartDataSetProtocol, - animator: Animator?) - { - self.set(chart: chart, dataSet: dataSet, animator: animator) - } - - /// Calculates the minimum and maximum x values as well as the range between them. - open func set(chart: BarLineScatterCandleBubbleChartDataProvider, - dataSet: BarLineScatterCandleBubbleChartDataSetProtocol, - animator: Animator?) - { - let phaseX = Swift.max(0.0, Swift.min(1.0, animator?.phaseX ?? 1.0)) - - let low = chart.lowestVisibleX - let high = chart.highestVisibleX - - let entryFrom = dataSet.entryForXValue(low, closestToY: .nan, rounding: .down) - let entryTo = dataSet.entryForXValue(high, closestToY: .nan, rounding: .up) - - self.min = entryFrom == nil ? 0 : dataSet.entryIndex(entry: entryFrom!) - self.max = entryTo == nil ? 0 : dataSet.entryIndex(entry: entryTo!) - range = Int(Double(self.max - self.min) * phaseX) - } - } - - public func createAccessibleHeader(usingChart chart: ChartViewBase, andData data: ChartData, withDefaultDescription defaultDescription: String) -> NSUIAccessibilityElement { - return AccessibleHeader.create(usingChart: chart, andData: data, withDefaultDescription: defaultDescription) - } -} - -extension BarLineScatterCandleBubbleRenderer.XBounds: RangeExpression { - public func relative(to collection: C) -> Swift.Range - where C : Collection, Bound == C.Index - { - return Swift.Range(min...min + range) - } - - public func contains(_ element: Int) -> Bool { - return (min...min + range).contains(element) - } -} - -extension BarLineScatterCandleBubbleRenderer.XBounds: Sequence { - public struct Iterator: IteratorProtocol { - private var iterator: IndexingIterator> - - fileprivate init(min: Int, max: Int) { - self.iterator = (min...max).makeIterator() - } - - public mutating func next() -> Int? { - return self.iterator.next() - } - } - - public func makeIterator() -> Iterator { - return Iterator(min: self.min, max: self.min + self.range) - } -} - -extension BarLineScatterCandleBubbleRenderer.XBounds: CustomDebugStringConvertible -{ - public var debugDescription: String - { - return "min:\(self.min), max:\(self.max), range:\(self.range)" - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/BubbleChartRenderer.swift b/Pods/Charts/Source/Charts/Renderers/BubbleChartRenderer.swift deleted file mode 100644 index fa96a6f91..000000000 --- a/Pods/Charts/Source/Charts/Renderers/BubbleChartRenderer.swift +++ /dev/null @@ -1,360 +0,0 @@ -// -// BubbleChartRenderer.swift -// Charts -// -// Bubble chart implementation: -// Copyright 2015 Pierre-Marc Airoldi -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class BubbleChartRenderer: BarLineScatterCandleBubbleRenderer -{ - /// A nested array of elements ordered logically (i.e not in visual/drawing order) for use with VoiceOver. - private lazy var accessibilityOrderedElements: [[NSUIAccessibilityElement]] = accessibilityCreateEmptyOrderedElements() - - @objc open weak var dataProvider: BubbleChartDataProvider? - - @objc public init(dataProvider: BubbleChartDataProvider, animator: Animator, viewPortHandler: ViewPortHandler) - { - super.init(animator: animator, viewPortHandler: viewPortHandler) - - self.dataProvider = dataProvider - } - - open override func drawData(context: CGContext) - { - guard - let dataProvider = dataProvider, - let bubbleData = dataProvider.bubbleData - else { return } - - // If we redraw the data, remove and repopulate accessible elements to update label values and frames - accessibleChartElements.removeAll() - accessibilityOrderedElements = accessibilityCreateEmptyOrderedElements() - - // Make the chart header the first element in the accessible elements array - if let chart = dataProvider as? BubbleChartView { - let element = createAccessibleHeader(usingChart: chart, - andData: bubbleData, - withDefaultDescription: "Bubble Chart") - accessibleChartElements.append(element) - } - - let sets = bubbleData.dataSets as! [BubbleChartDataSet] - for case let (i, set) in zip(sets.indices, sets) where set.isVisible - { - drawDataSet(context: context, dataSet: set, dataSetIndex: i) - } - - // Merge nested ordered arrays into the single accessibleChartElements. - accessibleChartElements.append(contentsOf: accessibilityOrderedElements.flatMap { $0 } ) - accessibilityPostLayoutChangedNotification() - } - - private func getShapeSize( - entrySize: CGFloat, - maxSize: CGFloat, - reference: CGFloat, - normalizeSize: Bool) -> CGFloat - { - let factor: CGFloat = normalizeSize - ? ((maxSize == 0.0) ? 1.0 : sqrt(entrySize / maxSize)) - : entrySize - let shapeSize: CGFloat = reference * factor - return shapeSize - } - - private var _pointBuffer = CGPoint() - private var _sizeBuffer = [CGPoint](repeating: CGPoint(), count: 2) - - @objc open func drawDataSet(context: CGContext, dataSet: BubbleChartDataSetProtocol, dataSetIndex: Int) - { - guard let dataProvider = dataProvider else { return } - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - - let phaseY = animator.phaseY - - _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) - - let valueToPixelMatrix = trans.valueToPixelMatrix - - _sizeBuffer[0].x = 0.0 - _sizeBuffer[0].y = 0.0 - _sizeBuffer[1].x = 1.0 - _sizeBuffer[1].y = 0.0 - - trans.pointValuesToPixel(&_sizeBuffer) - - context.saveGState() - defer { context.restoreGState() } - - let normalizeSize = dataSet.isNormalizeSizeEnabled - - // calcualte the full width of 1 step on the x-axis - let maxBubbleWidth: CGFloat = abs(_sizeBuffer[1].x - _sizeBuffer[0].x) - let maxBubbleHeight: CGFloat = abs(viewPortHandler.contentBottom - viewPortHandler.contentTop) - let referenceSize: CGFloat = min(maxBubbleHeight, maxBubbleWidth) - - for j in _xBounds - { - guard let entry = dataSet.entryForIndex(j) as? BubbleChartDataEntry else { continue } - - _pointBuffer.x = CGFloat(entry.x) - _pointBuffer.y = CGFloat(entry.y * phaseY) - _pointBuffer = _pointBuffer.applying(valueToPixelMatrix) - - let shapeSize = getShapeSize(entrySize: entry.size, maxSize: dataSet.maxSize, reference: referenceSize, normalizeSize: normalizeSize) - let shapeHalf = shapeSize / 2.0 - - guard - viewPortHandler.isInBoundsTop(_pointBuffer.y + shapeHalf), - viewPortHandler.isInBoundsBottom(_pointBuffer.y - shapeHalf), - viewPortHandler.isInBoundsLeft(_pointBuffer.x + shapeHalf) - else { continue } - - guard viewPortHandler.isInBoundsRight(_pointBuffer.x - shapeHalf) else { break } - - let color = dataSet.color(atIndex: j) - - let rect = CGRect( - x: _pointBuffer.x - shapeHalf, - y: _pointBuffer.y - shapeHalf, - width: shapeSize, - height: shapeSize - ) - - context.setFillColor(color.cgColor) - context.fillEllipse(in: rect) - - // Create and append the corresponding accessibility element to accessibilityOrderedElements - if let chart = dataProvider as? BubbleChartView - { - let element = createAccessibleElement(withIndex: j, - container: chart, - dataSet: dataSet, - dataSetIndex: dataSetIndex, - shapeSize: shapeSize) - { (element) in - element.accessibilityFrame = rect - } - - accessibilityOrderedElements[dataSetIndex].append(element) - } - } - } - - open override func drawValues(context: CGContext) - { - guard let - dataProvider = dataProvider, - let bubbleData = dataProvider.bubbleData, - isDrawingValuesAllowed(dataProvider: dataProvider) - else { return } - - let phaseX = max(0.0, min(1.0, animator.phaseX)) - let phaseY = animator.phaseY - - var pt = CGPoint() - - for i in bubbleData.indices - { - guard let dataSet = bubbleData[i] as? BubbleChartDataSetProtocol, - shouldDrawValues(forDataSet: dataSet) - else - { - continue - } - - let formatter = dataSet.valueFormatter - let alpha = phaseX == 1 ? phaseY : phaseX - - _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - let valueToPixelMatrix = trans.valueToPixelMatrix - - let iconsOffset = dataSet.iconsOffset - - let angleRadians = dataSet.valueLabelAngle.DEG2RAD - - for j in _xBounds - { - guard let e = dataSet.entryForIndex(j) as? BubbleChartDataEntry else { break } - - let valueTextColor = dataSet.valueTextColorAt(j).withAlphaComponent(CGFloat(alpha)) - - pt.x = CGFloat(e.x) - pt.y = CGFloat(e.y * phaseY) - pt = pt.applying(valueToPixelMatrix) - - guard viewPortHandler.isInBoundsRight(pt.x) else { break } - - guard - viewPortHandler.isInBoundsLeft(pt.x), - viewPortHandler.isInBoundsY(pt.y) - else { continue } - - let text = formatter.stringForValue( - Double(e.size), - entry: e, - dataSetIndex: i, - viewPortHandler: viewPortHandler) - - // Larger font for larger bubbles? - let valueFont = dataSet.valueFont - let lineHeight = valueFont.lineHeight - - if dataSet.isDrawValuesEnabled - { - context.drawText(text, - at: CGPoint(x: pt.x, - y: pt.y - (0.5 * lineHeight)), - align: .center, - angleRadians: angleRadians, - attributes: [.font: valueFont, - .foregroundColor: valueTextColor]) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - context.drawImage(icon, - atCenter: CGPoint(x: pt.x + iconsOffset.x, - y: pt.y + iconsOffset.y), - size: icon.size) - } - } - } - } - - open override func drawExtras(context: CGContext) - { - - } - - open override func drawHighlighted(context: CGContext, indices: [Highlight]) - { - guard - let dataProvider = dataProvider, - let bubbleData = dataProvider.bubbleData - else { return } - - context.saveGState() - defer { context.restoreGState() } - - let phaseY = animator.phaseY - - for high in indices - { - guard - let dataSet = bubbleData[high.dataSetIndex] as? BubbleChartDataSetProtocol, - dataSet.isHighlightEnabled, - let entry = dataSet.entryForXValue(high.x, closestToY: high.y) as? BubbleChartDataEntry, - isInBoundsX(entry: entry, dataSet: dataSet) - else { continue } - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - - _sizeBuffer[0].x = 0.0 - _sizeBuffer[0].y = 0.0 - _sizeBuffer[1].x = 1.0 - _sizeBuffer[1].y = 0.0 - - trans.pointValuesToPixel(&_sizeBuffer) - - let normalizeSize = dataSet.isNormalizeSizeEnabled - - // calcualte the full width of 1 step on the x-axis - let maxBubbleWidth: CGFloat = abs(_sizeBuffer[1].x - _sizeBuffer[0].x) - let maxBubbleHeight: CGFloat = abs(viewPortHandler.contentBottom - viewPortHandler.contentTop) - let referenceSize: CGFloat = min(maxBubbleHeight, maxBubbleWidth) - - _pointBuffer.x = CGFloat(entry.x) - _pointBuffer.y = CGFloat(entry.y * phaseY) - trans.pointValueToPixel(&_pointBuffer) - - let shapeSize = getShapeSize(entrySize: entry.size, maxSize: dataSet.maxSize, reference: referenceSize, normalizeSize: normalizeSize) - let shapeHalf = shapeSize / 2.0 - - guard - viewPortHandler.isInBoundsTop(_pointBuffer.y + shapeHalf), - viewPortHandler.isInBoundsBottom(_pointBuffer.y - shapeHalf), - viewPortHandler.isInBoundsLeft(_pointBuffer.x + shapeHalf) - else { continue } - - guard viewPortHandler.isInBoundsRight(_pointBuffer.x - shapeHalf) else { break } - - let originalColor = dataSet.color(atIndex: Int(entry.x)) - - var h: CGFloat = 0.0 - var s: CGFloat = 0.0 - var b: CGFloat = 0.0 - var a: CGFloat = 0.0 - - originalColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a) - - let color = NSUIColor(hue: h, saturation: s, brightness: b * 0.5, alpha: a) - let rect = CGRect( - x: _pointBuffer.x - shapeHalf, - y: _pointBuffer.y - shapeHalf, - width: shapeSize, - height: shapeSize) - - context.setLineWidth(dataSet.highlightCircleWidth) - context.setStrokeColor(color.cgColor) - context.strokeEllipse(in: rect) - - high.setDraw(x: _pointBuffer.x, y: _pointBuffer.y) - } - } - - /// Creates a nested array of empty subarrays each of which will be populated with NSUIAccessibilityElements. - private func accessibilityCreateEmptyOrderedElements() -> [[NSUIAccessibilityElement]] - { - guard let chart = dataProvider as? BubbleChartView else { return [] } - - let dataSetCount = chart.bubbleData?.dataSetCount ?? 0 - - return Array(repeating: [NSUIAccessibilityElement](), - count: dataSetCount) - } - - /// Creates an NSUIAccessibleElement representing individual bubbles location and relative size. - private func createAccessibleElement(withIndex idx: Int, - container: BubbleChartView, - dataSet: BubbleChartDataSetProtocol, - dataSetIndex: Int, - shapeSize: CGFloat, - modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement - { - let element = NSUIAccessibilityElement(accessibilityContainer: container) - let xAxis = container.xAxis - - guard let e = dataSet.entryForIndex(idx) else { return element } - guard let dataProvider = dataProvider else { return element } - - // NOTE: The formatter can cause issues when the x-axis labels are consecutive ints. - // i.e. due to the Double conversion, if there are more than one data set that are grouped, - // there is the possibility of some labels being rounded up. A floor() might fix this, but seems to be a brute force solution. - let label = xAxis.valueFormatter?.stringForValue(e.x, axis: xAxis) ?? "\(e.x)" - - let elementValueText = dataSet.valueFormatter.stringForValue(e.y, - entry: e, - dataSetIndex: dataSetIndex, - viewPortHandler: viewPortHandler) - - let dataSetCount = dataProvider.bubbleData?.dataSetCount ?? -1 - let doesContainMultipleDataSets = dataSetCount > 1 - - element.accessibilityLabel = "\(doesContainMultipleDataSets ? (dataSet.label ?? "") + ", " : "") \(label): \(elementValueText), bubble size: \(String(format: "%.2f", (shapeSize/dataSet.maxSize) * 100)) %" - - modifier(element) - - return element - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/CandleStickChartRenderer.swift b/Pods/Charts/Source/Charts/Renderers/CandleStickChartRenderer.swift deleted file mode 100644 index 40f80efb5..000000000 --- a/Pods/Charts/Source/Charts/Renderers/CandleStickChartRenderer.swift +++ /dev/null @@ -1,417 +0,0 @@ -// -// CandleStickChartRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class CandleStickChartRenderer: LineScatterCandleRadarRenderer -{ - @objc open weak var dataProvider: CandleChartDataProvider? - - @objc public init(dataProvider: CandleChartDataProvider, animator: Animator, viewPortHandler: ViewPortHandler) - { - super.init(animator: animator, viewPortHandler: viewPortHandler) - - self.dataProvider = dataProvider - } - - open override func drawData(context: CGContext) - { - guard let dataProvider = dataProvider, let candleData = dataProvider.candleData else { return } - - // If we redraw the data, remove and repopulate accessible elements to update label values and frames - accessibleChartElements.removeAll() - - // Make the chart header the first element in the accessible elements array - if let chart = dataProvider as? CandleStickChartView { - let element = createAccessibleHeader(usingChart: chart, - andData: candleData, - withDefaultDescription: "CandleStick Chart") - accessibleChartElements.append(element) - } - - for case let set as CandleChartDataSetProtocol in (candleData as ChartData) where set.isVisible - { - drawDataSet(context: context, dataSet: set) - } - } - - private var _shadowPoints = [CGPoint](repeating: CGPoint(), count: 4) - private var _rangePoints = [CGPoint](repeating: CGPoint(), count: 2) - private var _openPoints = [CGPoint](repeating: CGPoint(), count: 2) - private var _closePoints = [CGPoint](repeating: CGPoint(), count: 2) - private var _bodyRect = CGRect() - private var _lineSegments = [CGPoint](repeating: CGPoint(), count: 2) - - @objc open func drawDataSet(context: CGContext, dataSet: CandleChartDataSetProtocol) - { - guard - let dataProvider = dataProvider - else { return } - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - - let phaseY = animator.phaseY - let barSpace = dataSet.barSpace - let showCandleBar = dataSet.showCandleBar - - _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) - - context.saveGState() - - context.setLineWidth(dataSet.shadowWidth) - - for j in _xBounds - { - // get the entry - guard let e = dataSet.entryForIndex(j) as? CandleChartDataEntry else { continue } - - let xPos = e.x - - let open = e.open - let close = e.close - let high = e.high - let low = e.low - - let doesContainMultipleDataSets = (dataProvider.candleData?.count ?? 1) > 1 - var accessibilityMovementDescription = "neutral" - var accessibilityRect = CGRect(x: CGFloat(xPos) + 0.5 - barSpace, - y: CGFloat(low * phaseY), - width: (2 * barSpace) - 1.0, - height: (CGFloat(abs(high - low) * phaseY))) - trans.rectValueToPixel(&accessibilityRect) - - if showCandleBar - { - // calculate the shadow - - _shadowPoints[0].x = CGFloat(xPos) - _shadowPoints[1].x = CGFloat(xPos) - _shadowPoints[2].x = CGFloat(xPos) - _shadowPoints[3].x = CGFloat(xPos) - - if open > close - { - _shadowPoints[0].y = CGFloat(high * phaseY) - _shadowPoints[1].y = CGFloat(open * phaseY) - _shadowPoints[2].y = CGFloat(low * phaseY) - _shadowPoints[3].y = CGFloat(close * phaseY) - } - else if open < close - { - _shadowPoints[0].y = CGFloat(high * phaseY) - _shadowPoints[1].y = CGFloat(close * phaseY) - _shadowPoints[2].y = CGFloat(low * phaseY) - _shadowPoints[3].y = CGFloat(open * phaseY) - } - else - { - _shadowPoints[0].y = CGFloat(high * phaseY) - _shadowPoints[1].y = CGFloat(open * phaseY) - _shadowPoints[2].y = CGFloat(low * phaseY) - _shadowPoints[3].y = _shadowPoints[1].y - } - - trans.pointValuesToPixel(&_shadowPoints) - - // draw the shadows - - var shadowColor: NSUIColor! = nil - if dataSet.shadowColorSameAsCandle - { - if open > close - { - shadowColor = dataSet.decreasingColor ?? dataSet.color(atIndex: j) - } - else if open < close - { - shadowColor = dataSet.increasingColor ?? dataSet.color(atIndex: j) - } - else - { - shadowColor = dataSet.neutralColor ?? dataSet.color(atIndex: j) - } - } - - if shadowColor === nil - { - shadowColor = dataSet.shadowColor ?? dataSet.color(atIndex: j) - } - - context.setStrokeColor(shadowColor.cgColor) - context.strokeLineSegments(between: _shadowPoints) - - // calculate the body - - _bodyRect.origin.x = CGFloat(xPos) - 0.5 + barSpace - _bodyRect.origin.y = CGFloat(close * phaseY) - _bodyRect.size.width = (CGFloat(xPos) + 0.5 - barSpace) - _bodyRect.origin.x - _bodyRect.size.height = CGFloat(open * phaseY) - _bodyRect.origin.y - - trans.rectValueToPixel(&_bodyRect) - - // draw body differently for increasing and decreasing entry - - if open > close - { - accessibilityMovementDescription = "decreasing" - - let color = dataSet.decreasingColor ?? dataSet.color(atIndex: j) - - if dataSet.isDecreasingFilled - { - context.setFillColor(color.cgColor) - context.fill(_bodyRect) - } - else - { - context.setStrokeColor(color.cgColor) - context.stroke(_bodyRect) - } - } - else if open < close - { - accessibilityMovementDescription = "increasing" - - let color = dataSet.increasingColor ?? dataSet.color(atIndex: j) - - if dataSet.isIncreasingFilled - { - context.setFillColor(color.cgColor) - context.fill(_bodyRect) - } - else - { - context.setStrokeColor(color.cgColor) - context.stroke(_bodyRect) - } - } - else - { - let color = dataSet.neutralColor ?? dataSet.color(atIndex: j) - - context.setStrokeColor(color.cgColor) - context.stroke(_bodyRect) - } - } - else - { - _rangePoints[0].x = CGFloat(xPos) - _rangePoints[0].y = CGFloat(high * phaseY) - _rangePoints[1].x = CGFloat(xPos) - _rangePoints[1].y = CGFloat(low * phaseY) - - _openPoints[0].x = CGFloat(xPos) - 0.5 + barSpace - _openPoints[0].y = CGFloat(open * phaseY) - _openPoints[1].x = CGFloat(xPos) - _openPoints[1].y = CGFloat(open * phaseY) - - _closePoints[0].x = CGFloat(xPos) + 0.5 - barSpace - _closePoints[0].y = CGFloat(close * phaseY) - _closePoints[1].x = CGFloat(xPos) - _closePoints[1].y = CGFloat(close * phaseY) - - trans.pointValuesToPixel(&_rangePoints) - trans.pointValuesToPixel(&_openPoints) - trans.pointValuesToPixel(&_closePoints) - - // draw the ranges - var barColor: NSUIColor! = nil - - if open > close - { - accessibilityMovementDescription = "decreasing" - barColor = dataSet.decreasingColor ?? dataSet.color(atIndex: j) - } - else if open < close - { - accessibilityMovementDescription = "increasing" - barColor = dataSet.increasingColor ?? dataSet.color(atIndex: j) - } - else - { - barColor = dataSet.neutralColor ?? dataSet.color(atIndex: j) - } - - context.setStrokeColor(barColor.cgColor) - context.strokeLineSegments(between: _rangePoints) - context.strokeLineSegments(between: _openPoints) - context.strokeLineSegments(between: _closePoints) - } - - let axElement = createAccessibleElement(withIndex: j, - container: dataProvider, - dataSet: dataSet) - { (element) in - element.accessibilityLabel = "\(doesContainMultipleDataSets ? "\(dataSet.label ?? "Dataset")" : "") " + "\(xPos) - \(accessibilityMovementDescription). low: \(low), high: \(high), opening: \(open), closing: \(close)" - element.accessibilityFrame = accessibilityRect - } - - accessibleChartElements.append(axElement) - - } - - // Post this notification to let VoiceOver account for the redrawn frames - accessibilityPostLayoutChangedNotification() - - context.restoreGState() - } - - open override func drawValues(context: CGContext) - { - guard - let dataProvider = dataProvider, - let candleData = dataProvider.candleData - else { return } - - // if values are drawn - if isDrawingValuesAllowed(dataProvider: dataProvider) - { - let phaseY = animator.phaseY - - var pt = CGPoint() - - for i in candleData.indices - { - guard let - dataSet = candleData[i] as? BarLineScatterCandleBubbleChartDataSetProtocol, - shouldDrawValues(forDataSet: dataSet) - else { continue } - - let valueFont = dataSet.valueFont - - let formatter = dataSet.valueFormatter - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - let valueToPixelMatrix = trans.valueToPixelMatrix - - let iconsOffset = dataSet.iconsOffset - - let angleRadians = dataSet.valueLabelAngle.DEG2RAD - - _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) - - let lineHeight = valueFont.lineHeight - let yOffset: CGFloat = lineHeight + 5.0 - - for j in _xBounds - { - guard let e = dataSet.entryForIndex(j) as? CandleChartDataEntry else { break } - - pt.x = CGFloat(e.x) - pt.y = CGFloat(e.high * phaseY) - pt = pt.applying(valueToPixelMatrix) - - if (!viewPortHandler.isInBoundsRight(pt.x)) - { - break - } - - if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y)) - { - continue - } - - if dataSet.isDrawValuesEnabled - { - context.drawText(formatter.stringForValue(e.high, - entry: e, - dataSetIndex: i, - viewPortHandler: viewPortHandler), - at: CGPoint(x: pt.x, - y: pt.y - yOffset), - align: .center, - angleRadians: angleRadians, - attributes: [.font: valueFont, - .foregroundColor: dataSet.valueTextColorAt(j)]) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - context.drawImage(icon, - atCenter: CGPoint(x: pt.x + iconsOffset.x, - y: pt.y + iconsOffset.y), - size: icon.size) - } - } - } - } - } - - open override func drawExtras(context: CGContext) - { - } - - open override func drawHighlighted(context: CGContext, indices: [Highlight]) - { - guard - let dataProvider = dataProvider, - let candleData = dataProvider.candleData - else { return } - - context.saveGState() - - for high in indices - { - guard - let set = candleData[high.dataSetIndex] as? CandleChartDataSetProtocol, - set.isHighlightEnabled - else { continue } - - guard let e = set.entryForXValue(high.x, closestToY: high.y) as? CandleChartDataEntry else { continue } - - if !isInBoundsX(entry: e, dataSet: set) - { - continue - } - - let trans = dataProvider.getTransformer(forAxis: set.axisDependency) - - context.setStrokeColor(set.highlightColor.cgColor) - context.setLineWidth(set.highlightLineWidth) - - if set.highlightLineDashLengths != nil - { - context.setLineDash(phase: set.highlightLineDashPhase, lengths: set.highlightLineDashLengths!) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - let lowValue = e.low * Double(animator.phaseY) - let highValue = e.high * Double(animator.phaseY) - let y = (lowValue + highValue) / 2.0 - - let pt = trans.pixelForValues(x: e.x, y: y) - - high.setDraw(pt: pt) - - // draw the lines - drawHighlightLines(context: context, point: pt, set: set) - } - - context.restoreGState() - } - - private func createAccessibleElement(withIndex idx: Int, - container: CandleChartDataProvider, - dataSet: CandleChartDataSetProtocol, - modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement { - - let element = NSUIAccessibilityElement(accessibilityContainer: container) - - // The modifier allows changing of traits and frame depending on highlight, rotation, etc - modifier(element) - - return element - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/CombinedChartRenderer.swift b/Pods/Charts/Source/Charts/Renderers/CombinedChartRenderer.swift deleted file mode 100644 index de6121df6..000000000 --- a/Pods/Charts/Source/Charts/Renderers/CombinedChartRenderer.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// CombinedChartRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class CombinedChartRenderer: NSObject, DataRenderer -{ - public let viewPortHandler: ViewPortHandler - - public final var accessibleChartElements: [NSUIAccessibilityElement] = [] - - public let animator: Animator - - @objc open weak var chart: CombinedChartView? - - /// if set to true, all values are drawn above their bars, instead of below their top - @objc open var drawValueAboveBarEnabled = true - - /// if set to true, a grey area is drawn behind each bar that indicates the maximum value - @objc open var drawBarShadowEnabled = false - - internal var _renderers = [DataRenderer]() - - internal var _drawOrder: [CombinedChartView.DrawOrder] = [.bar, .bubble, .line, .candle, .scatter] - - @objc public init(chart: CombinedChartView, animator: Animator, viewPortHandler: ViewPortHandler) - { - self.chart = chart - self.viewPortHandler = viewPortHandler - self.animator = animator - - super.init() - - createRenderers() - } - - /// Creates the renderers needed for this combined-renderer in the required order. Also takes the DrawOrder into consideration. - internal func createRenderers() - { - _renderers = [DataRenderer]() - - guard let chart = chart else { return } - - for order in drawOrder - { - switch (order) - { - case .bar: - if chart.barData !== nil - { - _renderers.append(BarChartRenderer(dataProvider: chart, animator: animator, viewPortHandler: viewPortHandler)) - } - - case .line: - if chart.lineData !== nil - { - _renderers.append(LineChartRenderer(dataProvider: chart, animator: animator, viewPortHandler: viewPortHandler)) - } - - case .candle: - if chart.candleData !== nil - { - _renderers.append(CandleStickChartRenderer(dataProvider: chart, animator: animator, viewPortHandler: viewPortHandler)) - } - - case .scatter: - if chart.scatterData !== nil - { - _renderers.append(ScatterChartRenderer(dataProvider: chart, animator: animator, viewPortHandler: viewPortHandler)) - } - - case .bubble: - if chart.bubbleData !== nil - { - _renderers.append(BubbleChartRenderer(dataProvider: chart, animator: animator, viewPortHandler: viewPortHandler)) - } - } - } - - } - - open func initBuffers() - { - _renderers.forEach { $0.initBuffers() } - } - - open func drawData(context: CGContext) - { - // If we redraw the data, remove and repopulate accessible elements to update label values and frames - accessibleChartElements.removeAll() - - if - let combinedChart = chart, - let data = combinedChart.data { - // Make the chart header the first element in the accessible elements array - let element = createAccessibleHeader(usingChart: combinedChart, - andData: data, - withDefaultDescription: "Combined Chart") - accessibleChartElements.append(element) - } - - // TODO: Due to the potential complexity of data presented in Combined charts, a more usable way - // for VO accessibility would be to use axis based traversal rather than by dataset. - // Hence, accessibleChartElements is not populated below. (Individual renderers guard against dataSource being their respective views) - _renderers.forEach { $0.drawData(context: context) } - } - - open func drawValues(context: CGContext) - { - _renderers.forEach { $0.drawValues(context: context) } - } - - open func drawExtras(context: CGContext) - { - _renderers.forEach { $0.drawExtras(context: context) } - } - - open func drawHighlighted(context: CGContext, indices: [Highlight]) - { - for renderer in _renderers - { - var data: ChartData? - - if renderer is BarChartRenderer - { - data = (renderer as! BarChartRenderer).dataProvider?.barData - } - else if renderer is LineChartRenderer - { - data = (renderer as! LineChartRenderer).dataProvider?.lineData - } - else if renderer is CandleStickChartRenderer - { - data = (renderer as! CandleStickChartRenderer).dataProvider?.candleData - } - else if renderer is ScatterChartRenderer - { - data = (renderer as! ScatterChartRenderer).dataProvider?.scatterData - } - else if renderer is BubbleChartRenderer - { - data = (renderer as! BubbleChartRenderer).dataProvider?.bubbleData - } - - let dataIndex = data == nil ? nil : (chart?.data as? CombinedChartData)?.allData.firstIndex(of: data!) - - let dataIndices = indices.filter{ $0.dataIndex == dataIndex || $0.dataIndex == -1 } - - renderer.drawHighlighted(context: context, indices: dataIndices) - } - } - - open func isDrawingValuesAllowed(dataProvider: ChartDataProvider?) -> Bool - { - guard let data = dataProvider?.data else { return false } - return data.entryCount < Int(CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleX) - } - - /// All sub-renderers. - @objc open var subRenderers: [DataRenderer] - { - get { return _renderers } - set { _renderers = newValue } - } - - // MARK: Accessors - - /// `true` if drawing values above bars is enabled, `false` ifnot - @objc open var isDrawValueAboveBarEnabled: Bool { return drawValueAboveBarEnabled } - - /// `true` if drawing shadows (maxvalue) for each bar is enabled, `false` ifnot - @objc open var isDrawBarShadowEnabled: Bool { return drawBarShadowEnabled } - - /// the order in which the provided data objects should be drawn. - /// The earlier you place them in the provided array, the further they will be in the background. - /// e.g. if you provide [DrawOrder.Bar, DrawOrder.Line], the bars will be drawn behind the lines. - open var drawOrder: [CombinedChartView.DrawOrder] - { - get { _drawOrder } - set - { - if !newValue.isEmpty - { - _drawOrder = newValue - } - } - } - - public func createAccessibleHeader(usingChart chart: ChartViewBase, andData data: ChartData, withDefaultDescription defaultDescription: String) -> NSUIAccessibilityElement { - return AccessibleHeader.create(usingChart: chart, andData: data, withDefaultDescription: defaultDescription) - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/DataRenderer.swift b/Pods/Charts/Source/Charts/Renderers/DataRenderer.swift deleted file mode 100644 index e417b4280..000000000 --- a/Pods/Charts/Source/Charts/Renderers/DataRenderer.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// DataRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartDataRenderer) -public protocol DataRenderer: Renderer -{ - /// An array of accessibility elements that are presented to the ChartViewBase accessibility methods. - /// - /// Note that the order of elements in this array determines the order in which they are presented and navigated by - /// Accessibility clients such as VoiceOver. - /// - /// Renderers should ensure that the order of elements makes sense to a client presenting an audio-only interface to a user. - /// Subclasses should populate this array in drawData() or drawDataSet() to make the chart accessible. - var accessibleChartElements: [NSUIAccessibilityElement] { get } - - var animator: Animator { get } - - func drawData(context: CGContext) - - func drawValues(context: CGContext) - - func drawExtras(context: CGContext) - - /// Draws all highlight indicators for the values that are currently highlighted. - /// - /// - Parameters: - /// - indices: the highlighted values - func drawHighlighted(context: CGContext, indices: [Highlight]) - - /// An opportunity for initializing internal buffers used for rendering with a new size. - /// Since this might do memory allocations, it should only be called if necessary. - func initBuffers() - - func isDrawingValuesAllowed(dataProvider: ChartDataProvider?) -> Bool - - /// Creates an ```NSUIAccessibilityElement``` that acts as the first and primary header describing a chart view. - /// - /// - Parameters: - /// - chart: The chartView object being described - /// - data: A non optional data source about the chart - /// - defaultDescription: A simple string describing the type/design of Chart. - /// - Returns: A header ```NSUIAccessibilityElement``` that can be added to accessibleChartElements. - func createAccessibleHeader(usingChart chart: ChartViewBase, - andData data: ChartData, - withDefaultDescription defaultDescription: String) -> NSUIAccessibilityElement -} - -internal struct AccessibleHeader { - static func create(usingChart chart: ChartViewBase, - andData data: ChartData, - withDefaultDescription defaultDescription: String = "Chart") -> NSUIAccessibilityElement - { - let chartDescriptionText = chart.chartDescription.text ?? defaultDescription - let dataSetDescriptions = data.map { $0.label ?? "" } - let dataSetDescriptionText = dataSetDescriptions.joined(separator: ", ") - - let element = NSUIAccessibilityElement(accessibilityContainer: chart) - element.accessibilityLabel = chartDescriptionText + ". \(data.count) dataset\(data.count == 1 ? "" : "s"). \(dataSetDescriptionText)" - element.accessibilityFrame = chart.bounds - element.isHeader = true - - return element - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/HorizontalBarChartRenderer.swift b/Pods/Charts/Source/Charts/Renderers/HorizontalBarChartRenderer.swift deleted file mode 100644 index d2fed814f..000000000 --- a/Pods/Charts/Source/Charts/Renderers/HorizontalBarChartRenderer.swift +++ /dev/null @@ -1,627 +0,0 @@ -// -// HorizontalBarChartRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -#if !os(OSX) - import UIKit -#endif - - -open class HorizontalBarChartRenderer: BarChartRenderer -{ - private class Buffer - { - var rects = [CGRect]() - } - - public override init(dataProvider: BarChartDataProvider, animator: Animator, viewPortHandler: ViewPortHandler) - { - super.init(dataProvider: dataProvider, animator: animator, viewPortHandler: viewPortHandler) - } - - // [CGRect] per dataset - private var _buffers = [Buffer]() - - open override func initBuffers() - { - if let barData = dataProvider?.barData - { - // Matche buffers count to dataset count - if _buffers.count != barData.count - { - while _buffers.count < barData.count - { - _buffers.append(Buffer()) - } - while _buffers.count > barData.count - { - _buffers.removeLast() - } - } - - for i in barData.indices - { - let set = barData[i] as! BarChartDataSetProtocol - let size = set.entryCount * (set.isStacked ? set.stackSize : 1) - if _buffers[i].rects.count != size - { - _buffers[i].rects = [CGRect](repeating: CGRect(), count: size) - } - } - } - else - { - _buffers.removeAll() - } - } - - private func prepareBuffer(dataSet: BarChartDataSetProtocol, index: Int) - { - guard let - dataProvider = dataProvider, - let barData = dataProvider.barData - else { return } - - let barWidthHalf = barData.barWidth / 2.0 - - let buffer = _buffers[index] - var bufferIndex = 0 - let containsStacks = dataSet.isStacked - - let isInverted = dataProvider.isInverted(axis: dataSet.axisDependency) - let phaseY = animator.phaseY - var barRect = CGRect() - var x: Double - var y: Double - - for i in stride(from: 0, to: min(Int(ceil(Double(dataSet.entryCount) * animator.phaseX)), dataSet.entryCount), by: 1) - { - guard let e = dataSet.entryForIndex(i) as? BarChartDataEntry else { continue } - - let vals = e.yValues - - x = e.x - y = e.y - - if !containsStacks || vals == nil - { - let bottom = CGFloat(x - barWidthHalf) - let top = CGFloat(x + barWidthHalf) - var right = isInverted - ? (y <= 0.0 ? CGFloat(y) : 0) - : (y >= 0.0 ? CGFloat(y) : 0) - var left = isInverted - ? (y >= 0.0 ? CGFloat(y) : 0) - : (y <= 0.0 ? CGFloat(y) : 0) - - // multiply the height of the rect with the phase - if right > 0 - { - right *= CGFloat(phaseY) - } - else - { - left *= CGFloat(phaseY) - } - - barRect.origin.x = left - barRect.size.width = right - left - barRect.origin.y = top - barRect.size.height = bottom - top - - buffer.rects[bufferIndex] = barRect - bufferIndex += 1 - } - else - { - var posY = 0.0 - var negY = -e.negativeSum - var yStart = 0.0 - - // fill the stack - for k in vals!.indices - { - let value = vals![k] - - if value == 0.0 && (posY == 0.0 || negY == 0.0) - { - // Take care of the situation of a 0.0 value, which overlaps a non-zero bar - y = value - yStart = y - } - else if value >= 0.0 - { - y = posY - yStart = posY + value - posY = yStart - } - else - { - y = negY - yStart = negY + abs(value) - negY += abs(value) - } - - let bottom = CGFloat(x - barWidthHalf) - let top = CGFloat(x + barWidthHalf) - var right = isInverted - ? (y <= yStart ? CGFloat(y) : CGFloat(yStart)) - : (y >= yStart ? CGFloat(y) : CGFloat(yStart)) - var left = isInverted - ? (y >= yStart ? CGFloat(y) : CGFloat(yStart)) - : (y <= yStart ? CGFloat(y) : CGFloat(yStart)) - - // multiply the height of the rect with the phase - right *= CGFloat(phaseY) - left *= CGFloat(phaseY) - - barRect.origin.x = left - barRect.size.width = right - left - barRect.origin.y = top - barRect.size.height = bottom - top - - buffer.rects[bufferIndex] = barRect - bufferIndex += 1 - } - } - } - } - - private var _barShadowRectBuffer: CGRect = CGRect() - - open override func drawDataSet(context: CGContext, dataSet: BarChartDataSetProtocol, index: Int) - { - guard let dataProvider = dataProvider else { return } - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - - prepareBuffer(dataSet: dataSet, index: index) - trans.rectValuesToPixel(&_buffers[index].rects) - - let borderWidth = dataSet.barBorderWidth - let borderColor = dataSet.barBorderColor - let drawBorder = borderWidth > 0.0 - - context.saveGState() - - // draw the bar shadow before the values - if dataProvider.isDrawBarShadowEnabled - { - guard let barData = dataProvider.barData else { return } - - let barWidth = barData.barWidth - let barWidthHalf = barWidth / 2.0 - var x: Double = 0.0 - - for i in stride(from: 0, to: min(Int(ceil(Double(dataSet.entryCount) * animator.phaseX)), dataSet.entryCount), by: 1) - { - guard let e = dataSet.entryForIndex(i) as? BarChartDataEntry else { continue } - - x = e.x - - _barShadowRectBuffer.origin.y = CGFloat(x - barWidthHalf) - _barShadowRectBuffer.size.height = CGFloat(barWidth) - - trans.rectValueToPixel(&_barShadowRectBuffer) - - if !viewPortHandler.isInBoundsTop(_barShadowRectBuffer.origin.y + _barShadowRectBuffer.size.height) - { - break - } - - if !viewPortHandler.isInBoundsBottom(_barShadowRectBuffer.origin.y) - { - continue - } - - _barShadowRectBuffer.origin.x = viewPortHandler.contentLeft - _barShadowRectBuffer.size.width = viewPortHandler.contentWidth - - context.setFillColor(dataSet.barShadowColor.cgColor) - context.fill(_barShadowRectBuffer) - } - } - - let buffer = _buffers[index] - - let isSingleColor = dataSet.colors.count == 1 - - if isSingleColor - { - context.setFillColor(dataSet.color(atIndex: 0).cgColor) - } - - // In case the chart is stacked, we need to accomodate individual bars within accessibilityOrdereredElements - let isStacked = dataSet.isStacked - let stackSize = isStacked ? dataSet.stackSize : 1 - - for j in buffer.rects.indices - { - let barRect = buffer.rects[j] - - if (!viewPortHandler.isInBoundsTop(barRect.origin.y + barRect.size.height)) - { - break - } - - if (!viewPortHandler.isInBoundsBottom(barRect.origin.y)) - { - continue - } - - if !isSingleColor - { - // Set the color for the currently drawn value. If the index is out of bounds, reuse colors. - context.setFillColor(dataSet.color(atIndex: j).cgColor) - } - - context.fill(barRect) - - if drawBorder - { - context.setStrokeColor(borderColor.cgColor) - context.setLineWidth(borderWidth) - context.stroke(barRect) - } - - // Create and append the corresponding accessibility element to accessibilityOrderedElements (see BarChartRenderer) - if let chart = dataProvider as? BarChartView - { - let element = createAccessibleElement(withIndex: j, - container: chart, - dataSet: dataSet, - dataSetIndex: index, - stackSize: stackSize) - { (element) in - element.accessibilityFrame = barRect - } - - accessibilityOrderedElements[j/stackSize].append(element) - } - } - - context.restoreGState() - } - - open override func prepareBarHighlight( - x: Double, - y1: Double, - y2: Double, - barWidthHalf: Double, - trans: Transformer, - rect: inout CGRect) - { - let top = x - barWidthHalf - let bottom = x + barWidthHalf - let left = y1 - let right = y2 - - rect.origin.x = CGFloat(left) - rect.origin.y = CGFloat(top) - rect.size.width = CGFloat(right - left) - rect.size.height = CGFloat(bottom - top) - - trans.rectValueToPixelHorizontal(&rect, phaseY: animator.phaseY) - } - - open override func drawValues(context: CGContext) - { - // if values are drawn - if isDrawingValuesAllowed(dataProvider: dataProvider) - { - guard - let dataProvider = dataProvider, - let barData = dataProvider.barData - else { return } - - let textAlign = TextAlignment.left - - let valueOffsetPlus: CGFloat = 5.0 - var posOffset: CGFloat - var negOffset: CGFloat - let drawValueAboveBar = dataProvider.isDrawValueAboveBarEnabled - - for dataSetIndex in barData.indices - { - guard let - dataSet = barData[dataSetIndex] as? BarChartDataSetProtocol, - shouldDrawValues(forDataSet: dataSet) - else { continue } - - let angleRadians = dataSet.valueLabelAngle.DEG2RAD - - let isInverted = dataProvider.isInverted(axis: dataSet.axisDependency) - - let valueFont = dataSet.valueFont - let yOffset = -valueFont.lineHeight / 2.0 - - let formatter = dataSet.valueFormatter - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - - let phaseY = animator.phaseY - - let iconsOffset = dataSet.iconsOffset - - let buffer = _buffers[dataSetIndex] - - // if only single values are drawn (sum) - if !dataSet.isStacked - { - for j in 0 ..< Int(ceil(Double(dataSet.entryCount) * animator.phaseX)) - { - guard let e = dataSet.entryForIndex(j) as? BarChartDataEntry else { continue } - - let rect = buffer.rects[j] - - let y = rect.origin.y + rect.size.height / 2.0 - - if !viewPortHandler.isInBoundsTop(rect.origin.y) - { - break - } - - if !viewPortHandler.isInBoundsX(rect.origin.x) - { - continue - } - - if !viewPortHandler.isInBoundsBottom(rect.origin.y) - { - continue - } - - let val = e.y - let valueText = formatter.stringForValue( - val, - entry: e, - dataSetIndex: dataSetIndex, - viewPortHandler: viewPortHandler) - - // calculate the correct offset depending on the draw position of the value - let valueTextWidth = valueText.size(withAttributes: [.font: valueFont]).width - posOffset = (drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus)) - negOffset = (drawValueAboveBar ? -(valueTextWidth + valueOffsetPlus) : valueOffsetPlus) - rect.size.width - - if isInverted - { - posOffset = -posOffset - valueTextWidth - negOffset = -negOffset - valueTextWidth - } - - if dataSet.isDrawValuesEnabled - { - drawValue( - context: context, - value: valueText, - xPos: (rect.origin.x + rect.size.width) - + (val >= 0.0 ? posOffset : negOffset), - yPos: y + yOffset, - font: valueFont, - align: textAlign, - color: dataSet.valueTextColorAt(j), - anchor: CGPoint.zero, - angleRadians: angleRadians) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - var px = (rect.origin.x + rect.size.width) - + (val >= 0.0 ? posOffset : negOffset) - var py = y - - px += iconsOffset.x - py += iconsOffset.y - - context.drawImage(icon, - atCenter: CGPoint(x: px, y: py), - size: icon.size) - } - } - } - else - { - // if each value of a potential stack should be drawn - - var bufferIndex = 0 - - for index in 0 ..< Int(ceil(Double(dataSet.entryCount) * animator.phaseX)) - { - guard let e = dataSet.entryForIndex(index) as? BarChartDataEntry else { continue } - - let rect = buffer.rects[bufferIndex] - - let vals = e.yValues - - // we still draw stacked bars, but there is one non-stacked in between - if vals == nil - { - if !viewPortHandler.isInBoundsTop(rect.origin.y) - { - break - } - - if !viewPortHandler.isInBoundsX(rect.origin.x) - { - continue - } - - if !viewPortHandler.isInBoundsBottom(rect.origin.y) - { - continue - } - - let val = e.y - let valueText = formatter.stringForValue( - val, - entry: e, - dataSetIndex: dataSetIndex, - viewPortHandler: viewPortHandler) - - // calculate the correct offset depending on the draw position of the value - let valueTextWidth = valueText.size(withAttributes: [NSAttributedString.Key.font: valueFont]).width - posOffset = (drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus)) - negOffset = (drawValueAboveBar ? -(valueTextWidth + valueOffsetPlus) : valueOffsetPlus) - - if isInverted - { - posOffset = -posOffset - valueTextWidth - negOffset = -negOffset - valueTextWidth - } - - if dataSet.isDrawValuesEnabled - { - drawValue( - context: context, - value: valueText, - xPos: (rect.origin.x + rect.size.width) - + (val >= 0.0 ? posOffset : negOffset), - yPos: rect.origin.y + yOffset, - font: valueFont, - align: textAlign, - color: dataSet.valueTextColorAt(index), - anchor: CGPoint.zero, - angleRadians: angleRadians) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - var px = (rect.origin.x + rect.size.width) - + (val >= 0.0 ? posOffset : negOffset) - var py = rect.origin.y - - px += iconsOffset.x - py += iconsOffset.y - - context.drawImage(icon, - atCenter: CGPoint(x: px, y: py), - size: icon.size) - } - } - else - { - let vals = vals! - var transformed = [CGPoint]() - - var posY = 0.0 - var negY = -e.negativeSum - - for k in vals.indices - { - let value = vals[k] - var y: Double - - if value == 0.0 && (posY == 0.0 || negY == 0.0) - { - // Take care of the situation of a 0.0 value, which overlaps a non-zero bar - y = value - } - else if value >= 0.0 - { - posY += value - y = posY - } - else - { - y = negY - negY -= value - } - - transformed.append(CGPoint(x: CGFloat(y * phaseY), y: 0.0)) - } - - trans.pointValuesToPixel(&transformed) - - for k in transformed.indices - { - let val = vals[k] - let valueText = formatter.stringForValue( - val, - entry: e, - dataSetIndex: dataSetIndex, - viewPortHandler: viewPortHandler) - - // calculate the correct offset depending on the draw position of the value - let valueTextWidth = valueText.size(withAttributes: [.font: valueFont]).width - posOffset = (drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus)) - negOffset = (drawValueAboveBar ? -(valueTextWidth + valueOffsetPlus) : valueOffsetPlus) - - if isInverted - { - posOffset = -posOffset - valueTextWidth - negOffset = -negOffset - valueTextWidth - } - - let drawBelow = (val == 0.0 && negY == 0.0 && posY > 0.0) || val < 0.0 - - let x = transformed[k].x + (drawBelow ? negOffset : posOffset) - let y = rect.origin.y + rect.size.height / 2.0 - - if (!viewPortHandler.isInBoundsTop(y)) - { - break - } - - if (!viewPortHandler.isInBoundsX(x)) - { - continue - } - - if (!viewPortHandler.isInBoundsBottom(y)) - { - continue - } - - if dataSet.isDrawValuesEnabled - { - drawValue(context: context, - value: valueText, - xPos: x, - yPos: y + yOffset, - font: valueFont, - align: textAlign, - color: dataSet.valueTextColorAt(index), - anchor: CGPoint.zero, - angleRadians: angleRadians) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - context.drawImage(icon, - atCenter: CGPoint(x: x + iconsOffset.x, - y: y + iconsOffset.y), - size: icon.size) - } - } - } - - bufferIndex += vals?.count ?? 1 - } - } - } - } - } - - open override func isDrawingValuesAllowed(dataProvider: ChartDataProvider?) -> Bool - { - guard let data = dataProvider?.data - else { return false } - return data.entryCount < Int(CGFloat(dataProvider?.maxVisibleCount ?? 0) * self.viewPortHandler.scaleY) - } - - /// Sets the drawing position of the highlight object based on the riven bar-rect. - internal override func setHighlightDrawPos(highlight high: Highlight, barRect: CGRect) - { - high.setDraw(x: barRect.midY, y: barRect.origin.x + barRect.size.width) - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/LegendRenderer.swift b/Pods/Charts/Source/Charts/Renderers/LegendRenderer.swift deleted file mode 100755 index 41fd499db..000000000 --- a/Pods/Charts/Source/Charts/Renderers/LegendRenderer.swift +++ /dev/null @@ -1,553 +0,0 @@ -// -// LegendRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartLegendRenderer) -open class LegendRenderer: NSObject, Renderer -{ - @objc public let viewPortHandler: ViewPortHandler - - /// the legend object this renderer renders - @objc open var legend: Legend? - - @objc public init(viewPortHandler: ViewPortHandler, legend: Legend?) - { - self.viewPortHandler = viewPortHandler - self.legend = legend - - super.init() - } - - /// Prepares the legend and calculates all needed forms, labels and colors. - @objc open func computeLegend(data: ChartData) - { - guard let legend = legend else { return } - - if !legend.isLegendCustom - { - var entries: [LegendEntry] = [] - - // loop for building up the colors and labels used in the legend - for dataSet in data - { - let clrs: [NSUIColor] = dataSet.colors - let entryCount = dataSet.entryCount - - // if we have a barchart with stacked bars - if dataSet is BarChartDataSetProtocol && - (dataSet as! BarChartDataSetProtocol).isStacked - { - let bds = dataSet as! BarChartDataSetProtocol - let sLabels = bds.stackLabels - let minEntries = min(clrs.count, bds.stackSize) - - for j in 0.. 0 - { - let labelIndex = j % minEntries - label = sLabels.indices.contains(labelIndex) ? sLabels[labelIndex] : nil - } - else - { - label = nil - } - - let entry = LegendEntry(label: label) - entry.form = dataSet.form - entry.formSize = dataSet.formSize - entry.formLineWidth = dataSet.formLineWidth - entry.formLineDashPhase = dataSet.formLineDashPhase - entry.formLineDashLengths = dataSet.formLineDashLengths - entry.formColor = clrs[j] - - entries.append(entry) - } - - if dataSet.label != nil - { - // add the legend description label - let entry = LegendEntry(label: dataSet.label) - entry.form = .none - - entries.append(entry) - } - } - else if dataSet is PieChartDataSetProtocol - { - let pds = dataSet as! PieChartDataSetProtocol - - for j in 0..= 1 - { - var prevDx: CGFloat = 0.0 - var prevDy: CGFloat = 0.0 - var curDx: CGFloat = 0.0 - var curDy: CGFloat = 0.0 - - // Take an extra point from the left, and an extra from the right. - // That's because we need 4 points for a cubic bezier (cubic=4), otherwise we get lines moving and doing weird stuff on the edges of the chart. - // So in the starting `prev` and `cur`, go -2, -1 - - let firstIndex = _xBounds.min + 1 - - var prevPrev: ChartDataEntry! = nil - var prev: ChartDataEntry! = dataSet.entryForIndex(max(firstIndex - 2, 0)) - var cur: ChartDataEntry! = dataSet.entryForIndex(max(firstIndex - 1, 0)) - var next: ChartDataEntry! = cur - var nextIndex: Int = -1 - - if cur == nil { return } - - // let the spline start - cubicPath.move(to: CGPoint(x: CGFloat(cur.x), y: CGFloat(cur.y * phaseY)), transform: valueToPixelMatrix) - - for j in _xBounds.dropFirst() // same as firstIndex - { - prevPrev = prev - prev = cur - cur = nextIndex == j ? next : dataSet.entryForIndex(j) - - nextIndex = j + 1 < dataSet.entryCount ? j + 1 : j - next = dataSet.entryForIndex(nextIndex) - - if next == nil { break } - - prevDx = CGFloat(cur.x - prevPrev.x) * intensity - prevDy = CGFloat(cur.y - prevPrev.y) * intensity - curDx = CGFloat(next.x - prev.x) * intensity - curDy = CGFloat(next.y - prev.y) * intensity - - cubicPath.addCurve( - to: CGPoint( - x: CGFloat(cur.x), - y: CGFloat(cur.y) * CGFloat(phaseY)), - control1: CGPoint( - x: CGFloat(prev.x) + prevDx, - y: (CGFloat(prev.y) + prevDy) * CGFloat(phaseY)), - control2: CGPoint( - x: CGFloat(cur.x) - curDx, - y: (CGFloat(cur.y) - curDy) * CGFloat(phaseY)), - transform: valueToPixelMatrix) - } - } - - context.saveGState() - defer { context.restoreGState() } - - if dataSet.isDrawFilledEnabled - { - // Copy this path because we make changes to it - let fillPath = cubicPath.mutableCopy() - - drawCubicFill(context: context, dataSet: dataSet, spline: fillPath!, matrix: valueToPixelMatrix, bounds: _xBounds) - } - - if dataSet.isDrawLineWithGradientEnabled - { - drawGradientLine(context: context, dataSet: dataSet, spline: cubicPath, matrix: valueToPixelMatrix) - } - else - { - drawLine(context: context, spline: cubicPath, drawingColor: drawingColor) - } - } - - @objc open func drawHorizontalBezier(context: CGContext, dataSet: LineChartDataSetProtocol) - { - guard let dataProvider = dataProvider else { return } - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - - let phaseY = animator.phaseY - - _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) - - // get the color that is specified for this position from the DataSet - let drawingColor = dataSet.colors.first! - - // the path for the cubic-spline - let cubicPath = CGMutablePath() - - let valueToPixelMatrix = trans.valueToPixelMatrix - - if _xBounds.range >= 1 - { - var prev: ChartDataEntry! = dataSet.entryForIndex(_xBounds.min) - var cur: ChartDataEntry! = prev - - if cur == nil { return } - - // let the spline start - cubicPath.move(to: CGPoint(x: CGFloat(cur.x), y: CGFloat(cur.y * phaseY)), transform: valueToPixelMatrix) - - for j in _xBounds.dropFirst() - { - prev = cur - cur = dataSet.entryForIndex(j) - - let cpx = CGFloat(prev.x + (cur.x - prev.x) / 2.0) - - cubicPath.addCurve( - to: CGPoint( - x: CGFloat(cur.x), - y: CGFloat(cur.y * phaseY)), - control1: CGPoint( - x: cpx, - y: CGFloat(prev.y * phaseY)), - control2: CGPoint( - x: cpx, - y: CGFloat(cur.y * phaseY)), - transform: valueToPixelMatrix) - } - } - - context.saveGState() - defer { context.restoreGState() } - - if dataSet.isDrawFilledEnabled - { - // Copy this path because we make changes to it - let fillPath = cubicPath.mutableCopy() - - drawCubicFill(context: context, dataSet: dataSet, spline: fillPath!, matrix: valueToPixelMatrix, bounds: _xBounds) - } - - if dataSet.isDrawLineWithGradientEnabled - { - drawGradientLine(context: context, dataSet: dataSet, spline: cubicPath, matrix: valueToPixelMatrix) - } - else - { - drawLine(context: context, spline: cubicPath, drawingColor: drawingColor) - } - } - - open func drawCubicFill( - context: CGContext, - dataSet: LineChartDataSetProtocol, - spline: CGMutablePath, - matrix: CGAffineTransform, - bounds: XBounds) - { - guard - let dataProvider = dataProvider - else { return } - - if bounds.range <= 0 - { - return - } - - let fillMin = dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0 - - var pt1 = CGPoint(x: CGFloat(dataSet.entryForIndex(bounds.min + bounds.range)?.x ?? 0.0), y: fillMin) - var pt2 = CGPoint(x: CGFloat(dataSet.entryForIndex(bounds.min)?.x ?? 0.0), y: fillMin) - pt1 = pt1.applying(matrix) - pt2 = pt2.applying(matrix) - - spline.addLine(to: pt1) - spline.addLine(to: pt2) - spline.closeSubpath() - - if dataSet.fill != nil - { - drawFilledPath(context: context, path: spline, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha) - } - else - { - drawFilledPath(context: context, path: spline, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha) - } - } - - private var _lineSegments = [CGPoint](repeating: CGPoint(), count: 2) - - @objc open func drawLinear(context: CGContext, dataSet: LineChartDataSetProtocol) - { - guard let dataProvider = dataProvider else { return } - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - - let valueToPixelMatrix = trans.valueToPixelMatrix - - let entryCount = dataSet.entryCount - let isDrawSteppedEnabled = dataSet.mode == .stepped - let pointsPerEntryPair = isDrawSteppedEnabled ? 4 : 2 - - let phaseY = animator.phaseY - - _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) - - // if drawing filled is enabled - if dataSet.isDrawFilledEnabled && entryCount > 0 - { - drawLinearFill(context: context, dataSet: dataSet, trans: trans, bounds: _xBounds) - } - - context.saveGState() - defer { context.restoreGState() } - - // more than 1 color - if dataSet.colors.count > 1, !dataSet.isDrawLineWithGradientEnabled - { - if _lineSegments.count != pointsPerEntryPair - { - // Allocate once in correct size - _lineSegments = [CGPoint](repeating: CGPoint(), count: pointsPerEntryPair) - } - - for j in _xBounds.dropLast() - { - var e: ChartDataEntry! = dataSet.entryForIndex(j) - - if e == nil { continue } - - _lineSegments[0].x = CGFloat(e.x) - _lineSegments[0].y = CGFloat(e.y * phaseY) - - if j < _xBounds.max - { - // TODO: remove the check. - // With the new XBounds iterator, j is always smaller than _xBounds.max - // Keeping this check for a while, if xBounds have no further breaking changes, it should be safe to remove the check - e = dataSet.entryForIndex(j + 1) - - if e == nil { break } - - if isDrawSteppedEnabled - { - _lineSegments[1] = CGPoint(x: CGFloat(e.x), y: _lineSegments[0].y) - _lineSegments[2] = _lineSegments[1] - _lineSegments[3] = CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)) - } - else - { - _lineSegments[1] = CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)) - } - } - else - { - _lineSegments[1] = _lineSegments[0] - } - - _lineSegments = _lineSegments.map { $0.applying(valueToPixelMatrix) } - - if (!viewPortHandler.isInBoundsRight(_lineSegments[0].x)) - { - break - } - - // Determine the start and end coordinates of the line, and make sure they differ. - guard - let firstCoordinate = _lineSegments.first, - let lastCoordinate = _lineSegments.last, - firstCoordinate != lastCoordinate else { continue } - - // make sure the lines don't do shitty things outside bounds - if !viewPortHandler.isInBoundsLeft(lastCoordinate.x) || - !viewPortHandler.isInBoundsTop(max(firstCoordinate.y, lastCoordinate.y)) || - !viewPortHandler.isInBoundsBottom(min(firstCoordinate.y, lastCoordinate.y)) - { - continue - } - - // get the color that is set for this line-segment - context.setStrokeColor(dataSet.color(atIndex: j).cgColor) - context.strokeLineSegments(between: _lineSegments) - } - } - else - { // only one color per dataset - guard dataSet.entryForIndex(_xBounds.min) != nil else { - return - } - - var firstPoint = true - - let path = CGMutablePath() - for x in stride(from: _xBounds.min, through: _xBounds.range + _xBounds.min, by: 1) - { - guard let e1 = dataSet.entryForIndex(x == 0 ? 0 : (x - 1)) else { continue } - guard let e2 = dataSet.entryForIndex(x) else { continue } - - let startPoint = - CGPoint( - x: CGFloat(e1.x), - y: CGFloat(e1.y * phaseY)) - .applying(valueToPixelMatrix) - - if firstPoint - { - path.move(to: startPoint) - firstPoint = false - } - else - { - path.addLine(to: startPoint) - } - - if isDrawSteppedEnabled - { - let steppedPoint = - CGPoint( - x: CGFloat(e2.x), - y: CGFloat(e1.y * phaseY)) - .applying(valueToPixelMatrix) - path.addLine(to: steppedPoint) - } - - let endPoint = - CGPoint( - x: CGFloat(e2.x), - y: CGFloat(e2.y * phaseY)) - .applying(valueToPixelMatrix) - path.addLine(to: endPoint) - } - - if !firstPoint - { - if dataSet.isDrawLineWithGradientEnabled { - drawGradientLine(context: context, dataSet: dataSet, spline: path, matrix: valueToPixelMatrix) - } else { - context.beginPath() - context.addPath(path) - context.setStrokeColor(dataSet.color(atIndex: 0).cgColor) - context.strokePath() - } - } - } - } - - open func drawLinearFill(context: CGContext, dataSet: LineChartDataSetProtocol, trans: Transformer, bounds: XBounds) - { - guard let dataProvider = dataProvider else { return } - - let filled = generateFilledPath( - dataSet: dataSet, - fillMin: dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0, - bounds: bounds, - matrix: trans.valueToPixelMatrix) - - if dataSet.fill != nil - { - drawFilledPath(context: context, path: filled, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha) - } - else - { - drawFilledPath(context: context, path: filled, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha) - } - } - - /// Generates the path that is used for filled drawing. - private func generateFilledPath(dataSet: LineChartDataSetProtocol, fillMin: CGFloat, bounds: XBounds, matrix: CGAffineTransform) -> CGPath - { - let phaseY = animator.phaseY - let isDrawSteppedEnabled = dataSet.mode == .stepped - let matrix = matrix - - var e: ChartDataEntry! - - let filled = CGMutablePath() - - e = dataSet.entryForIndex(bounds.min) - if e != nil - { - filled.move(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix) - filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix) - } - - // create a new path - for x in stride(from: (bounds.min + 1), through: bounds.range + bounds.min, by: 1) - { - guard let e = dataSet.entryForIndex(x) else { continue } - - if isDrawSteppedEnabled - { - guard let ePrev = dataSet.entryForIndex(x-1) else { continue } - filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(ePrev.y * phaseY)), transform: matrix) - } - - filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix) - } - - // close up - e = dataSet.entryForIndex(bounds.range + bounds.min) - if e != nil - { - filled.addLine(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix) - } - filled.closeSubpath() - - return filled - } - - open override func drawValues(context: CGContext) - { - guard - let dataProvider = dataProvider, - let lineData = dataProvider.lineData - else { return } - - if isDrawingValuesAllowed(dataProvider: dataProvider) - { - let phaseY = animator.phaseY - - var pt = CGPoint() - - for i in lineData.indices - { - guard let - dataSet = lineData[i] as? LineChartDataSetProtocol, - shouldDrawValues(forDataSet: dataSet) - else { continue } - - let valueFont = dataSet.valueFont - - let formatter = dataSet.valueFormatter - - let angleRadians = dataSet.valueLabelAngle.DEG2RAD - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - let valueToPixelMatrix = trans.valueToPixelMatrix - - let iconsOffset = dataSet.iconsOffset - - // make sure the values do not interfear with the circles - var valOffset = Int(dataSet.circleRadius * 1.75) - - if !dataSet.isDrawCirclesEnabled - { - valOffset = valOffset / 2 - } - - _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) - - for j in _xBounds - { - guard let e = dataSet.entryForIndex(j) else { break } - - pt.x = CGFloat(e.x) - pt.y = CGFloat(e.y * phaseY) - pt = pt.applying(valueToPixelMatrix) - - if (!viewPortHandler.isInBoundsRight(pt.x)) - { - break - } - - if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y)) - { - continue - } - - if dataSet.isDrawValuesEnabled - { - context.drawText(formatter.stringForValue(e.y, - entry: e, - dataSetIndex: i, - viewPortHandler: viewPortHandler), - at: CGPoint(x: pt.x, - y: pt.y - CGFloat(valOffset) - valueFont.lineHeight), - align: .center, - angleRadians: angleRadians, - attributes: [.font: valueFont, - .foregroundColor: dataSet.valueTextColorAt(j)]) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - context.drawImage(icon, - atCenter: CGPoint(x: pt.x + iconsOffset.x, - y: pt.y + iconsOffset.y), - size: icon.size) - } - } - } - } - } - - open override func drawExtras(context: CGContext) - { - drawCircles(context: context) - } - - private func drawCircles(context: CGContext) - { - guard - let dataProvider = dataProvider, - let lineData = dataProvider.lineData - else { return } - - let phaseY = animator.phaseY - - var pt = CGPoint() - var rect = CGRect() - - // If we redraw the data, remove and repopulate accessible elements to update label values and frames - accessibleChartElements.removeAll() - accessibilityOrderedElements = accessibilityCreateEmptyOrderedElements() - - // Make the chart header the first element in the accessible elements array - if let chart = dataProvider as? LineChartView { - let element = createAccessibleHeader(usingChart: chart, - andData: lineData, - withDefaultDescription: "Line Chart") - accessibleChartElements.append(element) - } - - context.saveGState() - - for i in lineData.indices - { - guard let dataSet = lineData[i] as? LineChartDataSetProtocol else { continue } - - // Skip Circles and Accessibility if not enabled, - // reduces CPU significantly if not needed - if !dataSet.isVisible || !dataSet.isDrawCirclesEnabled || dataSet.entryCount == 0 - { - continue - } - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - let valueToPixelMatrix = trans.valueToPixelMatrix - - _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) - - let circleRadius = dataSet.circleRadius - let circleDiameter = circleRadius * 2.0 - let circleHoleRadius = dataSet.circleHoleRadius - let circleHoleDiameter = circleHoleRadius * 2.0 - - let drawCircleHole = dataSet.isDrawCircleHoleEnabled && - circleHoleRadius < circleRadius && - circleHoleRadius > 0.0 - let drawTransparentCircleHole = drawCircleHole && - (dataSet.circleHoleColor == nil || - dataSet.circleHoleColor == NSUIColor.clear) - - for j in _xBounds - { - guard let e = dataSet.entryForIndex(j) else { break } - - pt.x = CGFloat(e.x) - pt.y = CGFloat(e.y * phaseY) - pt = pt.applying(valueToPixelMatrix) - - if (!viewPortHandler.isInBoundsRight(pt.x)) - { - break - } - - // make sure the circles don't do shitty things outside bounds - if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y)) - { - continue - } - - // Accessibility element geometry - let scaleFactor: CGFloat = 3 - let accessibilityRect = CGRect(x: pt.x - (scaleFactor * circleRadius), - y: pt.y - (scaleFactor * circleRadius), - width: scaleFactor * circleDiameter, - height: scaleFactor * circleDiameter) - // Create and append the corresponding accessibility element to accessibilityOrderedElements - if let chart = dataProvider as? LineChartView - { - let element = createAccessibleElement(withIndex: j, - container: chart, - dataSet: dataSet, - dataSetIndex: i) - { (element) in - element.accessibilityFrame = accessibilityRect - } - - accessibilityOrderedElements[i].append(element) - } - - context.setFillColor(dataSet.getCircleColor(atIndex: j)!.cgColor) - - rect.origin.x = pt.x - circleRadius - rect.origin.y = pt.y - circleRadius - rect.size.width = circleDiameter - rect.size.height = circleDiameter - - if drawTransparentCircleHole - { - // Begin path for circle with hole - context.beginPath() - context.addEllipse(in: rect) - - // Cut hole in path - rect.origin.x = pt.x - circleHoleRadius - rect.origin.y = pt.y - circleHoleRadius - rect.size.width = circleHoleDiameter - rect.size.height = circleHoleDiameter - context.addEllipse(in: rect) - - // Fill in-between - context.fillPath(using: .evenOdd) - } - else - { - context.fillEllipse(in: rect) - - if drawCircleHole - { - context.setFillColor(dataSet.circleHoleColor!.cgColor) - - // The hole rect - rect.origin.x = pt.x - circleHoleRadius - rect.origin.y = pt.y - circleHoleRadius - rect.size.width = circleHoleDiameter - rect.size.height = circleHoleDiameter - - context.fillEllipse(in: rect) - } - } - } - } - - context.restoreGState() - - // Merge nested ordered arrays into the single accessibleChartElements. - accessibleChartElements.append(contentsOf: accessibilityOrderedElements.flatMap { $0 } ) - accessibilityPostLayoutChangedNotification() - } - - open override func drawHighlighted(context: CGContext, indices: [Highlight]) - { - guard - let dataProvider = dataProvider, - let lineData = dataProvider.lineData - else { return } - - let chartXMax = dataProvider.chartXMax - - context.saveGState() - - for high in indices - { - guard let set = lineData[high.dataSetIndex] as? LineChartDataSetProtocol, - set.isHighlightEnabled - else { continue } - - guard let e = set.entryForXValue(high.x, closestToY: high.y) else { continue } - - if !isInBoundsX(entry: e, dataSet: set) - { - continue - } - - context.setStrokeColor(set.highlightColor.cgColor) - context.setLineWidth(set.highlightLineWidth) - if set.highlightLineDashLengths != nil - { - context.setLineDash(phase: set.highlightLineDashPhase, lengths: set.highlightLineDashLengths!) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - let x = e.x // get the x-position - let y = e.y * Double(animator.phaseY) - - if x > chartXMax * animator.phaseX - { - continue - } - - let trans = dataProvider.getTransformer(forAxis: set.axisDependency) - - let pt = trans.pixelForValues(x: x, y: y) - - high.setDraw(pt: pt) - - // draw the lines - drawHighlightLines(context: context, point: pt, set: set) - } - - context.restoreGState() - } - - func drawGradientLine(context: CGContext, dataSet: LineChartDataSetProtocol, spline: CGPath, matrix: CGAffineTransform) - { - guard let gradientPositions = dataSet.gradientPositions else - { - assertionFailure("Must set `gradientPositions if `dataSet.isDrawLineWithGradientEnabled` is true") - return - } - - // `insetBy` is applied since bounding box - // doesn't take into account line width - // so that peaks are trimmed since - // gradient start and gradient end calculated wrong - let boundingBox = spline.boundingBox - .insetBy(dx: -dataSet.lineWidth / 2, dy: -dataSet.lineWidth / 2) - - guard !boundingBox.isNull, !boundingBox.isInfinite, !boundingBox.isEmpty else { - return - } - - let gradientStart = CGPoint(x: 0, y: boundingBox.minY) - let gradientEnd = CGPoint(x: 0, y: boundingBox.maxY) - let gradientColorComponents: [CGFloat] = dataSet.colors - .reversed() - .reduce(into: []) { (components, color) in - guard let (r, g, b, a) = color.nsuirgba else { - return - } - components += [r, g, b, a] - } - let gradientLocations: [CGFloat] = gradientPositions.reversed() - .map { (position) in - let location = CGPoint(x: boundingBox.minX, y: position) - .applying(matrix) - let normalizedLocation = (location.y - boundingBox.minY) - / (boundingBox.maxY - boundingBox.minY) - return normalizedLocation.clamped(to: 0...1) - } - - let baseColorSpace = CGColorSpaceCreateDeviceRGB() - guard let gradient = CGGradient( - colorSpace: baseColorSpace, - colorComponents: gradientColorComponents, - locations: gradientLocations, - count: gradientLocations.count) else { - return - } - - context.saveGState() - defer { context.restoreGState() } - - context.beginPath() - context.addPath(spline) - context.replacePathWithStrokedPath() - context.clip() - context.drawLinearGradient(gradient, start: gradientStart, end: gradientEnd, options: []) - } - - /// Creates a nested array of empty subarrays each of which will be populated with NSUIAccessibilityElements. - /// This is marked internal to support HorizontalBarChartRenderer as well. - private func accessibilityCreateEmptyOrderedElements() -> [[NSUIAccessibilityElement]] - { - guard let chart = dataProvider as? LineChartView else { return [] } - - let dataSetCount = chart.lineData?.dataSetCount ?? 0 - - return Array(repeating: [NSUIAccessibilityElement](), - count: dataSetCount) - } - - /// Creates an NSUIAccessibleElement representing the smallest meaningful bar of the chart - /// i.e. in case of a stacked chart, this returns each stack, not the combined bar. - /// Note that it is marked internal to support subclass modification in the HorizontalBarChart. - private func createAccessibleElement(withIndex idx: Int, - container: LineChartView, - dataSet: LineChartDataSetProtocol, - dataSetIndex: Int, - modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement - { - let element = NSUIAccessibilityElement(accessibilityContainer: container) - let xAxis = container.xAxis - - guard let e = dataSet.entryForIndex(idx) else { return element } - guard let dataProvider = dataProvider else { return element } - - // NOTE: The formatter can cause issues when the x-axis labels are consecutive ints. - // i.e. due to the Double conversion, if there are more than one data set that are grouped, - // there is the possibility of some labels being rounded up. A floor() might fix this, but seems to be a brute force solution. - let label = xAxis.valueFormatter?.stringForValue(e.x, axis: xAxis) ?? "\(e.x)" - - let elementValueText = dataSet.valueFormatter.stringForValue(e.y, - entry: e, - dataSetIndex: dataSetIndex, - viewPortHandler: viewPortHandler) - - let dataSetCount = dataProvider.lineData?.dataSetCount ?? -1 - let doesContainMultipleDataSets = dataSetCount > 1 - - element.accessibilityLabel = "\(doesContainMultipleDataSets ? (dataSet.label ?? "") + ", " : "") \(label): \(elementValueText)" - - modifier(element) - - return element - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/LineRadarRenderer.swift b/Pods/Charts/Source/Charts/Renderers/LineRadarRenderer.swift deleted file mode 100644 index 6b0282505..000000000 --- a/Pods/Charts/Source/Charts/Renderers/LineRadarRenderer.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// LineRadarRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(LineRadarChartRenderer) -open class LineRadarRenderer: LineScatterCandleRadarRenderer -{ - public override init(animator: Animator, viewPortHandler: ViewPortHandler) - { - super.init(animator: animator, viewPortHandler: viewPortHandler) - } - - /// Draws the provided path in filled mode with the provided drawable. - @objc open func drawFilledPath(context: CGContext, path: CGPath, fill: Fill, fillAlpha: CGFloat) - { - - context.saveGState() - context.beginPath() - context.addPath(path) - - // filled is usually drawn with less alpha - context.setAlpha(fillAlpha) - - fill.fillPath(context: context, rect: viewPortHandler.contentRect) - - context.restoreGState() - } - - /// Draws the provided path in filled mode with the provided color and alpha. - @objc open func drawFilledPath(context: CGContext, path: CGPath, fillColor: NSUIColor, fillAlpha: CGFloat) - { - context.saveGState() - context.beginPath() - context.addPath(path) - - // filled is usually drawn with less alpha - context.setAlpha(fillAlpha) - - context.setFillColor(fillColor.cgColor) - context.fillPath() - - context.restoreGState() - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/LineScatterCandleRadarRenderer.swift b/Pods/Charts/Source/Charts/Renderers/LineScatterCandleRadarRenderer.swift deleted file mode 100644 index dd33ffe86..000000000 --- a/Pods/Charts/Source/Charts/Renderers/LineScatterCandleRadarRenderer.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// LineScatterCandleRadarRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(LineScatterCandleRadarChartRenderer) -open class LineScatterCandleRadarRenderer: BarLineScatterCandleBubbleRenderer -{ - public override init(animator: Animator, viewPortHandler: ViewPortHandler) - { - super.init(animator: animator, viewPortHandler: viewPortHandler) - } - - /// Draws vertical & horizontal highlight-lines if enabled. - /// :param: context - /// :param: points - /// :param: horizontal - /// :param: vertical - @objc open func drawHighlightLines(context: CGContext, point: CGPoint, set: LineScatterCandleRadarChartDataSetProtocol) - { - - // draw vertical highlight lines - if set.isVerticalHighlightIndicatorEnabled - { - context.beginPath() - context.move(to: CGPoint(x: point.x, y: viewPortHandler.contentTop)) - context.addLine(to: CGPoint(x: point.x, y: viewPortHandler.contentBottom)) - context.strokePath() - } - - // draw horizontal highlight lines - if set.isHorizontalHighlightIndicatorEnabled - { - context.beginPath() - context.move(to: CGPoint(x: viewPortHandler.contentLeft, y: point.y)) - context.addLine(to: CGPoint(x: viewPortHandler.contentRight, y: point.y)) - context.strokePath() - } - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/PieChartRenderer.swift b/Pods/Charts/Source/Charts/Renderers/PieChartRenderer.swift deleted file mode 100644 index 18f571bc7..000000000 --- a/Pods/Charts/Source/Charts/Renderers/PieChartRenderer.swift +++ /dev/null @@ -1,937 +0,0 @@ -// -// PieChartRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -#if !os(OSX) - import UIKit -#endif - - -open class PieChartRenderer: NSObject, DataRenderer -{ - public let viewPortHandler: ViewPortHandler - - public final var accessibleChartElements: [NSUIAccessibilityElement] = [] - - public let animator: Animator - - @objc open weak var chart: PieChartView? - - @objc public init(chart: PieChartView, animator: Animator, viewPortHandler: ViewPortHandler) - { - self.viewPortHandler = viewPortHandler - self.animator = animator - self.chart = chart - - super.init() - } - - open func drawData(context: CGContext) - { - guard let chart = chart, let pieData = chart.data else { return } - - // If we redraw the data, remove and repopulate accessible elements to update label values and frames - accessibleChartElements.removeAll() - - for case let set as PieChartDataSetProtocol in pieData where - set.isVisible && set.entryCount > 0 - { - drawDataSet(context: context, dataSet: set) - } - } - - @objc open func calculateMinimumRadiusForSpacedSlice( - center: CGPoint, - radius: CGFloat, - angle: CGFloat, - arcStartPointX: CGFloat, - arcStartPointY: CGFloat, - startAngle: CGFloat, - sweepAngle: CGFloat) -> CGFloat - { - let angleMiddle = startAngle + sweepAngle / 2.0 - - // Other point of the arc - let arcEndPointX = center.x + radius * cos((startAngle + sweepAngle).DEG2RAD) - let arcEndPointY = center.y + radius * sin((startAngle + sweepAngle).DEG2RAD) - - // Middle point on the arc - let arcMidPointX = center.x + radius * cos(angleMiddle.DEG2RAD) - let arcMidPointY = center.y + radius * sin(angleMiddle.DEG2RAD) - - // This is the base of the contained triangle - let basePointsDistance = sqrt( - pow(arcEndPointX - arcStartPointX, 2) + - pow(arcEndPointY - arcStartPointY, 2)) - - // After reducing space from both sides of the "slice", - // the angle of the contained triangle should stay the same. - // So let's find out the height of that triangle. - let containedTriangleHeight = (basePointsDistance / 2.0 * - tan((180.0 - angle).DEG2RAD / 2.0)) - - // Now we subtract that from the radius - var spacedRadius = radius - containedTriangleHeight - - // And now subtract the height of the arc that's between the triangle and the outer circle - spacedRadius -= sqrt( - pow(arcMidPointX - (arcEndPointX + arcStartPointX) / 2.0, 2) + - pow(arcMidPointY - (arcEndPointY + arcStartPointY) / 2.0, 2)) - - return spacedRadius - } - - /// Calculates the sliceSpace to use based on visible values and their size compared to the set sliceSpace. - @objc open func getSliceSpace(dataSet: PieChartDataSetProtocol) -> CGFloat - { - guard - dataSet.automaticallyDisableSliceSpacing, - let data = chart?.data as? PieChartData - else { return dataSet.sliceSpace } - - let spaceSizeRatio = dataSet.sliceSpace / min(viewPortHandler.contentWidth, viewPortHandler.contentHeight) - let minValueRatio = dataSet.yMin / data.yValueSum * 2.0 - - let sliceSpace = spaceSizeRatio > CGFloat(minValueRatio) - ? 0.0 - : dataSet.sliceSpace - - return sliceSpace - } - - @objc open func drawDataSet(context: CGContext, dataSet: PieChartDataSetProtocol) - { - guard let chart = chart else {return } - - var angle: CGFloat = 0.0 - let rotationAngle = chart.rotationAngle - - let phaseX = animator.phaseX - let phaseY = animator.phaseY - - let entryCount = dataSet.entryCount - let drawAngles = chart.drawAngles - let center = chart.centerCircleBox - let radius = chart.radius - let drawInnerArc = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled - let userInnerRadius = drawInnerArc ? radius * chart.holeRadiusPercent : 0.0 - - var visibleAngleCount = 0 - for j in 0 ..< entryCount - { - guard let e = dataSet.entryForIndex(j) else { continue } - if ((abs(e.y) > Double.ulpOfOne)) - { - visibleAngleCount += 1 - } - } - - let sliceSpace = visibleAngleCount <= 1 ? 0.0 : getSliceSpace(dataSet: dataSet) - - context.saveGState() - - // Make the chart header the first element in the accessible elements array - // We can do this in drawDataSet, since we know PieChartView can have only 1 dataSet - // Also since there's only 1 dataset, we don't use the typical createAccessibleHeader() here. - // NOTE: - Since we want to summarize the total count of slices/portions/elements, use a default string here - // This is unlike when we are naming individual slices, wherein it's alright to not use a prefix as descriptor. - // i.e. We want to VO to say "3 Elements" even if the developer didn't specify an accessibility prefix - // If prefix is unspecified it is safe to assume they did not want to use "Element 1", so that uses a default empty string - let prefix: String = chart.data?.accessibilityEntryLabelPrefix ?? "Element" - let description = chart.chartDescription.text ?? dataSet.label ?? chart.centerText ?? "Pie Chart" - - let - element = NSUIAccessibilityElement(accessibilityContainer: chart) - element.accessibilityLabel = description + ". \(entryCount) \(prefix + (entryCount == 1 ? "" : "s"))" - element.accessibilityFrame = chart.bounds - element.isHeader = true - accessibleChartElements.append(element) - - for j in 0 ..< entryCount - { - let sliceAngle = drawAngles[j] - var innerRadius = userInnerRadius - - guard let e = dataSet.entryForIndex(j) else { continue } - - defer - { - // From here on, even when skipping (i.e for highlight), - // increase the angle - angle += sliceAngle * CGFloat(phaseX) - } - - // draw only if the value is greater than zero - if abs(e.y) < Double.ulpOfOne { continue } - - // Skip if highlighted - if dataSet.isHighlightEnabled && chart.needsHighlight(index: j) - { - continue - } - - let accountForSliceSpacing = sliceSpace > 0.0 && sliceAngle <= 180.0 - - context.setFillColor(dataSet.color(atIndex: j).cgColor) - - let sliceSpaceAngleOuter = visibleAngleCount == 1 ? - 0.0 : - sliceSpace / radius.DEG2RAD - let startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.0) * CGFloat(phaseY) - var sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * CGFloat(phaseY) - if sweepAngleOuter < 0.0 - { - sweepAngleOuter = 0.0 - } - - let arcStartPointX = center.x + radius * cos(startAngleOuter.DEG2RAD) - let arcStartPointY = center.y + radius * sin(startAngleOuter.DEG2RAD) - - let path = CGMutablePath() - - path.move(to: CGPoint(x: arcStartPointX, - y: arcStartPointY)) - - path.addRelativeArc(center: center, radius: radius, startAngle: startAngleOuter.DEG2RAD, delta: sweepAngleOuter.DEG2RAD) - - if drawInnerArc && - (innerRadius > 0.0 || accountForSliceSpacing) - { - if accountForSliceSpacing - { - var minSpacedRadius = calculateMinimumRadiusForSpacedSlice( - center: center, - radius: radius, - angle: sliceAngle * CGFloat(phaseY), - arcStartPointX: arcStartPointX, - arcStartPointY: arcStartPointY, - startAngle: startAngleOuter, - sweepAngle: sweepAngleOuter) - if minSpacedRadius < 0.0 - { - minSpacedRadius = -minSpacedRadius - } - innerRadius = min(max(innerRadius, minSpacedRadius), radius) - } - - let sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.0 ? - 0.0 : - sliceSpace / innerRadius.DEG2RAD - let startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.0) * CGFloat(phaseY) - var sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * CGFloat(phaseY) - if sweepAngleInner < 0.0 - { - sweepAngleInner = 0.0 - } - let endAngleInner = startAngleInner + sweepAngleInner - - path.addLine( - to: CGPoint( - x: center.x + innerRadius * cos(endAngleInner.DEG2RAD), - y: center.y + innerRadius * sin(endAngleInner.DEG2RAD))) - - path.addRelativeArc(center: center, radius: innerRadius, startAngle: endAngleInner.DEG2RAD, delta: -sweepAngleInner.DEG2RAD) - } - else - { - if accountForSliceSpacing - { - let angleMiddle = startAngleOuter + sweepAngleOuter / 2.0 - - let sliceSpaceOffset = - calculateMinimumRadiusForSpacedSlice( - center: center, - radius: radius, - angle: sliceAngle * CGFloat(phaseY), - arcStartPointX: arcStartPointX, - arcStartPointY: arcStartPointY, - startAngle: startAngleOuter, - sweepAngle: sweepAngleOuter) - - let arcEndPointX = center.x + sliceSpaceOffset * cos(angleMiddle.DEG2RAD) - let arcEndPointY = center.y + sliceSpaceOffset * sin(angleMiddle.DEG2RAD) - - path.addLine( - to: CGPoint( - x: arcEndPointX, - y: arcEndPointY)) - } - else - { - path.addLine(to: center) - } - } - - path.closeSubpath() - - context.beginPath() - context.addPath(path) - context.fillPath(using: .evenOdd) - - let axElement = createAccessibleElement(withIndex: j, - container: chart, - dataSet: dataSet) - { (element) in - element.accessibilityFrame = path.boundingBoxOfPath - } - - accessibleChartElements.append(axElement) - } - - // Post this notification to let VoiceOver account for the redrawn frames - accessibilityPostLayoutChangedNotification() - - context.restoreGState() - } - - open func drawValues(context: CGContext) - { - guard - let chart = chart, - let data = chart.data - else { return } - - let center = chart.centerCircleBox - - // get whole the radius - let radius = chart.radius - let rotationAngle = chart.rotationAngle - let drawAngles = chart.drawAngles - let absoluteAngles = chart.absoluteAngles - - let phaseX = animator.phaseX - let phaseY = animator.phaseY - - var labelRadiusOffset = radius / 10.0 * 3.0 - - if chart.drawHoleEnabled - { - labelRadiusOffset = (radius - (radius * chart.holeRadiusPercent)) / 2.0 - } - - let labelRadius = radius - labelRadiusOffset - - let yValueSum = (data as! PieChartData).yValueSum - - let drawEntryLabels = chart.isDrawEntryLabelsEnabled - let usePercentValuesEnabled = chart.usePercentValuesEnabled - let sliceTextDrawingThreshold = chart.sliceTextDrawingThreshold - - var angle: CGFloat = 0.0 - var xIndex = 0 - - context.saveGState() - defer { context.restoreGState() } - - for i in data.indices - { - guard let dataSet = data[i] as? PieChartDataSetProtocol else { continue } - - let drawValues = dataSet.isDrawValuesEnabled - - if !drawValues && !drawEntryLabels && !dataSet.isDrawIconsEnabled - { - continue - } - - let iconsOffset = dataSet.iconsOffset - - let angleRadians = dataSet.valueLabelAngle.DEG2RAD - - let xValuePosition = dataSet.xValuePosition - let yValuePosition = dataSet.yValuePosition - - let valueFont = dataSet.valueFont - let entryLabelFont = dataSet.entryLabelFont - let lineHeight = valueFont.lineHeight - - let formatter = dataSet.valueFormatter - - for j in 0 ..< dataSet.entryCount - { - guard let e = dataSet.entryForIndex(j) else { continue } - let pe = e as? PieChartDataEntry - - if xIndex == 0 - { - angle = 0.0 - } - else - { - angle = absoluteAngles[xIndex - 1] * CGFloat(phaseX) - } - - let sliceAngle = drawAngles[xIndex] - let sliceSpace = getSliceSpace(dataSet: dataSet) - let sliceSpaceMiddleAngle = sliceSpace / labelRadius.DEG2RAD - - // offset needed to center the drawn text in the slice - let angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2.0) / 2.0 - - angle = angle + angleOffset - - let transformedAngle = rotationAngle + angle * CGFloat(phaseY) - - let value = usePercentValuesEnabled ? e.y / yValueSum * 100.0 : e.y - let valueText = formatter.stringForValue( - value, - entry: e, - dataSetIndex: i, - viewPortHandler: viewPortHandler) - - let sliceXBase = cos(transformedAngle.DEG2RAD) - let sliceYBase = sin(transformedAngle.DEG2RAD) - - let drawXOutside = sliceAngle > sliceTextDrawingThreshold && drawEntryLabels && xValuePosition == .outsideSlice - let drawYOutside = sliceAngle > sliceTextDrawingThreshold && drawValues && yValuePosition == .outsideSlice - let drawXInside = sliceAngle > sliceTextDrawingThreshold && drawEntryLabels && xValuePosition == .insideSlice - let drawYInside = sliceAngle > sliceTextDrawingThreshold && drawValues && yValuePosition == .insideSlice - - let valueTextColor = dataSet.valueTextColorAt(j) - let entryLabelColor = dataSet.entryLabelColor - - if drawXOutside || drawYOutside - { - let valueLineLength1 = dataSet.valueLinePart1Length - let valueLineLength2 = dataSet.valueLinePart2Length - let valueLinePart1OffsetPercentage = dataSet.valueLinePart1OffsetPercentage - - var pt2: CGPoint - var labelPoint: CGPoint - var align: TextAlignment - - var line1Radius: CGFloat - - if chart.drawHoleEnabled - { - line1Radius = (radius - (radius * chart.holeRadiusPercent)) * valueLinePart1OffsetPercentage + (radius * chart.holeRadiusPercent) - } - else - { - line1Radius = radius * valueLinePart1OffsetPercentage - } - - let polyline2Length = dataSet.valueLineVariableLength - ? labelRadius * valueLineLength2 * abs(sin(transformedAngle.DEG2RAD)) - : labelRadius * valueLineLength2 - - let pt0 = CGPoint( - x: line1Radius * sliceXBase + center.x, - y: line1Radius * sliceYBase + center.y) - - let pt1 = CGPoint( - x: labelRadius * (1 + valueLineLength1) * sliceXBase + center.x, - y: labelRadius * (1 + valueLineLength1) * sliceYBase + center.y) - - if transformedAngle.truncatingRemainder(dividingBy: 360.0) >= 90.0 && transformedAngle.truncatingRemainder(dividingBy: 360.0) <= 270.0 - { - pt2 = CGPoint(x: pt1.x - polyline2Length, y: pt1.y) - align = .right - labelPoint = CGPoint(x: pt2.x - 5, y: pt2.y - lineHeight) - } - else - { - pt2 = CGPoint(x: pt1.x + polyline2Length, y: pt1.y) - align = .left - labelPoint = CGPoint(x: pt2.x + 5, y: pt2.y - lineHeight) - } - - DrawLine: do - { - if dataSet.useValueColorForLine - { - context.setStrokeColor(dataSet.color(atIndex: j).cgColor) - } - else if let valueLineColor = dataSet.valueLineColor - { - context.setStrokeColor(valueLineColor.cgColor) - } - else - { - return - } - context.setLineWidth(dataSet.valueLineWidth) - - context.move(to: CGPoint(x: pt0.x, y: pt0.y)) - context.addLine(to: CGPoint(x: pt1.x, y: pt1.y)) - context.addLine(to: CGPoint(x: pt2.x, y: pt2.y)) - - context.drawPath(using: CGPathDrawingMode.stroke) - } - - if drawXOutside && drawYOutside - { - context.drawText(valueText, - at: labelPoint, - align: align, - angleRadians: angleRadians, - attributes: [.font: valueFont, - .foregroundColor: valueTextColor]) - - if j < data.entryCount && pe?.label != nil - { - context.drawText(pe!.label!, - at: CGPoint(x: labelPoint.x, - y: labelPoint.y + lineHeight), - align: align, - angleRadians: angleRadians, - attributes: [.font: entryLabelFont ?? valueFont, - .foregroundColor: entryLabelColor ?? valueTextColor]) - } - } - else if drawXOutside - { - if j < data.entryCount && pe?.label != nil - { - context.drawText(pe!.label!, - at: CGPoint(x: labelPoint.x, - y: labelPoint.y + lineHeight / 2.0), - align: align, - angleRadians: angleRadians, - attributes: [.font: entryLabelFont ?? valueFont, - .foregroundColor: entryLabelColor ?? valueTextColor]) - } - } - else if drawYOutside - { - context.drawText(valueText, - at: CGPoint(x: labelPoint.x, - y: labelPoint.y + lineHeight / 2.0), - align: align, - angleRadians: angleRadians, - attributes: [.font: valueFont, - .foregroundColor: valueTextColor]) - } - } - - if drawXInside || drawYInside - { - // calculate the text position - let x = labelRadius * sliceXBase + center.x - let y = labelRadius * sliceYBase + center.y - lineHeight - - if drawXInside && drawYInside - { - context.drawText(valueText, - at: CGPoint(x: x, y: y), - align: .center, - angleRadians: angleRadians, - attributes: [.font: valueFont, .foregroundColor: valueTextColor]) - - if j < data.entryCount && pe?.label != nil - { - context.drawText(pe!.label!, - at: CGPoint(x: x, y: y + lineHeight), - align: .center, - angleRadians: angleRadians, - attributes: [.font: entryLabelFont ?? valueFont, - .foregroundColor: entryLabelColor ?? valueTextColor]) - } - } - else if drawXInside - { - if j < data.entryCount && pe?.label != nil - { - context.drawText(pe!.label!, - at: CGPoint(x: x, y: y + lineHeight / 2.0), - align: .center, - angleRadians: angleRadians, - attributes: [.font: entryLabelFont ?? valueFont, - .foregroundColor: entryLabelColor ?? valueTextColor]) - } - } - else if drawYInside - { - context.drawText(valueText, - at: CGPoint(x: x, y: y + lineHeight / 2.0), - align: .center, - angleRadians: angleRadians, - attributes: [.font: valueFont, .foregroundColor: valueTextColor]) - } - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - // calculate the icon's position - - let x = (labelRadius + iconsOffset.y) * sliceXBase + center.x - var y = (labelRadius + iconsOffset.y) * sliceYBase + center.y - y += iconsOffset.x - - context.drawImage(icon, - atCenter: CGPoint(x: x, y: y), - size: icon.size) - } - - xIndex += 1 - } - } - } - - open func drawExtras(context: CGContext) - { - drawHole(context: context) - drawCenterText(context: context) - } - - open func initBuffers() { } - - open func isDrawingValuesAllowed(dataProvider: ChartDataProvider?) -> Bool - { - guard let data = dataProvider?.data else { return false } - return data.entryCount < Int(CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleX) - } - - /// draws the hole in the center of the chart and the transparent circle / hole - private func drawHole(context: CGContext) - { - guard let chart = chart else { return } - - if chart.drawHoleEnabled - { - context.saveGState() - - let radius = chart.radius - let holeRadius = radius * chart.holeRadiusPercent - let center = chart.centerCircleBox - - if let holeColor = chart.holeColor - { - if holeColor != NSUIColor.clear - { - // draw the hole-circle - context.setFillColor(chart.holeColor!.cgColor) - context.fillEllipse(in: CGRect(x: center.x - holeRadius, y: center.y - holeRadius, width: holeRadius * 2.0, height: holeRadius * 2.0)) - } - } - - // only draw the circle if it can be seen (not covered by the hole) - if let transparentCircleColor = chart.transparentCircleColor - { - if transparentCircleColor != NSUIColor.clear && - chart.transparentCircleRadiusPercent > chart.holeRadiusPercent - { - let alpha = animator.phaseX * animator.phaseY - let secondHoleRadius = radius * chart.transparentCircleRadiusPercent - - // make transparent - context.setAlpha(CGFloat(alpha)) - context.setFillColor(transparentCircleColor.cgColor) - - // draw the transparent-circle - context.beginPath() - context.addEllipse(in: CGRect( - x: center.x - secondHoleRadius, - y: center.y - secondHoleRadius, - width: secondHoleRadius * 2.0, - height: secondHoleRadius * 2.0)) - context.addEllipse(in: CGRect( - x: center.x - holeRadius, - y: center.y - holeRadius, - width: holeRadius * 2.0, - height: holeRadius * 2.0)) - context.fillPath(using: .evenOdd) - } - } - - context.restoreGState() - } - } - - /// draws the description text in the center of the pie chart makes most sense when center-hole is enabled - private func drawCenterText(context: CGContext) - { - guard - let chart = chart, - let centerAttributedText = chart.centerAttributedText - else { return } - - if chart.drawCenterTextEnabled && centerAttributedText.length > 0 - { - let center = chart.centerCircleBox - let offset = chart.centerTextOffset - let innerRadius = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled ? chart.radius * chart.holeRadiusPercent : chart.radius - - let x = center.x + offset.x - let y = center.y + offset.y - - let holeRect = CGRect( - x: x - innerRadius, - y: y - innerRadius, - width: innerRadius * 2.0, - height: innerRadius * 2.0) - var boundingRect = holeRect - - if chart.centerTextRadiusPercent > 0.0 - { - boundingRect = boundingRect.insetBy(dx: (boundingRect.width - boundingRect.width * chart.centerTextRadiusPercent) / 2.0, dy: (boundingRect.height - boundingRect.height * chart.centerTextRadiusPercent) / 2.0) - } - - let textBounds = centerAttributedText.boundingRect(with: boundingRect.size, options: [.usesLineFragmentOrigin, .usesFontLeading, .truncatesLastVisibleLine], context: nil) - - var drawingRect = boundingRect - drawingRect.origin.x += (boundingRect.size.width - textBounds.size.width) / 2.0 - drawingRect.origin.y += (boundingRect.size.height - textBounds.size.height) / 2.0 - drawingRect.size = textBounds.size - - context.saveGState() - - let clippingPath = CGPath(ellipseIn: holeRect, transform: nil) - context.beginPath() - context.addPath(clippingPath) - context.clip() - - centerAttributedText.draw(with: drawingRect, options: [.usesLineFragmentOrigin, .usesFontLeading, .truncatesLastVisibleLine], context: nil) - - context.restoreGState() - } - } - - open func drawHighlighted(context: CGContext, indices highlights: [Highlight]) - { - guard - let chart = chart, - let data = chart.data - else { return } - - context.saveGState() - - let phaseX = animator.phaseX - let phaseY = animator.phaseY - - var angle: CGFloat = 0.0 - let rotationAngle = chart.rotationAngle - - let drawAngles = chart.drawAngles - let absoluteAngles = chart.absoluteAngles - let center = chart.centerCircleBox - let radius = chart.radius - let drawInnerArc = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled - let userInnerRadius = drawInnerArc ? radius * chart.holeRadiusPercent : 0.0 - - // Append highlighted accessibility slices into this array, so we can prioritize them over unselected slices - var highlightedAccessibleElements: [NSUIAccessibilityElement] = [] - - for hightlight in highlights - { - // get the index to highlight - let index = Int(hightlight.x) - guard index < drawAngles.count, - let set = data[hightlight.dataSetIndex] as? PieChartDataSetProtocol, - set.isHighlightEnabled - else - { - continue - } - - let entryCount = set.entryCount - var visibleAngleCount = 0 - for j in 0 ..< entryCount - { - guard let e = set.entryForIndex(j) else { continue } - if ((abs(e.y) > Double.ulpOfOne)) - { - visibleAngleCount += 1 - } - } - - if index == 0 - { - angle = 0.0 - } - else - { - angle = absoluteAngles[index - 1] * CGFloat(phaseX) - } - - let sliceSpace = visibleAngleCount <= 1 ? 0.0 : set.sliceSpace - - let sliceAngle = drawAngles[index] - var innerRadius = userInnerRadius - - let shift = set.selectionShift - let highlightedRadius = radius + shift - - let accountForSliceSpacing = sliceSpace > 0.0 && sliceAngle <= 180.0 - - context.setFillColor(set.highlightColor?.cgColor ?? set.color(atIndex: index).cgColor) - - let sliceSpaceAngleOuter = visibleAngleCount == 1 ? - 0.0 : - sliceSpace / radius.DEG2RAD - - let sliceSpaceAngleShifted = visibleAngleCount == 1 ? - 0.0 : - sliceSpace / highlightedRadius.DEG2RAD - - let startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.0) * CGFloat(phaseY) - var sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * CGFloat(phaseY) - if sweepAngleOuter < 0.0 - { - sweepAngleOuter = 0.0 - } - - let startAngleShifted = rotationAngle + (angle + sliceSpaceAngleShifted / 2.0) * CGFloat(phaseY) - var sweepAngleShifted = (sliceAngle - sliceSpaceAngleShifted) * CGFloat(phaseY) - if sweepAngleShifted < 0.0 - { - sweepAngleShifted = 0.0 - } - - let path = CGMutablePath() - - path.move(to: CGPoint(x: center.x + highlightedRadius * cos(startAngleShifted.DEG2RAD), - y: center.y + highlightedRadius * sin(startAngleShifted.DEG2RAD))) - - path.addRelativeArc(center: center, radius: highlightedRadius, startAngle: startAngleShifted.DEG2RAD, - delta: sweepAngleShifted.DEG2RAD) - - var sliceSpaceRadius: CGFloat = 0.0 - if accountForSliceSpacing - { - sliceSpaceRadius = calculateMinimumRadiusForSpacedSlice( - center: center, - radius: radius, - angle: sliceAngle * CGFloat(phaseY), - arcStartPointX: center.x + radius * cos(startAngleOuter.DEG2RAD), - arcStartPointY: center.y + radius * sin(startAngleOuter.DEG2RAD), - startAngle: startAngleOuter, - sweepAngle: sweepAngleOuter) - } - - if drawInnerArc && - (innerRadius > 0.0 || accountForSliceSpacing) - { - if accountForSliceSpacing - { - var minSpacedRadius = sliceSpaceRadius - if minSpacedRadius < 0.0 - { - minSpacedRadius = -minSpacedRadius - } - innerRadius = min(max(innerRadius, minSpacedRadius), radius) - } - - let sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.0 ? - 0.0 : - sliceSpace / innerRadius.DEG2RAD - let startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.0) * CGFloat(phaseY) - var sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * CGFloat(phaseY) - if sweepAngleInner < 0.0 - { - sweepAngleInner = 0.0 - } - let endAngleInner = startAngleInner + sweepAngleInner - - path.addLine( - to: CGPoint( - x: center.x + innerRadius * cos(endAngleInner.DEG2RAD), - y: center.y + innerRadius * sin(endAngleInner.DEG2RAD))) - - path.addRelativeArc(center: center, radius: innerRadius, - startAngle: endAngleInner.DEG2RAD, - delta: -sweepAngleInner.DEG2RAD) - } - else - { - if accountForSliceSpacing - { - let angleMiddle = startAngleOuter + sweepAngleOuter / 2.0 - - let arcEndPointX = center.x + sliceSpaceRadius * cos(angleMiddle.DEG2RAD) - let arcEndPointY = center.y + sliceSpaceRadius * sin(angleMiddle.DEG2RAD) - - path.addLine( - to: CGPoint( - x: arcEndPointX, - y: arcEndPointY)) - } - else - { - path.addLine(to: center) - } - } - - path.closeSubpath() - - context.beginPath() - context.addPath(path) - context.fillPath(using: .evenOdd) - - let axElement = createAccessibleElement(withIndex: index, - container: chart, - dataSet: set) - { (element) in - element.accessibilityFrame = path.boundingBoxOfPath - element.isSelected = true - } - - highlightedAccessibleElements.append(axElement) - } - - // Prepend selected slices before the already rendered unselected ones. - // NOTE: - This relies on drawDataSet() being called before drawHighlighted in PieChartView. - if !accessibleChartElements.isEmpty { - accessibleChartElements.insert(contentsOf: highlightedAccessibleElements, at: 1) - } - - context.restoreGState() - } - - /// Creates an NSUIAccessibilityElement representing a slice of the PieChart. - /// The element only has it's container and label set based on the chart and dataSet. Use the modifier to alter traits and frame. - private func createAccessibleElement(withIndex idx: Int, - container: PieChartView, - dataSet: PieChartDataSetProtocol, - modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement { - - let element = NSUIAccessibilityElement(accessibilityContainer: container) - - guard let e = dataSet.entryForIndex(idx) else { return element } - guard let data = container.data as? PieChartData else { return element } - - let formatter = dataSet.valueFormatter - - var elementValueText = formatter.stringForValue( - e.y, - entry: e, - dataSetIndex: idx, - viewPortHandler: viewPortHandler) - - if container.usePercentValuesEnabled { - let value = e.y / data.yValueSum * 100.0 - let valueText = formatter.stringForValue( - value, - entry: e, - dataSetIndex: idx, - viewPortHandler: viewPortHandler) - - elementValueText = valueText - } - - let pieChartDataEntry = (dataSet.entryForIndex(idx) as? PieChartDataEntry) - let isCount = data.accessibilityEntryLabelSuffixIsCount - let prefix = data.accessibilityEntryLabelPrefix?.appending("\(idx + 1)") ?? pieChartDataEntry?.label ?? "" - let suffix = data.accessibilityEntryLabelSuffix ?? "" - element.accessibilityLabel = "\(prefix) : \(elementValueText) \(suffix + (isCount ? (e.y == 1.0 ? "" : "s") : "") )" - - // The modifier allows changing of traits and frame depending on highlight, rotation, etc - modifier(element) - - return element - } - - public func createAccessibleHeader(usingChart chart: ChartViewBase, andData data: ChartData, withDefaultDescription defaultDescription: String) -> NSUIAccessibilityElement { - return AccessibleHeader.create(usingChart: chart, andData: data, withDefaultDescription: defaultDescription) - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/RadarChartRenderer.swift b/Pods/Charts/Source/Charts/Renderers/RadarChartRenderer.swift deleted file mode 100644 index 21f124604..000000000 --- a/Pods/Charts/Source/Charts/Renderers/RadarChartRenderer.swift +++ /dev/null @@ -1,469 +0,0 @@ -// -// RadarChartRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class RadarChartRenderer: LineRadarRenderer -{ - private lazy var accessibilityXLabels: [String] = { - guard let chart = chart else { return [] } - guard let formatter = chart.xAxis.valueFormatter else { return [] } - - let maxEntryCount = chart.data?.maxEntryCountSet?.entryCount ?? 0 - return stride(from: 0, to: maxEntryCount, by: 1).map { - formatter.stringForValue(Double($0), axis: chart.xAxis) - } - }() - - @objc open weak var chart: RadarChartView? - - @objc public init(chart: RadarChartView, animator: Animator, viewPortHandler: ViewPortHandler) - { - super.init(animator: animator, viewPortHandler: viewPortHandler) - - self.chart = chart - } - - open override func drawData(context: CGContext) - { - guard let chart = chart, - let radarData = chart.data as? RadarChartData else - { - return - } - - let mostEntries = radarData.maxEntryCountSet?.entryCount ?? 0 - - // If we redraw the data, remove and repopulate accessible elements to update label values and frames - self.accessibleChartElements.removeAll() - - // Make the chart header the first element in the accessible elements array - let element = createAccessibleHeader(usingChart: chart, - andData: radarData, - withDefaultDescription: "Radar Chart") - self.accessibleChartElements.append(element) - - for case let set as RadarChartDataSetProtocol in (radarData as ChartData) where set.isVisible - { - drawDataSet(context: context, dataSet: set, mostEntries: mostEntries) - } - } - - /// Draws the RadarDataSet - /// - /// - Parameters: - /// - context: - /// - dataSet: - /// - mostEntries: the entry count of the dataset with the most entries - internal func drawDataSet(context: CGContext, dataSet: RadarChartDataSetProtocol, mostEntries: Int) - { - guard let chart = chart else { return } - - context.saveGState() - - let phaseX = animator.phaseX - let phaseY = animator.phaseY - - let sliceangle = chart.sliceAngle - - // calculate the factor that is needed for transforming the value to pixels - let factor = chart.factor - - let center = chart.centerOffsets - let entryCount = dataSet.entryCount - let path = CGMutablePath() - var hasMovedToPoint = false - - let prefix: String = chart.data?.accessibilityEntryLabelPrefix ?? "Item" - let description = dataSet.label ?? "" - - // Make a tuple of (xLabels, value, originalIndex) then sort it - // This is done, so that the labels are narrated in decreasing order of their corresponding value - // Otherwise, there is no non-visual logic to the data presented - let accessibilityEntryValues = Array(0 ..< entryCount).map { (dataSet.entryForIndex($0)?.y ?? 0, $0) } - let accessibilityAxisLabelValueTuples = zip(accessibilityXLabels, accessibilityEntryValues).map { ($0, $1.0, $1.1) }.sorted { $0.1 > $1.1 } - let accessibilityDataSetDescription: String = description + ". \(entryCount) \(prefix + (entryCount == 1 ? "" : "s")). " - let accessibilityFrameWidth: CGFloat = 22.0 // To allow a tap target of 44x44 - - var accessibilityEntryElements: [NSUIAccessibilityElement] = [] - - for j in 0 ..< entryCount - { - guard let e = dataSet.entryForIndex(j) else { continue } - - let p = center.moving(distance: CGFloat((e.y - chart.chartYMin) * Double(factor) * phaseY), - atAngle: sliceangle * CGFloat(j) * CGFloat(phaseX) + chart.rotationAngle) - - if p.x.isNaN - { - continue - } - - if !hasMovedToPoint - { - path.move(to: p) - hasMovedToPoint = true - } - else - { - path.addLine(to: p) - } - - let accessibilityLabel = accessibilityAxisLabelValueTuples[j].0 - let accessibilityValue = accessibilityAxisLabelValueTuples[j].1 - let accessibilityValueIndex = accessibilityAxisLabelValueTuples[j].2 - - let axp = center.moving(distance: CGFloat((accessibilityValue - chart.chartYMin) * Double(factor) * phaseY), - atAngle: sliceangle * CGFloat(accessibilityValueIndex) * CGFloat(phaseX) + chart.rotationAngle) - - let axDescription = description + " - " + accessibilityLabel + ": \(accessibilityValue) \(chart.data?.accessibilityEntryLabelSuffix ?? "")" - let axElement = createAccessibleElement(withDescription: axDescription, - container: chart, - dataSet: dataSet) - { (element) in - element.accessibilityFrame = CGRect(x: axp.x - accessibilityFrameWidth, - y: axp.y - accessibilityFrameWidth, - width: 2 * accessibilityFrameWidth, - height: 2 * accessibilityFrameWidth) - } - - accessibilityEntryElements.append(axElement) - } - - // if this is the largest set, close it - if dataSet.entryCount < mostEntries - { - // if this is not the largest set, draw a line to the center before closing - path.addLine(to: center) - } - - path.closeSubpath() - - // draw filled - if dataSet.isDrawFilledEnabled - { - if dataSet.fill != nil - { - drawFilledPath(context: context, path: path, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha) - } - else - { - drawFilledPath(context: context, path: path, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha) - } - } - - // draw the line (only if filled is disabled or alpha is below 255) - if !dataSet.isDrawFilledEnabled || dataSet.fillAlpha < 1.0 - { - context.setStrokeColor(dataSet.color(atIndex: 0).cgColor) - context.setLineWidth(dataSet.lineWidth) - context.setAlpha(1.0) - - context.beginPath() - context.addPath(path) - context.strokePath() - - let axElement = createAccessibleElement(withDescription: accessibilityDataSetDescription, - container: chart, - dataSet: dataSet) - { (element) in - element.isHeader = true - element.accessibilityFrame = path.boundingBoxOfPath - } - - accessibleChartElements.append(axElement) - accessibleChartElements.append(contentsOf: accessibilityEntryElements) - } - - accessibilityPostLayoutChangedNotification() - - context.restoreGState() - } - - open override func drawValues(context: CGContext) - { - guard - let chart = chart, - let data = chart.data - else { return } - - let phaseX = animator.phaseX - let phaseY = animator.phaseY - - let sliceangle = chart.sliceAngle - - // calculate the factor that is needed for transforming the value to pixels - let factor = chart.factor - - let center = chart.centerOffsets - - let yoffset = CGFloat(5.0) - - for i in data.indices - { - guard let - dataSet = data[i] as? RadarChartDataSetProtocol, - shouldDrawValues(forDataSet: dataSet) - else { continue } - - let angleRadians = dataSet.valueLabelAngle.DEG2RAD - - let entryCount = dataSet.entryCount - - let iconsOffset = dataSet.iconsOffset - - for j in 0 ..< entryCount - { - guard let e = dataSet.entryForIndex(j) else { continue } - - let p = center.moving(distance: CGFloat(e.y - chart.chartYMin) * factor * CGFloat(phaseY), - atAngle: sliceangle * CGFloat(j) * CGFloat(phaseX) + chart.rotationAngle) - - let valueFont = dataSet.valueFont - - let formatter = dataSet.valueFormatter - - if dataSet.isDrawValuesEnabled - { - context.drawText(formatter.stringForValue(e.y, - entry: e, - dataSetIndex: i, - viewPortHandler: viewPortHandler), - at: CGPoint(x: p.x, y: p.y - yoffset - valueFont.lineHeight), - align: .center, - angleRadians: angleRadians, - attributes: [.font: valueFont, - .foregroundColor: dataSet.valueTextColorAt(j)]) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - var pIcon = center.moving(distance: CGFloat(e.y) * factor * CGFloat(phaseY) + iconsOffset.y, - atAngle: sliceangle * CGFloat(j) * CGFloat(phaseX) + chart.rotationAngle) - pIcon.y += iconsOffset.x - - context.drawImage(icon, - atCenter: CGPoint(x: pIcon.x, y: pIcon.y), - size: icon.size) - } - } - } - } - - open override func drawExtras(context: CGContext) - { - drawWeb(context: context) - } - - private var _webLineSegmentsBuffer = [CGPoint](repeating: CGPoint(), count: 2) - - @objc open func drawWeb(context: CGContext) - { - guard - let chart = chart, - let data = chart.data - else { return } - - let sliceangle = chart.sliceAngle - - context.saveGState() - - // calculate the factor that is needed for transforming the value to - // pixels - let factor = chart.factor - let rotationangle = chart.rotationAngle - - let center = chart.centerOffsets - - // draw the web lines that come from the center - context.setLineWidth(chart.webLineWidth) - context.setStrokeColor(chart.webColor.cgColor) - context.setAlpha(chart.webAlpha) - - let xIncrements = 1 + chart.skipWebLineCount - let maxEntryCount = chart.data?.maxEntryCountSet?.entryCount ?? 0 - - for i in stride(from: 0, to: maxEntryCount, by: xIncrements) - { - let p = center.moving(distance: CGFloat(chart.yRange) * factor, - atAngle: sliceangle * CGFloat(i) + rotationangle) - - _webLineSegmentsBuffer[0].x = center.x - _webLineSegmentsBuffer[0].y = center.y - _webLineSegmentsBuffer[1].x = p.x - _webLineSegmentsBuffer[1].y = p.y - - context.strokeLineSegments(between: _webLineSegmentsBuffer) - } - - // draw the inner-web - context.setLineWidth(chart.innerWebLineWidth) - context.setStrokeColor(chart.innerWebColor.cgColor) - context.setAlpha(chart.webAlpha) - - let labelCount = chart.yAxis.entryCount - - for j in 0 ..< labelCount - { - for i in 0 ..< data.entryCount - { - let r = CGFloat(chart.yAxis.entries[j] - chart.chartYMin) * factor - - let p1 = center.moving(distance: r, atAngle: sliceangle * CGFloat(i) + rotationangle) - let p2 = center.moving(distance: r, atAngle: sliceangle * CGFloat(i + 1) + rotationangle) - - _webLineSegmentsBuffer[0].x = p1.x - _webLineSegmentsBuffer[0].y = p1.y - _webLineSegmentsBuffer[1].x = p2.x - _webLineSegmentsBuffer[1].y = p2.y - - context.strokeLineSegments(between: _webLineSegmentsBuffer) - } - } - - context.restoreGState() - } - - private var _highlightPointBuffer = CGPoint() - - open override func drawHighlighted(context: CGContext, indices: [Highlight]) - { - guard - let chart = chart, - let radarData = chart.data as? RadarChartData - else { return } - - context.saveGState() - - let sliceangle = chart.sliceAngle - - // calculate the factor that is needed for transforming the value pixels - let factor = chart.factor - - let center = chart.centerOffsets - - for high in indices - { - guard - let set = chart.data?[high.dataSetIndex] as? RadarChartDataSetProtocol, - set.isHighlightEnabled - else { continue } - - guard let e = set.entryForIndex(Int(high.x)) as? RadarChartDataEntry - else { continue } - - if !isInBoundsX(entry: e, dataSet: set) - { - continue - } - - context.setLineWidth(radarData.highlightLineWidth) - if radarData.highlightLineDashLengths != nil - { - context.setLineDash(phase: radarData.highlightLineDashPhase, lengths: radarData.highlightLineDashLengths!) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - context.setStrokeColor(set.highlightColor.cgColor) - - let y = e.y - chart.chartYMin - - _highlightPointBuffer = center.moving(distance: CGFloat(y) * factor * CGFloat(animator.phaseY), - atAngle: sliceangle * CGFloat(high.x) * CGFloat(animator.phaseX) + chart.rotationAngle) - - high.setDraw(pt: _highlightPointBuffer) - - // draw the lines - drawHighlightLines(context: context, point: _highlightPointBuffer, set: set) - - if set.isDrawHighlightCircleEnabled - { - if !_highlightPointBuffer.x.isNaN && !_highlightPointBuffer.y.isNaN - { - var strokeColor = set.highlightCircleStrokeColor - if strokeColor == nil - { - strokeColor = set.color(atIndex: 0) - } - if set.highlightCircleStrokeAlpha < 1.0 - { - strokeColor = strokeColor?.withAlphaComponent(set.highlightCircleStrokeAlpha) - } - - drawHighlightCircle( - context: context, - atPoint: _highlightPointBuffer, - innerRadius: set.highlightCircleInnerRadius, - outerRadius: set.highlightCircleOuterRadius, - fillColor: set.highlightCircleFillColor, - strokeColor: strokeColor, - strokeWidth: set.highlightCircleStrokeWidth) - } - } - } - - context.restoreGState() - } - - internal func drawHighlightCircle( - context: CGContext, - atPoint point: CGPoint, - innerRadius: CGFloat, - outerRadius: CGFloat, - fillColor: NSUIColor?, - strokeColor: NSUIColor?, - strokeWidth: CGFloat) - { - context.saveGState() - - if let fillColor = fillColor - { - context.beginPath() - context.addEllipse(in: CGRect(x: point.x - outerRadius, y: point.y - outerRadius, width: outerRadius * 2.0, height: outerRadius * 2.0)) - if innerRadius > 0.0 - { - context.addEllipse(in: CGRect(x: point.x - innerRadius, y: point.y - innerRadius, width: innerRadius * 2.0, height: innerRadius * 2.0)) - } - - context.setFillColor(fillColor.cgColor) - context.fillPath(using: .evenOdd) - } - - if let strokeColor = strokeColor - { - context.beginPath() - context.addEllipse(in: CGRect(x: point.x - outerRadius, y: point.y - outerRadius, width: outerRadius * 2.0, height: outerRadius * 2.0)) - context.setStrokeColor(strokeColor.cgColor) - context.setLineWidth(strokeWidth) - context.strokePath() - } - - context.restoreGState() - } - - private func createAccessibleElement(withDescription description: String, - container: RadarChartView, - dataSet: RadarChartDataSetProtocol, - modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement { - - let element = NSUIAccessibilityElement(accessibilityContainer: container) - element.accessibilityLabel = description - - // The modifier allows changing of traits and frame depending on highlight, rotation, etc - modifier(element) - - return element - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/Renderer.swift b/Pods/Charts/Source/Charts/Renderers/Renderer.swift deleted file mode 100644 index dbdcb8ae3..000000000 --- a/Pods/Charts/Source/Charts/Renderers/Renderer.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Renderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartRenderer) -public protocol Renderer { - - /// the component that handles the drawing area of the chart and it's offsets - var viewPortHandler: ViewPortHandler { get } -} diff --git a/Pods/Charts/Source/Charts/Renderers/Scatter/ChevronDownShapeRenderer.swift b/Pods/Charts/Source/Charts/Renderers/Scatter/ChevronDownShapeRenderer.swift deleted file mode 100644 index 1dab1cc46..000000000 --- a/Pods/Charts/Source/Charts/Renderers/Scatter/ChevronDownShapeRenderer.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ChevronDownShapeRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// -import Foundation -import CoreGraphics - -open class ChevronDownShapeRenderer : NSObject, ShapeRenderer -{ - open func renderShape( - context: CGContext, - dataSet: ScatterChartDataSetProtocol, - viewPortHandler: ViewPortHandler, - point: CGPoint, - color: NSUIColor) - { - let shapeSize = dataSet.scatterShapeSize - let shapeHalf = shapeSize / 2.0 - - context.setLineWidth(1.0) - context.setStrokeColor(color.cgColor) - - context.beginPath() - context.move(to: CGPoint(x: point.x, y: point.y + 2 * shapeHalf)) - context.addLine(to: CGPoint(x: point.x + 2 * shapeHalf, y: point.y)) - context.move(to: CGPoint(x: point.x, y: point.y + 2 * shapeHalf)) - context.addLine(to: CGPoint(x: point.x - 2 * shapeHalf, y: point.y)) - context.strokePath() - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/Scatter/ChevronUpShapeRenderer.swift b/Pods/Charts/Source/Charts/Renderers/Scatter/ChevronUpShapeRenderer.swift deleted file mode 100644 index 0d077707f..000000000 --- a/Pods/Charts/Source/Charts/Renderers/Scatter/ChevronUpShapeRenderer.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ChevronUpShapeRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// -import Foundation -import CoreGraphics - -open class ChevronUpShapeRenderer : NSObject, ShapeRenderer -{ - open func renderShape( - context: CGContext, - dataSet: ScatterChartDataSetProtocol, - viewPortHandler: ViewPortHandler, - point: CGPoint, - color: NSUIColor) - { - let shapeSize = dataSet.scatterShapeSize - let shapeHalf = shapeSize / 2.0 - - context.setLineWidth(1.0) - context.setStrokeColor(color.cgColor) - - context.beginPath() - context.move(to: CGPoint(x: point.x, y: point.y - 2 * shapeHalf)) - context.addLine(to: CGPoint(x: point.x + 2 * shapeHalf, y: point.y)) - context.move(to: CGPoint(x: point.x, y: point.y - 2 * shapeHalf)) - context.addLine(to: CGPoint(x: point.x - 2 * shapeHalf, y: point.y)) - context.strokePath() - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/Scatter/CircleShapeRenderer.swift b/Pods/Charts/Source/Charts/Renderers/Scatter/CircleShapeRenderer.swift deleted file mode 100644 index fc0c4dfef..000000000 --- a/Pods/Charts/Source/Charts/Renderers/Scatter/CircleShapeRenderer.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// CircleShapeRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// -import Foundation -import CoreGraphics - -open class CircleShapeRenderer : NSObject, ShapeRenderer -{ - open func renderShape( - context: CGContext, - dataSet: ScatterChartDataSetProtocol, - viewPortHandler: ViewPortHandler, - point: CGPoint, - color: NSUIColor) - { - let shapeSize = dataSet.scatterShapeSize - let shapeHalf = shapeSize / 2.0 - let shapeHoleSizeHalf = dataSet.scatterShapeHoleRadius - let shapeHoleSize = shapeHoleSizeHalf * 2.0 - let shapeHoleColor = dataSet.scatterShapeHoleColor - let shapeStrokeSize = (shapeSize - shapeHoleSize) / 2.0 - let shapeStrokeSizeHalf = shapeStrokeSize / 2.0 - - if shapeHoleSize > 0.0 - { - context.setStrokeColor(color.cgColor) - context.setLineWidth(shapeStrokeSize) - var rect = CGRect() - rect.origin.x = point.x - shapeHoleSizeHalf - shapeStrokeSizeHalf - rect.origin.y = point.y - shapeHoleSizeHalf - shapeStrokeSizeHalf - rect.size.width = shapeHoleSize + shapeStrokeSize - rect.size.height = shapeHoleSize + shapeStrokeSize - context.strokeEllipse(in: rect) - - if let shapeHoleColor = shapeHoleColor - { - context.setFillColor(shapeHoleColor.cgColor) - rect.origin.x = point.x - shapeHoleSizeHalf - rect.origin.y = point.y - shapeHoleSizeHalf - rect.size.width = shapeHoleSize - rect.size.height = shapeHoleSize - context.fillEllipse(in: rect) - } - } - else - { - context.setFillColor(color.cgColor) - var rect = CGRect() - rect.origin.x = point.x - shapeHalf - rect.origin.y = point.y - shapeHalf - rect.size.width = shapeSize - rect.size.height = shapeSize - context.fillEllipse(in: rect) - } - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/Scatter/CrossShapeRenderer.swift b/Pods/Charts/Source/Charts/Renderers/Scatter/CrossShapeRenderer.swift deleted file mode 100644 index 7961da25a..000000000 --- a/Pods/Charts/Source/Charts/Renderers/Scatter/CrossShapeRenderer.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// CrossShapeRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// -import Foundation -import CoreGraphics - -open class CrossShapeRenderer : NSObject, ShapeRenderer -{ - open func renderShape( - context: CGContext, - dataSet: ScatterChartDataSetProtocol, - viewPortHandler: ViewPortHandler, - point: CGPoint, - color: NSUIColor) - { - let shapeSize = dataSet.scatterShapeSize - let shapeHalf = shapeSize / 2.0 - - context.setLineWidth(1.0) - context.setStrokeColor(color.cgColor) - - context.beginPath() - context.move(to: CGPoint(x: point.x - shapeHalf, y: point.y)) - context.addLine(to: CGPoint(x: point.x + shapeHalf, y: point.y)) - context.move(to: CGPoint(x: point.x, y: point.y - shapeHalf)) - context.addLine(to: CGPoint(x: point.x, y: point.y + shapeHalf)) - context.strokePath() - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/Scatter/ShapeRenderer.swift b/Pods/Charts/Source/Charts/Renderers/Scatter/ShapeRenderer.swift deleted file mode 100644 index afa2f9385..000000000 --- a/Pods/Charts/Source/Charts/Renderers/Scatter/ShapeRenderer.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ShapeRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc -public protocol ShapeRenderer: AnyObject -{ - /// Renders the provided ScatterDataSet with a shape. - /// - /// - Parameters: - /// - context: CGContext for drawing on - /// - dataSet: The DataSet to be drawn - /// - viewPortHandler: Contains information about the current state of the view - /// - point: Position to draw the shape at - /// - color: Color to draw the shape - func renderShape( - context: CGContext, - dataSet: ScatterChartDataSetProtocol, - viewPortHandler: ViewPortHandler, - point: CGPoint, - color: NSUIColor) -} diff --git a/Pods/Charts/Source/Charts/Renderers/Scatter/SquareShapeRenderer.swift b/Pods/Charts/Source/Charts/Renderers/Scatter/SquareShapeRenderer.swift deleted file mode 100644 index 948c1ff2e..000000000 --- a/Pods/Charts/Source/Charts/Renderers/Scatter/SquareShapeRenderer.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// SquareShapeRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// -import Foundation -import CoreGraphics - -open class SquareShapeRenderer : NSObject, ShapeRenderer -{ - open func renderShape( - context: CGContext, - dataSet: ScatterChartDataSetProtocol, - viewPortHandler: ViewPortHandler, - point: CGPoint, - color: NSUIColor) - { - let shapeSize = dataSet.scatterShapeSize - let shapeHalf = shapeSize / 2.0 - let shapeHoleSizeHalf = dataSet.scatterShapeHoleRadius - let shapeHoleSize = shapeHoleSizeHalf * 2.0 - let shapeHoleColor = dataSet.scatterShapeHoleColor - let shapeStrokeSize = (shapeSize - shapeHoleSize) / 2.0 - let shapeStrokeSizeHalf = shapeStrokeSize / 2.0 - - if shapeHoleSize > 0.0 - { - context.setStrokeColor(color.cgColor) - context.setLineWidth(shapeStrokeSize) - var rect = CGRect() - rect.origin.x = point.x - shapeHoleSizeHalf - shapeStrokeSizeHalf - rect.origin.y = point.y - shapeHoleSizeHalf - shapeStrokeSizeHalf - rect.size.width = shapeHoleSize + shapeStrokeSize - rect.size.height = shapeHoleSize + shapeStrokeSize - context.stroke(rect) - - if let shapeHoleColor = shapeHoleColor - { - context.setFillColor(shapeHoleColor.cgColor) - rect.origin.x = point.x - shapeHoleSizeHalf - rect.origin.y = point.y - shapeHoleSizeHalf - rect.size.width = shapeHoleSize - rect.size.height = shapeHoleSize - context.fill(rect) - } - } - else - { - context.setFillColor(color.cgColor) - var rect = CGRect() - rect.origin.x = point.x - shapeHalf - rect.origin.y = point.y - shapeHalf - rect.size.width = shapeSize - rect.size.height = shapeSize - context.fill(rect) - } - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/Scatter/TriangleShapeRenderer.swift b/Pods/Charts/Source/Charts/Renderers/Scatter/TriangleShapeRenderer.swift deleted file mode 100644 index 1f4bdca0d..000000000 --- a/Pods/Charts/Source/Charts/Renderers/Scatter/TriangleShapeRenderer.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// TriangleShapeRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// -import Foundation -import CoreGraphics - -open class TriangleShapeRenderer : NSObject, ShapeRenderer -{ - open func renderShape( - context: CGContext, - dataSet: ScatterChartDataSetProtocol, - viewPortHandler: ViewPortHandler, - point: CGPoint, - color: NSUIColor) - { - let shapeSize = dataSet.scatterShapeSize - let shapeHalf = shapeSize / 2.0 - let shapeHoleSizeHalf = dataSet.scatterShapeHoleRadius - let shapeHoleSize = shapeHoleSizeHalf * 2.0 - let shapeHoleColor = dataSet.scatterShapeHoleColor - let shapeStrokeSize = (shapeSize - shapeHoleSize) / 2.0 - - context.setFillColor(color.cgColor) - - // create a triangle path - context.beginPath() - context.move(to: CGPoint(x: point.x, y: point.y - shapeHalf)) - context.addLine(to: CGPoint(x: point.x + shapeHalf, y: point.y + shapeHalf)) - context.addLine(to: CGPoint(x: point.x - shapeHalf, y: point.y + shapeHalf)) - - if shapeHoleSize > 0.0 - { - context.addLine(to: CGPoint(x: point.x, y: point.y - shapeHalf)) - - context.move(to: CGPoint(x: point.x - shapeHalf + shapeStrokeSize, y: point.y + shapeHalf - shapeStrokeSize)) - context.addLine(to: CGPoint(x: point.x + shapeHalf - shapeStrokeSize, y: point.y + shapeHalf - shapeStrokeSize)) - context.addLine(to: CGPoint(x: point.x, y: point.y - shapeHalf + shapeStrokeSize)) - context.addLine(to: CGPoint(x: point.x - shapeHalf + shapeStrokeSize, y: point.y + shapeHalf - shapeStrokeSize)) - } - - context.closePath() - - context.fillPath() - - if shapeHoleSize > 0.0 && shapeHoleColor != nil - { - context.setFillColor(shapeHoleColor!.cgColor) - - // create a triangle path - context.beginPath() - context.move(to: CGPoint(x: point.x, y: point.y - shapeHalf + shapeStrokeSize)) - context.addLine(to: CGPoint(x: point.x + shapeHalf - shapeStrokeSize, y: point.y + shapeHalf - shapeStrokeSize)) - context.addLine(to: CGPoint(x: point.x - shapeHalf + shapeStrokeSize, y: point.y + shapeHalf - shapeStrokeSize)) - context.closePath() - - context.fillPath() - } - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/Scatter/XShapeRenderer.swift b/Pods/Charts/Source/Charts/Renderers/Scatter/XShapeRenderer.swift deleted file mode 100644 index 73e2efdd4..000000000 --- a/Pods/Charts/Source/Charts/Renderers/Scatter/XShapeRenderer.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// XShapeRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// -import Foundation -import CoreGraphics - -open class XShapeRenderer : NSObject, ShapeRenderer -{ - open func renderShape( - context: CGContext, - dataSet: ScatterChartDataSetProtocol, - viewPortHandler: ViewPortHandler, - point: CGPoint, - color: NSUIColor) - { - let shapeSize = dataSet.scatterShapeSize - let shapeHalf = shapeSize / 2.0 - - context.setLineWidth(1.0) - context.setStrokeColor(color.cgColor) - - context.beginPath() - context.move(to: CGPoint(x: point.x - shapeHalf, y: point.y - shapeHalf)) - context.addLine(to: CGPoint(x: point.x + shapeHalf, y: point.y + shapeHalf)) - context.move(to: CGPoint(x: point.x + shapeHalf, y: point.y - shapeHalf)) - context.addLine(to: CGPoint(x: point.x - shapeHalf, y: point.y + shapeHalf)) - context.strokePath() - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/ScatterChartRenderer.swift b/Pods/Charts/Source/Charts/Renderers/ScatterChartRenderer.swift deleted file mode 100644 index 9fb1f560c..000000000 --- a/Pods/Charts/Source/Charts/Renderers/ScatterChartRenderer.swift +++ /dev/null @@ -1,240 +0,0 @@ -// -// ScatterChartRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class ScatterChartRenderer: LineScatterCandleRadarRenderer -{ - @objc open weak var dataProvider: ScatterChartDataProvider? - - @objc public init(dataProvider: ScatterChartDataProvider, animator: Animator, viewPortHandler: ViewPortHandler) - { - super.init(animator: animator, viewPortHandler: viewPortHandler) - - self.dataProvider = dataProvider - } - - open override func drawData(context: CGContext) - { - guard let scatterData = dataProvider?.scatterData else { return } - - // If we redraw the data, remove and repopulate accessible elements to update label values and frames - accessibleChartElements.removeAll() - - if let chart = dataProvider as? ScatterChartView { - // Make the chart header the first element in the accessible elements array - let element = createAccessibleHeader(usingChart: chart, - andData: scatterData, - withDefaultDescription: "Scatter Chart") - accessibleChartElements.append(element) - } - - // TODO: Due to the potential complexity of data presented in Scatter charts, a more usable way - // for VO accessibility would be to use axis based traversal rather than by dataset. - // Hence, accessibleChartElements is not populated below. (Individual renderers guard against dataSource being their respective views) - let sets = scatterData.dataSets as? [ScatterChartDataSet] - assert(sets != nil, "Datasets for ScatterChartRenderer must conform to IScatterChartDataSet") - - let drawDataSet = { self.drawDataSet(context: context, dataSet: $0) } - sets!.lazy - .filter(\.isVisible) - .forEach(drawDataSet) - } - - private var _lineSegments = [CGPoint](repeating: CGPoint(), count: 2) - - @objc open func drawDataSet(context: CGContext, dataSet: ScatterChartDataSetProtocol) - { - guard let dataProvider = dataProvider else { return } - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - - let phaseY = animator.phaseY - - let entryCount = dataSet.entryCount - - var point = CGPoint() - - let valueToPixelMatrix = trans.valueToPixelMatrix - - if let renderer = dataSet.shapeRenderer - { - context.saveGState() - - for j in 0 ..< Int(min(ceil(Double(entryCount) * animator.phaseX), Double(entryCount))) - { - guard let e = dataSet.entryForIndex(j) else { continue } - - point.x = CGFloat(e.x) - point.y = CGFloat(e.y * phaseY) - point = point.applying(valueToPixelMatrix) - - if !viewPortHandler.isInBoundsRight(point.x) - { - break - } - - if !viewPortHandler.isInBoundsLeft(point.x) || - !viewPortHandler.isInBoundsY(point.y) - { - continue - } - - renderer.renderShape(context: context, dataSet: dataSet, viewPortHandler: viewPortHandler, point: point, color: dataSet.color(atIndex: j)) - } - - context.restoreGState() - } - else - { - print("There's no ShapeRenderer specified for ScatterDataSet", terminator: "\n") - } - } - - open override func drawValues(context: CGContext) - { - guard - let dataProvider = dataProvider, - let scatterData = dataProvider.scatterData - else { return } - - // if values are drawn - if isDrawingValuesAllowed(dataProvider: dataProvider) - { - let phaseY = animator.phaseY - - var pt = CGPoint() - - for i in scatterData.indices - { - guard let dataSet = scatterData[i] as? ScatterChartDataSetProtocol, - shouldDrawValues(forDataSet: dataSet) - else { continue } - - let valueFont = dataSet.valueFont - - let formatter = dataSet.valueFormatter - - let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - let valueToPixelMatrix = trans.valueToPixelMatrix - - let iconsOffset = dataSet.iconsOffset - - let angleRadians = dataSet.valueLabelAngle.DEG2RAD - - let shapeSize = dataSet.scatterShapeSize - let lineHeight = valueFont.lineHeight - - _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) - - for j in _xBounds - { - guard let e = dataSet.entryForIndex(j) else { break } - - pt.x = CGFloat(e.x) - pt.y = CGFloat(e.y * phaseY) - pt = pt.applying(valueToPixelMatrix) - - if (!viewPortHandler.isInBoundsRight(pt.x)) - { - break - } - - // make sure the lines don't do shitty things outside bounds - if (!viewPortHandler.isInBoundsLeft(pt.x) - || !viewPortHandler.isInBoundsY(pt.y)) - { - continue - } - - let text = formatter.stringForValue( - e.y, - entry: e, - dataSetIndex: i, - viewPortHandler: viewPortHandler) - - if dataSet.isDrawValuesEnabled - { - context.drawText(text, - at: CGPoint(x: pt.x, - y: pt.y - shapeSize - lineHeight), - align: .center, - angleRadians: angleRadians, - attributes: [.font: valueFont, - .foregroundColor: dataSet.valueTextColorAt(j)] - ) - } - - if let icon = e.icon, dataSet.isDrawIconsEnabled - { - context.drawImage(icon, - atCenter: CGPoint(x: pt.x + iconsOffset.x, - y: pt.y + iconsOffset.y), - size: icon.size) - } - } - } - } - } - - open override func drawExtras(context: CGContext) - { - - } - - open override func drawHighlighted(context: CGContext, indices: [Highlight]) - { - guard - let dataProvider = dataProvider, - let scatterData = dataProvider.scatterData - else { return } - - context.saveGState() - - for high in indices - { - guard - let set = scatterData[high.dataSetIndex] as? ScatterChartDataSetProtocol, - set.isHighlightEnabled - else { continue } - - guard let entry = set.entryForXValue(high.x, closestToY: high.y) else { continue } - - if !isInBoundsX(entry: entry, dataSet: set) { continue } - - context.setStrokeColor(set.highlightColor.cgColor) - context.setLineWidth(set.highlightLineWidth) - if set.highlightLineDashLengths != nil - { - context.setLineDash(phase: set.highlightLineDashPhase, lengths: set.highlightLineDashLengths!) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - let x = entry.x // get the x-position - let y = entry.y * Double(animator.phaseY) - - let trans = dataProvider.getTransformer(forAxis: set.axisDependency) - - let pt = trans.pixelForValues(x: x, y: y) - - high.setDraw(pt: pt) - - // draw the lines - drawHighlightLines(context: context, point: pt, set: set) - } - - context.restoreGState() - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/XAxisRenderer.swift b/Pods/Charts/Source/Charts/Renderers/XAxisRenderer.swift deleted file mode 100644 index 966bf3b7e..000000000 --- a/Pods/Charts/Source/Charts/Renderers/XAxisRenderer.swift +++ /dev/null @@ -1,488 +0,0 @@ -// -// XAxisRenderer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -@objc(ChartXAxisRenderer) -open class XAxisRenderer: NSObject, AxisRenderer -{ - @objc public let viewPortHandler: ViewPortHandler - @objc public let axis: XAxis - @objc public let transformer: Transformer? - - @objc public init(viewPortHandler: ViewPortHandler, axis: XAxis, transformer: Transformer?) - { - self.viewPortHandler = viewPortHandler - self.axis = axis - self.transformer = transformer - - super.init() - } - - open func computeAxis(min: Double, max: Double, inverted: Bool) - { - var min = min, max = max - - if let transformer = self.transformer, - viewPortHandler.contentWidth > 10, - !viewPortHandler.isFullyZoomedOutX - { - // calculate the starting and entry point of the y-labels (depending on - // zoom / contentrect bounds) - let p1 = transformer.valueForTouchPoint(CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentTop)) - let p2 = transformer.valueForTouchPoint(CGPoint(x: viewPortHandler.contentRight, y: viewPortHandler.contentTop)) - - min = inverted ? Double(p2.x) : Double(p1.x) - max = inverted ? Double(p1.x) : Double(p2.x) - } - - computeAxisValues(min: min, max: max) - } - - open func computeAxisValues(min: Double, max: Double) - { - let yMin = min - let yMax = max - - let labelCount = axis.labelCount - let range = abs(yMax - yMin) - - guard - labelCount != 0, - range > 0, - range.isFinite - else - { - axis.entries = [] - axis.centeredEntries = [] - return - } - - // Find out how much spacing (in y value space) between axis values - let rawInterval = range / Double(labelCount) - var interval = rawInterval.roundedToNextSignificant() - - // If granularity is enabled, then do not allow the interval to go below specified granularity. - // This is used to avoid repeated values when rounding values for display. - if axis.granularityEnabled - { - interval = Swift.max(interval, axis.granularity) - } - - // Normalize interval - let intervalMagnitude = pow(10.0, Double(Int(log10(interval)))).roundedToNextSignificant() - let intervalSigDigit = Int(interval / intervalMagnitude) - if intervalSigDigit > 5 - { - // Use one order of magnitude higher, to avoid intervals like 0.9 or 90 - interval = floor(10.0 * Double(intervalMagnitude)) - } - - var n = axis.centerAxisLabelsEnabled ? 1 : 0 - - // force label count - if axis.isForceLabelsEnabled - { - interval = range / Double(labelCount - 1) - - // Ensure stops contains at least n elements. - axis.entries.removeAll(keepingCapacity: true) - axis.entries.reserveCapacity(labelCount) - - let values = stride(from: yMin, to: Double(labelCount) * interval + yMin, by: interval) - axis.entries.append(contentsOf: values) - - n = labelCount - } - else - { - // no forced count - - var first = interval == 0.0 ? 0.0 : ceil(yMin / interval) * interval - - if axis.centerAxisLabelsEnabled - { - first -= interval - } - - let last = interval == 0.0 ? 0.0 : (floor(yMax / interval) * interval).nextUp - - if interval != 0.0, last != first - { - stride(from: first, through: last, by: interval).forEach { _ in n += 1 } - } - - // Ensure stops contains at least n elements. - axis.entries.removeAll(keepingCapacity: true) - axis.entries.reserveCapacity(labelCount) - - let start = first, end = first + Double(n) * interval - - // Fix for IEEE negative zero case (Where value == -0.0, and 0.0 == -0.0) - let values = stride(from: start, to: end, by: interval).map { $0 == 0.0 ? 0.0 : $0 } - axis.entries.append(contentsOf: values) - } - - // set decimals - if interval < 1 - { - axis.decimals = Int(ceil(-log10(interval))) - } - else - { - axis.decimals = 0 - } - - if axis.centerAxisLabelsEnabled - { - let offset: Double = interval / 2.0 - axis.centeredEntries = axis.entries[.. 1 - { - let width = labelns.boundingRect(with: labelMaxSize, options: .usesLineFragmentOrigin, attributes: labelAttrs, context: nil).size.width - - if width > viewPortHandler.offsetRight * 2.0, - position.x + width > viewPortHandler.chartWidth - { - position.x -= width / 2.0 - } - } - else if i == 0 - { // avoid clipping of the first - let width = labelns.boundingRect(with: labelMaxSize, options: .usesLineFragmentOrigin, attributes: labelAttrs, context: nil).size.width - position.x += width / 2.0 - } - } - - drawLabel(context: context, - formattedLabel: label, - x: position.x, - y: pos, - attributes: labelAttrs, - constrainedTo: labelMaxSize, - anchor: anchor, - angleRadians: labelRotationAngleRadians) - } - } - - @objc open func drawLabel( - context: CGContext, - formattedLabel: String, - x: CGFloat, - y: CGFloat, - attributes: [NSAttributedString.Key : Any], - constrainedTo size: CGSize, - anchor: CGPoint, - angleRadians: CGFloat) - { - context.drawMultilineText(formattedLabel, - at: CGPoint(x: x, y: y), - constrainedTo: size, - anchor: anchor, - angleRadians: angleRadians, - attributes: attributes) - } - - open func renderGridLines(context: CGContext) - { - guard - let transformer = self.transformer, - axis.isEnabled, - axis.isDrawGridLinesEnabled - else { return } - - context.saveGState() - defer { context.restoreGState() } - - context.clip(to: self.gridClippingRect) - - context.setShouldAntialias(axis.gridAntialiasEnabled) - context.setStrokeColor(axis.gridColor.cgColor) - context.setLineWidth(axis.gridLineWidth) - context.setLineCap(axis.gridLineCap) - - if axis.gridLineDashLengths != nil - { - context.setLineDash(phase: axis.gridLineDashPhase, lengths: axis.gridLineDashLengths) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - let valueToPixelMatrix = transformer.valueToPixelMatrix - - var position = CGPoint.zero - - let entries = axis.entries - - for entry in entries - { - position.x = CGFloat(entry) - position.y = CGFloat(entry) - position = position.applying(valueToPixelMatrix) - - drawGridLine(context: context, x: position.x, y: position.y) - } - } - - @objc open var gridClippingRect: CGRect - { - var contentRect = viewPortHandler.contentRect - let dx = self.axis.gridLineWidth - contentRect.origin.x -= dx / 2.0 - contentRect.size.width += dx - return contentRect - } - - @objc open func drawGridLine(context: CGContext, x: CGFloat, y: CGFloat) - { - guard x >= viewPortHandler.offsetLeft && x <= viewPortHandler.chartWidth else { return } - - context.beginPath() - context.move(to: CGPoint(x: x, y: viewPortHandler.contentTop)) - context.addLine(to: CGPoint(x: x, y: viewPortHandler.contentBottom)) - context.strokePath() - } - - open func renderLimitLines(context: CGContext) - { - guard - let transformer = self.transformer, - !axis.limitLines.isEmpty - else { return } - - let trans = transformer.valueToPixelMatrix - - var position = CGPoint.zero - - for l in axis.limitLines where l.isEnabled - { - context.saveGState() - defer { context.restoreGState() } - - var clippingRect = viewPortHandler.contentRect - clippingRect.origin.x -= l.lineWidth / 2.0 - clippingRect.size.width += l.lineWidth - context.clip(to: clippingRect) - - position.x = CGFloat(l.limit) - position.y = 0.0 - position = position.applying(trans) - - renderLimitLineLine(context: context, limitLine: l, position: position) - renderLimitLineLabel(context: context, limitLine: l, position: position, yOffset: 2.0 + l.yOffset) - } - } - - @objc open func renderLimitLineLine(context: CGContext, limitLine: ChartLimitLine, position: CGPoint) - { - context.beginPath() - context.move(to: CGPoint(x: position.x, y: viewPortHandler.contentTop)) - context.addLine(to: CGPoint(x: position.x, y: viewPortHandler.contentBottom)) - - context.setStrokeColor(limitLine.lineColor.cgColor) - context.setLineWidth(limitLine.lineWidth) - if limitLine.lineDashLengths != nil - { - context.setLineDash(phase: limitLine.lineDashPhase, lengths: limitLine.lineDashLengths!) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - context.strokePath() - } - - @objc open func renderLimitLineLabel(context: CGContext, limitLine: ChartLimitLine, position: CGPoint, yOffset: CGFloat) - { - let label = limitLine.label - - // if drawing the limit-value label is enabled - guard limitLine.drawLabelEnabled, !label.isEmpty else { return } - - let labelLineHeight = limitLine.valueFont.lineHeight - - let xOffset: CGFloat = limitLine.lineWidth + limitLine.xOffset - - let align: TextAlignment - let point: CGPoint - - switch limitLine.labelPosition - { - case .rightTop: - align = .left - point = CGPoint(x: position.x + xOffset, - y: viewPortHandler.contentTop + yOffset) - - case .rightBottom: - align = .left - point = CGPoint(x: position.x + xOffset, - y: viewPortHandler.contentBottom - labelLineHeight - yOffset) - - case .leftTop: - align = .right - point = CGPoint(x: position.x - xOffset, - y: viewPortHandler.contentTop + yOffset) - - case .leftBottom: - align = .right - point = CGPoint(x: position.x - xOffset, - y: viewPortHandler.contentBottom - labelLineHeight - yOffset) - } - - context.drawText(label, - at: point, - align: align, - attributes: [.font: limitLine.valueFont, - .foregroundColor: limitLine.valueTextColor]) - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/XAxisRendererHorizontalBarChart.swift b/Pods/Charts/Source/Charts/Renderers/XAxisRendererHorizontalBarChart.swift deleted file mode 100644 index a7429dbb6..000000000 --- a/Pods/Charts/Source/Charts/Renderers/XAxisRendererHorizontalBarChart.swift +++ /dev/null @@ -1,290 +0,0 @@ -// -// XAxisRendererHorizontalBarChart.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class XAxisRendererHorizontalBarChart: XAxisRenderer -{ - internal weak var chart: BarChartView? - - @objc public init(viewPortHandler: ViewPortHandler, axis: XAxis, transformer: Transformer?, chart: BarChartView) - { - super.init(viewPortHandler: viewPortHandler, axis: axis, transformer: transformer) - - self.chart = chart - } - - open override func computeAxis(min: Double, max: Double, inverted: Bool) - { - var min = min, max = max - - if let transformer = self.transformer, - viewPortHandler.contentWidth > 10, - !viewPortHandler.isFullyZoomedOutY - { - // calculate the starting and entry point of the y-labels (depending on - // zoom / contentrect bounds) - let p1 = transformer.valueForTouchPoint(CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentBottom)) - let p2 = transformer.valueForTouchPoint(CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentTop)) - - min = inverted ? Double(p2.y) : Double(p1.y) - max = inverted ? Double(p1.y) : Double(p2.y) - } - - computeAxisValues(min: min, max: max) - } - - open override func computeSize() - { - let longest = axis.getLongestLabel() as NSString - - let labelSize = longest.size(withAttributes: [.font: axis.labelFont]) - - let labelWidth = floor(labelSize.width + axis.xOffset * 3.5) - let labelHeight = labelSize.height - let labelRotatedSize = CGSize(width: labelSize.width, height: labelHeight).rotatedBy(degrees: axis.labelRotationAngle) - - axis.labelWidth = labelWidth - axis.labelHeight = labelHeight - axis.labelRotatedWidth = round(labelRotatedSize.width + axis.xOffset * 3.5) - axis.labelRotatedHeight = round(labelRotatedSize.height) - } - - open override func renderAxisLabels(context: CGContext) - { - guard - axis.isEnabled, - axis.isDrawLabelsEnabled, - chart?.data != nil - else { return } - - let xoffset = axis.xOffset - - switch axis.labelPosition { - case .top: - drawLabels(context: context, pos: viewPortHandler.contentRight + xoffset, anchor: CGPoint(x: 0.0, y: 0.5)) - - case .topInside: - drawLabels(context: context, pos: viewPortHandler.contentRight - xoffset, anchor: CGPoint(x: 1.0, y: 0.5)) - - case .bottom: - drawLabels(context: context, pos: viewPortHandler.contentLeft - xoffset, anchor: CGPoint(x: 1.0, y: 0.5)) - - case .bottomInside: - drawLabels(context: context, pos: viewPortHandler.contentLeft + xoffset, anchor: CGPoint(x: 0.0, y: 0.5)) - - case .bothSided: - drawLabels(context: context, pos: viewPortHandler.contentRight + xoffset, anchor: CGPoint(x: 0.0, y: 0.5)) - drawLabels(context: context, pos: viewPortHandler.contentLeft - xoffset, anchor: CGPoint(x: 1.0, y: 0.5)) - } - } - - /// draws the x-labels on the specified y-position - open override func drawLabels(context: CGContext, pos: CGFloat, anchor: CGPoint) - { - guard let transformer = self.transformer else { return } - - let labelFont = axis.labelFont - let labelTextColor = axis.labelTextColor - let labelRotationAngleRadians = axis.labelRotationAngle.DEG2RAD - - let centeringEnabled = axis.isCenterAxisLabelsEnabled - - // pre allocate to save performance (dont allocate in loop) - var position = CGPoint.zero - - for i in 0.. [CGPoint] - { - guard let transformer = self.transformer else { return [] } - - var positions = axis.entries.map { CGPoint(x: 0.0, y: $0) } - transformer.pointValuesToPixel(&positions) - - return positions - } - - /// Draws the zero line at the specified position. - @objc open func drawZeroLine(context: CGContext) - { - guard - let transformer = self.transformer, - let zeroLineColor = axis.zeroLineColor - else { return } - - context.saveGState() - defer { context.restoreGState() } - - var clippingRect = viewPortHandler.contentRect - clippingRect.origin.y -= axis.zeroLineWidth / 2.0 - clippingRect.size.height += axis.zeroLineWidth - context.clip(to: clippingRect) - - context.setStrokeColor(zeroLineColor.cgColor) - context.setLineWidth(axis.zeroLineWidth) - - let pos = transformer.pixelForValues(x: 0.0, y: 0.0) - - if axis.zeroLineDashLengths != nil - { - context.setLineDash(phase: axis.zeroLineDashPhase, lengths: axis.zeroLineDashLengths!) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - context.move(to: CGPoint(x: viewPortHandler.contentLeft, y: pos.y)) - context.addLine(to: CGPoint(x: viewPortHandler.contentRight, y: pos.y)) - context.drawPath(using: CGPathDrawingMode.stroke) - } - - open func renderLimitLines(context: CGContext) - { - guard let transformer = self.transformer else { return } - - let limitLines = axis.limitLines - - guard !limitLines.isEmpty else { return } - - context.saveGState() - defer { context.restoreGState() } - - let trans = transformer.valueToPixelMatrix - - var position = CGPoint(x: 0.0, y: 0.0) - - for l in limitLines where l.isEnabled - { - context.saveGState() - defer { context.restoreGState() } - - var clippingRect = viewPortHandler.contentRect - clippingRect.origin.y -= l.lineWidth / 2.0 - clippingRect.size.height += l.lineWidth - context.clip(to: clippingRect) - - position.x = 0.0 - position.y = CGFloat(l.limit) - position = position.applying(trans) - - context.beginPath() - context.move(to: CGPoint(x: viewPortHandler.contentLeft, y: position.y)) - context.addLine(to: CGPoint(x: viewPortHandler.contentRight, y: position.y)) - - context.setStrokeColor(l.lineColor.cgColor) - context.setLineWidth(l.lineWidth) - if l.lineDashLengths != nil - { - context.setLineDash(phase: l.lineDashPhase, lengths: l.lineDashLengths!) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - context.strokePath() - - let label = l.label - - // if drawing the limit-value label is enabled - guard l.drawLabelEnabled, !label.isEmpty else { continue } - - let labelLineHeight = l.valueFont.lineHeight - - let xOffset = 4.0 + l.xOffset - let yOffset = l.lineWidth + labelLineHeight + l.yOffset - - let align: TextAlignment - let point: CGPoint - - switch l.labelPosition - { - case .rightTop: - align = .right - point = CGPoint(x: viewPortHandler.contentRight - xOffset, - y: position.y - yOffset) - - case .rightBottom: - align = .right - point = CGPoint(x: viewPortHandler.contentRight - xOffset, - y: position.y + yOffset - labelLineHeight) - - case .leftTop: - align = .left - point = CGPoint(x: viewPortHandler.contentLeft + xOffset, - y: position.y - yOffset) - - case .leftBottom: - align = .left - point = CGPoint(x: viewPortHandler.contentLeft + xOffset, - y: position.y + yOffset - labelLineHeight) - } - - context.drawText(label, - at: point, - align: align, - attributes: [.font: l.valueFont, .foregroundColor: l.valueTextColor]) - } - } - - @objc open func computeAxis(min: Double, max: Double, inverted: Bool) - { - var min = min, max = max - - if let transformer = self.transformer, - viewPortHandler.contentWidth > 10.0, - !viewPortHandler.isFullyZoomedOutY - { - // calculate the starting and entry point of the y-labels (depending on zoom / contentrect bounds) - let p1 = transformer.valueForTouchPoint(CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentTop)) - let p2 = transformer.valueForTouchPoint(CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentBottom)) - - min = inverted ? Double(p1.y) : Double(p2.y) - max = inverted ? Double(p2.y) : Double(p1.y) - } - - computeAxisValues(min: min, max: max) - } - - @objc open func computeAxisValues(min: Double, max: Double) - { - let yMin = min - let yMax = max - - let labelCount = axis.labelCount - let range = abs(yMax - yMin) - - guard - labelCount != 0, - range > 0, - range.isFinite - else - { - axis.entries = [] - axis.centeredEntries = [] - return - } - - // Find out how much spacing (in y value space) between axis values - let rawInterval = range / Double(labelCount) - var interval = rawInterval.roundedToNextSignificant() - - // If granularity is enabled, then do not allow the interval to go below specified granularity. - // This is used to avoid repeated values when rounding values for display. - if axis.granularityEnabled - { - interval = Swift.max(interval, axis.granularity) - } - - // Normalize interval - let intervalMagnitude = pow(10.0, Double(Int(log10(interval)))).roundedToNextSignificant() - let intervalSigDigit = Int(interval / intervalMagnitude) - if intervalSigDigit > 5 - { - // Use one order of magnitude higher, to avoid intervals like 0.9 or 90 - interval = floor(10.0 * Double(intervalMagnitude)) - } - - var n = axis.centerAxisLabelsEnabled ? 1 : 0 - - // force label count - if axis.isForceLabelsEnabled - { - interval = Double(range) / Double(labelCount - 1) - - // Ensure stops contains at least n elements. - axis.entries.removeAll(keepingCapacity: true) - axis.entries.reserveCapacity(labelCount) - - let values = stride(from: yMin, to: Double(labelCount) * interval + yMin, by: interval) - axis.entries.append(contentsOf: values) - - n = labelCount - } - else - { - // no forced count - - var first = interval == 0.0 ? 0.0 : ceil(yMin / interval) * interval - - if axis.centerAxisLabelsEnabled - { - first -= interval - } - - let last = interval == 0.0 ? 0.0 : (floor(yMax / interval) * interval).nextUp - - if interval != 0.0, last != first - { - stride(from: first, through: last, by: interval).forEach { _ in n += 1 } - } - - // Ensure stops contains at least n elements. - axis.entries.removeAll(keepingCapacity: true) - axis.entries.reserveCapacity(labelCount) - - // Fix for IEEE negative zero case (Where value == -0.0, and 0.0 == -0.0) - let values = stride(from: first, to: Double(n) * interval + first, by: interval).map { $0 == 0.0 ? 0.0 : $0 } - axis.entries.append(contentsOf: values) - } - - // set decimals - if interval < 1 - { - axis.decimals = Int(ceil(-log10(interval))) - } - else - { - axis.decimals = 0 - } - - if axis.centerAxisLabelsEnabled - { - axis.centeredEntries.reserveCapacity(n) - axis.centeredEntries.removeAll() - - let offset: Double = interval / 2.0 - axis.centeredEntries.append(contentsOf: axis.entries.map { $0 + offset }) - } - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/YAxisRendererHorizontalBarChart.swift b/Pods/Charts/Source/Charts/Renderers/YAxisRendererHorizontalBarChart.swift deleted file mode 100644 index f4aa877d0..000000000 --- a/Pods/Charts/Source/Charts/Renderers/YAxisRendererHorizontalBarChart.swift +++ /dev/null @@ -1,294 +0,0 @@ -// -// YAxisRendererHorizontalBarChart.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -open class YAxisRendererHorizontalBarChart: YAxisRenderer -{ - public override init(viewPortHandler: ViewPortHandler, axis: YAxis, transformer: Transformer?) - { - super.init(viewPortHandler: viewPortHandler, axis: axis, transformer: transformer) - } - - /// Computes the axis values. - open override func computeAxis(min: Double, max: Double, inverted: Bool) - { - var min = min, max = max - - // calculate the starting and entry point of the y-labels (depending on zoom / contentrect bounds) - if let transformer = transformer, - viewPortHandler.contentHeight > 10.0, - !viewPortHandler.isFullyZoomedOutX - { - let p1 = transformer.valueForTouchPoint(CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentTop)) - let p2 = transformer.valueForTouchPoint(CGPoint(x: viewPortHandler.contentRight, y: viewPortHandler.contentTop)) - - min = inverted ? Double(p2.x) : Double(p1.x) - max = inverted ? Double(p1.x) : Double(p2.x) - } - - computeAxisValues(min: min, max: max) - } - - /// draws the y-axis labels to the screen - open override func renderAxisLabels(context: CGContext) - { - guard - axis.isEnabled, - axis.isDrawLabelsEnabled - else { return } - - let lineHeight = axis.labelFont.lineHeight - let baseYOffset: CGFloat = 2.5 - - let dependency = axis.axisDependency - let labelPosition = axis.labelPosition - - // Comparing with Android code, the code here is slightly different about lineHeight - let yPos: CGFloat = - { - switch (dependency, labelPosition) - { - case (.left, .outsideChart): - return viewPortHandler.contentTop - baseYOffset - lineHeight - - case (.left, .insideChart): - return viewPortHandler.contentTop - baseYOffset - lineHeight - - case (.right, .outsideChart): - return viewPortHandler.contentBottom + baseYOffset - - case (.right, .insideChart): - return viewPortHandler.contentBottom + baseYOffset - } - }() - - drawYLabels( - context: context, - fixedPosition: yPos, - positions: transformedPositions(), - offset: axis.yOffset - ) - } - - open override func renderAxisLine(context: CGContext) - { - guard - axis.isEnabled, - axis.drawAxisLineEnabled - else { return } - - context.saveGState() - defer { context.restoreGState() } - - context.setStrokeColor(axis.axisLineColor.cgColor) - context.setLineWidth(axis.axisLineWidth) - if axis.axisLineDashLengths != nil - { - context.setLineDash(phase: axis.axisLineDashPhase, lengths: axis.axisLineDashLengths) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - context.beginPath() - if axis.axisDependency == .left - { - context.move(to: CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentTop)) - context.addLine(to: CGPoint(x: viewPortHandler.contentRight, y: viewPortHandler.contentTop)) - } - else - { - context.move(to: CGPoint(x: viewPortHandler.contentLeft, y: viewPortHandler.contentBottom)) - context.addLine(to: CGPoint(x: viewPortHandler.contentRight, y: viewPortHandler.contentBottom)) - } - context.strokePath() - } - - /// draws the y-labels on the specified x-position - @objc open func drawYLabels( - context: CGContext, - fixedPosition: CGFloat, - positions: [CGPoint], - offset: CGFloat) - { - let labelFont = axis.labelFont - let labelTextColor = axis.labelTextColor - - let from = axis.isDrawBottomYLabelEntryEnabled ? 0 : 1 - let to = axis.isDrawTopYLabelEntryEnabled ? axis.entryCount : (axis.entryCount - 1) - - let xOffset = axis.labelXOffset - - for i in from.. [CGPoint] - { - guard let transformer = self.transformer else { return [] } - - var positions = axis.entries.map { CGPoint(x: $0, y: 0.0) } - transformer.pointValuesToPixel(&positions) - - return positions - } - - /// Draws the zero line at the specified position. - open override func drawZeroLine(context: CGContext) - { - guard - let transformer = self.transformer, - let zeroLineColor = axis.zeroLineColor - else { return } - - context.saveGState() - defer { context.restoreGState() } - - var clippingRect = viewPortHandler.contentRect - clippingRect.origin.x -= axis.zeroLineWidth / 2.0 - clippingRect.size.width += axis.zeroLineWidth - context.clip(to: clippingRect) - - context.setStrokeColor(zeroLineColor.cgColor) - context.setLineWidth(axis.zeroLineWidth) - - let pos = transformer.pixelForValues(x: 0.0, y: 0.0) - - if axis.zeroLineDashLengths != nil - { - context.setLineDash(phase: axis.zeroLineDashPhase, lengths: axis.zeroLineDashLengths!) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - context.move(to: CGPoint(x: pos.x - 1.0, y: viewPortHandler.contentTop)) - context.addLine(to: CGPoint(x: pos.x - 1.0, y: viewPortHandler.contentBottom)) - context.drawPath(using: .stroke) - } - - open override func renderLimitLines(context: CGContext) - { - guard let transformer = self.transformer else { return } - - let limitLines = axis.limitLines - - guard !limitLines.isEmpty else { return } - - context.saveGState() - defer { context.restoreGState() } - - let trans = transformer.valueToPixelMatrix - var position = CGPoint.zero - - for l in limitLines where l.isEnabled - { - context.saveGState() - defer { context.restoreGState() } - - var clippingRect = viewPortHandler.contentRect - clippingRect.origin.x -= l.lineWidth / 2.0 - clippingRect.size.width += l.lineWidth - context.clip(to: clippingRect) - - position = CGPoint(x: l.limit, y: 0) - .applying(trans) - - context.beginPath() - context.move(to: CGPoint(x: position.x, y: viewPortHandler.contentTop)) - context.addLine(to: CGPoint(x: position.x, y: viewPortHandler.contentBottom)) - - context.setStrokeColor(l.lineColor.cgColor) - context.setLineWidth(l.lineWidth) - if l.lineDashLengths != nil - { - context.setLineDash(phase: l.lineDashPhase, lengths: l.lineDashLengths!) - } - else - { - context.setLineDash(phase: 0.0, lengths: []) - } - - context.strokePath() - - let label = l.label - - // if drawing the limit-value label is enabled - if l.drawLabelEnabled, !label.isEmpty - { - let labelLineHeight = l.valueFont.lineHeight - - let xOffset = l.lineWidth + l.xOffset - let yOffset = 2.0 + l.yOffset - - let align: TextAlignment - let point: CGPoint - - switch l.labelPosition - { - case .rightTop: - align = .left - point = CGPoint(x: position.x + xOffset, - y: viewPortHandler.contentTop + yOffset) - - case .rightBottom: - align = .left - point = CGPoint(x: position.x + xOffset, - y: viewPortHandler.contentBottom - labelLineHeight - yOffset) - - case .leftTop: - align = .right - point = CGPoint(x: position.x - xOffset, - y: viewPortHandler.contentTop + yOffset) - - case .leftBottom: - align = .right - point = CGPoint(x: position.x - xOffset, - y: viewPortHandler.contentBottom - labelLineHeight - yOffset) - } - - context.drawText(label, - at: point, - align: align, - attributes: [.font: l.valueFont, .foregroundColor: l.valueTextColor]) - } - } - } -} diff --git a/Pods/Charts/Source/Charts/Renderers/YAxisRendererRadarChart.swift b/Pods/Charts/Source/Charts/Renderers/YAxisRendererRadarChart.swift deleted file mode 100644 index 3b3cde0bf..000000000 --- a/Pods/Charts/Source/Charts/Renderers/YAxisRendererRadarChart.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// YAxisRendererRadarChart.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - - -open class YAxisRendererRadarChart: YAxisRenderer -{ - private weak var chart: RadarChartView? - - @objc public init(viewPortHandler: ViewPortHandler, axis: YAxis, chart: RadarChartView) - { - self.chart = chart - - super.init(viewPortHandler: viewPortHandler, axis: axis, transformer: nil) - } - - open override func computeAxisValues(min yMin: Double, max yMax: Double) - { - let labelCount = axis.labelCount - let range = abs(yMax - yMin) - - guard labelCount != 0, - range > 0, - range.isFinite - else - { - axis.entries = [] - axis.centeredEntries = [] - return - } - - // Find out how much spacing (in yValue space) between axis values - let rawInterval = range / Double(labelCount) - var interval = rawInterval.roundedToNextSignificant() - - // If granularity is enabled, then do not allow the interval to go below specified granularity. - // This is used to avoid repeated values when rounding values for display. - if axis.isGranularityEnabled - { - interval = max(interval, axis.granularity) - } - - // Normalize interval - let intervalMagnitude = pow(10.0, floor(log10(interval))).roundedToNextSignificant() - let intervalSigDigit = Int(interval / intervalMagnitude) - - if intervalSigDigit > 5 - { - // Use one order of magnitude higher, to avoid intervals like 0.9 or 90 - // if it's 0.0 after floor(), we use the old value - interval = floor(10.0 * intervalMagnitude) == 0.0 ? interval : floor(10.0 * intervalMagnitude) - } - - let centeringEnabled = axis.isCenterAxisLabelsEnabled - var n = centeringEnabled ? 1 : 0 - - // force label count - if axis.isForceLabelsEnabled - { - let step = range / Double(labelCount - 1) - - // Ensure stops contains at least n elements. - axis.entries.removeAll(keepingCapacity: true) - axis.entries.reserveCapacity(labelCount) - - let values = stride(from: yMin, to: Double(labelCount) * step + yMin, by: step) - axis.entries.append(contentsOf: values) - - n = labelCount - } - else - { - // no forced count - - var first = interval == 0.0 ? 0.0 : ceil(yMin / interval) * interval - - if centeringEnabled - { - first -= interval - } - - let last = interval == 0.0 ? 0.0 : (floor(yMax / interval) * interval).nextUp - - if interval != 0.0 - { - stride(from: first, through: last, by: interval).forEach { _ in n += 1 } - } - - n += 1 - - // Ensure stops contains at least n elements. - axis.entries.removeAll(keepingCapacity: true) - axis.entries.reserveCapacity(labelCount) - - // Fix for IEEE negative zero case (Where value == -0.0, and 0.0 == -0.0) - let values = stride(from: first, to: Double(n) * interval + first, by: interval).map { $0 == 0.0 ? 0.0 : $0 } - axis.entries.append(contentsOf: values) - } - - // set decimals - if interval < 1 - { - axis.decimals = Int(ceil(-log10(interval))) - } - else - { - axis.decimals = 0 - } - - if centeringEnabled - { - let offset = (axis.entries[1] - axis.entries[0]) / 2.0 - axis.centeredEntries = axis.entries.map { $0 + offset } - } - - axis._axisMinimum = axis.entries.first! - axis._axisMaximum = axis.entries.last! - axis.axisRange = abs(axis._axisMaximum - axis._axisMinimum) - } - - open override func renderAxisLabels(context: CGContext) - { - guard - let chart = chart, - axis.isEnabled, - axis.isDrawLabelsEnabled - else { return } - - let labelFont = axis.labelFont - let labelTextColor = axis.labelTextColor - - let center = chart.centerOffsets - let factor = chart.factor - - let labelLineHeight = axis.labelFont.lineHeight - - let from = axis.isDrawBottomYLabelEntryEnabled ? 0 : 1 - let to = axis.isDrawTopYLabelEntryEnabled ? axis.entryCount : (axis.entryCount - 1) - - let alignment = axis.labelAlignment - let xOffset = axis.labelXOffset - - let entries = axis.entries[from.. [NSUIColor] - { - return [ - NSUIColor(red: 207/255.0, green: 248/255.0, blue: 246/255.0, alpha: 1.0), - NSUIColor(red: 148/255.0, green: 212/255.0, blue: 212/255.0, alpha: 1.0), - NSUIColor(red: 136/255.0, green: 180/255.0, blue: 187/255.0, alpha: 1.0), - NSUIColor(red: 118/255.0, green: 174/255.0, blue: 175/255.0, alpha: 1.0), - NSUIColor(red: 42/255.0, green: 109/255.0, blue: 130/255.0, alpha: 1.0) - ] - } - - @objc open class func joyful () -> [NSUIColor] - { - return [ - NSUIColor(red: 217/255.0, green: 80/255.0, blue: 138/255.0, alpha: 1.0), - NSUIColor(red: 254/255.0, green: 149/255.0, blue: 7/255.0, alpha: 1.0), - NSUIColor(red: 254/255.0, green: 247/255.0, blue: 120/255.0, alpha: 1.0), - NSUIColor(red: 106/255.0, green: 167/255.0, blue: 134/255.0, alpha: 1.0), - NSUIColor(red: 53/255.0, green: 194/255.0, blue: 209/255.0, alpha: 1.0) - ] - } - - @objc open class func pastel () -> [NSUIColor] - { - return [ - NSUIColor(red: 64/255.0, green: 89/255.0, blue: 128/255.0, alpha: 1.0), - NSUIColor(red: 149/255.0, green: 165/255.0, blue: 124/255.0, alpha: 1.0), - NSUIColor(red: 217/255.0, green: 184/255.0, blue: 162/255.0, alpha: 1.0), - NSUIColor(red: 191/255.0, green: 134/255.0, blue: 134/255.0, alpha: 1.0), - NSUIColor(red: 179/255.0, green: 48/255.0, blue: 80/255.0, alpha: 1.0) - ] - } - - @objc open class func colorful () -> [NSUIColor] - { - return [ - NSUIColor(red: 193/255.0, green: 37/255.0, blue: 82/255.0, alpha: 1.0), - NSUIColor(red: 255/255.0, green: 102/255.0, blue: 0/255.0, alpha: 1.0), - NSUIColor(red: 245/255.0, green: 199/255.0, blue: 0/255.0, alpha: 1.0), - NSUIColor(red: 106/255.0, green: 150/255.0, blue: 31/255.0, alpha: 1.0), - NSUIColor(red: 179/255.0, green: 100/255.0, blue: 53/255.0, alpha: 1.0) - ] - } - - @objc open class func vordiplom () -> [NSUIColor] - { - return [ - NSUIColor(red: 192/255.0, green: 255/255.0, blue: 140/255.0, alpha: 1.0), - NSUIColor(red: 255/255.0, green: 247/255.0, blue: 140/255.0, alpha: 1.0), - NSUIColor(red: 255/255.0, green: 208/255.0, blue: 140/255.0, alpha: 1.0), - NSUIColor(red: 140/255.0, green: 234/255.0, blue: 255/255.0, alpha: 1.0), - NSUIColor(red: 255/255.0, green: 140/255.0, blue: 157/255.0, alpha: 1.0) - ] - } - - @objc open class func material () -> [NSUIColor] - { - return [ - NSUIColor(red: 46/255.0, green: 204/255.0, blue: 113/255.0, alpha: 1.0), - NSUIColor(red: 241/255.0, green: 196/255.0, blue: 15/255.0, alpha: 1.0), - NSUIColor(red: 231/255.0, green: 76/255.0, blue: 60/255.0, alpha: 1.0), - NSUIColor(red: 52/255.0, green: 152/255.0, blue: 219/255.0, alpha: 1.0) - ] - } - - @objc open class func colorFromString(_ colorString: String) -> NSUIColor - { - let leftParenCharset: CharacterSet = CharacterSet(charactersIn: "( ") - let commaCharset: CharacterSet = CharacterSet(charactersIn: ", ") - - let colorString = colorString.lowercased() - - if colorString.hasPrefix("#") - { - var argb: [UInt] = [255, 0, 0, 0] - let colorString = colorString.unicodeScalars.dropFirst() - let length = colorString.count - var index = colorString.startIndex - let endIndex = colorString.endIndex - - guard [3, 6, 8].contains(length) else { return .black } - - var i = length == 8 ? 0 : 1 - while index < endIndex - { - var c = colorString[index] - index = colorString.index(after: index) - - var val = (c.value >= 0x61 && c.value <= 0x66) ? (c.value - 0x61 + 10) : c.value - 0x30 - argb[i] = UInt(val) * 16 - if length == 3 - { - argb[i] = argb[i] + UInt(val) - } - else - { - c = colorString[index] - index = colorString.index(after: index) - - val = (c.value >= 0x61 && c.value <= 0x66) ? (c.value - 0x61 + 10) : c.value - 0x30 - argb[i] = argb[i] + UInt(val) - } - - i += 1 - } - - return NSUIColor(red: CGFloat(argb[1]) / 255.0, green: CGFloat(argb[2]) / 255.0, blue: CGFloat(argb[3]) / 255.0, alpha: CGFloat(argb[0]) / 255.0) - } - else if colorString.hasPrefix("rgba") - { - var a: Float = 1.0 - var r: Int32 = 0 - var g: Int32 = 0 - var b: Int32 = 0 - let scanner: Scanner = Scanner(string: colorString) - scanner.scanString("rgba", into: nil) - scanner.scanCharacters(from: leftParenCharset, into: nil) - scanner.scanInt32(&r) - scanner.scanCharacters(from: commaCharset, into: nil) - scanner.scanInt32(&g) - scanner.scanCharacters(from: commaCharset, into: nil) - scanner.scanInt32(&b) - scanner.scanCharacters(from: commaCharset, into: nil) - scanner.scanFloat(&a) - return NSUIColor( - red: CGFloat(r) / 255.0, - green: CGFloat(g) / 255.0, - blue: CGFloat(b) / 255.0, - alpha: CGFloat(a) - ) - } - else if colorString.hasPrefix("argb") - { - var a: Float = 1.0 - var r: Int32 = 0 - var g: Int32 = 0 - var b: Int32 = 0 - let scanner: Scanner = Scanner(string: colorString) - scanner.scanString("argb", into: nil) - scanner.scanCharacters(from: leftParenCharset, into: nil) - scanner.scanFloat(&a) - scanner.scanCharacters(from: commaCharset, into: nil) - scanner.scanInt32(&r) - scanner.scanCharacters(from: commaCharset, into: nil) - scanner.scanInt32(&g) - scanner.scanCharacters(from: commaCharset, into: nil) - scanner.scanInt32(&b) - return NSUIColor( - red: CGFloat(r) / 255.0, - green: CGFloat(g) / 255.0, - blue: CGFloat(b) / 255.0, - alpha: CGFloat(a) - ) - } - else if colorString.hasPrefix("rgb") - { - var r: Int32 = 0 - var g: Int32 = 0 - var b: Int32 = 0 - let scanner: Scanner = Scanner(string: colorString) - scanner.scanString("rgb", into: nil) - scanner.scanCharacters(from: leftParenCharset, into: nil) - scanner.scanInt32(&r) - scanner.scanCharacters(from: commaCharset, into: nil) - scanner.scanInt32(&g) - scanner.scanCharacters(from: commaCharset, into: nil) - scanner.scanInt32(&b) - return NSUIColor( - red: CGFloat(r) / 255.0, - green: CGFloat(g) / 255.0, - blue: CGFloat(b) / 255.0, - alpha: 1.0 - ) - } - - return NSUIColor.clear - } -} diff --git a/Pods/Charts/Source/Charts/Utils/ChartUtils.swift b/Pods/Charts/Source/Charts/Utils/ChartUtils.swift deleted file mode 100644 index 3103fac6c..000000000 --- a/Pods/Charts/Source/Charts/Utils/ChartUtils.swift +++ /dev/null @@ -1,298 +0,0 @@ -// -// Utils.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -extension Comparable -{ - func clamped(to range: ClosedRange) -> Self - { - if self > range.upperBound - { - return range.upperBound - } - else if self < range.lowerBound - { - return range.lowerBound - } - else - { - return self - } - } -} - -extension FloatingPoint -{ - var DEG2RAD: Self - { - return self * .pi / 180 - } - - var RAD2DEG: Self - { - return self * 180 / .pi - } - - /// - Note: Value must be in degrees - /// - Returns: An angle between 0.0 < 360.0 (not less than zero, less than 360) - var normalizedAngle: Self - { - let angle = truncatingRemainder(dividingBy: 360) - return (sign == .minus) ? angle + 360 : angle - } -} - -extension CGSize -{ - func rotatedBy(degrees: CGFloat) -> CGSize - { - let radians = degrees.DEG2RAD - return rotatedBy(radians: radians) - } - - func rotatedBy(radians: CGFloat) -> CGSize - { - return CGSize( - width: abs(width * cos(radians)) + abs(height * sin(radians)), - height: abs(width * sin(radians)) + abs(height * cos(radians)) - ) - } -} - -extension Double -{ - /// Rounds the number to the nearest multiple of it's order of magnitude, rounding away from zero if halfway. - func roundedToNextSignificant() -> Double - { - guard - !isInfinite, - !isNaN, - self != 0 - else { return self } - - let d = ceil(log10(self < 0 ? -self : self)) - let pw = 1 - Int(d) - let magnitude = pow(10.0, Double(pw)) - let shifted = (self * magnitude).rounded() - return shifted / magnitude - } - - var decimalPlaces: Int - { - guard - !isNaN, - !isInfinite, - self != 0.0 - else { return 0 } - - let i = roundedToNextSignificant() - - guard - !i.isInfinite, - !i.isNaN - else { return 0 } - - return Int(ceil(-log10(i))) + 2 - } -} - -extension CGPoint -{ - /// Calculates the position around a center point, depending on the distance from the center, and the angle of the position around the center. - func moving(distance: CGFloat, atAngle angle: CGFloat) -> CGPoint - { - return CGPoint(x: x + distance * cos(angle.DEG2RAD), - y: y + distance * sin(angle.DEG2RAD)) - } -} - -extension CGContext -{ - - public func drawImage(_ image: NSUIImage, atCenter center: CGPoint, size: CGSize) - { - var drawOffset = CGPoint() - drawOffset.x = center.x - (size.width / 2) - drawOffset.y = center.y - (size.height / 2) - - NSUIGraphicsPushContext(self) - - if image.size.width != size.width && image.size.height != size.height - { - let key = "resized_\(size.width)_\(size.height)" - - // Try to take scaled image from cache of this image - var scaledImage = objc_getAssociatedObject(image, key) as? NSUIImage - if scaledImage == nil - { - // Scale the image - NSUIGraphicsBeginImageContextWithOptions(size, false, 0.0) - - image.draw(in: CGRect(origin: .zero, size: size)) - - scaledImage = NSUIGraphicsGetImageFromCurrentImageContext() - NSUIGraphicsEndImageContext() - - // Put the scaled image in a cache owned by the original image - objc_setAssociatedObject(image, key, scaledImage, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - - scaledImage?.draw(in: CGRect(origin: drawOffset, size: size)) - } - else - { - image.draw(in: CGRect(origin: drawOffset, size: size)) - } - - NSUIGraphicsPopContext() - } - - public func drawText(_ text: String, at point: CGPoint, align: TextAlignment, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5), angleRadians: CGFloat = 0.0, attributes: [NSAttributedString.Key : Any]?) - { - let drawPoint = getDrawPoint(text: text, point: point, align: align, attributes: attributes) - - if (angleRadians == 0.0) - { - NSUIGraphicsPushContext(self) - - (text as NSString).draw(at: drawPoint, withAttributes: attributes) - - NSUIGraphicsPopContext() - } - else - { - drawText(text, at: drawPoint, anchor: anchor, angleRadians: angleRadians, attributes: attributes) - } - } - - public func drawText(_ text: String, at point: CGPoint, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5), angleRadians: CGFloat, attributes: [NSAttributedString.Key : Any]?) - { - var drawOffset = CGPoint() - - NSUIGraphicsPushContext(self) - - if angleRadians != 0.0 - { - let size = text.size(withAttributes: attributes) - - // Move the text drawing rect in a way that it always rotates around its center - drawOffset.x = -size.width * 0.5 - drawOffset.y = -size.height * 0.5 - - var translate = point - - // Move the "outer" rect relative to the anchor, assuming its centered - if anchor.x != 0.5 || anchor.y != 0.5 - { - let rotatedSize = size.rotatedBy(radians: angleRadians) - - translate.x -= rotatedSize.width * (anchor.x - 0.5) - translate.y -= rotatedSize.height * (anchor.y - 0.5) - } - - saveGState() - translateBy(x: translate.x, y: translate.y) - rotate(by: angleRadians) - - (text as NSString).draw(at: drawOffset, withAttributes: attributes) - - restoreGState() - } - else - { - if anchor.x != 0.0 || anchor.y != 0.0 - { - let size = text.size(withAttributes: attributes) - - drawOffset.x = -size.width * anchor.x - drawOffset.y = -size.height * anchor.y - } - - drawOffset.x += point.x - drawOffset.y += point.y - - (text as NSString).draw(at: drawOffset, withAttributes: attributes) - } - - NSUIGraphicsPopContext() - } - - private func getDrawPoint(text: String, point: CGPoint, align: TextAlignment, attributes: [NSAttributedString.Key : Any]?) -> CGPoint - { - var point = point - - if align == .center - { - point.x -= text.size(withAttributes: attributes).width / 2.0 - } - else if align == .right - { - point.x -= text.size(withAttributes: attributes).width - } - return point - } - - func drawMultilineText(_ text: String, at point: CGPoint, constrainedTo size: CGSize, anchor: CGPoint, knownTextSize: CGSize, angleRadians: CGFloat, attributes: [NSAttributedString.Key : Any]?) - { - var rect = CGRect(origin: .zero, size: knownTextSize) - - NSUIGraphicsPushContext(self) - - if angleRadians != 0.0 - { - // Move the text drawing rect in a way that it always rotates around its center - rect.origin.x = -knownTextSize.width * 0.5 - rect.origin.y = -knownTextSize.height * 0.5 - - var translate = point - - // Move the "outer" rect relative to the anchor, assuming its centered - if anchor.x != 0.5 || anchor.y != 0.5 - { - let rotatedSize = knownTextSize.rotatedBy(radians: angleRadians) - - translate.x -= rotatedSize.width * (anchor.x - 0.5) - translate.y -= rotatedSize.height * (anchor.y - 0.5) - } - - saveGState() - translateBy(x: translate.x, y: translate.y) - rotate(by: angleRadians) - - (text as NSString).draw(with: rect, options: .usesLineFragmentOrigin, attributes: attributes, context: nil) - - restoreGState() - } - else - { - if anchor.x != 0.0 || anchor.y != 0.0 - { - rect.origin.x = -knownTextSize.width * anchor.x - rect.origin.y = -knownTextSize.height * anchor.y - } - - rect.origin.x += point.x - rect.origin.y += point.y - - (text as NSString).draw(with: rect, options: .usesLineFragmentOrigin, attributes: attributes, context: nil) - } - - NSUIGraphicsPopContext() - } - - func drawMultilineText(_ text: String, at point: CGPoint, constrainedTo size: CGSize, anchor: CGPoint, angleRadians: CGFloat, attributes: [NSAttributedString.Key : Any]?) - { - let rect = text.boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: attributes, context: nil) - drawMultilineText(text, at: point, constrainedTo: size, anchor: anchor, knownTextSize: rect.size, angleRadians: angleRadians, attributes: attributes) - } -} diff --git a/Pods/Charts/Source/Charts/Utils/Fill.swift b/Pods/Charts/Source/Charts/Utils/Fill.swift deleted file mode 100644 index 7a6520f19..000000000 --- a/Pods/Charts/Source/Charts/Utils/Fill.swift +++ /dev/null @@ -1,210 +0,0 @@ -// -// Fill.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartFill) -public protocol Fill -{ - - /// Draws the provided path in filled mode with the provided area - @objc func fillPath(context: CGContext, rect: CGRect) -} - -@objc(ChartEmptyFill) -public class EmptyFill: NSObject, Fill -{ - - public func fillPath(context: CGContext, rect: CGRect) { } -} - -@objc(ChartColorFill) -public class ColorFill: NSObject, Fill -{ - - @objc public let color: CGColor - - @objc public init(cgColor: CGColor) - { - self.color = cgColor - super.init() - } - - @objc public convenience init(color: NSUIColor) - { - self.init(cgColor: color.cgColor) - } - - public func fillPath(context: CGContext, rect: CGRect) - { - context.saveGState() - defer { context.restoreGState() } - - context.setFillColor(color) - context.fillPath() - } -} - -@objc(ChartImageFill) -public class ImageFill: NSObject, Fill -{ - - @objc public let image: CGImage - @objc public let isTiled: Bool - - @objc public init(cgImage: CGImage, isTiled: Bool = false) - { - image = cgImage - self.isTiled = isTiled - super.init() - } - - @objc public convenience init(image: NSUIImage, isTiled: Bool = false) - { - self.init(cgImage: image.cgImage!, isTiled: isTiled) - } - - public func fillPath(context: CGContext, rect: CGRect) - { - context.saveGState() - defer { context.restoreGState() } - - context.clip() - context.draw(image, in: rect, byTiling: isTiled) - } -} - -@objc(ChartLayerFill) -public class LayerFill: NSObject, Fill -{ - - @objc public let layer: CGLayer - - @objc public init(layer: CGLayer) - { - self.layer = layer - super.init() - } - - public func fillPath(context: CGContext, rect: CGRect) - { - context.saveGState() - defer { context.restoreGState() } - - context.clip() - context.draw(layer, in: rect) - } -} - -@objc(ChartLinearGradientFill) -public class LinearGradientFill: NSObject, Fill -{ - - @objc public let gradient: CGGradient - @objc public let angle: CGFloat - - @objc public init(gradient: CGGradient, angle: CGFloat = 0) - { - self.gradient = gradient - self.angle = angle - super.init() - } - - public func fillPath(context: CGContext, rect: CGRect) - { - context.saveGState() - defer { context.restoreGState() } - - let radians = (360.0 - angle).DEG2RAD - let centerPoint = CGPoint(x: rect.midX, y: rect.midY) - let xAngleDelta = cos(radians) * rect.width / 2.0 - let yAngleDelta = sin(radians) * rect.height / 2.0 - let startPoint = CGPoint( - x: centerPoint.x - xAngleDelta, - y: centerPoint.y - yAngleDelta - ) - let endPoint = CGPoint( - x: centerPoint.x + xAngleDelta, - y: centerPoint.y + yAngleDelta - ) - - context.clip() - context.drawLinearGradient( - gradient, - start: startPoint, - end: endPoint, - options: [.drawsAfterEndLocation, .drawsBeforeStartLocation] - ) - } -} - -@objc(ChartRadialGradientFill) -public class RadialGradientFill: NSObject, Fill -{ - - @objc public let gradient: CGGradient - @objc public let startOffsetPercent: CGPoint - @objc public let endOffsetPercent: CGPoint - @objc public let startRadiusPercent: CGFloat - @objc public let endRadiusPercent: CGFloat - - @objc public init( - gradient: CGGradient, - startOffsetPercent: CGPoint, - endOffsetPercent: CGPoint, - startRadiusPercent: CGFloat, - endRadiusPercent: CGFloat) - { - self.gradient = gradient - self.startOffsetPercent = startOffsetPercent - self.endOffsetPercent = endOffsetPercent - self.startRadiusPercent = startRadiusPercent - self.endRadiusPercent = endRadiusPercent - super.init() - } - - @objc public convenience init(gradient: CGGradient) - { - self.init( - gradient: gradient, - startOffsetPercent: .zero, - endOffsetPercent: .zero, - startRadiusPercent: 0, - endRadiusPercent: 1 - ) - } - - @objc public func fillPath(context: CGContext, rect: CGRect) - { - context.saveGState() - defer { context.restoreGState() } - - let centerPoint = CGPoint(x: rect.midX, y: rect.midY) - let radius = max(rect.width, rect.height) / 2.0 - - context.clip() - context.drawRadialGradient( - gradient, - startCenter: CGPoint( - x: centerPoint.x + rect.width * startOffsetPercent.x, - y: centerPoint.y + rect.height * startOffsetPercent.y - ), - startRadius: radius * startRadiusPercent, - endCenter: CGPoint( - x: centerPoint.x + rect.width * endOffsetPercent.x, - y: centerPoint.y + rect.height * endOffsetPercent.y - ), - endRadius: radius * endRadiusPercent, - options: [.drawsAfterEndLocation, .drawsBeforeStartLocation] - ) - } -} diff --git a/Pods/Charts/Source/Charts/Utils/Platform+Accessibility.swift b/Pods/Charts/Source/Charts/Utils/Platform+Accessibility.swift deleted file mode 100644 index d2bced7b2..000000000 --- a/Pods/Charts/Source/Charts/Utils/Platform+Accessibility.swift +++ /dev/null @@ -1,210 +0,0 @@ -// -// Platform+Accessibility.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation - -#if os(iOS) || os(tvOS) -import UIKit - -internal func accessibilityPostLayoutChangedNotification(withElement element: Any? = nil) -{ - UIAccessibility.post(notification: .layoutChanged, argument: element) -} - -internal func accessibilityPostScreenChangedNotification(withElement element: Any? = nil) -{ - UIAccessibility.post(notification: .screenChanged, argument: element) -} - -/// A simple abstraction over UIAccessibilityElement and NSAccessibilityElement. -open class NSUIAccessibilityElement: UIAccessibilityElement -{ - private weak var containerView: UIView? - - final var isHeader: Bool = false - { - didSet - { - accessibilityTraits = isHeader ? .header : .none - } - } - - final var isSelected: Bool = false - { - didSet - { - accessibilityTraits = isSelected ? .selected : .none - } - } - - override public init(accessibilityContainer container: Any) - { - // We can force unwrap since all chart views are subclasses of UIView - containerView = container as? UIView - super.init(accessibilityContainer: container) - } - - override open var accessibilityFrame: CGRect - { - get - { - return super.accessibilityFrame - } - - set - { - guard let containerView = containerView else { return } - super.accessibilityFrame = containerView.convert(newValue, to: UIScreen.main.coordinateSpace) - } - } -} - -extension NSUIView -{ - /// An array of accessibilityElements that is used to implement UIAccessibilityContainer internally. - /// Subclasses **MUST** override this with an array of such elements. - @objc open func accessibilityChildren() -> [Any]? - { - return nil - } - - public final override var isAccessibilityElement: Bool - { - get { return false } // Return false here, so we can make individual elements accessible - set { } - } - - open override func accessibilityElementCount() -> Int - { - return accessibilityChildren()?.count ?? 0 - } - - open override func accessibilityElement(at index: Int) -> Any? - { - return accessibilityChildren()?[index] - } - - open override func index(ofAccessibilityElement element: Any) -> Int - { - guard let axElement = element as? NSUIAccessibilityElement else { return NSNotFound } - return (accessibilityChildren() as? [NSUIAccessibilityElement])?.firstIndex(of: axElement) ?? NSNotFound - } -} - -#endif - -#if os(OSX) -import AppKit - - -internal func accessibilityPostLayoutChangedNotification(withElement element: Any? = nil) -{ - guard let validElement = element else { return } - NSAccessibility.post(element: validElement, notification: .layoutChanged) -} - -internal func accessibilityPostScreenChangedNotification(withElement element: Any? = nil) -{ - // Placeholder -} - -/// A simple abstraction over UIAccessibilityElement and NSAccessibilityElement. -open class NSUIAccessibilityElement: NSAccessibilityElement -{ - private weak var containerView: NSView? - - final var isHeader: Bool = false - { - didSet - { - setAccessibilityRole(isHeader ? .staticText : .none) - } - } - - final var isSelected: Bool = false - { - didSet - { - setAccessibilitySelected(isSelected) - } - } - - open var accessibilityLabel: String - { - get - { - return accessibilityLabel() ?? "" - } - - set - { - setAccessibilityLabel(newValue) - } - } - - open var accessibilityFrame: NSRect - { - get - { - return accessibilityFrame() - } - - set - { - guard let containerView = containerView else { return } - - let bounds = NSAccessibility.screenRect(fromView: containerView, rect: newValue) - - // This works, but won't auto update if the window is resized or moved. - // setAccessibilityFrame(bounds) - - // using FrameInParentSpace allows for automatic updating of frame when windows are moved and resized. - // However, there seems to be a bug right now where using it causes an offset in the frame. - // This is a slightly hacky workaround that calculates the offset and removes it from frame calculation. - setAccessibilityFrameInParentSpace(bounds) - let axFrame = accessibilityFrame() - let widthOffset = abs(axFrame.origin.x - bounds.origin.x) - let heightOffset = abs(axFrame.origin.y - bounds.origin.y) - let rect = NSRect(x: bounds.origin.x - widthOffset, - y: bounds.origin.y - heightOffset, - width: bounds.width, - height: bounds.height) - setAccessibilityFrameInParentSpace(rect) - } - } - - public init(accessibilityContainer container: Any) - { - // We can force unwrap since all chart views are subclasses of NSView - containerView = (container as! NSView) - - super.init() - - setAccessibilityParent(containerView) - setAccessibilityRole(.row) - } -} - -/// - Note: setAccessibilityRole(.list) is called at init. See Platform.swift. -extension NSUIView: NSAccessibilityGroup -{ - open override func accessibilityLabel() -> String? - { - return "Chart View" - } - - open override func accessibilityRows() -> [Any]? - { - return accessibilityChildren() - } -} - -#endif diff --git a/Pods/Charts/Source/Charts/Utils/Platform+Color.swift b/Pods/Charts/Source/Charts/Utils/Platform+Color.swift deleted file mode 100644 index 02bff08ed..000000000 --- a/Pods/Charts/Source/Charts/Utils/Platform+Color.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Platform+Color.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -#if canImport(UIKit) -import UIKit - -public typealias NSUIColor = UIColor -private func fetchLabelColor() -> UIColor -{ - if #available(iOS 13, tvOS 13, *) - { - return .label - } - else - { - return .black - } -} -private let labelColor: UIColor = fetchLabelColor() - -extension UIColor -{ - static var labelOrBlack: UIColor { labelColor } -} -#endif - -#if canImport(AppKit) && !targetEnvironment(macCatalyst) - -import AppKit - -public typealias NSUIColor = NSColor -private func fetchLabelColor() -> NSColor -{ - if #available(macOS 10.14, *) - { - return .labelColor - } - else - { - return .black - } -} -private let labelColor: NSColor = fetchLabelColor() - -extension NSColor -{ - static var labelOrBlack: NSColor { labelColor } -} -#endif diff --git a/Pods/Charts/Source/Charts/Utils/Platform+Gestures.swift b/Pods/Charts/Source/Charts/Utils/Platform+Gestures.swift deleted file mode 100644 index e22e16887..000000000 --- a/Pods/Charts/Source/Charts/Utils/Platform+Gestures.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// Platform+Gestures.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -// MARK: - UIKit -#if canImport(UIKit) -import UIKit - -public typealias NSUIGestureRecognizer = UIGestureRecognizer -public typealias NSUIGestureRecognizerState = UIGestureRecognizer.State -public typealias NSUIGestureRecognizerDelegate = UIGestureRecognizerDelegate -public typealias NSUITapGestureRecognizer = UITapGestureRecognizer -public typealias NSUIPanGestureRecognizer = UIPanGestureRecognizer - -extension NSUITapGestureRecognizer -{ - @objc final func nsuiNumberOfTouches() -> Int - { - return numberOfTouches - } - - @objc final var nsuiNumberOfTapsRequired: Int - { - get - { - return self.numberOfTapsRequired - } - set - { - self.numberOfTapsRequired = newValue - } - } -} - -extension NSUIPanGestureRecognizer -{ - @objc final func nsuiNumberOfTouches() -> Int - { - return numberOfTouches - } - - @objc final func nsuiLocationOfTouch(_ touch: Int, inView: UIView?) -> CGPoint - { - return super.location(ofTouch: touch, in: inView) - } -} - -#if !os(tvOS) -public typealias NSUIPinchGestureRecognizer = UIPinchGestureRecognizer -public typealias NSUIRotationGestureRecognizer = UIRotationGestureRecognizer - -extension NSUIRotationGestureRecognizer -{ - @objc final var nsuiRotation: CGFloat - { - get { return rotation } - set { rotation = newValue } - } -} - -extension NSUIPinchGestureRecognizer -{ - @objc final var nsuiScale: CGFloat - { - get - { - return scale - } - set - { - scale = newValue - } - } - - @objc final func nsuiLocationOfTouch(_ touch: Int, inView: UIView?) -> CGPoint - { - return super.location(ofTouch: touch, in: inView) - } -} -#endif -#endif - -// MARK: - AppKit -#if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit - -public typealias NSUIGestureRecognizer = NSGestureRecognizer -public typealias NSUIGestureRecognizerState = NSGestureRecognizer.State -public typealias NSUIGestureRecognizerDelegate = NSGestureRecognizerDelegate -public typealias NSUITapGestureRecognizer = NSClickGestureRecognizer -public typealias NSUIPanGestureRecognizer = NSPanGestureRecognizer -public typealias NSUIPinchGestureRecognizer = NSMagnificationGestureRecognizer -public typealias NSUIRotationGestureRecognizer = NSRotationGestureRecognizer - -/** The 'tap' gesture is mapped to clicks. */ -extension NSUITapGestureRecognizer -{ - final func nsuiNumberOfTouches() -> Int - { - return 1 - } - - final var nsuiNumberOfTapsRequired: Int - { - get - { - return self.numberOfClicksRequired - } - set - { - self.numberOfClicksRequired = newValue - } - } -} - -extension NSUIPanGestureRecognizer -{ - final func nsuiNumberOfTouches() -> Int - { - return 1 - } - - /// FIXME: Currently there are no more than 1 touch in OSX gestures, and not way to create custom touch gestures. - final func nsuiLocationOfTouch(_ touch: Int, inView: NSView?) -> NSPoint - { - return super.location(in: inView) - } -} - -extension NSUIRotationGestureRecognizer -{ - /// FIXME: Currently there are no velocities in OSX gestures, and not way to create custom touch gestures. - final var velocity: CGFloat - { - return 0.1 - } - - final var nsuiRotation: CGFloat - { - get { return -rotation } - set { rotation = -newValue } - } -} - -extension NSUIPinchGestureRecognizer -{ - final var nsuiScale: CGFloat - { - get - { - return magnification + 1.0 - } - set - { - magnification = newValue - 1.0 - } - } - - /// FIXME: Currently there are no more than 1 touch in OSX gestures, and not way to create custom touch gestures. - final func nsuiLocationOfTouch(_ touch: Int, inView view: NSView?) -> NSPoint - { - return super.location(in: view) - } -} -#endif diff --git a/Pods/Charts/Source/Charts/Utils/Platform+Graphics.swift b/Pods/Charts/Source/Charts/Utils/Platform+Graphics.swift deleted file mode 100644 index a8427a4fb..000000000 --- a/Pods/Charts/Source/Charts/Utils/Platform+Graphics.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// Platform+Graphics.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -enum Orientation -{ - case portrait, landscape -} - -extension CGSize -{ - var orientation: Orientation { return width > height ? .landscape : .portrait } -} - -extension CGRect -{ - var orientation: Orientation { size.orientation } -} - -// MARK: - UIKit -#if canImport(UIKit) -import UIKit - -func NSUIGraphicsGetCurrentContext() -> CGContext? -{ - return UIGraphicsGetCurrentContext() -} - -func NSUIGraphicsGetImageFromCurrentImageContext() -> NSUIImage! -{ - return UIGraphicsGetImageFromCurrentImageContext() -} - -func NSUIGraphicsPushContext(_ context: CGContext) -{ - UIGraphicsPushContext(context) -} - -func NSUIGraphicsPopContext() -{ - UIGraphicsPopContext() -} - -func NSUIGraphicsEndImageContext() -{ - UIGraphicsEndImageContext() -} - -func NSUIImagePNGRepresentation(_ image: NSUIImage) -> Data? -{ - return image.pngData() -} - -func NSUIImageJPEGRepresentation(_ image: NSUIImage, _ quality: CGFloat = 0.8) -> Data? -{ - return image.jpegData(compressionQuality: quality) -} - -func NSUIGraphicsBeginImageContextWithOptions(_ size: CGSize, _ opaque: Bool, _ scale: CGFloat) -{ - UIGraphicsBeginImageContextWithOptions(size, opaque, scale) -} -#endif - -// MARK: - AppKit -#if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit - -func NSUIGraphicsGetCurrentContext() -> CGContext? -{ - return NSGraphicsContext.current?.cgContext -} - -func NSUIGraphicsPushContext(_ context: CGContext) -{ - let cx = NSGraphicsContext(cgContext: context, flipped: true) - NSGraphicsContext.saveGraphicsState() - NSGraphicsContext.current = cx -} - -func NSUIGraphicsPopContext() -{ - NSGraphicsContext.restoreGraphicsState() -} - -func NSUIImagePNGRepresentation(_ image: NSUIImage) -> Data? -{ - image.lockFocus() - let rep = NSBitmapImageRep(focusedViewRect: NSMakeRect(0, 0, image.size.width, image.size.height)) - image.unlockFocus() - return rep?.representation(using: .png, properties: [:]) -} - -func NSUIImageJPEGRepresentation(_ image: NSUIImage, _ quality: CGFloat = 0.9) -> Data? -{ - image.lockFocus() - let rep = NSBitmapImageRep(focusedViewRect: NSMakeRect(0, 0, image.size.width, image.size.height)) - image.unlockFocus() - return rep?.representation(using: .jpeg, properties: [NSBitmapImageRep.PropertyKey.compressionFactor: quality]) -} - -private var imageContextStack: [CGFloat] = [] - -func NSUIGraphicsBeginImageContextWithOptions(_ size: CGSize, _ opaque: Bool, _ scale: CGFloat) -{ - var scale = scale - if scale == 0.0 - { - scale = NSScreen.main?.backingScaleFactor ?? 1.0 - } - - let width = Int(size.width * scale) - let height = Int(size.height * scale) - - if width > 0 && height > 0 - { - imageContextStack.append(scale) - - let colorSpace = CGColorSpaceCreateDeviceRGB() - - guard let ctx = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 4*width, space: colorSpace, bitmapInfo: (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue)) - else { return } - - ctx.concatenate(CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(height))) - ctx.scaleBy(x: scale, y: scale) - NSUIGraphicsPushContext(ctx) - } -} - -func NSUIGraphicsGetImageFromCurrentImageContext() -> NSUIImage? -{ - if !imageContextStack.isEmpty - { - guard let ctx = NSUIGraphicsGetCurrentContext() - else { return nil } - - let scale = imageContextStack.last! - if let theCGImage = ctx.makeImage() - { - let size = CGSize(width: CGFloat(ctx.width) / scale, height: CGFloat(ctx.height) / scale) - let image = NSImage(cgImage: theCGImage, size: size) - return image - } - } - return nil -} - -func NSUIGraphicsEndImageContext() -{ - if imageContextStack.last != nil - { - imageContextStack.removeLast() - NSUIGraphicsPopContext() - } -} -#endif diff --git a/Pods/Charts/Source/Charts/Utils/Platform+Touch Handling.swift b/Pods/Charts/Source/Charts/Utils/Platform+Touch Handling.swift deleted file mode 100644 index acb402ecb..000000000 --- a/Pods/Charts/Source/Charts/Utils/Platform+Touch Handling.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// Platform+Touch Handling.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -#if canImport(UIKit) -import UIKit - -public typealias NSUIEvent = UIEvent -public typealias NSUITouch = UITouch - -@objc -extension NSUIView { - public final override func touchesBegan(_ touches: Set, with event: NSUIEvent?) - { - self.nsuiTouchesBegan(touches, withEvent: event) - } - - public final override func touchesMoved(_ touches: Set, with event: NSUIEvent?) - { - self.nsuiTouchesMoved(touches, withEvent: event) - } - - public final override func touchesEnded(_ touches: Set, with event: NSUIEvent?) - { - self.nsuiTouchesEnded(touches, withEvent: event) - } - - public final override func touchesCancelled(_ touches: Set, with event: NSUIEvent?) - { - self.nsuiTouchesCancelled(touches, withEvent: event) - } - - open func nsuiTouchesBegan(_ touches: Set, withEvent event: NSUIEvent?) - { - super.touchesBegan(touches, with: event!) - } - - open func nsuiTouchesMoved(_ touches: Set, withEvent event: NSUIEvent?) - { - super.touchesMoved(touches, with: event!) - } - - open func nsuiTouchesEnded(_ touches: Set, withEvent event: NSUIEvent?) - { - super.touchesEnded(touches, with: event!) - } - - open func nsuiTouchesCancelled(_ touches: Set?, withEvent event: NSUIEvent?) - { - super.touchesCancelled(touches!, with: event!) - } -} - -extension UIView -{ - @objc final var nsuiGestureRecognizers: [NSUIGestureRecognizer]? - { - return self.gestureRecognizers - } -} -#endif - - -#if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit - -public typealias NSUIEvent = NSEvent -public typealias NSUITouch = NSTouch - -@objc -extension NSUIView -{ - public final override func touchesBegan(with event: NSEvent) - { - self.nsuiTouchesBegan(event.touches(matching: .any, in: self), withEvent: event) - } - - public final override func touchesEnded(with event: NSEvent) - { - self.nsuiTouchesEnded(event.touches(matching: .any, in: self), withEvent: event) - } - - public final override func touchesMoved(with event: NSEvent) - { - self.nsuiTouchesMoved(event.touches(matching: .any, in: self), withEvent: event) - } - - open override func touchesCancelled(with event: NSEvent) - { - self.nsuiTouchesCancelled(event.touches(matching: .any, in: self), withEvent: event) - } - - open func nsuiTouchesBegan(_ touches: Set, withEvent event: NSUIEvent?) - { - super.touchesBegan(with: event!) - } - - open func nsuiTouchesMoved(_ touches: Set, withEvent event: NSUIEvent?) - { - super.touchesMoved(with: event!) - } - - open func nsuiTouchesEnded(_ touches: Set, withEvent event: NSUIEvent?) - { - super.touchesEnded(with: event!) - } - - open func nsuiTouchesCancelled(_ touches: Set?, withEvent event: NSUIEvent?) - { - super.touchesCancelled(with: event!) - } -} - -extension NSTouch -{ - /** Touch locations on OS X are relative to the trackpad, whereas on iOS they are actually *on* the view. */ - func locationInView(view: NSView) -> NSPoint - { - let n = self.normalizedPosition - let b = view.bounds - return NSPoint( - x: b.origin.x + b.size.width * n.x, - y: b.origin.y + b.size.height * n.y - ) - } -} -#endif diff --git a/Pods/Charts/Source/Charts/Utils/Platform.swift b/Pods/Charts/Source/Charts/Utils/Platform.swift deleted file mode 100644 index 927191cf4..000000000 --- a/Pods/Charts/Source/Charts/Utils/Platform.swift +++ /dev/null @@ -1,285 +0,0 @@ -import Foundation - -/** This file provides a thin abstraction layer atop of UIKit (iOS, tvOS) and Cocoa (OS X). The two APIs are very much -alike, and for the chart library's usage of the APIs it is often sufficient to typealias one to the other. The NSUI* -types are aliased to either their UI* implementation (on iOS) or their NS* implementation (on OS X). */ -#if os(iOS) || os(tvOS) - import UIKit - - - public typealias ParagraphStyle = NSParagraphStyle - public typealias MutableParagraphStyle = NSMutableParagraphStyle - public typealias TextAlignment = NSTextAlignment - public typealias NSUIFont = UIFont - public typealias NSUIImage = UIImage - public typealias NSUIScrollView = UIScrollView - public typealias NSUIScreen = UIScreen - public typealias NSUIDisplayLink = CADisplayLink - - extension NSUIColor - { - var nsuirgba: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)? { - var red: CGFloat = 0 - var green: CGFloat = 0 - var blue: CGFloat = 0 - var alpha: CGFloat = 0 - - guard getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { - return nil - } - - return (red: red, green: green, blue: blue, alpha: alpha) - } - } - - open class NSUIView: UIView - { - @objc var nsuiLayer: CALayer? - { - return self.layer - } - } - - extension UIScrollView - { - @objc var nsuiIsScrollEnabled: Bool - { - get { return isScrollEnabled } - set { isScrollEnabled = newValue } - } - } - - extension UIScreen - { - @objc final var nsuiScale: CGFloat - { - return self.scale - } - } - - func NSUIMainScreen() -> NSUIScreen? - { - return NSUIScreen.main - } - -#endif - -#if os(OSX) - import Cocoa - import Quartz - - public typealias ParagraphStyle = NSParagraphStyle - public typealias MutableParagraphStyle = NSMutableParagraphStyle - public typealias TextAlignment = NSTextAlignment - public typealias NSUIFont = NSFont - public typealias NSUIImage = NSImage - public typealias NSUIScrollView = NSScrollView - public typealias NSUIScreen = NSScreen - - /** On OS X there is no CADisplayLink. Use a 60 fps timer to render the animations. */ - public class NSUIDisplayLink - { - private var timer: Timer? - private var displayLink: CVDisplayLink? - private var _timestamp: CFTimeInterval = 0.0 - - private weak var _target: AnyObject? - private var _selector: Selector - - public var timestamp: CFTimeInterval - { - return _timestamp - } - - init(target: AnyObject, selector: Selector) - { - _target = target - _selector = selector - - if CVDisplayLinkCreateWithActiveCGDisplays(&displayLink) == kCVReturnSuccess - { - - CVDisplayLinkSetOutputCallback(displayLink!, { (displayLink, inNow, inOutputTime, flagsIn, flagsOut, userData) -> CVReturn in - - let _self = unsafeBitCast(userData, to: NSUIDisplayLink.self) - - _self._timestamp = CFAbsoluteTimeGetCurrent() - _self._target?.performSelector(onMainThread: _self._selector, with: _self, waitUntilDone: false) - - return kCVReturnSuccess - }, Unmanaged.passUnretained(self).toOpaque()) - } - else - { - timer = Timer(timeInterval: 1.0 / 60.0, target: target, selector: selector, userInfo: nil, repeats: true) - } - } - - deinit - { - stop() - } - - open func add(to runloop: RunLoop, forMode mode: RunLoop.Mode) - { - if displayLink != nil - { - CVDisplayLinkStart(displayLink!) - } - else if timer != nil - { - runloop.add(timer!, forMode: mode) - } - } - - open func remove(from: RunLoop, forMode: RunLoop.Mode) - { - stop() - } - - private func stop() - { - if displayLink != nil - { - CVDisplayLinkStop(displayLink!) - } - if timer != nil - { - timer?.invalidate() - } - } - } - - extension NSUIColor - { - var nsuirgba: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)? { - var red: CGFloat = 0 - var green: CGFloat = 0 - var blue: CGFloat = 0 - var alpha: CGFloat = 0 - - guard let colorSpaceModel = cgColor.colorSpace?.model else { - return nil - } - guard colorSpaceModel == .rgb else { - return nil - } - - getRed(&red, green: &green, blue: &blue, alpha: &alpha) - return (red: red, green: green, blue: blue, alpha: alpha) - } - } - - extension NSView - { - final var nsuiGestureRecognizers: [NSGestureRecognizer]? - { - return self.gestureRecognizers - } - } - - extension NSScrollView - { - var nsuiIsScrollEnabled: Bool - { - get { return scrollEnabled } - set { scrollEnabled = newValue } - } - } - - open class NSUIView: NSView - { - /// A private constant to set the accessibility role during initialization. - /// It ensures parity with the iOS element ordering as well as numbered counts of chart components. - /// (See Platform+Accessibility for details) - private let role: NSAccessibility.Role = .list - - public override init(frame frameRect: NSRect) - { - super.init(frame: frameRect) - setAccessibilityRole(role) - } - - required public init?(coder decoder: NSCoder) - { - super.init(coder: decoder) - setAccessibilityRole(role) - } - - public final override var isFlipped: Bool - { - return true - } - - func setNeedsDisplay() - { - self.setNeedsDisplay(self.bounds) - } - - open var backgroundColor: NSUIColor? - { - get - { - return self.layer?.backgroundColor == nil - ? nil - : NSColor(cgColor: self.layer!.backgroundColor!) - } - set - { - self.wantsLayer = true - self.layer?.backgroundColor = newValue == nil ? nil : newValue!.cgColor - } - } - - final var nsuiLayer: CALayer? - { - return self.layer - } - } - - extension NSFont - { - var lineHeight: CGFloat - { - // Not sure if this is right, but it looks okay - return self.boundingRectForFont.size.height - } - } - - extension NSScreen - { - final var nsuiScale: CGFloat - { - return self.backingScaleFactor - } - } - - extension NSImage - { - var cgImage: CGImage? - { - return self.cgImage(forProposedRect: nil, context: nil, hints: nil) - } - } - - extension NSScrollView - { - var scrollEnabled: Bool - { - get - { - return true - } - set - { - // FIXME: We can't disable scrolling it on OSX - } - } - } - - func NSUIMainScreen() -> NSUIScreen? - { - return NSUIScreen.main - } - -#endif diff --git a/Pods/Charts/Source/Charts/Utils/Sequence+KeyPath.swift b/Pods/Charts/Source/Charts/Utils/Sequence+KeyPath.swift deleted file mode 100644 index fd3679893..000000000 --- a/Pods/Charts/Source/Charts/Utils/Sequence+KeyPath.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Sequence+KeyPath.swift -// Charts -// -// Created by Jacob Christie on 2020-12-15. -// - -extension Sequence { - func max( - by keyPath: KeyPath, - areInIncreasingOrder: (T, T) -> Bool - ) -> Element? { - self.max { areInIncreasingOrder($0[keyPath: keyPath], $1[keyPath: keyPath]) } - } - - func max(by keyPath: KeyPath) -> Element? { - max(by: keyPath, areInIncreasingOrder: <) - } -} diff --git a/Pods/Charts/Source/Charts/Utils/Transformer.swift b/Pods/Charts/Source/Charts/Utils/Transformer.swift deleted file mode 100644 index 9630d7f07..000000000 --- a/Pods/Charts/Source/Charts/Utils/Transformer.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// Transformer.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// Transformer class that contains all matrices and is responsible for transforming values into pixels on the screen and backwards. -@objc(ChartTransformer) -open class Transformer: NSObject -{ - /// matrix to map the values to the screen pixels - internal var matrixValueToPx = CGAffineTransform.identity - - /// matrix for handling the different offsets of the chart - internal var matrixOffset = CGAffineTransform.identity - - internal var viewPortHandler: ViewPortHandler - - @objc public init(viewPortHandler: ViewPortHandler) - { - self.viewPortHandler = viewPortHandler - } - - /// Prepares the matrix that transforms values to pixels. Calculates the scale factors from the charts size and offsets. - @objc open func prepareMatrixValuePx(chartXMin: Double, deltaX: CGFloat, deltaY: CGFloat, chartYMin: Double) - { - var scaleX = (viewPortHandler.contentWidth / deltaX) - var scaleY = (viewPortHandler.contentHeight / deltaY) - - if .infinity == scaleX - { - scaleX = 0.0 - } - if .infinity == scaleY - { - scaleY = 0.0 - } - - // setup all matrices - matrixValueToPx = CGAffineTransform.identity - .scaledBy(x: scaleX, y: -scaleY) - .translatedBy(x: CGFloat(-chartXMin), y: CGFloat(-chartYMin)) - } - - /// Prepares the matrix that contains all offsets. - @objc open func prepareMatrixOffset(inverted: Bool) - { - if !inverted - { - matrixOffset = CGAffineTransform(translationX: viewPortHandler.offsetLeft, y: viewPortHandler.chartHeight - viewPortHandler.offsetBottom) - } - else - { - matrixOffset = CGAffineTransform(scaleX: 1.0, y: -1.0) - .translatedBy(x: viewPortHandler.offsetLeft, y: -viewPortHandler.offsetTop) - } - } - - /// Transform an array of points with all matrices. - // VERY IMPORTANT: Keep matrix order "value-touch-offset" when transforming. - open func pointValuesToPixel(_ points: inout [CGPoint]) - { - let trans = valueToPixelMatrix - points = points.map { $0.applying(trans) } - } - - open func pointValueToPixel(_ point: inout CGPoint) - { - point = point.applying(valueToPixelMatrix) - } - - @objc open func pixelForValues(x: Double, y: Double) -> CGPoint - { - return CGPoint(x: x, y: y).applying(valueToPixelMatrix) - } - - /// Transform a rectangle with all matrices. - open func rectValueToPixel(_ r: inout CGRect) - { - r = r.applying(valueToPixelMatrix) - } - - /// Transform a rectangle with all matrices with potential animation phases. - open func rectValueToPixel(_ r: inout CGRect, phaseY: Double) - { - // multiply the height of the rect with the phase - var bottom = r.origin.y + r.size.height - bottom *= CGFloat(phaseY) - let top = r.origin.y * CGFloat(phaseY) - r.size.height = bottom - top - r.origin.y = top - - r = r.applying(valueToPixelMatrix) - } - - /// Transform a rectangle with all matrices. - open func rectValueToPixelHorizontal(_ r: inout CGRect) - { - r = r.applying(valueToPixelMatrix) - } - - /// Transform a rectangle with all matrices with potential animation phases. - open func rectValueToPixelHorizontal(_ r: inout CGRect, phaseY: Double) - { - // multiply the height of the rect with the phase - let left = r.origin.x * CGFloat(phaseY) - let right = (r.origin.x + r.size.width) * CGFloat(phaseY) - r.size.width = right - left - r.origin.x = left - - r = r.applying(valueToPixelMatrix) - } - - /// transforms multiple rects with all matrices - open func rectValuesToPixel(_ rects: inout [CGRect]) - { - let trans = valueToPixelMatrix - rects = rects.map { $0.applying(trans) } - } - - /// Transforms the given array of touch points (pixels) into values on the chart. - open func pixelsToValues(_ pixels: inout [CGPoint]) - { - let trans = pixelToValueMatrix - pixels = pixels.map { $0.applying(trans) } - } - - /// Transforms the given touch point (pixels) into a value on the chart. - open func pixelToValues(_ pixel: inout CGPoint) - { - pixel = pixel.applying(pixelToValueMatrix) - } - - /// - Returns: The x and y values in the chart at the given touch point - /// (encapsulated in a CGPoint). This method transforms pixel coordinates to - /// coordinates / values in the chart. - @objc open func valueForTouchPoint(_ point: CGPoint) -> CGPoint - { - return point.applying(pixelToValueMatrix) - } - - /// - Returns: The x and y values in the chart at the given touch point - /// (x/y). This method transforms pixel coordinates to - /// coordinates / values in the chart. - @objc open func valueForTouchPoint(x: CGFloat, y: CGFloat) -> CGPoint - { - return CGPoint(x: x, y: y).applying(pixelToValueMatrix) - } - - @objc open var valueToPixelMatrix: CGAffineTransform - { - return - matrixValueToPx.concatenating(viewPortHandler.touchMatrix) - .concatenating(matrixOffset - ) - } - - @objc open var pixelToValueMatrix: CGAffineTransform - { - let matrix = valueToPixelMatrix - // Guard against singular matrix when chart has zero dimensions (before layout) - guard matrix.a * matrix.d - matrix.b * matrix.c != 0 else { - return .identity - } - return matrix.inverted() - } -} diff --git a/Pods/Charts/Source/Charts/Utils/TransformerHorizontalBarChart.swift b/Pods/Charts/Source/Charts/Utils/TransformerHorizontalBarChart.swift deleted file mode 100644 index 1a7d5579c..000000000 --- a/Pods/Charts/Source/Charts/Utils/TransformerHorizontalBarChart.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// TransformerHorizontalBarChart.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -@objc(ChartTransformerHorizontalBarChart) -open class TransformerHorizontalBarChart: Transformer -{ - /// Prepares the matrix that contains all offsets. - open override func prepareMatrixOffset(inverted: Bool) - { - if !inverted - { - matrixOffset = CGAffineTransform(translationX: viewPortHandler.offsetLeft, y: viewPortHandler.chartHeight - viewPortHandler.offsetBottom) - } - else - { - matrixOffset = CGAffineTransform(scaleX: -1.0, y: 1.0) - .translatedBy(x: -(viewPortHandler.chartWidth - viewPortHandler.offsetRight), - y: viewPortHandler.chartHeight - viewPortHandler.offsetBottom) - } - } -} diff --git a/Pods/Charts/Source/Charts/Utils/ViewPortHandler.swift b/Pods/Charts/Source/Charts/Utils/ViewPortHandler.swift deleted file mode 100644 index bf27481b1..000000000 --- a/Pods/Charts/Source/Charts/Utils/ViewPortHandler.swift +++ /dev/null @@ -1,471 +0,0 @@ -// -// ViewPortHandler.swift -// Charts -// -// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda -// A port of MPAndroidChart for iOS -// Licensed under Apache License 2.0 -// -// https://github.com/danielgindi/Charts -// - -import Foundation -import CoreGraphics - -/// Class that contains information about the charts current viewport settings, including offsets, scale & translation levels, ... -@objc(ChartViewPortHandler) -open class ViewPortHandler: NSObject -{ - /// matrix used for touch events - @objc open private(set) var touchMatrix = CGAffineTransform.identity - - /// this rectangle defines the area in which graph values can be drawn - @objc open private(set) var contentRect = CGRect() - - @objc open private(set) var chartWidth: CGFloat = 0 - @objc open private(set) var chartHeight: CGFloat = 0 - - /// minimum scale value on the y-axis - @objc open private(set) var minScaleY: CGFloat = 1.0 - - /// maximum scale value on the y-axis - @objc open private(set) var maxScaleY = CGFloat.greatestFiniteMagnitude - - /// minimum scale value on the x-axis - @objc open private(set) var minScaleX: CGFloat = 1.0 - - /// maximum scale value on the x-axis - @objc open private(set) var maxScaleX = CGFloat.greatestFiniteMagnitude - - /// contains the current scale factor of the x-axis - @objc open private(set) var scaleX: CGFloat = 1.0 - - /// contains the current scale factor of the y-axis - @objc open private(set) var scaleY: CGFloat = 1.0 - - /// current translation (drag / pan) distance on the x-axis - @objc open private(set) var transX: CGFloat = 0 - - /// current translation (drag / pan) distance on the y-axis - @objc open private(set) var transY: CGFloat = 0 - - /// offset that allows the chart to be dragged over its bounds on the x-axis - private var transOffsetX: CGFloat = 0 - - /// offset that allows the chart to be dragged over its bounds on the x-axis - private var transOffsetY: CGFloat = 0 - - /// Constructor - don't forget calling setChartDimens(...) - @objc public init(width: CGFloat, height: CGFloat) - { - super.init() - - setChartDimens(width: width, height: height) - } - - @objc open func setChartDimens(width: CGFloat, height: CGFloat) - { - let offsetLeft = self.offsetLeft - let offsetTop = self.offsetTop - let offsetRight = self.offsetRight - let offsetBottom = self.offsetBottom - - chartHeight = height - chartWidth = width - - restrainViewPort(offsetLeft: offsetLeft, offsetTop: offsetTop, offsetRight: offsetRight, offsetBottom: offsetBottom) - } - - @objc open var hasChartDimens: Bool - { - return chartHeight > 0.0 - && chartWidth > 0.0 - } - - @objc open func restrainViewPort(offsetLeft: CGFloat, offsetTop: CGFloat, offsetRight: CGFloat, offsetBottom: CGFloat) - { - contentRect.origin.x = offsetLeft - contentRect.origin.y = offsetTop - contentRect.size.width = chartWidth - offsetLeft - offsetRight - contentRect.size.height = chartHeight - offsetBottom - offsetTop - } - - @objc open var offsetLeft: CGFloat - { - return contentRect.origin.x - } - - @objc open var offsetRight: CGFloat - { - return chartWidth - contentRect.size.width - contentRect.origin.x - } - - @objc open var offsetTop: CGFloat - { - return contentRect.origin.y - } - - @objc open var offsetBottom: CGFloat - { - return chartHeight - contentRect.size.height - contentRect.origin.y - } - - @objc open var contentTop: CGFloat - { - return contentRect.origin.y - } - - @objc open var contentLeft: CGFloat - { - return contentRect.origin.x - } - - @objc open var contentRight: CGFloat - { - return contentRect.origin.x + contentRect.size.width - } - - @objc open var contentBottom: CGFloat - { - return contentRect.origin.y + contentRect.size.height - } - - @objc open var contentWidth: CGFloat - { - return contentRect.size.width - } - - @objc open var contentHeight: CGFloat - { - return contentRect.size.height - } - - @objc open var contentCenter: CGPoint - { - return CGPoint(x: contentRect.origin.x + contentRect.size.width / 2.0, y: contentRect.origin.y + contentRect.size.height / 2.0) - } - - // MARK: - Scaling/Panning etc. - - /// Zooms by the specified zoom factors. - @objc open func zoom(scaleX: CGFloat, scaleY: CGFloat) -> CGAffineTransform - { - return touchMatrix.scaledBy(x: scaleX, y: scaleY) - } - - /// Zooms around the specified center - @objc open func zoom(scaleX: CGFloat, scaleY: CGFloat, x: CGFloat, y: CGFloat) -> CGAffineTransform - { - return touchMatrix.translatedBy(x: x, y: y) - .scaledBy(x: scaleX, y: scaleY) - .translatedBy(x: -x, y: -y) - } - - /// Zooms in by 1.4, x and y are the coordinates (in pixels) of the zoom center. - @objc open func zoomIn(x: CGFloat, y: CGFloat) -> CGAffineTransform - { - return zoom(scaleX: 1.4, scaleY: 1.4, x: x, y: y) - } - - /// Zooms out by 0.7, x and y are the coordinates (in pixels) of the zoom center. - @objc open func zoomOut(x: CGFloat, y: CGFloat) -> CGAffineTransform - { - return zoom(scaleX: 0.7, scaleY: 0.7, x: x, y: y) - } - - /// Zooms out to original size. - @objc open func resetZoom() -> CGAffineTransform - { - return zoom(scaleX: 1.0, scaleY: 1.0, x: 0.0, y: 0.0) - } - - /// Sets the scale factor to the specified values. - @objc open func setZoom(scaleX: CGFloat, scaleY: CGFloat) -> CGAffineTransform - { - var matrix = touchMatrix - matrix.a = scaleX - matrix.d = scaleY - return matrix - } - - /// Sets the scale factor to the specified values. x and y is pivot. - @objc open func setZoom(scaleX: CGFloat, scaleY: CGFloat, x: CGFloat, y: CGFloat) -> CGAffineTransform - { - var matrix = touchMatrix - matrix.a = 1.0 - matrix.d = 1.0 - matrix = matrix.translatedBy(x: x, y: y) - .scaledBy(x: scaleX, y: scaleY) - .translatedBy(x: -x, y: -y) - return matrix - } - - /// Resets all zooming and dragging and makes the chart fit exactly it's bounds. - @objc open func fitScreen() -> CGAffineTransform - { - minScaleX = 1.0 - minScaleY = 1.0 - - return .identity - } - - /// Translates to the specified point. - @objc open func translate(pt: CGPoint) -> CGAffineTransform - { - let translateX = pt.x - offsetLeft - let translateY = pt.y - offsetTop - - let matrix = touchMatrix.concatenating(CGAffineTransform(translationX: -translateX, y: -translateY)) - - return matrix - } - - /// Centers the viewport around the specified position (x-index and y-value) in the chart. - /// Centering the viewport outside the bounds of the chart is not possible. - /// Makes most sense in combination with the setScaleMinima(...) method. - @objc open func centerViewPort(pt: CGPoint, chart: ChartViewBase) - { - let translateX = pt.x - offsetLeft - let translateY = pt.y - offsetTop - - let matrix = touchMatrix.concatenating(CGAffineTransform(translationX: -translateX, y: -translateY)) - refresh(newMatrix: matrix, chart: chart, invalidate: true) - } - - /// call this method to refresh the graph with a given matrix - @objc @discardableResult open func refresh(newMatrix: CGAffineTransform, chart: ChartViewBase, invalidate: Bool) -> CGAffineTransform - { - touchMatrix = newMatrix - - // make sure scale and translation are within their bounds - limitTransAndScale(matrix: &touchMatrix, content: contentRect) - - chart.setNeedsDisplay() - - return touchMatrix - } - - /// limits the maximum scale and X translation of the given matrix - private func limitTransAndScale(matrix: inout CGAffineTransform, content: CGRect) - { - // min scale-x is 1 - scaleX = min(max(minScaleX, matrix.a), maxScaleX) - - // min scale-y is 1 - scaleY = min(max(minScaleY, matrix.d), maxScaleY) - - let width = content.width - let height = content.height - - let maxTransX = -width * (scaleX - 1.0) - transX = min(max(matrix.tx, maxTransX - transOffsetX), transOffsetX) - - let maxTransY = height * (scaleY - 1.0) - transY = max(min(matrix.ty, maxTransY + transOffsetY), -transOffsetY) - - matrix.tx = transX - matrix.a = scaleX - matrix.ty = transY - matrix.d = scaleY - } - - /// Sets the minimum scale factor for the x-axis - @objc open func setMinimumScaleX(_ xScale: CGFloat) - { - minScaleX = max(xScale, 1) - limitTransAndScale(matrix: &touchMatrix, content: contentRect) - } - - /// Sets the maximum scale factor for the x-axis - @objc open func setMaximumScaleX(_ xScale: CGFloat) - { - maxScaleX = xScale == 0 ? .greatestFiniteMagnitude : xScale - limitTransAndScale(matrix: &touchMatrix, content: contentRect) - } - - /// Sets the minimum and maximum scale factors for the x-axis - @objc open func setMinMaxScaleX(minScaleX: CGFloat, maxScaleX: CGFloat) - { - self.minScaleX = max(minScaleX, 1) - self.maxScaleX = maxScaleX == 0 ? .greatestFiniteMagnitude : maxScaleX - limitTransAndScale(matrix: &touchMatrix, content: contentRect) - } - - /// Sets the minimum scale factor for the y-axis - @objc open func setMinimumScaleY(_ yScale: CGFloat) - { - minScaleY = max(yScale, 1) - limitTransAndScale(matrix: &touchMatrix, content: contentRect) - } - - /// Sets the maximum scale factor for the y-axis - @objc open func setMaximumScaleY(_ yScale: CGFloat) - { - maxScaleY = yScale == 0 ? .greatestFiniteMagnitude : yScale - limitTransAndScale(matrix: &touchMatrix, content: contentRect) - } - - @objc open func setMinMaxScaleY(minScaleY: CGFloat, maxScaleY: CGFloat) - { - - self.minScaleY = max(minScaleY, 1) - self.maxScaleY = maxScaleY == 0 ? .greatestFiniteMagnitude : maxScaleY - limitTransAndScale(matrix: &touchMatrix, content: contentRect) - } - - // MARK: - Boundaries Check - - @objc open func isInBoundsX(_ x: CGFloat) -> Bool - { - return isInBoundsLeft(x) && isInBoundsRight(x) - } - - @objc open func isInBoundsY(_ y: CGFloat) -> Bool - { - return isInBoundsTop(y) && isInBoundsBottom(y) - } - - /** - A method to check whether coordinate lies within the viewport. - - - Parameters: - - point: a coordinate. - */ - @objc open func isInBounds(point: CGPoint) -> Bool - { - return isInBounds(x: point.x, y: point.y) - } - - @objc open func isInBounds(x: CGFloat, y: CGFloat) -> Bool - { - return isInBoundsX(x) && isInBoundsY(y) - } - - @objc open func isInBoundsLeft(_ x: CGFloat) -> Bool - { - return contentRect.origin.x <= x + 1.0 - } - - @objc open func isInBoundsRight(_ x: CGFloat) -> Bool - { - let x = floor(x * 100.0) / 100.0 - return (contentRect.origin.x + contentRect.size.width) >= x - 1.0 - } - - @objc open func isInBoundsTop(_ y: CGFloat) -> Bool - { - return contentRect.origin.y <= y - } - - @objc open func isInBoundsBottom(_ y: CGFloat) -> Bool - { - let normalizedY = floor(y * 100.0) / 100.0 - return (contentRect.origin.y + contentRect.size.height) >= normalizedY - } - - /** - A method to check whether a line between two coordinates intersects with the view port by using a linear function. - - Linear function (calculus): `y = ax + b` - - Note: this method will not check for collision with the right edge of the view port, as we assume lines run from left - to right (e.g. `startPoint < endPoint`). - - - Parameters: - - startPoint: the start coordinate of the line. - - endPoint: the end coordinate of the line. - */ - @objc open func isIntersectingLine(from startPoint: CGPoint, to endPoint: CGPoint) -> Bool - { - // If start- and/or endpoint fall within the viewport, bail out early. - if isInBounds(point: startPoint) || isInBounds(point: endPoint) { return true } - // check if x in bound when it's a vertical line - if startPoint.x == endPoint.x { return isInBoundsX(startPoint.x) } - - // Calculate the slope (`a`) of the line (e.g. `a = (y2 - y1) / (x2 - x1)`). - let a = (endPoint.y - startPoint.y) / (endPoint.x - startPoint.x) - // Calculate the y-correction (`b`) of the line (e.g. `b = y1 - (a * x1)`). - let b = startPoint.y - (a * startPoint.x) - - // Check for colission with the left edge of the view port (e.g. `y = (a * minX) + b`). - // if a is 0, it's a horizontal line; checking b here is still valid, as b is `point.y` all the time - if isInBoundsY((a * contentRect.minX) + b) { return true } - - // Skip unnecessary check for collision with the right edge of the view port - // (e.g. `y = (a * maxX) + b`), as such a line will either begin inside the view port, - // or intersect the left, top or bottom edges of the view port. Leaving this logic here for clarity's sake: - // if isInBoundsY((a * contentRect.maxX) + b) { return true } - - // While slope `a` can theoretically never be `0`, we should protect against division by zero. - guard a != 0 else { return false } - - // Check for collision with the bottom edge of the view port (e.g. `x = (maxY - b) / a`). - if isInBoundsX((contentRect.maxY - b) / a) { return true } - - // Check for collision with the top edge of the view port (e.g. `x = (minY - b) / a`). - if isInBoundsX((contentRect.minY - b) / a) { return true } - - // This line does not intersect the view port. - return false - } - - /// if the chart is fully zoomed out, return true - @objc open var isFullyZoomedOut: Bool - { - return isFullyZoomedOutX && isFullyZoomedOutY - } - - /// `true` if the chart is fully zoomed out on it's y-axis (vertical). - @objc open var isFullyZoomedOutY: Bool - { - return !(scaleY > minScaleY || minScaleY > 1.0) - } - - /// `true` if the chart is fully zoomed out on it's x-axis (horizontal). - @objc open var isFullyZoomedOutX: Bool - { - return !(scaleX > minScaleX || minScaleX > 1.0) - } - - /// Set an offset in pixels that allows the user to drag the chart over it's bounds on the x-axis. - @objc open func setDragOffsetX(_ offset: CGFloat) - { - transOffsetX = offset - } - - /// Set an offset in pixels that allows the user to drag the chart over it's bounds on the y-axis. - @objc open func setDragOffsetY(_ offset: CGFloat) - { - transOffsetY = offset - } - - /// `true` if both drag offsets (x and y) are zero or smaller. - @objc open var hasNoDragOffset: Bool - { - return transOffsetX <= 0.0 && transOffsetY <= 0.0 - } - - /// `true` if the chart is not yet fully zoomed out on the x-axis - @objc open var canZoomOutMoreX: Bool - { - return scaleX > minScaleX - } - - /// `true` if the chart is not yet fully zoomed in on the x-axis - @objc open var canZoomInMoreX: Bool - { - return scaleX < maxScaleX - } - - /// `true` if the chart is not yet fully zoomed out on the y-axis - @objc open var canZoomOutMoreY: Bool - { - return scaleY > minScaleY - } - - /// `true` if the chart is not yet fully zoomed in on the y-axis - @objc open var canZoomInMoreY: Bool - { - return scaleY < maxScaleY - } -} diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock index 9a7875837..22a172618 100644 --- a/Pods/Manifest.lock +++ b/Pods/Manifest.lock @@ -1,20 +1,9 @@ PODS: - - Charts (4.1.0): - - Charts/Core (= 4.1.0) - - Charts/Core (4.1.0): - - SwiftAlgorithms (~> 1.0) - ShareClient (1.2) - - SwiftAlgorithms (1.0.0) DEPENDENCIES: - - Charts - ShareClient (from `https://github.com/loopandlearn/dexcom-share-client-swift.git`, branch `loopfollow`) -SPEC REPOS: - trunk: - - Charts - - SwiftAlgorithms - EXTERNAL SOURCES: ShareClient: :branch: loopfollow @@ -26,10 +15,8 @@ CHECKOUT OPTIONS: :git: https://github.com/loopandlearn/dexcom-share-client-swift.git SPEC CHECKSUMS: - Charts: ce0768268078eee0336f122c3c4ca248e4e204c5 ShareClient: 60b911c95e73b0ea9c5aad6d194a9c6b5f34b741 - SwiftAlgorithms: 38dda4731d19027fdeee1125f973111bf3386b53 -PODFILE CHECKSUM: d241dda366d5454279fe3ff8869458d19e1bbebb +PODFILE CHECKSUM: 617b13cfc623f15ca31e14e19086cb729b408fd7 -COCOAPODS: 1.12.1 +COCOAPODS: 1.16.2 diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj index 489d852f5..a4042c2db 100644 --- a/Pods/Pods.xcodeproj/project.pbxproj +++ b/Pods/Pods.xcodeproj/project.pbxproj @@ -3,496 +3,83 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ - BEEF11110000000000000002 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BEEF11110000000000000001 /* PrivacyInfo.xcprivacy */; }; - 0031CEAD857A1BA158C0D2BE1870A438 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB10D8BF2F0C2E9B18C2130751D75984 /* Animator.swift */; }; - 00351B69906B07C1950B97C40F65FABE /* RadarChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51F17D0D2858BA593E783811A2CF7E6 /* RadarChartData.swift */; }; - 00E171A24B8B214FDC447BED6BB62C31 /* BarLineScatterCandleBubbleChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90B940BBEE17272D0FA9602CB3F1E1 /* BarLineScatterCandleBubbleChartDataSet.swift */; }; - 0382A6D93C8BB1218EB1DBA3CF1B3BA7 /* BubbleChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1D0991883487B252954C6AD93262DD /* BubbleChartData.swift */; }; - 04360A4DF646DD99E7AF0AC83FAA56AB /* CandleChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF58BE2BB556CCFFD3F153CD246BB3B2 /* CandleChartDataSet.swift */; }; - 078FF70DB7D0BDBE768AB0FA72E19D14 /* RadarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631260726B8649201DF3262C04AE423B /* RadarChartDataSet.swift */; }; - 094130854F03255D5BD94EAE3C6797B4 /* LineRadarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C07C88A101976F7D43BE7BE4673A37 /* LineRadarChartDataSet.swift */; }; - 0BB393E009EB565C1342EEA8D02C9098 /* BarLineScatterCandleBubbleChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C00C3496A26A3B8ED45E73F3E3AFDAC /* BarLineScatterCandleBubbleChartData.swift */; }; - 0CA67293F83A1EB1004D16D1878B4007 /* LineRadarChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4A498BBC8E4513D82F78F75036A171 /* LineRadarChartDataSetProtocol.swift */; }; - 0D73D4FBCA95E51CDA036332FE5031B4 /* Pods-LoopFollow-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 7E41188828C3ADE7A96C4DA6ED1CCDA2 /* Pods-LoopFollow-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 1251E7AD01C341EFCB0A7A4B70942433 /* BarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAB9310175D2D05534B5FAA40BDABB8 /* BarChartDataSet.swift */; }; - 1267184A54805C22C30908BFA58279C9 /* BubbleChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9B0CD14B51E9BB24006EA87AD5758F /* BubbleChartDataProvider.swift */; }; - 13CC355E8703534C3651E1676F54989C /* Compacted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 417ACD10C75E540CA5E23120521A8BA9 /* Compacted.swift */; }; - 150DB6950349AEAEBDB84A1994DF55DE /* PieChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE92153A023DA702752FCCCC960EC20B /* PieChartView.swift */; }; - 1611A02A50E8ECD5F31ECF1CDDCCFF7F /* LegendEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719A12B42A88A75148E0A9C8D6C618F0 /* LegendEntry.swift */; }; - 16362413D2756A8A8BD2E5D20726520D /* LineScatterCandleRadarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524C2C5AB08DAC0B62469BC9BAD2BBFC /* LineScatterCandleRadarChartDataSet.swift */; }; - 16C5A86B21B4BF6C530A96DC25C9ECDB /* PieChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9D1DB1BA530D126D35D218E4439458 /* PieChartDataEntry.swift */; }; - 18F6D2659567747929F4F67444C6DB5A /* Indexed.swift in Sources */ = {isa = PBXBuildFile; fileRef = F574273E89B9C97C369C430B67186E14 /* Indexed.swift */; }; - 195360D36D20DEA54AC170FB9C9ECB44 /* Suffix.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72D375E25E043A018C9D9E87EF64256 /* Suffix.swift */; }; - 1956BBD2D0434340107A6BE2B68FE4FE /* Intersperse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3897C4A6B7F162E479564409C5CEFB13 /* Intersperse.swift */; }; - 19F40EB97D44872C2F155EB91889EEDD /* BarChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C5C283029332E30F3E763EA8969893 /* BarChartDataSetProtocol.swift */; }; - 1C7423F964AEF53EA37E1B4F2F4C8A3D /* MoveViewJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0651C9C3E1F7212ACFC0DF4E3E00323D /* MoveViewJob.swift */; }; - 1C903C670AE552000F47F9FF69946132 /* Chunked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 593C5B76ABCE8F5324D1A34783FACC5B /* Chunked.swift */; }; - 1CFCB7624A2D0A7B088428BB898E452E /* Chain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A34EC75AF41BCC80FDC54C5AB3DC008 /* Chain.swift */; }; - 22725F307C8A12F85C2E583CE9D67A68 /* CrossShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09B56CDA6CE4B961DE77CE43BF6517D /* CrossShapeRenderer.swift */; }; - 25D7B9CB6D83A2899E364531C8B465E0 /* XAxisRendererRadarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FC9F1F7847D290D9CD7AB6024341FF /* XAxisRendererRadarChart.swift */; }; - 26414DCD243931AD2DF5A7FAEFA9A2F1 /* AxisBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC79A108E546A80FD2E858996EB42D7 /* AxisBase.swift */; }; - 280B9154E146F8A47E47F484ACC815F0 /* ValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ACE4DAB3C5C84A671AFE0A24BC32E8 /* ValueFormatter.swift */; }; - 28699DA45BB94636CDD55D302CC36D4C /* FillFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421EDDDD69FD4DEF3F201DC7F4EDD885 /* FillFormatter.swift */; }; - 28C0C336867276BB3F88C94BDCF5924F /* AnimatedZoomViewJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F619C3ACAC8BB97F4CBF04C055CCB24 /* AnimatedZoomViewJob.swift */; }; - 290F4D27B047ECE9E11FE65D45659AA9 /* Platform+Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0ACEEB9003A6780541B487AD7AD20F /* Platform+Color.swift */; }; - 295E7D31962EAA43CE02A22D291AC827 /* YAxisRendererHorizontalBarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = F448E25F6F3095E674026E503F71BED7 /* YAxisRendererHorizontalBarChart.swift */; }; - 2D7A2EBC2FC440678E3F1D63D375EA31 /* CombinedChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7210F7399765A3931B62D5370EF1B4FC /* CombinedChartData.swift */; }; - 30F6164C5F15445D6BC5D8B22A4131BE /* Permutations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4695EDCBBD2F14A96C466589AC26B4 /* Permutations.swift */; }; - 341C4C56C45E00A6B58E63A7EDF7F7C1 /* BubbleChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C19D311E35002501E760D893F35E2F /* BubbleChartDataSetProtocol.swift */; }; - 35BFC4C3A46CF7EEC15BEA91AC6CE39B /* HorizontalBarHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896D7BB34CDE275E92DC01AC1F533F26 /* HorizontalBarHighlighter.swift */; }; - 3641D2A955EB369FD725C07BAF82E96F /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6124C7D917D4C7DC28F7503EBCD596D7 /* Renderer.swift */; }; - 37C035EBB523276E74097ACE8B88D7DE /* ChartLimitLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE2943C8EAB9AD013BB0D24A3E135AF /* ChartLimitLine.swift */; }; - 3BCB9E6E546029CDD29933889ECF313D /* LineChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F86CBC5DB9A328A34A07A52C1CD5605 /* LineChartDataProvider.swift */; }; - 3BFFA0E6A588A61617609032FF7D48DF /* SwiftAlgorithms-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 44A177D8AC2CE9A04AEC62303E1C012C /* SwiftAlgorithms-dummy.m */; }; - 3C1FAA2B78B8750635AC8D2B088703BA /* PieHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97D67CE26870DFAA8AFE096DDF8F6CF /* PieHighlighter.swift */; }; - 3C863D4E5E34C7C2173E0B557157A469 /* ChevronDownShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E2D718053926F7C362A0242C707398 /* ChevronDownShapeRenderer.swift */; }; - 3CCE8A616A323BDE56FA02CAB953BA18 /* BubbleChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F21D683A3D7551D0D6ED48F9BCBDDCB /* BubbleChartView.swift */; }; - 3F7DE7DD33C07E28ABBF80B3AFBCDF44 /* HorizontalBarChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC882B9BE58FE2D2FCB58677AABEF941 /* HorizontalBarChartRenderer.swift */; }; - 42361A518637B458CBB971D21E43346E /* CombinedChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B4B217AE01ABE5E9A1B3FEE24ACD3E /* CombinedChartView.swift */; }; - 4304139B7049F735A4879E7A14DCA236 /* ScatterChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D975F08944D56553EE8F518D883AABD /* ScatterChartRenderer.swift */; }; - 4412B151ED4921518E19FA644D282D99 /* LineRadarRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B0F987BD66CF8FC4B3368F8EB644D6A /* LineRadarRenderer.swift */; }; - 46D629F718073D7735C888E42E1EAB17 /* PieChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E1E5F4713322837D7506143802ED222 /* PieChartDataSet.swift */; }; - 494DAE02C6BD413F95D9B35E4C9B8473 /* Platform+Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7724018046776D153BF2F519E62ED100 /* Platform+Accessibility.swift */; }; - 49F28A30CAC0F0F5E874185195C05EAA /* Joined.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5D397F5631F7981765A1517F8E752B /* Joined.swift */; }; - 4C3E4C1A74F42DC91C726DC9BD262891 /* CombinedChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF14DB72944084EE52D83024A8168D3A /* CombinedChartDataProvider.swift */; }; - 4C7DA896256A295E7DEADFCA8B0136EE /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0047677618FCD329E548FDDC0454A8AC /* LineChartView.swift */; }; - 4CDE5AD6B91F8E1859C196089A01F927 /* BarLineScatterCandleBubbleChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDBEAB7607F1CDCCB6229DF611C0D3B /* BarLineScatterCandleBubbleChartDataProvider.swift */; }; - 4E0CE7C1CD75F9926C1453AC1AFCFBF7 /* BarChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EF3C460FF3BA09B964B4B078B2FFDC /* BarChartDataProvider.swift */; }; - 50711078539C633D63AEE4D4F4B0642D /* TransformerHorizontalBarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127D620D9103C646821EE1A9FFDC6AB4 /* TransformerHorizontalBarChart.swift */; }; - 56C71B82E08666FBEC2E57178FF7EB18 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D8A4241B43E6A486B3ABFE7CE176D2 /* Platform.swift */; }; - 5751FF8F73931CF52C588FC93DF87069 /* HorizontalBarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34D137537BF079F35059435323D7452 /* HorizontalBarChartView.swift */; }; - 5829E60F3B1D9D7166301BC4AA17E83D /* ViewPortJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3651675689D3E922771781C1C7EBF4 /* ViewPortJob.swift */; }; - 5851F6B6C46E7426DA8693AD1781EDB5 /* ShareClient-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 185455CC1A16D809E6CD52CA7ED7C218 /* ShareClient-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 585FD9DA0B21341F777A5B39ED1CA562 /* RadarChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 323A7094F3358B4E346E033FE9AAF79E /* RadarChartDataEntry.swift */; }; - 58889ECB9711CC8D285F5689C9DF4584 /* ChartColorTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CA046D8066D2043078135381049F0A /* ChartColorTemplates.swift */; }; - 5D998F2454D188B00E592ECF3DFB6371 /* PieChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FBB9A8A3775D305F8D23740840158D /* PieChartData.swift */; }; - 5E6D0EA6A7250B44F8D1416E97248FC2 /* Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35CE8AE4FB4FAA915248FFA91AE62338 /* Split.swift */; }; - 5FFB0442423AFC0BFC70DA91859C1FA7 /* SquareShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8151C448D7FF1AE4C18A1D9BAAB84F5 /* SquareShapeRenderer.swift */; }; - 60F208C320EA137DF1E35F58D848DE9A /* CandleStickChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16B9EB6993E134E20A22EFB3F975FAAB /* CandleStickChartView.swift */; }; - 6130B294DE88ECF9F4706D1895CC6279 /* ChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18735DC81F8F3BB4967404CF8ED7E84 /* ChartDataProvider.swift */; }; - 6138784D2228EEEEC197C541EDB9282C /* BarChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640DB089E7BC7FA9C4C5D67D1E58393A /* BarChartData.swift */; }; - 62425D090FD054F868558C00A83BBCF6 /* MarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C90F7AE53155C1804331C13F95834B2 /* MarkerView.swift */; }; - 6291997C9089F0F4FF4B0172331B31AC /* RadarChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D024D84271BC3B8711840FD1825F8191 /* RadarChartRenderer.swift */; }; - 6351CC48D4CD08A64B93027110D0BFD7 /* Marker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347B2727DAB3007AE54E938A6EDAC926 /* Marker.swift */; }; - 646FE6A25A724BC685702BFB101E021E /* ChartBaseDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14145B7AD1FC1EC76D1DB43D3100AC2 /* ChartBaseDataSet.swift */; }; - 64B59451EB2C31914C278348F911C715 /* CandleChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E41D7AE4F0AB44736E2E49B07F83AC6 /* CandleChartDataSetProtocol.swift */; }; - 64E13AF9E7842CDAD6F7E9F205082C09 /* Legend.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6D57171DBF58C479650D72C1FE7CA4 /* Legend.swift */; }; - 64E36FF14DDE083130EAD49EB027C6F4 /* LineChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDCF1B7DBEAD927140D88A4876E56FDA /* LineChartDataSetProtocol.swift */; }; - 67AF0FFA8C4DD3594D2D4F7F0EB13D6F /* LineChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EEDBD82A70398B3251ACA676701B65C /* LineChartDataSet.swift */; }; - 68047C5BD5B0C77274FAB5CD38F2EF0E /* TriangleShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9222FB7877363AAC2E66F4E9BD6BE22F /* TriangleShapeRenderer.swift */; }; - 68A3CE924047F6FF8D7BDDE68BCBD4C1 /* BarHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB44185A42C56CCC7B7F24D8A4259044 /* BarHighlighter.swift */; }; - 6916BF3166BE703933C43515B50ADBD7 /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D152598E6F7AD2209244F3255EBE2B3 /* Windows.swift */; }; - 69CED83D06805776035CF76DD1DE2C2A /* ChartViewBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FC29B40706B2526531F041CD6E5F37 /* ChartViewBase.swift */; }; - 6B89D2717D3A324DF55655D69A21AAAC /* ChartHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A488FE97E31BCF19AE78B98C18EE81 /* ChartHighlighter.swift */; }; - 6B9186D7227163690B8397FAADFF77D3 /* DefaultAxisValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194A8936BCA6DD2C7CC0C0003C0F27BB /* DefaultAxisValueFormatter.swift */; }; - 6D878FE6964089A7F47EB406F566ECC0 /* ScatterChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60B7899F2AE639742DDEE13C16276275 /* ScatterChartDataSetProtocol.swift */; }; - 6E65B4450EE038F9F1DF3F7443BF53DB /* Unique.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99770C7A6099A970C34D7824A7CE280D /* Unique.swift */; }; - 701A1DF09A74742FFCF8BCE3F5A9370B /* SwiftAlgorithms-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B7EBBA8C350C40C7F423CDD638EDB2E /* SwiftAlgorithms-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 70CACB146AA2603CDD775285FA9A4CE9 /* Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D58369D6A0FF961EC41F36B0B7D29C /* Range.swift */; }; - 75E493B239F29100777CBB38AAAE08D7 /* BarLineScatterCandleBubbleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5AFD1B5788F5477102C8DD0F7EAF50 /* BarLineScatterCandleBubbleRenderer.swift */; }; - 782E096BE9488B722DA0F64794A45D40 /* ShareClient.h in Headers */ = {isa = PBXBuildFile; fileRef = A5822CA2A05671BAA9BC5BD4A3BA5D6B /* ShareClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 7899A9D1751880EC7A2649DF35617F67 /* Sequence+KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2629C2C6B479EEB26E2F6DA58C30B81 /* Sequence+KeyPath.swift */; }; - 7A52005D206FA5BEC462EC1ED5E34423 /* XAxisRendererHorizontalBarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48FC9D5D68CA4D70155A8959C1179E28 /* XAxisRendererHorizontalBarChart.swift */; }; - 7B8211FE68633C9F9EF8C2729FACE32A /* MinMax.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939E4F680A1A71C7BFC00F9917F3E5EB /* MinMax.swift */; }; - 7C09D13338B7A0BF4A0EB9763EE60339 /* ChevronUpShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910799424A3C8A57366C4CE76BE79683 /* ChevronUpShapeRenderer.swift */; }; - 80E88D848625ADD942D49552233940C3 /* XAxisRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837358B5E2BC5C139A82049EB2895D05 /* XAxisRenderer.swift */; }; - 839F873A001FE9169E9978BE89195B5F /* ScatterChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BD1A15A0561B886A937317473FCF4D /* ScatterChartDataSet.swift */; }; - 8809E0E77C38BC2164874FB080404B2C /* Transformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 162C2F9A6894149851210E55E6188A43 /* Transformer.swift */; }; - 88DD483D2937E149E31768EAB7567563 /* ZoomViewJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD6267883E63FA87B3CF88C8773CCD6 /* ZoomViewJob.swift */; }; - 8BF58ABE3315B02170CA950504A6AF7D /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DDB7F7B5FBDC211213044A06C72601 /* Product.swift */; }; - 8FD826D1119F87EB638FAC888F74BE51 /* LineChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12C56B79F0D3D4E449C70724B3B0D2 /* LineChartData.swift */; }; - 8FDCA72F349897774711AA8AB1F23522 /* CandleChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1714C5DC04BE72E509DC4E7686AA857F /* CandleChartDataEntry.swift */; }; - 905D5792F0F95A1D50E0FA79AFF58278 /* FlattenCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C320E80DD7BFAAA5494A8BB4D7A226F /* FlattenCollection.swift */; }; - 92C7BAE4CC1EA95C35F3CE1D2DFB463B /* YAxis.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8FE53DE5C6214CB9D501CF937A4A88A /* YAxis.swift */; }; - 92F85BA954CAD476F1964EFD067DF138 /* LineChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 791893088C1075827778DDAFED99EF63 /* LineChartRenderer.swift */; }; - 93E19EA5B507AAA18B0961FD52FDD452 /* CircleShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFA8A221F0F4BA00670608D2F68AFBC2 /* CircleShapeRenderer.swift */; }; - 941AA0A965F188AD2989B5B10CAEBB5D /* BubbleChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84585BA74D5FD5927560642A3D8484D /* BubbleChartDataSet.swift */; }; - 95826C07F14DFF13BAA349F49C84917F /* Platform+Graphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECF04D337A793DE5FFF6D492CE60D96 /* Platform+Graphics.swift */; }; - 968CA639D9CA2E9D8A41F1038F03C2AE /* Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E525EC535F76BFC47EF4F3E62BA3654 /* Description.swift */; }; - 97528183B1A64DF1AFB370063327C922 /* PieRadarChartViewBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46A27661B10DDD04786AD58706C55BC /* PieRadarChartViewBase.swift */; }; - 99288EBF5E5716EF2F27C0978E602446 /* AxisValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA175D42C8D8C40B7D7B4EBA4DFCB613 /* AxisValueFormatter.swift */; }; - 99F7945EE1671BCBB0ED04D6AC9361C4 /* Reductions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAB281721B2A421266EF830D312BC5A /* Reductions.swift */; }; - 9BCAF45D55CDD35A801F95CFE539AE56 /* Platform+Touch Handling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E2A8B52E2E9746619F1216001D0241C /* Platform+Touch Handling.swift */; }; - 9C6B381899CCB8A5784CAEA7FCF140F8 /* RadarChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDEC8B77727E37AE06D95B16E7B4E24 /* RadarChartDataSetProtocol.swift */; }; - 9E9C24BA6400B4A9823C82804D34BF76 /* ChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08518D8C01A8EA542E198BA722EA03C9 /* ChartDataEntry.swift */; }; - 9FA6B60B4529E4268565A183F5613DC6 /* Trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFAEA5F16C2629852715CED66DB5173B /* Trim.swift */; }; - 9FDE058C5DF72BAE2D498F2EEDD060CA /* CandleStickChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF9EB617791E412932FA1E750D934E36 /* CandleStickChartRenderer.swift */; }; - A09370996D959442F01BF83FAD70F978 /* ScatterChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B26B283F8811F230A0E0B4FDD225F42 /* ScatterChartView.swift */; }; - A18DBD8983244A378ED1D37297C8B2D1 /* IndexAxisValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0AC8618DB750D79CB08822A30BEEB0 /* IndexAxisValueFormatter.swift */; }; - A3BC53817CA91A9716FD1CE460F088A5 /* ChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D18390EFF0AD4A6E0E073DD78EC6A8A /* ChartDataSetProtocol.swift */; }; - A5A2B62222D6B52255157E45BBFC2D78 /* DefaultFillFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FF6EA327297107B81FF9FC08218F94 /* DefaultFillFormatter.swift */; }; - A90A272F4B1735E0644FD2464064E428 /* LineScatterCandleRadarChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AE70E6A38EFD6CC5B06E07C9B8C51DE /* LineScatterCandleRadarChartDataSetProtocol.swift */; }; - AC65E8EF9D49A6E8636511F3B7400AC5 /* Cycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7634384D32BB7CAD35606CF6E2A6DC01 /* Cycle.swift */; }; - AD6A9FC5127879A7389236329CB2B58A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; }; - ADA9E28A7FE4AFE7047AD93EFD890A6A /* ChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3533AA36E9AC21252154C8D96477D1DE /* ChartDataSet.swift */; }; - AE29D92CF287E95326A50122F8279EE8 /* Charts-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 64CDF688CF8AB9F834E7C349894B685F /* Charts-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - AE3DA54A9DB44D03DF6609CCE1E028F6 /* Pods-LoopFollow-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 28609CC9D3828C150821F3AA18752A77 /* Pods-LoopFollow-dummy.m */; }; - AED0A95039ECF3204009B8DE9DAE62CE /* FirstNonNil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24CB775967538B3FB4C47212A2F8B948 /* FirstNonNil.swift */; }; - B0A7C2F3CAB5C031F93D81777FD5A421 /* CandleChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F904F74C2B50A8192A7167CAC3C05E /* CandleChartDataProvider.swift */; }; - B2162A26B36709F6F1B6976DF1A65DAB /* DataRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40805D1503067A2E679DBFB30BBB8C5 /* DataRenderer.swift */; }; - B2DD7CF96035374C7AC80D88B8904691 /* ShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7614E1E025FCFFE4FAEA31D79ABD0661 /* ShapeRenderer.swift */; }; - B4015BE1313094DACA3DBB86191C5030 /* CombinedHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABCA6CDFA3950D0A999A692D99867D2 /* CombinedHighlighter.swift */; }; - B4D7DBBE6B8329E0947FFC9D1721B3CD /* YAxisRendererRadarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35FFE6EE2BE8E73855DC66951E7AA12 /* YAxisRendererRadarChart.swift */; }; - B6580C63B353D52672121417CD3E4DF9 /* LegendRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC42ADFADC10F4104EF63EB5AAA16E1 /* LegendRenderer.swift */; }; - B709B3F0FD7D89C78DE139C07924760B /* Highlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910C2C8431806F8015BAADD54CFF96DC /* Highlighter.swift */; }; - BDFE5C038BFAEDE5843A3BE7B78DAB9B /* AnimatedViewPortJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7554208627CE880C0922FFD835465C /* AnimatedViewPortJob.swift */; }; - BE85BF81DBD61E106AE6168F19DBBF0A /* CandleChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3F77E6983047F48C68D513994F9217 /* CandleChartData.swift */; }; - BEE4705FCD33AD7CCB2B1C8C593CE13D /* DataApproximator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C12EA28BEC17D2657CB75BFAEAC0C7A /* DataApproximator.swift */; }; - BF4F92B93CCB21881A32201D5283A39E /* BarChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7017CD37B02CFCAD0F28034D7D28355A /* BarChartRenderer.swift */; }; - C13FB2874A90207FF36E5D61F352A95B /* Stride.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A750E80653FD14B27F8579B9E00B94F /* Stride.swift */; }; - C2CBE67B4FDE638A3DEB5A444B075D28 /* ScatterChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0744A6F4A9EC646DAE57C8935BB5D5B /* ScatterChartData.swift */; }; - C3CF1A177501EA2D297FDABC1E531C77 /* ChartDataEntryBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F3AE2D43E5A05FCCE568A3DEC1D68F /* ChartDataEntryBase.swift */; }; - C472373DFA73FC416CCDB97CFA586249 /* ChartAnimationEasing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E81B8B1A7437FAF56106A8F686AF40F /* ChartAnimationEasing.swift */; }; - C66E82B3E5B66A711DD8F256171BE4B8 /* Rotate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD62CC8A6BE60A76674236FFF80DBAF /* Rotate.swift */; }; - C7E4A381095F15F90606D7C157BD9ABA /* ChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73DBDE6ADB4EFCB0BEC6D4BEBE993C55 /* ChartUtils.swift */; }; - CA589FE7EC72C5BD7743D8EADBDB914E /* BarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0055C3AB0AE0067E0AF50DA569843398 /* BarChartView.swift */; }; - CE4052508D5568AC510602198FDDF212 /* Combinations.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC11923FC3EEE39D1E9BB7D730ABD68B /* Combinations.swift */; }; - CF491A4A998823F62438EFF79A37028E /* ViewPortHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D54B32DD68706AD7AB243DE8165B8F /* ViewPortHandler.swift */; }; - CF53CF4998B07E3B054A32F34598D911 /* CombinedChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CFF79657B6FDE2B1BC8C2C101E4819 /* CombinedChartRenderer.swift */; }; - D41B06FABC2611E44E4C8E39FC415E19 /* ShareClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269AF5F8DD17D78A0DC4ADFF2FC6E161 /* ShareClient.swift */; }; - D44078A1F1CDB70D6EE87689EE274CAD /* AnimatedMoveViewJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550FC939A985EF1F598C47237FCEADD0 /* AnimatedMoveViewJob.swift */; }; - D806EE791E0C1722A40410F0084F63E6 /* BarChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026F25159638B0615F69DBE2054DD74D /* BarChartDataEntry.swift */; }; - DB13171585A147FFDE19FED165897AD2 /* PieChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 053D919370829DCF76BE4128B30656A0 /* PieChartDataSetProtocol.swift */; }; - DC9F92CD3F1388C597428F67A8D931B7 /* Partition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD660179AA5D052339B088934ECB658 /* Partition.swift */; }; - DD062811D95B55D3C25F0FDE3008F558 /* RadarHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948A283C104BDD9D9688A0E2D795698E /* RadarHighlighter.swift */; }; - DD9B9073ABC03421461C05BF1124562A /* LineScatterCandleRadarRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7647DE512322E9172332E77B1A4DEA /* LineScatterCandleRadarRenderer.swift */; }; - DE7D71CF6E97ABA1C18E4536B1403EF2 /* BarLineChartViewBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D2868AF0526000F8348B80A354EEF4B /* BarLineChartViewBase.swift */; }; - E01BD50071FE4EBA7ABB4E7CC1CAD71B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; }; - E03E65D99F5EB2C733237512E3EEEE45 /* ChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D7BD095AE41ABB08964C328CA70F842 /* ChartData.swift */; }; - E0505857120DFFE9D41FE45D55406372 /* ComponentBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B8C70494C7C7B66D6F1FB0A3EE70381 /* ComponentBase.swift */; }; - E0B8C1B3C25E624638B2C2FD105BAD35 /* YAxisRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8B684C281EB3348C147390CC832689 /* YAxisRenderer.swift */; }; - E0C8CFE39BA0D706F759E929033C0091 /* DefaultValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3473E42A5000F936DAB93D6009E6F9 /* DefaultValueFormatter.swift */; }; - E184578DB7A0DC4E3FE5C1AE680CB77C /* ShareClient-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = D41E1543E1C48B54FFD88D04046B84EB /* ShareClient-dummy.m */; }; - E2618C372D7762C1796CE24D6AAAFC37 /* Charts-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B141C9B60C3C9033C8984CDD8092AF4 /* Charts-dummy.m */; }; - E36428757F9C876042F5E93B2FF83F10 /* PieRadarHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0778A4002F3ADC7C1B16EC33774A0A /* PieRadarHighlighter.swift */; }; - E4465C942848B98723C8F3D642B92BC0 /* ScatterChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AED254D55D17BB8EA5337BC6262FC1 /* ScatterChartDataProvider.swift */; }; - E769F96699384D9B8E2E7D35D86438BC /* BarLineScatterCandleBubbleChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38871A0A7CA272870A24C2EEBC9F2B8 /* BarLineScatterCandleBubbleChartDataSetProtocol.swift */; }; - E887E039D78DBED7F592D9F4782FD30E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; }; - E91ABF4FF1105B2C7E0C812E8AF4B7EE /* PieChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F278BBD4B126E05895AFDAB808E64C68 /* PieChartRenderer.swift */; }; - E95EA2841D37DFBF3466B135E5230EE8 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF7405D232DFDC4008C8401DE983BCD /* Highlight.swift */; }; - E97DAF36AF1DDA6607CCE6FFEF2A070F /* EitherSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF672D34AEB39D95AD9315C751AA09E1 /* EitherSequence.swift */; }; - EAED453972AC1583C649AB1A2B1E02E4 /* Fill.swift in Sources */ = {isa = PBXBuildFile; fileRef = B15B2590757F14DE05C07A427600E68A /* Fill.swift */; }; - EBCA949CA3D0FC990102FA358E0E26C2 /* AdjacentPairs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F370D1C8ED64C66C14E6C1099B5E972 /* AdjacentPairs.swift */; }; - EF96959C3049DAF932F792D19608F437 /* BubbleChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21D1B75DCEF0D4F9B98A11E33C41EDB /* BubbleChartDataEntry.swift */; }; - EFE503C8BD962191136CFAB1255DFFE1 /* MarkerImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4AA57736F60FDBCF6AA4FA44E71682 /* MarkerImage.swift */; }; - F4FAC7F902EC4E73E672C6840BAD1ACE /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; }; - F8DDF36D6B4FF40D16F8D1F19DD11173 /* XAxis.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA19E07D8FCAFF8056DDCD4180BD7604 /* XAxis.swift */; }; - F8EF071EA2E3480E0895B783AA777028 /* RadarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0187F74F0F2E1ADE4F205C203F88DE85 /* RadarChartView.swift */; }; - F905753A7CEA0990F510062AA8ED20E3 /* BubbleChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EC4B9836E8A2EDD714B8642902033B4 /* BubbleChartRenderer.swift */; }; - FC1FAF373A9EA9E5B476E3A30C226C56 /* AxisRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7EBFD830ED3E0392581253C75C715F /* AxisRenderer.swift */; }; - FD8BFF36768B8F569E269ED06BD10557 /* Platform+Gestures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C552536D704E376A3AB7010EEA316A /* Platform+Gestures.swift */; }; - FEAE72583E6CFCD21D3AF7406D7C3A22 /* DataApproximator+N.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC49C8B5D37DFB82AA08BC6BC19AD0C5 /* DataApproximator+N.swift */; }; - FF363E21E3A10ADC15B24525D6E6F3E9 /* XShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58E677C70EAC3CA7313DCFEC5A60D81 /* XShapeRenderer.swift */; }; + 2404D4AA0A06D717C442BD2A63277F5B /* ShareClient-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 025981F765C3B190C90FAE60B0296AB3 /* ShareClient-dummy.m */; }; + 304E524CD5EF91E000F43C063DC673E2 /* Pods-LoopFollow-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 7E41188828C3ADE7A96C4DA6ED1CCDA2 /* Pods-LoopFollow-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6153631ED7F3CC355088BB931091F197 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */; }; + 743589B7D0B145D753F9F62A81073810 /* ShareClient-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4D698411D305DFBC8BDF991979BFFB /* ShareClient-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 78A24728955B868708A837528FF8053D /* ShareClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 04A3BB493071A19E2A23F519AA1EA2E1 /* ShareClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 99FD761A257F8E00652083D2A4232F42 /* ShareClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683D1D5360E174CD561D48DC413473BD /* ShareClient.swift */; }; + A34CE8210B1E5B85B91AE0A1E9B11FAE /* Pods-LoopFollow-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 28609CC9D3828C150821F3AA18752A77 /* Pods-LoopFollow-dummy.m */; }; + F8860626850172A84D421491AB326200 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 4412C8E3FC605429E853FF245804DDAE /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 2C511EE63C6E7D5843B971D1E441119E; - remoteInfo = SwiftAlgorithms; - }; - 823B9D4EEE275B39993140132B20E2DE /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 2C511EE63C6E7D5843B971D1E441119E; - remoteInfo = SwiftAlgorithms; - }; - 877AF7A15B9F6A61763C90BAEED8DEF7 /* PBXContainerItemProxy */ = { + E96156992EF2AFE5B9638B3B158C3D7B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 32507FDB9BAD6EF17DCB14A888ECA5D9; remoteInfo = ShareClient; }; - D1CEF5E80FC13999B22AC6EAF15E629A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = BF03B0B2B8B052092CB5F90CC5FB3757; - remoteInfo = Charts; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - BEEF11110000000000000001 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; - 0047677618FCD329E548FDDC0454A8AC /* LineChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineChartView.swift; path = Source/Charts/Charts/LineChartView.swift; sourceTree = ""; }; - 0055C3AB0AE0067E0AF50DA569843398 /* BarChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartView.swift; path = Source/Charts/Charts/BarChartView.swift; sourceTree = ""; }; - 0187F74F0F2E1ADE4F205C203F88DE85 /* RadarChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartView.swift; path = Source/Charts/Charts/RadarChartView.swift; sourceTree = ""; }; - 026F25159638B0615F69DBE2054DD74D /* BarChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/BarChartDataEntry.swift; sourceTree = ""; }; - 02BD1A15A0561B886A937317473FCF4D /* ScatterChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/ScatterChartDataSet.swift; sourceTree = ""; }; - 04CA046D8066D2043078135381049F0A /* ChartColorTemplates.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartColorTemplates.swift; path = Source/Charts/Utils/ChartColorTemplates.swift; sourceTree = ""; }; - 053D919370829DCF76BE4128B30656A0 /* PieChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/PieChartDataSetProtocol.swift; sourceTree = ""; }; - 0651C9C3E1F7212ACFC0DF4E3E00323D /* MoveViewJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MoveViewJob.swift; path = Source/Charts/Jobs/MoveViewJob.swift; sourceTree = ""; }; - 08518D8C01A8EA542E198BA722EA03C9 /* ChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/ChartDataEntry.swift; sourceTree = ""; }; - 09C552536D704E376A3AB7010EEA316A /* Platform+Gestures.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Platform+Gestures.swift"; path = "Source/Charts/Utils/Platform+Gestures.swift"; sourceTree = ""; }; - 0E12C56B79F0D3D4E449C70724B3B0D2 /* LineChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineChartData.swift; path = Source/Charts/Data/Implementations/Standard/LineChartData.swift; sourceTree = ""; }; - 10E2D718053926F7C362A0242C707398 /* ChevronDownShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChevronDownShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/ChevronDownShapeRenderer.swift; sourceTree = ""; }; - 127D620D9103C646821EE1A9FFDC6AB4 /* TransformerHorizontalBarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransformerHorizontalBarChart.swift; path = Source/Charts/Utils/TransformerHorizontalBarChart.swift; sourceTree = ""; }; + 025981F765C3B190C90FAE60B0296AB3 /* ShareClient-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "ShareClient-dummy.m"; sourceTree = ""; }; + 04A3BB493071A19E2A23F519AA1EA2E1 /* ShareClient.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ShareClient.h; path = ShareClient/ShareClient.h; sourceTree = ""; }; 1572E89B2070E8BDB7D3E2CBF51DE672 /* Pods-LoopFollow-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-LoopFollow-acknowledgements.plist"; sourceTree = ""; }; - 162C2F9A6894149851210E55E6188A43 /* Transformer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Transformer.swift; path = Source/Charts/Utils/Transformer.swift; sourceTree = ""; }; - 16B9EB6993E134E20A22EFB3F975FAAB /* CandleStickChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleStickChartView.swift; path = Source/Charts/Charts/CandleStickChartView.swift; sourceTree = ""; }; 16D6C652182636AC8246D4751AEC2B19 /* Pods-LoopFollow-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-LoopFollow-Info.plist"; sourceTree = ""; }; - 1714C5DC04BE72E509DC4E7686AA857F /* CandleChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/CandleChartDataEntry.swift; sourceTree = ""; }; - 185455CC1A16D809E6CD52CA7ED7C218 /* ShareClient-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ShareClient-umbrella.h"; sourceTree = ""; }; - 18F3AE2D43E5A05FCCE568A3DEC1D68F /* ChartDataEntryBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataEntryBase.swift; path = Source/Charts/Data/Implementations/Standard/ChartDataEntryBase.swift; sourceTree = ""; }; - 194A8936BCA6DD2C7CC0C0003C0F27BB /* DefaultAxisValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DefaultAxisValueFormatter.swift; path = Source/Charts/Formatters/DefaultAxisValueFormatter.swift; sourceTree = ""; }; 1955DA138EA7F33CEC4481481CBCD451 /* Pods-LoopFollow.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-LoopFollow.modulemap"; sourceTree = ""; }; - 1B0F987BD66CF8FC4B3368F8EB644D6A /* LineRadarRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineRadarRenderer.swift; path = Source/Charts/Renderers/LineRadarRenderer.swift; sourceTree = ""; }; - 1B141C9B60C3C9033C8984CDD8092AF4 /* Charts-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Charts-dummy.m"; sourceTree = ""; }; - 1D975F08944D56553EE8F518D883AABD /* ScatterChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartRenderer.swift; path = Source/Charts/Renderers/ScatterChartRenderer.swift; sourceTree = ""; }; - 1D9B0CD14B51E9BB24006EA87AD5758F /* BubbleChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartDataProvider.swift; path = Source/Charts/Interfaces/BubbleChartDataProvider.swift; sourceTree = ""; }; - 1EE2943C8EAB9AD013BB0D24A3E135AF /* ChartLimitLine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartLimitLine.swift; path = Source/Charts/Components/ChartLimitLine.swift; sourceTree = ""; }; - 1F1D0991883487B252954C6AD93262DD /* BubbleChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartData.swift; path = Source/Charts/Data/Implementations/Standard/BubbleChartData.swift; sourceTree = ""; }; - 22D8A4241B43E6A486B3ABFE7CE176D2 /* Platform.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Platform.swift; path = Source/Charts/Utils/Platform.swift; sourceTree = ""; }; - 24CB775967538B3FB4C47212A2F8B948 /* FirstNonNil.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FirstNonNil.swift; path = Sources/Algorithms/FirstNonNil.swift; sourceTree = ""; }; - 25F46CE8645206F6372CC648F2859C38 /* SwiftAlgorithms.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftAlgorithms.modulemap; sourceTree = ""; }; - 269AF5F8DD17D78A0DC4ADFF2FC6E161 /* ShareClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShareClient.swift; path = ShareClient/ShareClient.swift; sourceTree = ""; }; + 1A4D698411D305DFBC8BDF991979BFFB /* ShareClient-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ShareClient-umbrella.h"; sourceTree = ""; }; 28609CC9D3828C150821F3AA18752A77 /* Pods-LoopFollow-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-LoopFollow-dummy.m"; sourceTree = ""; }; - 29D95C88FFDCD29BA7600CB351EEFE0D /* Charts.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Charts.release.xcconfig; sourceTree = ""; }; - 2A750E80653FD14B27F8579B9E00B94F /* Stride.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Stride.swift; path = Sources/Algorithms/Stride.swift; sourceTree = ""; }; - 2B5D397F5631F7981765A1517F8E752B /* Joined.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Joined.swift; path = Sources/Algorithms/Joined.swift; sourceTree = ""; }; - 2C12EA28BEC17D2657CB75BFAEAC0C7A /* DataApproximator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DataApproximator.swift; path = Source/Charts/Filters/DataApproximator.swift; sourceTree = ""; }; - 2C320E80DD7BFAAA5494A8BB4D7A226F /* FlattenCollection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FlattenCollection.swift; path = Sources/Algorithms/FlattenCollection.swift; sourceTree = ""; }; - 2CDBEAB7607F1CDCCB6229DF611C0D3B /* BarLineScatterCandleBubbleChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineScatterCandleBubbleChartDataProvider.swift; path = Source/Charts/Interfaces/BarLineScatterCandleBubbleChartDataProvider.swift; sourceTree = ""; }; - 2D4695EDCBBD2F14A96C466589AC26B4 /* Permutations.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Permutations.swift; path = Sources/Algorithms/Permutations.swift; sourceTree = ""; }; - 2E2A8B52E2E9746619F1216001D0241C /* Platform+Touch Handling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Platform+Touch Handling.swift"; path = "Source/Charts/Utils/Platform+Touch Handling.swift"; sourceTree = ""; }; - 2E525EC535F76BFC47EF4F3E62BA3654 /* Description.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Description.swift; path = Source/Charts/Components/Description.swift; sourceTree = ""; }; - 2EF7405D232DFDC4008C8401DE983BCD /* Highlight.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Highlight.swift; path = Source/Charts/Highlight/Highlight.swift; sourceTree = ""; }; - 323A7094F3358B4E346E033FE9AAF79E /* RadarChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/RadarChartDataEntry.swift; sourceTree = ""; }; - 347B2727DAB3007AE54E938A6EDAC926 /* Marker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Marker.swift; path = Source/Charts/Components/Marker.swift; sourceTree = ""; }; - 3533AA36E9AC21252154C8D96477D1DE /* ChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/ChartDataSet.swift; sourceTree = ""; }; - 35C07C88A101976F7D43BE7BE4673A37 /* LineRadarChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineRadarChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/LineRadarChartDataSet.swift; sourceTree = ""; }; - 35CE8AE4FB4FAA915248FFA91AE62338 /* Split.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Split.swift; path = Sources/Algorithms/Split.swift; sourceTree = ""; }; - 35D58369D6A0FF961EC41F36B0B7D29C /* Range.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Range.swift; path = Source/Charts/Highlight/Range.swift; sourceTree = ""; }; - 372DF5E1B5F170A496FC14377B49E5BA /* ShareClient-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ShareClient-Info.plist"; sourceTree = ""; }; - 3897C4A6B7F162E479564409C5CEFB13 /* Intersperse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Intersperse.swift; path = Sources/Algorithms/Intersperse.swift; sourceTree = ""; }; - 3B26B283F8811F230A0E0B4FDD225F42 /* ScatterChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartView.swift; path = Source/Charts/Charts/ScatterChartView.swift; sourceTree = ""; }; - 3BD660179AA5D052339B088934ECB658 /* Partition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Partition.swift; path = Sources/Algorithms/Partition.swift; sourceTree = ""; }; - 3C3473E42A5000F936DAB93D6009E6F9 /* DefaultValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DefaultValueFormatter.swift; path = Source/Charts/Formatters/DefaultValueFormatter.swift; sourceTree = ""; }; - 3C5AFD1B5788F5477102C8DD0F7EAF50 /* BarLineScatterCandleBubbleRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineScatterCandleBubbleRenderer.swift; path = Source/Charts/Renderers/BarLineScatterCandleBubbleRenderer.swift; sourceTree = ""; }; - 3E81B8B1A7437FAF56106A8F686AF40F /* ChartAnimationEasing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartAnimationEasing.swift; path = Source/Charts/Animation/ChartAnimationEasing.swift; sourceTree = ""; }; - 3E98F5EAEA338E68783D29F0409AE596 /* Charts-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Charts-prefix.pch"; sourceTree = ""; }; - 3F8B684C281EB3348C147390CC832689 /* YAxisRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = YAxisRenderer.swift; path = Source/Charts/Renderers/YAxisRenderer.swift; sourceTree = ""; }; - 417ACD10C75E540CA5E23120521A8BA9 /* Compacted.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Compacted.swift; path = Sources/Algorithms/Compacted.swift; sourceTree = ""; }; - 421EDDDD69FD4DEF3F201DC7F4EDD885 /* FillFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FillFormatter.swift; path = Source/Charts/Formatters/FillFormatter.swift; sourceTree = ""; }; - 42CC27FD093CC4B3D0F27B928F7744AF /* ShareClient.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ShareClient.debug.xcconfig; sourceTree = ""; }; - 42EF3C460FF3BA09B964B4B078B2FFDC /* BarChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartDataProvider.swift; path = Source/Charts/Interfaces/BarChartDataProvider.swift; sourceTree = ""; }; - 44A177D8AC2CE9A04AEC62303E1C012C /* SwiftAlgorithms-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SwiftAlgorithms-dummy.m"; sourceTree = ""; }; - 44CEB1F80C1D008B41FFA0F59DCBBD9E /* SwiftAlgorithms.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftAlgorithms.release.xcconfig; sourceTree = ""; }; - 48FC9D5D68CA4D70155A8959C1179E28 /* XAxisRendererHorizontalBarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XAxisRendererHorizontalBarChart.swift; path = Source/Charts/Renderers/XAxisRendererHorizontalBarChart.swift; sourceTree = ""; }; - 49B4B217AE01ABE5E9A1B3FEE24ACD3E /* CombinedChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedChartView.swift; path = Source/Charts/Charts/CombinedChartView.swift; sourceTree = ""; }; - 4AE70E6A38EFD6CC5B06E07C9B8C51DE /* LineScatterCandleRadarChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineScatterCandleRadarChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/LineScatterCandleRadarChartDataSetProtocol.swift; sourceTree = ""; }; - 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4F370D1C8ED64C66C14E6C1099B5E972 /* AdjacentPairs.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AdjacentPairs.swift; path = Sources/Algorithms/AdjacentPairs.swift; sourceTree = ""; }; - 4F619C3ACAC8BB97F4CBF04C055CCB24 /* AnimatedZoomViewJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedZoomViewJob.swift; path = Source/Charts/Jobs/AnimatedZoomViewJob.swift; sourceTree = ""; }; - 4FDEC8B77727E37AE06D95B16E7B4E24 /* RadarChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/RadarChartDataSetProtocol.swift; sourceTree = ""; }; - 51D54B32DD68706AD7AB243DE8165B8F /* ViewPortHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ViewPortHandler.swift; path = Source/Charts/Utils/ViewPortHandler.swift; sourceTree = ""; }; - 524C2C5AB08DAC0B62469BC9BAD2BBFC /* LineScatterCandleRadarChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineScatterCandleRadarChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/LineScatterCandleRadarChartDataSet.swift; sourceTree = ""; }; - 52FC9F1F7847D290D9CD7AB6024341FF /* XAxisRendererRadarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XAxisRendererRadarChart.swift; path = Source/Charts/Renderers/XAxisRendererRadarChart.swift; sourceTree = ""; }; - 536232CFD052147F39AE8D2636847BB8 /* SwiftAlgorithms-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftAlgorithms-prefix.pch"; sourceTree = ""; }; - 550FC939A985EF1F598C47237FCEADD0 /* AnimatedMoveViewJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedMoveViewJob.swift; path = Source/Charts/Jobs/AnimatedMoveViewJob.swift; sourceTree = ""; }; - 55CFF79657B6FDE2B1BC8C2C101E4819 /* CombinedChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedChartRenderer.swift; path = Source/Charts/Renderers/CombinedChartRenderer.swift; sourceTree = ""; }; - 593C5B76ABCE8F5324D1A34783FACC5B /* Chunked.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Chunked.swift; path = Sources/Algorithms/Chunked.swift; sourceTree = ""; }; - 5B90B940BBEE17272D0FA9602CB3F1E1 /* BarLineScatterCandleBubbleChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineScatterCandleBubbleChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartDataSet.swift; sourceTree = ""; }; - 5C4AA57736F60FDBCF6AA4FA44E71682 /* MarkerImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MarkerImage.swift; path = Source/Charts/Components/MarkerImage.swift; sourceTree = ""; }; - 5CAB9310175D2D05534B5FAA40BDABB8 /* BarChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift; sourceTree = ""; }; - 5D152598E6F7AD2209244F3255EBE2B3 /* Windows.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Windows.swift; path = Sources/Algorithms/Windows.swift; sourceTree = ""; }; - 5E0778A4002F3ADC7C1B16EC33774A0A /* PieRadarHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieRadarHighlighter.swift; path = Source/Charts/Highlight/PieRadarHighlighter.swift; sourceTree = ""; }; - 60B7899F2AE639742DDEE13C16276275 /* ScatterChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/ScatterChartDataSetProtocol.swift; sourceTree = ""; }; - 6124C7D917D4C7DC28F7503EBCD596D7 /* Renderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Renderer.swift; path = Source/Charts/Renderers/Renderer.swift; sourceTree = ""; }; - 631260726B8649201DF3262C04AE423B /* RadarChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/RadarChartDataSet.swift; sourceTree = ""; }; - 640DB089E7BC7FA9C4C5D67D1E58393A /* BarChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartData.swift; path = Source/Charts/Data/Implementations/Standard/BarChartData.swift; sourceTree = ""; }; - 64CDF688CF8AB9F834E7C349894B685F /* Charts-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Charts-umbrella.h"; sourceTree = ""; }; - 6C90F7AE53155C1804331C13F95834B2 /* MarkerView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MarkerView.swift; path = Source/Charts/Components/MarkerView.swift; sourceTree = ""; }; - 6CC42ADFADC10F4104EF63EB5AAA16E1 /* LegendRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LegendRenderer.swift; path = Source/Charts/Renderers/LegendRenderer.swift; sourceTree = ""; }; - 6D4A498BBC8E4513D82F78F75036A171 /* LineRadarChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineRadarChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/LineRadarChartDataSetProtocol.swift; sourceTree = ""; }; - 6E1E5F4713322837D7506143802ED222 /* PieChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/PieChartDataSet.swift; sourceTree = ""; }; - 6E3651675689D3E922771781C1C7EBF4 /* ViewPortJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ViewPortJob.swift; path = Source/Charts/Jobs/ViewPortJob.swift; sourceTree = ""; }; - 6F86CBC5DB9A328A34A07A52C1CD5605 /* LineChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineChartDataProvider.swift; path = Source/Charts/Interfaces/LineChartDataProvider.swift; sourceTree = ""; }; - 6FC4C00D1C249E3DA3F61804AE87A559 /* Charts.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Charts.debug.xcconfig; sourceTree = ""; }; - 7017CD37B02CFCAD0F28034D7D28355A /* BarChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartRenderer.swift; path = Source/Charts/Renderers/BarChartRenderer.swift; sourceTree = ""; }; - 719A12B42A88A75148E0A9C8D6C618F0 /* LegendEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LegendEntry.swift; path = Source/Charts/Components/LegendEntry.swift; sourceTree = ""; }; - 7210F7399765A3931B62D5370EF1B4FC /* CombinedChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedChartData.swift; path = Source/Charts/Data/Implementations/Standard/CombinedChartData.swift; sourceTree = ""; }; - 73DBDE6ADB4EFCB0BEC6D4BEBE993C55 /* ChartUtils.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartUtils.swift; path = Source/Charts/Utils/ChartUtils.swift; sourceTree = ""; }; - 74FF6EA327297107B81FF9FC08218F94 /* DefaultFillFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DefaultFillFormatter.swift; path = Source/Charts/Formatters/DefaultFillFormatter.swift; sourceTree = ""; }; - 7614E1E025FCFFE4FAEA31D79ABD0661 /* ShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/ShapeRenderer.swift; sourceTree = ""; }; - 7634384D32BB7CAD35606CF6E2A6DC01 /* Cycle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Cycle.swift; path = Sources/Algorithms/Cycle.swift; sourceTree = ""; }; - 7724018046776D153BF2F519E62ED100 /* Platform+Accessibility.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Platform+Accessibility.swift"; path = "Source/Charts/Utils/Platform+Accessibility.swift"; sourceTree = ""; }; - 77C5C283029332E30F3E763EA8969893 /* BarChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift; sourceTree = ""; }; - 78F904F74C2B50A8192A7167CAC3C05E /* CandleChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartDataProvider.swift; path = Source/Charts/Interfaces/CandleChartDataProvider.swift; sourceTree = ""; }; - 791893088C1075827778DDAFED99EF63 /* LineChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineChartRenderer.swift; path = Source/Charts/Renderers/LineChartRenderer.swift; sourceTree = ""; }; - 7ABCA6CDFA3950D0A999A692D99867D2 /* CombinedHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedHighlighter.swift; path = Source/Charts/Highlight/CombinedHighlighter.swift; sourceTree = ""; }; - 7C7647DE512322E9172332E77B1A4DEA /* LineScatterCandleRadarRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineScatterCandleRadarRenderer.swift; path = Source/Charts/Renderers/LineScatterCandleRadarRenderer.swift; sourceTree = ""; }; + 2AD44B715D56685AE698AC4D99715DD8 /* ShareClient.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ShareClient.release.xcconfig; sourceTree = ""; }; + 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods-LoopFollow */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-LoopFollow"; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5F1AD1CF004DACD8176162896D94385A /* ShareClient-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ShareClient-prefix.pch"; sourceTree = ""; }; + 683D1D5360E174CD561D48DC413473BD /* ShareClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShareClient.swift; path = ShareClient/ShareClient.swift; sourceTree = ""; }; + 76B092CA2E9A874174A11B7C942095FA /* ShareClient.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ShareClient.debug.xcconfig; sourceTree = ""; }; + 7DA764FB38FE23DA9AF0F43546E05515 /* ShareClient.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = ShareClient.modulemap; sourceTree = ""; }; 7E41188828C3ADE7A96C4DA6ED1CCDA2 /* Pods-LoopFollow-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-LoopFollow-umbrella.h"; sourceTree = ""; }; - 7EC4B9836E8A2EDD714B8642902033B4 /* BubbleChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartRenderer.swift; path = Source/Charts/Renderers/BubbleChartRenderer.swift; sourceTree = ""; }; - 7EEDBD82A70398B3251ACA676701B65C /* LineChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/LineChartDataSet.swift; sourceTree = ""; }; - 80DDB7F7B5FBDC211213044A06C72601 /* Product.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Product.swift; path = Sources/Algorithms/Product.swift; sourceTree = ""; }; - 837358B5E2BC5C139A82049EB2895D05 /* XAxisRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XAxisRenderer.swift; path = Source/Charts/Renderers/XAxisRenderer.swift; sourceTree = ""; }; - 83DF5AE64A65703C1C26F23853110388 /* ShareClient-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ShareClient-prefix.pch"; sourceTree = ""; }; - 85FBB9A8A3775D305F8D23740840158D /* PieChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartData.swift; path = Source/Charts/Data/Implementations/Standard/PieChartData.swift; sourceTree = ""; }; - 896D7BB34CDE275E92DC01AC1F533F26 /* HorizontalBarHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HorizontalBarHighlighter.swift; path = Source/Charts/Highlight/HorizontalBarHighlighter.swift; sourceTree = ""; }; - 8A34EC75AF41BCC80FDC54C5AB3DC008 /* Chain.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Chain.swift; path = Sources/Algorithms/Chain.swift; sourceTree = ""; }; - 8B8C70494C7C7B66D6F1FB0A3EE70381 /* ComponentBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ComponentBase.swift; path = Source/Charts/Components/ComponentBase.swift; sourceTree = ""; }; - 8D2868AF0526000F8348B80A354EEF4B /* BarLineChartViewBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineChartViewBase.swift; path = Source/Charts/Charts/BarLineChartViewBase.swift; sourceTree = ""; }; - 8D7BD095AE41ABB08964C328CA70F842 /* ChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartData.swift; path = Source/Charts/Data/Implementations/Standard/ChartData.swift; sourceTree = ""; }; - 910799424A3C8A57366C4CE76BE79683 /* ChevronUpShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChevronUpShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/ChevronUpShapeRenderer.swift; sourceTree = ""; }; - 910C2C8431806F8015BAADD54CFF96DC /* Highlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Highlighter.swift; path = Source/Charts/Highlight/Highlighter.swift; sourceTree = ""; }; - 9222FB7877363AAC2E66F4E9BD6BE22F /* TriangleShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TriangleShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/TriangleShapeRenderer.swift; sourceTree = ""; }; - 939E4F680A1A71C7BFC00F9917F3E5EB /* MinMax.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MinMax.swift; path = Sources/Algorithms/MinMax.swift; sourceTree = ""; }; - 948A283C104BDD9D9688A0E2D795698E /* RadarHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarHighlighter.swift; path = Source/Charts/Highlight/RadarHighlighter.swift; sourceTree = ""; }; - 97C19D311E35002501E760D893F35E2F /* BubbleChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/BubbleChartDataSetProtocol.swift; sourceTree = ""; }; - 98BFFBB31523D438EF5F22D84F734782 /* Charts.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Charts.modulemap; sourceTree = ""; }; - 99770C7A6099A970C34D7824A7CE280D /* Unique.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Unique.swift; path = Sources/Algorithms/Unique.swift; sourceTree = ""; }; - 9B7EBBA8C350C40C7F423CDD638EDB2E /* SwiftAlgorithms-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftAlgorithms-umbrella.h"; sourceTree = ""; }; - 9C00C3496A26A3B8ED45E73F3E3AFDAC /* BarLineScatterCandleBubbleChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineScatterCandleBubbleChartData.swift; path = Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartData.swift; sourceTree = ""; }; - 9D18390EFF0AD4A6E0E073DD78EC6A8A /* ChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/ChartDataSetProtocol.swift; sourceTree = ""; }; - 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; - 9E41D7AE4F0AB44736E2E49B07F83AC6 /* CandleChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/CandleChartDataSetProtocol.swift; sourceTree = ""; }; - 9F21D683A3D7551D0D6ED48F9BCBDDCB /* BubbleChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartView.swift; path = Source/Charts/Charts/BubbleChartView.swift; sourceTree = ""; }; - 9F3F77E6983047F48C68D513994F9217 /* CandleChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartData.swift; path = Source/Charts/Data/Implementations/Standard/CandleChartData.swift; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; A0878FA8411033349498923D4630B956 /* Pods-LoopFollow-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-LoopFollow-frameworks.sh"; sourceTree = ""; }; - A1C1B977ED8804E8AEEC884E7359EE58 /* Charts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Charts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - A21D1B75DCEF0D4F9B98A11E33C41EDB /* BubbleChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/BubbleChartDataEntry.swift; sourceTree = ""; }; - A46A27661B10DDD04786AD58706C55BC /* PieRadarChartViewBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieRadarChartViewBase.swift; path = Source/Charts/Charts/PieRadarChartViewBase.swift; sourceTree = ""; }; - A5822CA2A05671BAA9BC5BD4A3BA5D6B /* ShareClient.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ShareClient.h; path = ShareClient/ShareClient.h; sourceTree = ""; }; - A7ACE4DAB3C5C84A671AFE0A24BC32E8 /* ValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ValueFormatter.swift; path = Source/Charts/Formatters/ValueFormatter.swift; sourceTree = ""; }; - A7FC29B40706B2526531F041CD6E5F37 /* ChartViewBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartViewBase.swift; path = Source/Charts/Charts/ChartViewBase.swift; sourceTree = ""; }; - AAAB281721B2A421266EF830D312BC5A /* Reductions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Reductions.swift; path = Sources/Algorithms/Reductions.swift; sourceTree = ""; }; - AB44185A42C56CCC7B7F24D8A4259044 /* BarHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarHighlighter.swift; path = Source/Charts/Highlight/BarHighlighter.swift; sourceTree = ""; }; - AB5301EE4DDA3C7B5DF0C20E0BC9B49E /* SwiftAlgorithms-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SwiftAlgorithms-Info.plist"; sourceTree = ""; }; - AE92153A023DA702752FCCCC960EC20B /* PieChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartView.swift; path = Source/Charts/Charts/PieChartView.swift; sourceTree = ""; }; - B15B2590757F14DE05C07A427600E68A /* Fill.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Fill.swift; path = Source/Charts/Utils/Fill.swift; sourceTree = ""; }; B1DF43F4B1524F385F6090756FA3016A /* Pods-LoopFollow.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-LoopFollow.debug.xcconfig"; sourceTree = ""; }; - B9A488FE97E31BCF19AE78B98C18EE81 /* ChartHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartHighlighter.swift; path = Source/Charts/Highlight/ChartHighlighter.swift; sourceTree = ""; }; - BC11923FC3EEE39D1E9BB7D730ABD68B /* Combinations.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Combinations.swift; path = Sources/Algorithms/Combinations.swift; sourceTree = ""; }; - BE6D57171DBF58C479650D72C1FE7CA4 /* Legend.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Legend.swift; path = Source/Charts/Components/Legend.swift; sourceTree = ""; }; - C2465E4C7B40A2D725278AEE6113E535 /* ShareClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ShareClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C51F17D0D2858BA593E783811A2CF7E6 /* RadarChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartData.swift; path = Source/Charts/Data/Implementations/Standard/RadarChartData.swift; sourceTree = ""; }; - C72D375E25E043A018C9D9E87EF64256 /* Suffix.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Suffix.swift; path = Sources/Algorithms/Suffix.swift; sourceTree = ""; }; - C8FE53DE5C6214CB9D501CF937A4A88A /* YAxis.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = YAxis.swift; path = Source/Charts/Components/YAxis.swift; sourceTree = ""; }; - C97D67CE26870DFAA8AFE096DDF8F6CF /* PieHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieHighlighter.swift; path = Source/Charts/Highlight/PieHighlighter.swift; sourceTree = ""; }; - C9A7AF5397F6039307E067E746BEA9DC /* ShareClient.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = ShareClient.modulemap; sourceTree = ""; }; - CC49C8B5D37DFB82AA08BC6BC19AD0C5 /* DataApproximator+N.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DataApproximator+N.swift"; path = "Source/Charts/Filters/DataApproximator+N.swift"; sourceTree = ""; }; - CE0ACEEB9003A6780541B487AD7AD20F /* Platform+Color.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Platform+Color.swift"; path = "Source/Charts/Utils/Platform+Color.swift"; sourceTree = ""; }; - CF0AC8618DB750D79CB08822A30BEEB0 /* IndexAxisValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IndexAxisValueFormatter.swift; path = Source/Charts/Formatters/IndexAxisValueFormatter.swift; sourceTree = ""; }; - CF58BE2BB556CCFFD3F153CD246BB3B2 /* CandleChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/CandleChartDataSet.swift; sourceTree = ""; }; - CFA8A221F0F4BA00670608D2F68AFBC2 /* CircleShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CircleShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/CircleShapeRenderer.swift; sourceTree = ""; }; - D024D84271BC3B8711840FD1825F8191 /* RadarChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartRenderer.swift; path = Source/Charts/Renderers/RadarChartRenderer.swift; sourceTree = ""; }; - D14145B7AD1FC1EC76D1DB43D3100AC2 /* ChartBaseDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartBaseDataSet.swift; path = Source/Charts/Data/Implementations/ChartBaseDataSet.swift; sourceTree = ""; }; - D245E0514AAC1A2B9A6D5EA2F383E90F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; - D2629C2C6B479EEB26E2F6DA58C30B81 /* Sequence+KeyPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Sequence+KeyPath.swift"; path = "Source/Charts/Utils/Sequence+KeyPath.swift"; sourceTree = ""; }; - D35FFE6EE2BE8E73855DC66951E7AA12 /* YAxisRendererRadarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = YAxisRendererRadarChart.swift; path = Source/Charts/Renderers/YAxisRendererRadarChart.swift; sourceTree = ""; }; - D41E1543E1C48B54FFD88D04046B84EB /* ShareClient-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "ShareClient-dummy.m"; sourceTree = ""; }; - D58E677C70EAC3CA7313DCFEC5A60D81 /* XShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/XShapeRenderer.swift; sourceTree = ""; }; - D8DF094DF5E5637883401F7A64517AB0 /* ShareClient.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ShareClient.release.xcconfig; sourceTree = ""; }; - D93D093D2D751BD6FA681010A1ECCCA5 /* SwiftAlgorithms.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftAlgorithms.debug.xcconfig; sourceTree = ""; }; - DA50DFBF9517D3E54497B473A622FFD5 /* Charts-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Charts-Info.plist"; sourceTree = ""; }; - DB10D8BF2F0C2E9B18C2130751D75984 /* Animator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Animator.swift; path = Source/Charts/Animation/Animator.swift; sourceTree = ""; }; - DB7554208627CE880C0922FFD835465C /* AnimatedViewPortJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedViewPortJob.swift; path = Source/Charts/Jobs/AnimatedViewPortJob.swift; sourceTree = ""; }; - DC882B9BE58FE2D2FCB58677AABEF941 /* HorizontalBarChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HorizontalBarChartRenderer.swift; path = Source/Charts/Renderers/HorizontalBarChartRenderer.swift; sourceTree = ""; }; - DF14DB72944084EE52D83024A8168D3A /* CombinedChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedChartDataProvider.swift; path = Source/Charts/Interfaces/CombinedChartDataProvider.swift; sourceTree = ""; }; - DF672D34AEB39D95AD9315C751AA09E1 /* EitherSequence.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EitherSequence.swift; path = Sources/Algorithms/EitherSequence.swift; sourceTree = ""; }; - DFD6267883E63FA87B3CF88C8773CCD6 /* ZoomViewJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ZoomViewJob.swift; path = Source/Charts/Jobs/ZoomViewJob.swift; sourceTree = ""; }; - E09B56CDA6CE4B961DE77CE43BF6517D /* CrossShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CrossShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/CrossShapeRenderer.swift; sourceTree = ""; }; - E18735DC81F8F3BB4967404CF8ED7E84 /* ChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataProvider.swift; path = Source/Charts/Interfaces/ChartDataProvider.swift; sourceTree = ""; }; - E38871A0A7CA272870A24C2EEBC9F2B8 /* BarLineScatterCandleBubbleChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineScatterCandleBubbleChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/BarLineScatterCandleBubbleChartDataSetProtocol.swift; sourceTree = ""; }; - E40805D1503067A2E679DBFB30BBB8C5 /* DataRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DataRenderer.swift; path = Source/Charts/Renderers/DataRenderer.swift; sourceTree = ""; }; - E8151C448D7FF1AE4C18A1D9BAAB84F5 /* SquareShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SquareShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/SquareShapeRenderer.swift; sourceTree = ""; }; - EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; - EB7EBFD830ED3E0392581253C75C715F /* AxisRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AxisRenderer.swift; path = Source/Charts/Renderers/AxisRenderer.swift; sourceTree = ""; }; + C2465E4C7B40A2D725278AEE6113E535 /* ShareClient */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = ShareClient; path = ShareClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D1F88334B08A60B9423B674EA56F9263 /* ShareClient-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ShareClient-Info.plist"; sourceTree = ""; }; EBE2A8C176CC9C76279BE195BDA90B7F /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-LoopFollow.release.xcconfig"; sourceTree = ""; }; - ECD62CC8A6BE60A76674236FFF80DBAF /* Rotate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Rotate.swift; path = Sources/Algorithms/Rotate.swift; sourceTree = ""; }; - EDCF1B7DBEAD927140D88A4876E56FDA /* LineChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/LineChartDataSetProtocol.swift; sourceTree = ""; }; - EECF04D337A793DE5FFF6D492CE60D96 /* Platform+Graphics.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Platform+Graphics.swift"; path = "Source/Charts/Utils/Platform+Graphics.swift"; sourceTree = ""; }; - EF9EB617791E412932FA1E750D934E36 /* CandleStickChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleStickChartRenderer.swift; path = Source/Charts/Renderers/CandleStickChartRenderer.swift; sourceTree = ""; }; - EFAEA5F16C2629852715CED66DB5173B /* Trim.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Trim.swift; path = Sources/Algorithms/Trim.swift; sourceTree = ""; }; - F0744A6F4A9EC646DAE57C8935BB5D5B /* ScatterChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartData.swift; path = Source/Charts/Data/Implementations/Standard/ScatterChartData.swift; sourceTree = ""; }; F0EFA056547237258760018B2FE0CAD6 /* Pods-LoopFollow-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-LoopFollow-acknowledgements.markdown"; sourceTree = ""; }; - F278BBD4B126E05895AFDAB808E64C68 /* PieChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartRenderer.swift; path = Source/Charts/Renderers/PieChartRenderer.swift; sourceTree = ""; }; - F34D137537BF079F35059435323D7452 /* HorizontalBarChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HorizontalBarChartView.swift; path = Source/Charts/Charts/HorizontalBarChartView.swift; sourceTree = ""; }; - F3AED254D55D17BB8EA5337BC6262FC1 /* ScatterChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartDataProvider.swift; path = Source/Charts/Interfaces/ScatterChartDataProvider.swift; sourceTree = ""; }; - F3E7458F7D38C89A9DE4BBACD3450C88 /* Algorithms.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Algorithms.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F448E25F6F3095E674026E503F71BED7 /* YAxisRendererHorizontalBarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = YAxisRendererHorizontalBarChart.swift; path = Source/Charts/Renderers/YAxisRendererHorizontalBarChart.swift; sourceTree = ""; }; - F574273E89B9C97C369C430B67186E14 /* Indexed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Indexed.swift; path = Sources/Algorithms/Indexed.swift; sourceTree = ""; }; - F84585BA74D5FD5927560642A3D8484D /* BubbleChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/BubbleChartDataSet.swift; sourceTree = ""; }; - FA175D42C8D8C40B7D7B4EBA4DFCB613 /* AxisValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AxisValueFormatter.swift; path = Source/Charts/Formatters/AxisValueFormatter.swift; sourceTree = ""; }; - FA19E07D8FCAFF8056DDCD4180BD7604 /* XAxis.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XAxis.swift; path = Source/Charts/Components/XAxis.swift; sourceTree = ""; }; - FAC79A108E546A80FD2E858996EB42D7 /* AxisBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AxisBase.swift; path = Source/Charts/Components/AxisBase.swift; sourceTree = ""; }; - FD9D1DB1BA530D126D35D218E4439458 /* PieChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/PieChartDataEntry.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 5DA9E0A9AAAF4649D65F510D01A75121 /* Frameworks */ = { + 673BE47176AEF55F3B61066228A54D3F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E01BD50071FE4EBA7ABB4E7CC1CAD71B /* Foundation.framework in Frameworks */, + 6153631ED7F3CC355088BB931091F197 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 63B312E7EB0E295CEDE5D015E60A331F /* Frameworks */ = { + 753CAC1164C57948BACD1FACC0A12CC8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F4FAC7F902EC4E73E672C6840BAD1ACE /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 929E1596790AD4B75CC107C87F3BC9C3 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - E887E039D78DBED7F592D9F4782FD30E /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - AE8F88ABDD468147C0B225493AD2873B /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - AD6A9FC5127879A7389236329CB2B58A /* Foundation.framework in Frameworks */, + F8860626850172A84D421491AB326200 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 13FDBA381EFB6D82130DECCB3D232C00 /* Pods */ = { + 2C5E4C0BB7867509F98EAE6A3071C017 /* Pods */ = { isa = PBXGroup; children = ( - E5EBEB4E3DD769CF77FBA0FB4DD5A204 /* Charts */, - 93B5F75356B6491998DC2064F7F3591F /* ShareClient */, - A03D44A5466EE402B13CCEE8C95D8E91 /* SwiftAlgorithms */, + 8C8F322FB3B61AA148DEBAFE8ED1EEE2 /* ShareClient */, ); name = Pods; sourceTree = ""; }; - 1628BF05B4CAFDCC3549A101F5A10A17 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 59DA5C1F72E1D5BABC43EACBA672C3BA /* iOS */, - ); - name = Frameworks; - sourceTree = ""; - }; - 2C61111B3BA91EE50CDF6C9086AE0897 /* Products */ = { - isa = PBXGroup; - children = ( - A1C1B977ED8804E8AEEC884E7359EE58 /* Charts.framework */, - 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods_LoopFollow.framework */, - C2465E4C7B40A2D725278AEE6113E535 /* ShareClient.framework */, - F3E7458F7D38C89A9DE4BBACD3450C88 /* Algorithms.framework */, - ); - name = Products; - sourceTree = ""; - }; 4F2B32159E3EA4B0DF32393843F9E975 /* Targets Support Files */ = { isa = PBXGroup; children = ( @@ -501,244 +88,58 @@ name = "Targets Support Files"; sourceTree = ""; }; - 59DA5C1F72E1D5BABC43EACBA672C3BA /* iOS */ = { - isa = PBXGroup; - children = ( - EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */, - D245E0514AAC1A2B9A6D5EA2F383E90F /* UIKit.framework */, - ); - name = iOS; - sourceTree = ""; - }; - 64FE9452040001E25B5A3EB8D58108DB /* Core */ = { - isa = PBXGroup; - children = ( - 550FC939A985EF1F598C47237FCEADD0 /* AnimatedMoveViewJob.swift */, - DB7554208627CE880C0922FFD835465C /* AnimatedViewPortJob.swift */, - 4F619C3ACAC8BB97F4CBF04C055CCB24 /* AnimatedZoomViewJob.swift */, - DB10D8BF2F0C2E9B18C2130751D75984 /* Animator.swift */, - FAC79A108E546A80FD2E858996EB42D7 /* AxisBase.swift */, - EB7EBFD830ED3E0392581253C75C715F /* AxisRenderer.swift */, - FA175D42C8D8C40B7D7B4EBA4DFCB613 /* AxisValueFormatter.swift */, - 640DB089E7BC7FA9C4C5D67D1E58393A /* BarChartData.swift */, - 026F25159638B0615F69DBE2054DD74D /* BarChartDataEntry.swift */, - 42EF3C460FF3BA09B964B4B078B2FFDC /* BarChartDataProvider.swift */, - 5CAB9310175D2D05534B5FAA40BDABB8 /* BarChartDataSet.swift */, - 77C5C283029332E30F3E763EA8969893 /* BarChartDataSetProtocol.swift */, - 7017CD37B02CFCAD0F28034D7D28355A /* BarChartRenderer.swift */, - 0055C3AB0AE0067E0AF50DA569843398 /* BarChartView.swift */, - AB44185A42C56CCC7B7F24D8A4259044 /* BarHighlighter.swift */, - 8D2868AF0526000F8348B80A354EEF4B /* BarLineChartViewBase.swift */, - 9C00C3496A26A3B8ED45E73F3E3AFDAC /* BarLineScatterCandleBubbleChartData.swift */, - 2CDBEAB7607F1CDCCB6229DF611C0D3B /* BarLineScatterCandleBubbleChartDataProvider.swift */, - 5B90B940BBEE17272D0FA9602CB3F1E1 /* BarLineScatterCandleBubbleChartDataSet.swift */, - E38871A0A7CA272870A24C2EEBC9F2B8 /* BarLineScatterCandleBubbleChartDataSetProtocol.swift */, - 3C5AFD1B5788F5477102C8DD0F7EAF50 /* BarLineScatterCandleBubbleRenderer.swift */, - 1F1D0991883487B252954C6AD93262DD /* BubbleChartData.swift */, - A21D1B75DCEF0D4F9B98A11E33C41EDB /* BubbleChartDataEntry.swift */, - 1D9B0CD14B51E9BB24006EA87AD5758F /* BubbleChartDataProvider.swift */, - F84585BA74D5FD5927560642A3D8484D /* BubbleChartDataSet.swift */, - 97C19D311E35002501E760D893F35E2F /* BubbleChartDataSetProtocol.swift */, - 7EC4B9836E8A2EDD714B8642902033B4 /* BubbleChartRenderer.swift */, - 9F21D683A3D7551D0D6ED48F9BCBDDCB /* BubbleChartView.swift */, - 9F3F77E6983047F48C68D513994F9217 /* CandleChartData.swift */, - 1714C5DC04BE72E509DC4E7686AA857F /* CandleChartDataEntry.swift */, - 78F904F74C2B50A8192A7167CAC3C05E /* CandleChartDataProvider.swift */, - CF58BE2BB556CCFFD3F153CD246BB3B2 /* CandleChartDataSet.swift */, - 9E41D7AE4F0AB44736E2E49B07F83AC6 /* CandleChartDataSetProtocol.swift */, - EF9EB617791E412932FA1E750D934E36 /* CandleStickChartRenderer.swift */, - 16B9EB6993E134E20A22EFB3F975FAAB /* CandleStickChartView.swift */, - 3E81B8B1A7437FAF56106A8F686AF40F /* ChartAnimationEasing.swift */, - D14145B7AD1FC1EC76D1DB43D3100AC2 /* ChartBaseDataSet.swift */, - 04CA046D8066D2043078135381049F0A /* ChartColorTemplates.swift */, - 8D7BD095AE41ABB08964C328CA70F842 /* ChartData.swift */, - 08518D8C01A8EA542E198BA722EA03C9 /* ChartDataEntry.swift */, - 18F3AE2D43E5A05FCCE568A3DEC1D68F /* ChartDataEntryBase.swift */, - E18735DC81F8F3BB4967404CF8ED7E84 /* ChartDataProvider.swift */, - 3533AA36E9AC21252154C8D96477D1DE /* ChartDataSet.swift */, - 9D18390EFF0AD4A6E0E073DD78EC6A8A /* ChartDataSetProtocol.swift */, - B9A488FE97E31BCF19AE78B98C18EE81 /* ChartHighlighter.swift */, - 1EE2943C8EAB9AD013BB0D24A3E135AF /* ChartLimitLine.swift */, - 73DBDE6ADB4EFCB0BEC6D4BEBE993C55 /* ChartUtils.swift */, - A7FC29B40706B2526531F041CD6E5F37 /* ChartViewBase.swift */, - 10E2D718053926F7C362A0242C707398 /* ChevronDownShapeRenderer.swift */, - 910799424A3C8A57366C4CE76BE79683 /* ChevronUpShapeRenderer.swift */, - CFA8A221F0F4BA00670608D2F68AFBC2 /* CircleShapeRenderer.swift */, - 7210F7399765A3931B62D5370EF1B4FC /* CombinedChartData.swift */, - DF14DB72944084EE52D83024A8168D3A /* CombinedChartDataProvider.swift */, - 55CFF79657B6FDE2B1BC8C2C101E4819 /* CombinedChartRenderer.swift */, - 49B4B217AE01ABE5E9A1B3FEE24ACD3E /* CombinedChartView.swift */, - 7ABCA6CDFA3950D0A999A692D99867D2 /* CombinedHighlighter.swift */, - 8B8C70494C7C7B66D6F1FB0A3EE70381 /* ComponentBase.swift */, - E09B56CDA6CE4B961DE77CE43BF6517D /* CrossShapeRenderer.swift */, - 2C12EA28BEC17D2657CB75BFAEAC0C7A /* DataApproximator.swift */, - CC49C8B5D37DFB82AA08BC6BC19AD0C5 /* DataApproximator+N.swift */, - E40805D1503067A2E679DBFB30BBB8C5 /* DataRenderer.swift */, - 194A8936BCA6DD2C7CC0C0003C0F27BB /* DefaultAxisValueFormatter.swift */, - 74FF6EA327297107B81FF9FC08218F94 /* DefaultFillFormatter.swift */, - 3C3473E42A5000F936DAB93D6009E6F9 /* DefaultValueFormatter.swift */, - 2E525EC535F76BFC47EF4F3E62BA3654 /* Description.swift */, - B15B2590757F14DE05C07A427600E68A /* Fill.swift */, - 421EDDDD69FD4DEF3F201DC7F4EDD885 /* FillFormatter.swift */, - 2EF7405D232DFDC4008C8401DE983BCD /* Highlight.swift */, - 910C2C8431806F8015BAADD54CFF96DC /* Highlighter.swift */, - DC882B9BE58FE2D2FCB58677AABEF941 /* HorizontalBarChartRenderer.swift */, - F34D137537BF079F35059435323D7452 /* HorizontalBarChartView.swift */, - 896D7BB34CDE275E92DC01AC1F533F26 /* HorizontalBarHighlighter.swift */, - CF0AC8618DB750D79CB08822A30BEEB0 /* IndexAxisValueFormatter.swift */, - BE6D57171DBF58C479650D72C1FE7CA4 /* Legend.swift */, - 719A12B42A88A75148E0A9C8D6C618F0 /* LegendEntry.swift */, - 6CC42ADFADC10F4104EF63EB5AAA16E1 /* LegendRenderer.swift */, - 0E12C56B79F0D3D4E449C70724B3B0D2 /* LineChartData.swift */, - 6F86CBC5DB9A328A34A07A52C1CD5605 /* LineChartDataProvider.swift */, - 7EEDBD82A70398B3251ACA676701B65C /* LineChartDataSet.swift */, - EDCF1B7DBEAD927140D88A4876E56FDA /* LineChartDataSetProtocol.swift */, - 791893088C1075827778DDAFED99EF63 /* LineChartRenderer.swift */, - 0047677618FCD329E548FDDC0454A8AC /* LineChartView.swift */, - 35C07C88A101976F7D43BE7BE4673A37 /* LineRadarChartDataSet.swift */, - 6D4A498BBC8E4513D82F78F75036A171 /* LineRadarChartDataSetProtocol.swift */, - 1B0F987BD66CF8FC4B3368F8EB644D6A /* LineRadarRenderer.swift */, - 524C2C5AB08DAC0B62469BC9BAD2BBFC /* LineScatterCandleRadarChartDataSet.swift */, - 4AE70E6A38EFD6CC5B06E07C9B8C51DE /* LineScatterCandleRadarChartDataSetProtocol.swift */, - 7C7647DE512322E9172332E77B1A4DEA /* LineScatterCandleRadarRenderer.swift */, - 347B2727DAB3007AE54E938A6EDAC926 /* Marker.swift */, - 5C4AA57736F60FDBCF6AA4FA44E71682 /* MarkerImage.swift */, - 6C90F7AE53155C1804331C13F95834B2 /* MarkerView.swift */, - 0651C9C3E1F7212ACFC0DF4E3E00323D /* MoveViewJob.swift */, - 85FBB9A8A3775D305F8D23740840158D /* PieChartData.swift */, - FD9D1DB1BA530D126D35D218E4439458 /* PieChartDataEntry.swift */, - 6E1E5F4713322837D7506143802ED222 /* PieChartDataSet.swift */, - 053D919370829DCF76BE4128B30656A0 /* PieChartDataSetProtocol.swift */, - F278BBD4B126E05895AFDAB808E64C68 /* PieChartRenderer.swift */, - AE92153A023DA702752FCCCC960EC20B /* PieChartView.swift */, - C97D67CE26870DFAA8AFE096DDF8F6CF /* PieHighlighter.swift */, - A46A27661B10DDD04786AD58706C55BC /* PieRadarChartViewBase.swift */, - 5E0778A4002F3ADC7C1B16EC33774A0A /* PieRadarHighlighter.swift */, - 22D8A4241B43E6A486B3ABFE7CE176D2 /* Platform.swift */, - 7724018046776D153BF2F519E62ED100 /* Platform+Accessibility.swift */, - CE0ACEEB9003A6780541B487AD7AD20F /* Platform+Color.swift */, - 09C552536D704E376A3AB7010EEA316A /* Platform+Gestures.swift */, - EECF04D337A793DE5FFF6D492CE60D96 /* Platform+Graphics.swift */, - 2E2A8B52E2E9746619F1216001D0241C /* Platform+Touch Handling.swift */, - C51F17D0D2858BA593E783811A2CF7E6 /* RadarChartData.swift */, - 323A7094F3358B4E346E033FE9AAF79E /* RadarChartDataEntry.swift */, - 631260726B8649201DF3262C04AE423B /* RadarChartDataSet.swift */, - 4FDEC8B77727E37AE06D95B16E7B4E24 /* RadarChartDataSetProtocol.swift */, - D024D84271BC3B8711840FD1825F8191 /* RadarChartRenderer.swift */, - 0187F74F0F2E1ADE4F205C203F88DE85 /* RadarChartView.swift */, - 948A283C104BDD9D9688A0E2D795698E /* RadarHighlighter.swift */, - 35D58369D6A0FF961EC41F36B0B7D29C /* Range.swift */, - 6124C7D917D4C7DC28F7503EBCD596D7 /* Renderer.swift */, - F0744A6F4A9EC646DAE57C8935BB5D5B /* ScatterChartData.swift */, - F3AED254D55D17BB8EA5337BC6262FC1 /* ScatterChartDataProvider.swift */, - 02BD1A15A0561B886A937317473FCF4D /* ScatterChartDataSet.swift */, - 60B7899F2AE639742DDEE13C16276275 /* ScatterChartDataSetProtocol.swift */, - 1D975F08944D56553EE8F518D883AABD /* ScatterChartRenderer.swift */, - 3B26B283F8811F230A0E0B4FDD225F42 /* ScatterChartView.swift */, - D2629C2C6B479EEB26E2F6DA58C30B81 /* Sequence+KeyPath.swift */, - 7614E1E025FCFFE4FAEA31D79ABD0661 /* ShapeRenderer.swift */, - E8151C448D7FF1AE4C18A1D9BAAB84F5 /* SquareShapeRenderer.swift */, - 162C2F9A6894149851210E55E6188A43 /* Transformer.swift */, - 127D620D9103C646821EE1A9FFDC6AB4 /* TransformerHorizontalBarChart.swift */, - 9222FB7877363AAC2E66F4E9BD6BE22F /* TriangleShapeRenderer.swift */, - A7ACE4DAB3C5C84A671AFE0A24BC32E8 /* ValueFormatter.swift */, - 51D54B32DD68706AD7AB243DE8165B8F /* ViewPortHandler.swift */, - 6E3651675689D3E922771781C1C7EBF4 /* ViewPortJob.swift */, - FA19E07D8FCAFF8056DDCD4180BD7604 /* XAxis.swift */, - 837358B5E2BC5C139A82049EB2895D05 /* XAxisRenderer.swift */, - 48FC9D5D68CA4D70155A8959C1179E28 /* XAxisRendererHorizontalBarChart.swift */, - 52FC9F1F7847D290D9CD7AB6024341FF /* XAxisRendererRadarChart.swift */, - D58E677C70EAC3CA7313DCFEC5A60D81 /* XShapeRenderer.swift */, - C8FE53DE5C6214CB9D501CF937A4A88A /* YAxis.swift */, - 3F8B684C281EB3348C147390CC832689 /* YAxisRenderer.swift */, - F448E25F6F3095E674026E503F71BED7 /* YAxisRendererHorizontalBarChart.swift */, - D35FFE6EE2BE8E73855DC66951E7AA12 /* YAxisRendererRadarChart.swift */, - DFD6267883E63FA87B3CF88C8773CCD6 /* ZoomViewJob.swift */, - ); - name = Core; - sourceTree = ""; - }; - 8D3A1BD14866C606F6AE6DA32BA8C1F4 /* Support Files */ = { + 5318506822951E7929AEEEE8A5454B09 /* Support Files */ = { isa = PBXGroup; children = ( - C9A7AF5397F6039307E067E746BEA9DC /* ShareClient.modulemap */, - D41E1543E1C48B54FFD88D04046B84EB /* ShareClient-dummy.m */, - 372DF5E1B5F170A496FC14377B49E5BA /* ShareClient-Info.plist */, - 83DF5AE64A65703C1C26F23853110388 /* ShareClient-prefix.pch */, - 185455CC1A16D809E6CD52CA7ED7C218 /* ShareClient-umbrella.h */, - 42CC27FD093CC4B3D0F27B928F7744AF /* ShareClient.debug.xcconfig */, - D8DF094DF5E5637883401F7A64517AB0 /* ShareClient.release.xcconfig */, + 7DA764FB38FE23DA9AF0F43546E05515 /* ShareClient.modulemap */, + 025981F765C3B190C90FAE60B0296AB3 /* ShareClient-dummy.m */, + D1F88334B08A60B9423B674EA56F9263 /* ShareClient-Info.plist */, + 5F1AD1CF004DACD8176162896D94385A /* ShareClient-prefix.pch */, + 1A4D698411D305DFBC8BDF991979BFFB /* ShareClient-umbrella.h */, + 76B092CA2E9A874174A11B7C942095FA /* ShareClient.debug.xcconfig */, + 2AD44B715D56685AE698AC4D99715DD8 /* ShareClient.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/ShareClient"; sourceTree = ""; }; - 93B5F75356B6491998DC2064F7F3591F /* ShareClient */ = { + 8C8F322FB3B61AA148DEBAFE8ED1EEE2 /* ShareClient */ = { isa = PBXGroup; children = ( - A5822CA2A05671BAA9BC5BD4A3BA5D6B /* ShareClient.h */, - 269AF5F8DD17D78A0DC4ADFF2FC6E161 /* ShareClient.swift */, - 8D3A1BD14866C606F6AE6DA32BA8C1F4 /* Support Files */, + 04A3BB493071A19E2A23F519AA1EA2E1 /* ShareClient.h */, + 683D1D5360E174CD561D48DC413473BD /* ShareClient.swift */, + 5318506822951E7929AEEEE8A5454B09 /* Support Files */, ); + name = ShareClient; path = ShareClient; sourceTree = ""; }; - A03D44A5466EE402B13CCEE8C95D8E91 /* SwiftAlgorithms */ = { + 98D6B91193F59BDEF0E1E4891A3ADD42 /* Products */ = { isa = PBXGroup; children = ( - 4F370D1C8ED64C66C14E6C1099B5E972 /* AdjacentPairs.swift */, - 8A34EC75AF41BCC80FDC54C5AB3DC008 /* Chain.swift */, - 593C5B76ABCE8F5324D1A34783FACC5B /* Chunked.swift */, - BC11923FC3EEE39D1E9BB7D730ABD68B /* Combinations.swift */, - 417ACD10C75E540CA5E23120521A8BA9 /* Compacted.swift */, - 7634384D32BB7CAD35606CF6E2A6DC01 /* Cycle.swift */, - DF672D34AEB39D95AD9315C751AA09E1 /* EitherSequence.swift */, - 24CB775967538B3FB4C47212A2F8B948 /* FirstNonNil.swift */, - 2C320E80DD7BFAAA5494A8BB4D7A226F /* FlattenCollection.swift */, - F574273E89B9C97C369C430B67186E14 /* Indexed.swift */, - 3897C4A6B7F162E479564409C5CEFB13 /* Intersperse.swift */, - 2B5D397F5631F7981765A1517F8E752B /* Joined.swift */, - 939E4F680A1A71C7BFC00F9917F3E5EB /* MinMax.swift */, - 3BD660179AA5D052339B088934ECB658 /* Partition.swift */, - 2D4695EDCBBD2F14A96C466589AC26B4 /* Permutations.swift */, - 80DDB7F7B5FBDC211213044A06C72601 /* Product.swift */, - AAAB281721B2A421266EF830D312BC5A /* Reductions.swift */, - ECD62CC8A6BE60A76674236FFF80DBAF /* Rotate.swift */, - 35CE8AE4FB4FAA915248FFA91AE62338 /* Split.swift */, - 2A750E80653FD14B27F8579B9E00B94F /* Stride.swift */, - C72D375E25E043A018C9D9E87EF64256 /* Suffix.swift */, - EFAEA5F16C2629852715CED66DB5173B /* Trim.swift */, - 99770C7A6099A970C34D7824A7CE280D /* Unique.swift */, - 5D152598E6F7AD2209244F3255EBE2B3 /* Windows.swift */, - ED8149DD2EC8C7BA0180268D4B9A8034 /* Support Files */, + 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods-LoopFollow */, + C2465E4C7B40A2D725278AEE6113E535 /* ShareClient */, ); - path = SwiftAlgorithms; + name = Products; sourceTree = ""; }; - AEDEF3B82F3D135001FB2D1778DAD53F /* Support Files */ = { + CF1408CF629C7361332E53B88F7BD30C = { isa = PBXGroup; children = ( - 98BFFBB31523D438EF5F22D84F734782 /* Charts.modulemap */, - 1B141C9B60C3C9033C8984CDD8092AF4 /* Charts-dummy.m */, - DA50DFBF9517D3E54497B473A622FFD5 /* Charts-Info.plist */, - 3E98F5EAEA338E68783D29F0409AE596 /* Charts-prefix.pch */, - 64CDF688CF8AB9F834E7C349894B685F /* Charts-umbrella.h */, - 6FC4C00D1C249E3DA3F61804AE87A559 /* Charts.debug.xcconfig */, - 29D95C88FFDCD29BA7600CB351EEFE0D /* Charts.release.xcconfig */, + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */, + 2C5E4C0BB7867509F98EAE6A3071C017 /* Pods */, + 98D6B91193F59BDEF0E1E4891A3ADD42 /* Products */, + 4F2B32159E3EA4B0DF32393843F9E975 /* Targets Support Files */, ); - name = "Support Files"; - path = "../Target Support Files/Charts"; sourceTree = ""; }; - CF1408CF629C7361332E53B88F7BD30C = { + D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */ = { isa = PBXGroup; children = ( - 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, - 1628BF05B4CAFDCC3549A101F5A10A17 /* Frameworks */, - 13FDBA381EFB6D82130DECCB3D232C00 /* Pods */, - 2C61111B3BA91EE50CDF6C9086AE0897 /* Products */, - 4F2B32159E3EA4B0DF32393843F9E975 /* Targets Support Files */, + E4801F62A6B08CD9B5410329F1A18FDE /* iOS */, ); + name = Frameworks; sourceTree = ""; }; DB261ABD9474868F278295D2D489B4C2 /* Pods-LoopFollow */ = { @@ -758,96 +159,45 @@ path = "Target Support Files/Pods-LoopFollow"; sourceTree = ""; }; - E5EBEB4E3DD769CF77FBA0FB4DD5A204 /* Charts */ = { + E4801F62A6B08CD9B5410329F1A18FDE /* iOS */ = { isa = PBXGroup; children = ( - BEEF11110000000000000001 /* PrivacyInfo.xcprivacy */, - 64FE9452040001E25B5A3EB8D58108DB /* Core */, - AEDEF3B82F3D135001FB2D1778DAD53F /* Support Files */, + 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */, ); - path = Charts; - sourceTree = ""; - }; - ED8149DD2EC8C7BA0180268D4B9A8034 /* Support Files */ = { - isa = PBXGroup; - children = ( - 25F46CE8645206F6372CC648F2859C38 /* SwiftAlgorithms.modulemap */, - 44A177D8AC2CE9A04AEC62303E1C012C /* SwiftAlgorithms-dummy.m */, - AB5301EE4DDA3C7B5DF0C20E0BC9B49E /* SwiftAlgorithms-Info.plist */, - 536232CFD052147F39AE8D2636847BB8 /* SwiftAlgorithms-prefix.pch */, - 9B7EBBA8C350C40C7F423CDD638EDB2E /* SwiftAlgorithms-umbrella.h */, - D93D093D2D751BD6FA681010A1ECCCA5 /* SwiftAlgorithms.debug.xcconfig */, - 44CEB1F80C1D008B41FFA0F59DCBBD9E /* SwiftAlgorithms.release.xcconfig */, - ); - name = "Support Files"; - path = "../Target Support Files/SwiftAlgorithms"; + name = iOS; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - 2D480C09AACD90D4C214D8CB1D26CDC8 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 701A1DF09A74742FFCF8BCE3F5A9370B /* SwiftAlgorithms-umbrella.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7212DF84AE5A3275F30ABCEC4859A1DD /* Headers */ = { + 1294F686ADA0FE5AA7DDEB2A8852C673 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 782E096BE9488B722DA0F64794A45D40 /* ShareClient.h in Headers */, - 5851F6B6C46E7426DA8693AD1781EDB5 /* ShareClient-umbrella.h in Headers */, + 78A24728955B868708A837528FF8053D /* ShareClient.h in Headers */, + 743589B7D0B145D753F9F62A81073810 /* ShareClient-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; - 98A4DF28820DA7EDA8DDFE7EF89A84B3 /* Headers */ = { + 5780D821A4E0FEFDDB2F996A27FE5F60 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 0D73D4FBCA95E51CDA036332FE5031B4 /* Pods-LoopFollow-umbrella.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EDE61DD0FA4F27F177FC2851874BD2A3 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - AE29D92CF287E95326A50122F8279EE8 /* Charts-umbrella.h in Headers */, + 304E524CD5EF91E000F43C063DC673E2 /* Pods-LoopFollow-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 2C511EE63C6E7D5843B971D1E441119E /* SwiftAlgorithms */ = { - isa = PBXNativeTarget; - buildConfigurationList = 4B03C504CCECECDB4BC19C5CE5CB746B /* Build configuration list for PBXNativeTarget "SwiftAlgorithms" */; - buildPhases = ( - 2D480C09AACD90D4C214D8CB1D26CDC8 /* Headers */, - D6D8EC531B387AE324F4FBC6AFF93EAC /* Sources */, - 63B312E7EB0E295CEDE5D015E60A331F /* Frameworks */, - 78477CEC13E706CAE27BD3259C55EF31 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = SwiftAlgorithms; - productName = Algorithms; - productReference = F3E7458F7D38C89A9DE4BBACD3450C88 /* Algorithms.framework */; - productType = "com.apple.product-type.framework"; - }; 32507FDB9BAD6EF17DCB14A888ECA5D9 /* ShareClient */ = { isa = PBXNativeTarget; - buildConfigurationList = FA6C888F2DD1E3B58603566F7FAD248D /* Build configuration list for PBXNativeTarget "ShareClient" */; + buildConfigurationList = B85A618EE6D68DB19889DCB3C59D4B71 /* Build configuration list for PBXNativeTarget "ShareClient" */; buildPhases = ( - 7212DF84AE5A3275F30ABCEC4859A1DD /* Headers */, - DB630AA1C1D9C8F7E7126B015749FBE6 /* Sources */, - 5DA9E0A9AAAF4649D65F510D01A75121 /* Frameworks */, - F0985DF3FCB396C10277C61C2746A59B /* Resources */, + 1294F686ADA0FE5AA7DDEB2A8852C673 /* Headers */, + DA86D0FC7206BF9CB717300B82230775 /* Sources */, + 753CAC1164C57948BACD1FACC0A12CC8 /* Frameworks */, + 669185A762F46898DDCE3641BEE297A3 /* Resources */, ); buildRules = ( ); @@ -855,47 +205,26 @@ ); name = ShareClient; productName = ShareClient; - productReference = C2465E4C7B40A2D725278AEE6113E535 /* ShareClient.framework */; - productType = "com.apple.product-type.framework"; - }; - BF03B0B2B8B052092CB5F90CC5FB3757 /* Charts */ = { - isa = PBXNativeTarget; - buildConfigurationList = 3C9F1E8CC2EB7C944989DFC3E98E9033 /* Build configuration list for PBXNativeTarget "Charts" */; - buildPhases = ( - EDE61DD0FA4F27F177FC2851874BD2A3 /* Headers */, - 04E5DB91AA99A863A53C9D8295F20335 /* Sources */, - AE8F88ABDD468147C0B225493AD2873B /* Frameworks */, - A3AF8C643337EC60F250280E4F422D92 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 779EEA19ADDCE2714CD833FC88BF26F5 /* PBXTargetDependency */, - ); - name = Charts; - productName = Charts; - productReference = A1C1B977ED8804E8AEEC884E7359EE58 /* Charts.framework */; + productReference = C2465E4C7B40A2D725278AEE6113E535 /* ShareClient */; productType = "com.apple.product-type.framework"; }; E187D4B9C2B76F773546AF1E39D2ECCD /* Pods-LoopFollow */ = { isa = PBXNativeTarget; - buildConfigurationList = 15BFD27C2C74E85837FC247C1D72BFB6 /* Build configuration list for PBXNativeTarget "Pods-LoopFollow" */; + buildConfigurationList = 66228F6317D7B419D1205F36DEF3C90B /* Build configuration list for PBXNativeTarget "Pods-LoopFollow" */; buildPhases = ( - 98A4DF28820DA7EDA8DDFE7EF89A84B3 /* Headers */, - EFD0EAFAE9F87A44489E1BD0CD6AF091 /* Sources */, - 929E1596790AD4B75CC107C87F3BC9C3 /* Frameworks */, - 5D2354A3923872623EFEC988B407F8A7 /* Resources */, + 5780D821A4E0FEFDDB2F996A27FE5F60 /* Headers */, + EAADB6E6E9DD820F69093D7D5D829687 /* Sources */, + 673BE47176AEF55F3B61066228A54D3F /* Frameworks */, + 814D0A70D77BB645EC6138B8588CB7EC /* Resources */, ); buildRules = ( ); dependencies = ( - BD234F929A36F384DAB2F1A302F4F777 /* PBXTargetDependency */, - 72D00FFECA0A202EFBCFBD5DC6BDFB6C /* PBXTargetDependency */, - FBBCCF26094223773400B746C8E1F6DB /* PBXTargetDependency */, + 931B88622248F2227294A564C988F4C6 /* PBXTargetDependency */, ); name = "Pods-LoopFollow"; productName = Pods_LoopFollow; - productReference = 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods_LoopFollow.framework */; + productReference = 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods-LoopFollow */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ @@ -904,11 +233,11 @@ BFDFE7DC352907FC980B868725387E98 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1300; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; }; buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; - compatibilityVersion = "Xcode 10.0"; + compatibilityVersion = "Xcode 16.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -916,42 +245,27 @@ en, ); mainGroup = CF1408CF629C7361332E53B88F7BD30C; - productRefGroup = 2C61111B3BA91EE50CDF6C9086AE0897 /* Products */; + minimizedProjectReferenceProxies = 0; + preferredProjectObjectVersion = 77; + productRefGroup = 98D6B91193F59BDEF0E1E4891A3ADD42 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - BF03B0B2B8B052092CB5F90CC5FB3757 /* Charts */, E187D4B9C2B76F773546AF1E39D2ECCD /* Pods-LoopFollow */, 32507FDB9BAD6EF17DCB14A888ECA5D9 /* ShareClient */, - 2C511EE63C6E7D5843B971D1E441119E /* SwiftAlgorithms */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 5D2354A3923872623EFEC988B407F8A7 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 78477CEC13E706CAE27BD3259C55EF31 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A3AF8C643337EC60F250280E4F422D92 /* Resources */ = { + 669185A762F46898DDCE3641BEE297A3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - BEEF11110000000000000002 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - F0985DF3FCB396C10277C61C2746A59B /* Resources */ = { + 814D0A70D77BB645EC6138B8588CB7EC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -961,238 +275,40 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 04E5DB91AA99A863A53C9D8295F20335 /* Sources */ = { + DA86D0FC7206BF9CB717300B82230775 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D44078A1F1CDB70D6EE87689EE274CAD /* AnimatedMoveViewJob.swift in Sources */, - BDFE5C038BFAEDE5843A3BE7B78DAB9B /* AnimatedViewPortJob.swift in Sources */, - 28C0C336867276BB3F88C94BDCF5924F /* AnimatedZoomViewJob.swift in Sources */, - 0031CEAD857A1BA158C0D2BE1870A438 /* Animator.swift in Sources */, - 26414DCD243931AD2DF5A7FAEFA9A2F1 /* AxisBase.swift in Sources */, - FC1FAF373A9EA9E5B476E3A30C226C56 /* AxisRenderer.swift in Sources */, - 99288EBF5E5716EF2F27C0978E602446 /* AxisValueFormatter.swift in Sources */, - 6138784D2228EEEEC197C541EDB9282C /* BarChartData.swift in Sources */, - D806EE791E0C1722A40410F0084F63E6 /* BarChartDataEntry.swift in Sources */, - 4E0CE7C1CD75F9926C1453AC1AFCFBF7 /* BarChartDataProvider.swift in Sources */, - 1251E7AD01C341EFCB0A7A4B70942433 /* BarChartDataSet.swift in Sources */, - 19F40EB97D44872C2F155EB91889EEDD /* BarChartDataSetProtocol.swift in Sources */, - BF4F92B93CCB21881A32201D5283A39E /* BarChartRenderer.swift in Sources */, - CA589FE7EC72C5BD7743D8EADBDB914E /* BarChartView.swift in Sources */, - 68A3CE924047F6FF8D7BDDE68BCBD4C1 /* BarHighlighter.swift in Sources */, - DE7D71CF6E97ABA1C18E4536B1403EF2 /* BarLineChartViewBase.swift in Sources */, - 0BB393E009EB565C1342EEA8D02C9098 /* BarLineScatterCandleBubbleChartData.swift in Sources */, - 4CDE5AD6B91F8E1859C196089A01F927 /* BarLineScatterCandleBubbleChartDataProvider.swift in Sources */, - 00E171A24B8B214FDC447BED6BB62C31 /* BarLineScatterCandleBubbleChartDataSet.swift in Sources */, - E769F96699384D9B8E2E7D35D86438BC /* BarLineScatterCandleBubbleChartDataSetProtocol.swift in Sources */, - 75E493B239F29100777CBB38AAAE08D7 /* BarLineScatterCandleBubbleRenderer.swift in Sources */, - 0382A6D93C8BB1218EB1DBA3CF1B3BA7 /* BubbleChartData.swift in Sources */, - EF96959C3049DAF932F792D19608F437 /* BubbleChartDataEntry.swift in Sources */, - 1267184A54805C22C30908BFA58279C9 /* BubbleChartDataProvider.swift in Sources */, - 941AA0A965F188AD2989B5B10CAEBB5D /* BubbleChartDataSet.swift in Sources */, - 341C4C56C45E00A6B58E63A7EDF7F7C1 /* BubbleChartDataSetProtocol.swift in Sources */, - F905753A7CEA0990F510062AA8ED20E3 /* BubbleChartRenderer.swift in Sources */, - 3CCE8A616A323BDE56FA02CAB953BA18 /* BubbleChartView.swift in Sources */, - BE85BF81DBD61E106AE6168F19DBBF0A /* CandleChartData.swift in Sources */, - 8FDCA72F349897774711AA8AB1F23522 /* CandleChartDataEntry.swift in Sources */, - B0A7C2F3CAB5C031F93D81777FD5A421 /* CandleChartDataProvider.swift in Sources */, - 04360A4DF646DD99E7AF0AC83FAA56AB /* CandleChartDataSet.swift in Sources */, - 64B59451EB2C31914C278348F911C715 /* CandleChartDataSetProtocol.swift in Sources */, - 9FDE058C5DF72BAE2D498F2EEDD060CA /* CandleStickChartRenderer.swift in Sources */, - 60F208C320EA137DF1E35F58D848DE9A /* CandleStickChartView.swift in Sources */, - C472373DFA73FC416CCDB97CFA586249 /* ChartAnimationEasing.swift in Sources */, - 646FE6A25A724BC685702BFB101E021E /* ChartBaseDataSet.swift in Sources */, - 58889ECB9711CC8D285F5689C9DF4584 /* ChartColorTemplates.swift in Sources */, - E03E65D99F5EB2C733237512E3EEEE45 /* ChartData.swift in Sources */, - 9E9C24BA6400B4A9823C82804D34BF76 /* ChartDataEntry.swift in Sources */, - C3CF1A177501EA2D297FDABC1E531C77 /* ChartDataEntryBase.swift in Sources */, - 6130B294DE88ECF9F4706D1895CC6279 /* ChartDataProvider.swift in Sources */, - ADA9E28A7FE4AFE7047AD93EFD890A6A /* ChartDataSet.swift in Sources */, - A3BC53817CA91A9716FD1CE460F088A5 /* ChartDataSetProtocol.swift in Sources */, - 6B89D2717D3A324DF55655D69A21AAAC /* ChartHighlighter.swift in Sources */, - 37C035EBB523276E74097ACE8B88D7DE /* ChartLimitLine.swift in Sources */, - E2618C372D7762C1796CE24D6AAAFC37 /* Charts-dummy.m in Sources */, - C7E4A381095F15F90606D7C157BD9ABA /* ChartUtils.swift in Sources */, - 69CED83D06805776035CF76DD1DE2C2A /* ChartViewBase.swift in Sources */, - 3C863D4E5E34C7C2173E0B557157A469 /* ChevronDownShapeRenderer.swift in Sources */, - 7C09D13338B7A0BF4A0EB9763EE60339 /* ChevronUpShapeRenderer.swift in Sources */, - 93E19EA5B507AAA18B0961FD52FDD452 /* CircleShapeRenderer.swift in Sources */, - 2D7A2EBC2FC440678E3F1D63D375EA31 /* CombinedChartData.swift in Sources */, - 4C3E4C1A74F42DC91C726DC9BD262891 /* CombinedChartDataProvider.swift in Sources */, - CF53CF4998B07E3B054A32F34598D911 /* CombinedChartRenderer.swift in Sources */, - 42361A518637B458CBB971D21E43346E /* CombinedChartView.swift in Sources */, - B4015BE1313094DACA3DBB86191C5030 /* CombinedHighlighter.swift in Sources */, - E0505857120DFFE9D41FE45D55406372 /* ComponentBase.swift in Sources */, - 22725F307C8A12F85C2E583CE9D67A68 /* CrossShapeRenderer.swift in Sources */, - BEE4705FCD33AD7CCB2B1C8C593CE13D /* DataApproximator.swift in Sources */, - FEAE72583E6CFCD21D3AF7406D7C3A22 /* DataApproximator+N.swift in Sources */, - B2162A26B36709F6F1B6976DF1A65DAB /* DataRenderer.swift in Sources */, - 6B9186D7227163690B8397FAADFF77D3 /* DefaultAxisValueFormatter.swift in Sources */, - A5A2B62222D6B52255157E45BBFC2D78 /* DefaultFillFormatter.swift in Sources */, - E0C8CFE39BA0D706F759E929033C0091 /* DefaultValueFormatter.swift in Sources */, - 968CA639D9CA2E9D8A41F1038F03C2AE /* Description.swift in Sources */, - EAED453972AC1583C649AB1A2B1E02E4 /* Fill.swift in Sources */, - 28699DA45BB94636CDD55D302CC36D4C /* FillFormatter.swift in Sources */, - E95EA2841D37DFBF3466B135E5230EE8 /* Highlight.swift in Sources */, - B709B3F0FD7D89C78DE139C07924760B /* Highlighter.swift in Sources */, - 3F7DE7DD33C07E28ABBF80B3AFBCDF44 /* HorizontalBarChartRenderer.swift in Sources */, - 5751FF8F73931CF52C588FC93DF87069 /* HorizontalBarChartView.swift in Sources */, - 35BFC4C3A46CF7EEC15BEA91AC6CE39B /* HorizontalBarHighlighter.swift in Sources */, - A18DBD8983244A378ED1D37297C8B2D1 /* IndexAxisValueFormatter.swift in Sources */, - 64E13AF9E7842CDAD6F7E9F205082C09 /* Legend.swift in Sources */, - 1611A02A50E8ECD5F31ECF1CDDCCFF7F /* LegendEntry.swift in Sources */, - B6580C63B353D52672121417CD3E4DF9 /* LegendRenderer.swift in Sources */, - 8FD826D1119F87EB638FAC888F74BE51 /* LineChartData.swift in Sources */, - 3BCB9E6E546029CDD29933889ECF313D /* LineChartDataProvider.swift in Sources */, - 67AF0FFA8C4DD3594D2D4F7F0EB13D6F /* LineChartDataSet.swift in Sources */, - 64E36FF14DDE083130EAD49EB027C6F4 /* LineChartDataSetProtocol.swift in Sources */, - 92F85BA954CAD476F1964EFD067DF138 /* LineChartRenderer.swift in Sources */, - 4C7DA896256A295E7DEADFCA8B0136EE /* LineChartView.swift in Sources */, - 094130854F03255D5BD94EAE3C6797B4 /* LineRadarChartDataSet.swift in Sources */, - 0CA67293F83A1EB1004D16D1878B4007 /* LineRadarChartDataSetProtocol.swift in Sources */, - 4412B151ED4921518E19FA644D282D99 /* LineRadarRenderer.swift in Sources */, - 16362413D2756A8A8BD2E5D20726520D /* LineScatterCandleRadarChartDataSet.swift in Sources */, - A90A272F4B1735E0644FD2464064E428 /* LineScatterCandleRadarChartDataSetProtocol.swift in Sources */, - DD9B9073ABC03421461C05BF1124562A /* LineScatterCandleRadarRenderer.swift in Sources */, - 6351CC48D4CD08A64B93027110D0BFD7 /* Marker.swift in Sources */, - EFE503C8BD962191136CFAB1255DFFE1 /* MarkerImage.swift in Sources */, - 62425D090FD054F868558C00A83BBCF6 /* MarkerView.swift in Sources */, - 1C7423F964AEF53EA37E1B4F2F4C8A3D /* MoveViewJob.swift in Sources */, - 5D998F2454D188B00E592ECF3DFB6371 /* PieChartData.swift in Sources */, - 16C5A86B21B4BF6C530A96DC25C9ECDB /* PieChartDataEntry.swift in Sources */, - 46D629F718073D7735C888E42E1EAB17 /* PieChartDataSet.swift in Sources */, - DB13171585A147FFDE19FED165897AD2 /* PieChartDataSetProtocol.swift in Sources */, - E91ABF4FF1105B2C7E0C812E8AF4B7EE /* PieChartRenderer.swift in Sources */, - 150DB6950349AEAEBDB84A1994DF55DE /* PieChartView.swift in Sources */, - 3C1FAA2B78B8750635AC8D2B088703BA /* PieHighlighter.swift in Sources */, - 97528183B1A64DF1AFB370063327C922 /* PieRadarChartViewBase.swift in Sources */, - E36428757F9C876042F5E93B2FF83F10 /* PieRadarHighlighter.swift in Sources */, - 56C71B82E08666FBEC2E57178FF7EB18 /* Platform.swift in Sources */, - 494DAE02C6BD413F95D9B35E4C9B8473 /* Platform+Accessibility.swift in Sources */, - 290F4D27B047ECE9E11FE65D45659AA9 /* Platform+Color.swift in Sources */, - FD8BFF36768B8F569E269ED06BD10557 /* Platform+Gestures.swift in Sources */, - 95826C07F14DFF13BAA349F49C84917F /* Platform+Graphics.swift in Sources */, - 9BCAF45D55CDD35A801F95CFE539AE56 /* Platform+Touch Handling.swift in Sources */, - 00351B69906B07C1950B97C40F65FABE /* RadarChartData.swift in Sources */, - 585FD9DA0B21341F777A5B39ED1CA562 /* RadarChartDataEntry.swift in Sources */, - 078FF70DB7D0BDBE768AB0FA72E19D14 /* RadarChartDataSet.swift in Sources */, - 9C6B381899CCB8A5784CAEA7FCF140F8 /* RadarChartDataSetProtocol.swift in Sources */, - 6291997C9089F0F4FF4B0172331B31AC /* RadarChartRenderer.swift in Sources */, - F8EF071EA2E3480E0895B783AA777028 /* RadarChartView.swift in Sources */, - DD062811D95B55D3C25F0FDE3008F558 /* RadarHighlighter.swift in Sources */, - 70CACB146AA2603CDD775285FA9A4CE9 /* Range.swift in Sources */, - 3641D2A955EB369FD725C07BAF82E96F /* Renderer.swift in Sources */, - C2CBE67B4FDE638A3DEB5A444B075D28 /* ScatterChartData.swift in Sources */, - E4465C942848B98723C8F3D642B92BC0 /* ScatterChartDataProvider.swift in Sources */, - 839F873A001FE9169E9978BE89195B5F /* ScatterChartDataSet.swift in Sources */, - 6D878FE6964089A7F47EB406F566ECC0 /* ScatterChartDataSetProtocol.swift in Sources */, - 4304139B7049F735A4879E7A14DCA236 /* ScatterChartRenderer.swift in Sources */, - A09370996D959442F01BF83FAD70F978 /* ScatterChartView.swift in Sources */, - 7899A9D1751880EC7A2649DF35617F67 /* Sequence+KeyPath.swift in Sources */, - B2DD7CF96035374C7AC80D88B8904691 /* ShapeRenderer.swift in Sources */, - 5FFB0442423AFC0BFC70DA91859C1FA7 /* SquareShapeRenderer.swift in Sources */, - 8809E0E77C38BC2164874FB080404B2C /* Transformer.swift in Sources */, - 50711078539C633D63AEE4D4F4B0642D /* TransformerHorizontalBarChart.swift in Sources */, - 68047C5BD5B0C77274FAB5CD38F2EF0E /* TriangleShapeRenderer.swift in Sources */, - 280B9154E146F8A47E47F484ACC815F0 /* ValueFormatter.swift in Sources */, - CF491A4A998823F62438EFF79A37028E /* ViewPortHandler.swift in Sources */, - 5829E60F3B1D9D7166301BC4AA17E83D /* ViewPortJob.swift in Sources */, - F8DDF36D6B4FF40D16F8D1F19DD11173 /* XAxis.swift in Sources */, - 80E88D848625ADD942D49552233940C3 /* XAxisRenderer.swift in Sources */, - 7A52005D206FA5BEC462EC1ED5E34423 /* XAxisRendererHorizontalBarChart.swift in Sources */, - 25D7B9CB6D83A2899E364531C8B465E0 /* XAxisRendererRadarChart.swift in Sources */, - FF363E21E3A10ADC15B24525D6E6F3E9 /* XShapeRenderer.swift in Sources */, - 92C7BAE4CC1EA95C35F3CE1D2DFB463B /* YAxis.swift in Sources */, - E0B8C1B3C25E624638B2C2FD105BAD35 /* YAxisRenderer.swift in Sources */, - 295E7D31962EAA43CE02A22D291AC827 /* YAxisRendererHorizontalBarChart.swift in Sources */, - B4D7DBBE6B8329E0947FFC9D1721B3CD /* YAxisRendererRadarChart.swift in Sources */, - 88DD483D2937E149E31768EAB7567563 /* ZoomViewJob.swift in Sources */, + 99FD761A257F8E00652083D2A4232F42 /* ShareClient.swift in Sources */, + 2404D4AA0A06D717C442BD2A63277F5B /* ShareClient-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - D6D8EC531B387AE324F4FBC6AFF93EAC /* Sources */ = { + EAADB6E6E9DD820F69093D7D5D829687 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EBCA949CA3D0FC990102FA358E0E26C2 /* AdjacentPairs.swift in Sources */, - 1CFCB7624A2D0A7B088428BB898E452E /* Chain.swift in Sources */, - 1C903C670AE552000F47F9FF69946132 /* Chunked.swift in Sources */, - CE4052508D5568AC510602198FDDF212 /* Combinations.swift in Sources */, - 13CC355E8703534C3651E1676F54989C /* Compacted.swift in Sources */, - AC65E8EF9D49A6E8636511F3B7400AC5 /* Cycle.swift in Sources */, - E97DAF36AF1DDA6607CCE6FFEF2A070F /* EitherSequence.swift in Sources */, - AED0A95039ECF3204009B8DE9DAE62CE /* FirstNonNil.swift in Sources */, - 905D5792F0F95A1D50E0FA79AFF58278 /* FlattenCollection.swift in Sources */, - 18F6D2659567747929F4F67444C6DB5A /* Indexed.swift in Sources */, - 1956BBD2D0434340107A6BE2B68FE4FE /* Intersperse.swift in Sources */, - 49F28A30CAC0F0F5E874185195C05EAA /* Joined.swift in Sources */, - 7B8211FE68633C9F9EF8C2729FACE32A /* MinMax.swift in Sources */, - DC9F92CD3F1388C597428F67A8D931B7 /* Partition.swift in Sources */, - 30F6164C5F15445D6BC5D8B22A4131BE /* Permutations.swift in Sources */, - 8BF58ABE3315B02170CA950504A6AF7D /* Product.swift in Sources */, - 99F7945EE1671BCBB0ED04D6AC9361C4 /* Reductions.swift in Sources */, - C66E82B3E5B66A711DD8F256171BE4B8 /* Rotate.swift in Sources */, - 5E6D0EA6A7250B44F8D1416E97248FC2 /* Split.swift in Sources */, - C13FB2874A90207FF36E5D61F352A95B /* Stride.swift in Sources */, - 195360D36D20DEA54AC170FB9C9ECB44 /* Suffix.swift in Sources */, - 3BFFA0E6A588A61617609032FF7D48DF /* SwiftAlgorithms-dummy.m in Sources */, - 9FA6B60B4529E4268565A183F5613DC6 /* Trim.swift in Sources */, - 6E65B4450EE038F9F1DF3F7443BF53DB /* Unique.swift in Sources */, - 6916BF3166BE703933C43515B50ADBD7 /* Windows.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DB630AA1C1D9C8F7E7126B015749FBE6 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D41B06FABC2611E44E4C8E39FC415E19 /* ShareClient.swift in Sources */, - E184578DB7A0DC4E3FE5C1AE680CB77C /* ShareClient-dummy.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EFD0EAFAE9F87A44489E1BD0CD6AF091 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - AE3DA54A9DB44D03DF6609CCE1E028F6 /* Pods-LoopFollow-dummy.m in Sources */, + A34CE8210B1E5B85B91AE0A1E9B11FAE /* Pods-LoopFollow-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 72D00FFECA0A202EFBCFBD5DC6BDFB6C /* PBXTargetDependency */ = { + 931B88622248F2227294A564C988F4C6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = ShareClient; target = 32507FDB9BAD6EF17DCB14A888ECA5D9 /* ShareClient */; - targetProxy = 877AF7A15B9F6A61763C90BAEED8DEF7 /* PBXContainerItemProxy */; - }; - 779EEA19ADDCE2714CD833FC88BF26F5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = SwiftAlgorithms; - target = 2C511EE63C6E7D5843B971D1E441119E /* SwiftAlgorithms */; - targetProxy = 823B9D4EEE275B39993140132B20E2DE /* PBXContainerItemProxy */; - }; - BD234F929A36F384DAB2F1A302F4F777 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = Charts; - target = BF03B0B2B8B052092CB5F90CC5FB3757 /* Charts */; - targetProxy = D1CEF5E80FC13999B22AC6EAF15E629A /* PBXContainerItemProxy */; - }; - FBBCCF26094223773400B746C8E1F6DB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = SwiftAlgorithms; - target = 2C511EE63C6E7D5843B971D1E441119E /* SwiftAlgorithms */; - targetProxy = 4412C8E3FC605429E853FF245804DDAE /* PBXContainerItemProxy */; + targetProxy = E96156992EF2AFE5B9638B3B158C3D7B /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 0A509661AD0E795D94DDD5B4E3E25F3C /* Debug */ = { + 0707AA4C74BC735D8DE827545E5EF7A8 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = B1DF43F4B1524F385F6090756FA3016A /* Pods-LoopFollow.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; @@ -1202,9 +318,11 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1225,9 +343,9 @@ }; name = Debug; }; - 49F09D4213E786A2DE8C5FF61F769BB2 /* Release */ = { + 1A6BD89BF8BB37AAD583FA2F348BEAED /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D8DF094DF5E5637883401F7A64517AB0 /* ShareClient.release.xcconfig */; + baseConfigurationReference = 76B092CA2E9A874174A11B7C942095FA /* ShareClient.debug.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; @@ -1238,10 +356,13 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/ShareClient/ShareClient-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/ShareClient/ShareClient-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1253,17 +374,17 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Release; + name = Debug; }; - 53F93D21706D3CE12180BACB09FE6C87 /* Debug */ = { + 6A01356D1A71102A6257F16B7627C474 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D93D093D2D751BD6FA681010A1ECCCA5 /* SwiftAlgorithms.debug.xcconfig */; + baseConfigurationReference = 2AD44B715D56685AE698AC4D99715DD8 /* ShareClient.release.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; @@ -1274,29 +395,34 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/SwiftAlgorithms/SwiftAlgorithms-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/SwiftAlgorithms/SwiftAlgorithms-Info.plist"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/ShareClient/ShareClient-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/ShareClient/ShareClient-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "Target Support Files/SwiftAlgorithms/SwiftAlgorithms.modulemap"; - PRODUCT_MODULE_NAME = Algorithms; - PRODUCT_NAME = Algorithms; + MODULEMAP_FILE = "Target Support Files/ShareClient/ShareClient.modulemap"; + PRODUCT_MODULE_NAME = ShareClient; + PRODUCT_NAME = ShareClient; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 5; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Debug; + name = Release; }; - 963BD7925C123C698115AE45CD264E7F /* Release */ = { + 6A5C026ED62BBFE3380CD257EEFAEB20 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -1331,13 +457,16 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; + DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( - "POD_CONFIGURATION_RELEASE=1", + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -1346,22 +475,24 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; - MTL_ENABLE_DEBUG_INFO = NO; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; STRIP_INSTALLED_PRODUCT = NO; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; SYMROOT = "${SRCROOT}/../build"; }; - name = Release; + name = Debug; }; - 9CAC30E0BBD2D1426D0FB1E094C891E9 /* Release */ = { + AB501C1A4478BF4DEB01455902BBB1D8 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = EBE2A8C176CC9C76279BE195BDA90B7F /* Pods-LoopFollow.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; @@ -1371,9 +502,11 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1395,77 +528,7 @@ }; name = Release; }; - D1AADB344ABE211C7A14EADDE0C68908 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 42CC27FD093CC4B3D0F27B928F7744AF /* ShareClient.debug.xcconfig */; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/ShareClient/ShareClient-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/ShareClient/ShareClient-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/ShareClient/ShareClient.modulemap"; - PRODUCT_MODULE_NAME = ShareClient; - PRODUCT_NAME = ShareClient; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - E06F8FA0F4D85544D6C2044D15E26DF0 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6FC4C00D1C249E3DA3F61804AE87A559 /* Charts.debug.xcconfig */; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/Charts/Charts-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/Charts/Charts-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/Charts/Charts.modulemap"; - PRODUCT_MODULE_NAME = Charts; - PRODUCT_NAME = Charts; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 5.7; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - E8256E1B894D64F9B6D6E0F27C2CABC5 /* Debug */ = { + E873CA97BD124B21839AF13C9BE1CD18 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -1500,16 +563,13 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( - "POD_CONFIGURATION_DEBUG=1", - "DEBUG=1", + "POD_CONFIGURATION_RELEASE=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -1518,135 +578,44 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; STRIP_INSTALLED_PRODUCT = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; SYMROOT = "${SRCROOT}/../build"; }; - name = Debug; - }; - E9240666D51F5612F61294DA734C2DEE /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 44CEB1F80C1D008B41FFA0F59DCBBD9E /* SwiftAlgorithms.release.xcconfig */; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/SwiftAlgorithms/SwiftAlgorithms-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/SwiftAlgorithms/SwiftAlgorithms-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/SwiftAlgorithms/SwiftAlgorithms.modulemap"; - PRODUCT_MODULE_NAME = Algorithms; - PRODUCT_NAME = Algorithms; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 5; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - F824425E585E9F1E198419CD61EA8E6B /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 29D95C88FFDCD29BA7600CB351EEFE0D /* Charts.release.xcconfig */; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/Charts/Charts-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/Charts/Charts-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/Charts/Charts.modulemap"; - PRODUCT_MODULE_NAME = Charts; - PRODUCT_NAME = Charts; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 5.7; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 15BFD27C2C74E85837FC247C1D72BFB6 /* Build configuration list for PBXNativeTarget "Pods-LoopFollow" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 0A509661AD0E795D94DDD5B4E3E25F3C /* Debug */, - 9CAC30E0BBD2D1426D0FB1E094C891E9 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 3C9F1E8CC2EB7C944989DFC3E98E9033 /* Build configuration list for PBXNativeTarget "Charts" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - E06F8FA0F4D85544D6C2044D15E26DF0 /* Debug */, - F824425E585E9F1E198419CD61EA8E6B /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { isa = XCConfigurationList; buildConfigurations = ( - E8256E1B894D64F9B6D6E0F27C2CABC5 /* Debug */, - 963BD7925C123C698115AE45CD264E7F /* Release */, + 6A5C026ED62BBFE3380CD257EEFAEB20 /* Debug */, + E873CA97BD124B21839AF13C9BE1CD18 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 4B03C504CCECECDB4BC19C5CE5CB746B /* Build configuration list for PBXNativeTarget "SwiftAlgorithms" */ = { + 66228F6317D7B419D1205F36DEF3C90B /* Build configuration list for PBXNativeTarget "Pods-LoopFollow" */ = { isa = XCConfigurationList; buildConfigurations = ( - 53F93D21706D3CE12180BACB09FE6C87 /* Debug */, - E9240666D51F5612F61294DA734C2DEE /* Release */, + 0707AA4C74BC735D8DE827545E5EF7A8 /* Debug */, + AB501C1A4478BF4DEB01455902BBB1D8 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - FA6C888F2DD1E3B58603566F7FAD248D /* Build configuration list for PBXNativeTarget "ShareClient" */ = { + B85A618EE6D68DB19889DCB3C59D4B71 /* Build configuration list for PBXNativeTarget "ShareClient" */ = { isa = XCConfigurationList; buildConfigurations = ( - D1AADB344ABE211C7A14EADDE0C68908 /* Debug */, - 49F09D4213E786A2DE8C5FF61F769BB2 /* Release */, + 1A6BD89BF8BB37AAD583FA2F348BEAED /* Debug */, + 6A01356D1A71102A6257F16B7627C474 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Pods/SwiftAlgorithms/LICENSE.txt b/Pods/SwiftAlgorithms/LICENSE.txt deleted file mode 100644 index 61b0c7819..000000000 --- a/Pods/SwiftAlgorithms/LICENSE.txt +++ /dev/null @@ -1,211 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - -## Runtime Library Exception to the Apache 2.0 License: ## - - - As an exception, if you use this Software to compile your source code and - portions of this Software are embedded into the binary product as a result, - you may redistribute such product without providing attribution as would - otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. diff --git a/Pods/SwiftAlgorithms/README.md b/Pods/SwiftAlgorithms/README.md deleted file mode 100644 index ce07182d3..000000000 --- a/Pods/SwiftAlgorithms/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Swift Algorithms - -**Swift Algorithms** is an open-source package of sequence and collection algorithms, along with their related types. - -Read more about the package, and the intent behind it, in the [announcement on swift.org](https://swift.org/blog/swift-algorithms/). - -## Contents - -#### Combinations / permutations - -- [`combinations(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Combinations.md): Combinations of particular sizes of the elements in a collection. -- [`permutations(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Permutations.md): Permutations of a particular size of the elements in a collection, or of the full collection. -- [`uniquePermutations(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Permutations.md): Permutations of a collection's elements, skipping any duplicate permutations. - -#### Mutating algorithms - -- [`rotate(toStartAt:)`, `rotate(subrange:toStartAt:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Rotate.md): In-place rotation of elements. -- [`stablePartition(by:)`, `stablePartition(subrange:by:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Partition.md): A partition that preserves the relative order of the resulting prefix and suffix. - -#### Combining collections - -- [`chain(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Chain.md): Concatenates two collections with the same element type. -- [`cycled()`, `cycled(times:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Cycle.md): Repeats the elements of a collection forever or a set number of times. -- [`joined(by:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Joined.md): Concatenate sequences of sequences, using an element or sequence as a separator, or using a closure to generate each separator. -- [`product(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Product.md): Iterates over all the pairs of two collections; equivalent to nested `for`-`in` loops. - -#### Subsetting operations - -- [`compacted()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md): Drops the `nil`s from a sequence or collection, unwrapping the remaining elements. -- [`randomSample(count:)`, `randomSample(count:using:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/RandomSampling.md): Randomly selects a specific number of elements from a collection. -- [`randomStableSample(count:)`, `randomStableSample(count:using:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/RandomSampling.md): Randomly selects a specific number of elements from a collection, preserving their original relative order. -- [`striding(by:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Stride.md): Returns every nth element of a collection. -- [`suffix(while:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Suffix.md): Returns the suffix of a collection where all element pass a given predicate. -- [`trimmingPrefix(while:)`, `trimmingSuffix(while)`, `trimming(while:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Trim.md): Returns a slice by trimming elements from a collection's start, end, or both. The mutating `trim...` methods trim a collection in place. -- [`uniqued()`, `uniqued(on:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Unique.md): The unique elements of a collection, preserving their order. -- [`minAndMax()`, `minAndMax(by:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/MinMax.md): Returns the smallest and largest elements of a sequence. - -#### Partial sorting - -- [`min(count:)`, `max(count:)`, `min(count:sortedBy:)`, `max(count:sortedBy:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/MinMax.md): Returns the smallest or largest elements of a collection, sorted by a predicate. - -#### Other useful operations - -- [`adjacentPairs()`](https://github.com/apple/swift-algorithms/blob/main/Guides/AdjacentPairs.md): Lazily iterates over tuples of adjacent elements. -- [`chunked(by:)`, `chunked(on:)`, `chunks(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Chunked.md): Eager and lazy operations that break a collection into chunks based on either a binary predicate or when the result of a projection changes or chunks of a given count. -- [`firstNonNil(_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/FirstNonNil.md): Returns the first non-`nil` result from transforming a sequence's elements. -- [`indexed()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Indexed.md): Iterate over tuples of a collection's indices and elements. -- [`interspersed(with:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Intersperse.md): Place a value between every two elements of a sequence. -- [`partitioningIndex(where:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Partition.md): Returns the starting index of the partition of a collection that matches a predicate. -- [`reductions(_:)`, `reductions(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Reductions.md): Returns all the intermediate states of reducing the elements of a sequence or collection. -- [`split(maxSplits:omittingEmptySubsequences:whereSeparator)`, `split(separator:maxSplits:omittingEmptySubsequences)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Split.md): Lazy versions of the Standard Library's eager operations that split sequences and collections into subsequences separated by the specified separator element. -- [`windows(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Windows.md): Breaks a collection into overlapping subsequences where elements are slices from the original collection. - -## Adding Swift Algorithms as a Dependency - -To use the `Algorithms` library in a SwiftPM project, -add the following line to the dependencies in your `Package.swift` file: - -```swift -.package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0"), -``` - -Include `"Algorithms"` as a dependency for your executable target: - -```swift -.target(name: "", dependencies: [ - .product(name: "Algorithms", package: "swift-algorithms"), -]), -``` - -Finally, add `import Algorithms` to your source code. - -## Source Stability - -The Swift Numerics package is source stable; version numbers follow [Semantic Versioning](https://semver.org/). Source breaking changes to public API can only land in a new major version. - -The public API of version 1.0 of the `swift-algorithms` package consists of non-underscored declarations that are marked `public` in the `Algorithms` module. Interfaces that aren't part of the public API may continue to change in any release, including patch releases. - -Future minor versions of the package may introduce changes to these rules as needed. - -We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. Accordingly, from time to time, we expect that new versions of this package will require clients to upgrade to a more recent Swift toolchain release. Requiring a new Swift release will only require a minor version bump. diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/AdjacentPairs.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/AdjacentPairs.swift deleted file mode 100644 index 1c1fd713d..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/AdjacentPairs.swift +++ /dev/null @@ -1,298 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -extension Sequence { - /// Creates a sequence of adjacent pairs of elements from this sequence. - /// - /// In the `AdjacentPairsSequence` returned by this method, the elements of - /// the *i*th pair are the *i*th and *(i+1)*th elements of the underlying - /// sequence. The following example uses the `adjacentPairs()` method to - /// iterate over adjacent pairs of integers: - /// - /// for pair in (1...).prefix(5).adjacentPairs() { - /// print(pair) - /// } - /// // Prints "(1, 2)" - /// // Prints "(2, 3)" - /// // Prints "(3, 4)" - /// // Prints "(4, 5)" - @inlinable - public func adjacentPairs() -> AdjacentPairsSequence { - AdjacentPairsSequence(base: self) - } -} - -extension Collection { - /// A collection of adjacent pairs of elements built from an underlying - /// collection. - /// - /// In an `AdjacentPairsCollection`, the elements of the *i*th pair are the - /// *i*th and *(i+1)*th elements of the underlying sequence. The following - /// example uses the `adjacentPairs()` method to iterate over adjacent pairs - /// of integers: - /// - /// for pair in (1...5).adjacentPairs() { - /// print(pair) - /// } - /// // Prints "(1, 2)" - /// // Prints "(2, 3)" - /// // Prints "(3, 4)" - /// // Prints "(4, 5)" - @inlinable - public func adjacentPairs() -> AdjacentPairsCollection { - AdjacentPairsCollection(base: self) - } -} - -/// A sequence of adjacent pairs of elements built from an underlying sequence. -/// -/// Use the `adjacentPairs()` method on a sequence to create an -/// `AdjacentPairsSequence` instance. -public struct AdjacentPairsSequence { - @usableFromInline - internal let base: Base - - /// Creates an instance that makes pairs of adjacent elements from `base`. - @inlinable - internal init(base: Base) { - self.base = base - } -} - -extension AdjacentPairsSequence { - /// The iterator for an `AdjacentPairsSequence` or `AdjacentPairsCollection` - /// instance. - public struct Iterator { - @usableFromInline - internal var base: Base.Iterator - - @usableFromInline - internal var previousElement: Base.Element? - - @inlinable - internal init(base: Base.Iterator) { - self.base = base - } - } -} - -extension AdjacentPairsSequence.Iterator: IteratorProtocol { - public typealias Element = (Base.Element, Base.Element) - - @inlinable - public mutating func next() -> Element? { - if previousElement == nil { - previousElement = base.next() - } - - guard let previous = previousElement, let next = base.next() else { - return nil - } - - previousElement = next - return (previous, next) - } -} - -extension AdjacentPairsSequence: Sequence { - @inlinable - public func makeIterator() -> Iterator { - Iterator(base: base.makeIterator()) - } - - @inlinable - public var underestimatedCount: Int { - Swift.max(0, base.underestimatedCount - 1) - } -} - -extension AdjacentPairsSequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -/// A collection of adjacent pairs of elements built from an underlying -/// collection. -/// -/// Use the `adjacentPairs()` method on a collection to create an -/// `AdjacentPairsCollection` instance. -public struct AdjacentPairsCollection { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let secondBaseIndex: Base.Index - - @inlinable - internal init(base: Base) { - self.base = base - self.secondBaseIndex = base.isEmpty - ? base.endIndex - : base.index(after: base.startIndex) - } -} - -extension AdjacentPairsCollection { - /// A position in an `AdjacentPairsCollection` instance. - public struct Index: Comparable { - @usableFromInline - internal var first: Base.Index - - @usableFromInline - internal var second: Base.Index - - @inlinable - internal init(first: Base.Index, second: Base.Index) { - self.first = first - self.second = second - } - - @inlinable - public static func == (lhs: Index, rhs: Index) -> Bool { - lhs.first == rhs.first - } - - @inlinable - public static func < (lhs: Index, rhs: Index) -> Bool { - lhs.first < rhs.first - } - } -} - -extension AdjacentPairsCollection: Collection { - @inlinable - public var startIndex: Index { - Index( - first: secondBaseIndex == base.endIndex ? base.endIndex : base.startIndex, - second: secondBaseIndex) - } - - @inlinable - public var endIndex: Index { - Index(first: base.endIndex, second: base.endIndex) - } - - @inlinable - public subscript(position: Index) -> (Base.Element, Base.Element) { - (base[position.first], base[position.second]) - } - - @inlinable - public func index(after i: Index) -> Index { - precondition(i != endIndex, "Can't advance beyond endIndex") - let next = base.index(after: i.second) - return next == base.endIndex - ? endIndex - : Index(first: i.second, second: next) - } - - @inlinable - public func index(_ i: Index, offsetBy distance: Int) -> Index { - guard distance != 0 else { return i } - - guard let result = distance > 0 - ? offsetForward(i, by: distance, limitedBy: endIndex) - : offsetBackward(i, by: -distance, limitedBy: startIndex) - else { fatalError("Index out of bounds") } - return result - } - - @inlinable - public func index( - _ i: Index, offsetBy distance: Int, limitedBy limit: Index - ) -> Index? { - guard distance != 0 else { return i } - guard limit != i else { return nil } - - if distance > 0 { - let limit = limit > i ? limit : endIndex - return offsetForward(i, by: distance, limitedBy: limit) - } else { - let limit = limit < i ? limit : startIndex - return offsetBackward(i, by: -distance, limitedBy: limit) - } - } - - @inlinable - internal func offsetForward( - _ i: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance > 0) - assert(limit > i) - - guard let newFirst = base.index(i.second, offsetBy: distance - 1, limitedBy: limit.first), - newFirst != base.endIndex - else { return nil } - let newSecond = base.index(after: newFirst) - - precondition(newSecond <= base.endIndex, "Can't advance beyond endIndex") - return newSecond == base.endIndex - ? endIndex - : Index(first: newFirst, second: newSecond) - } - - @inlinable - internal func offsetBackward( - _ i: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance > 0) - assert(limit < i) - - let offset = i == endIndex ? 0 : 1 - guard let newSecond = base.index( - i.first, - offsetBy: -(distance - offset), - limitedBy: limit.second) - else { return nil } - let newFirst = base.index(newSecond, offsetBy: -1) - precondition(newFirst >= base.startIndex, "Can't move before startIndex") - return Index(first: newFirst, second: newSecond) - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - // While there's a 2-step gap between the `first` base index values in - // `endIndex` and the penultimate index of this collection, the `second` - // base index values are consistently one step apart throughout the - // entire collection. - base.distance(from: start.second, to: end.second) - } - - @inlinable - public var count: Int { - Swift.max(0, base.count - 1) - } -} - -extension AdjacentPairsCollection: BidirectionalCollection - where Base: BidirectionalCollection -{ - @inlinable - public func index(before i: Index) -> Index { - precondition(i != startIndex, "Can't offset before startIndex") - let second = i == endIndex - ? base.index(before: base.endIndex) - : i.first - let first = base.index(before: second) - return Index(first: first, second: second) - } -} - -extension AdjacentPairsCollection: RandomAccessCollection - where Base: RandomAccessCollection {} - -extension AdjacentPairsCollection: LazySequenceProtocol, LazyCollectionProtocol - where Base: LazySequenceProtocol {} - -extension AdjacentPairsCollection.Index: Hashable where Base.Index: Hashable { - @inlinable - public func hash(into hasher: inout Hasher) { - hasher.combine(first) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Chain.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Chain.swift deleted file mode 100644 index 572050ec8..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Chain.swift +++ /dev/null @@ -1,319 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// A concatenation of two sequences with the same element type. -public struct Chain2Sequence - where Base1.Element == Base2.Element -{ - /// The first sequence in this chain. - @usableFromInline - internal let base1: Base1 - - /// The second sequence in this chain. - @usableFromInline - internal let base2: Base2 - - @inlinable - internal init(base1: Base1, base2: Base2) { - self.base1 = base1 - self.base2 = base2 - } -} - -extension Chain2Sequence: Sequence { - /// The iterator for a `Chain2Sequence` instance. - public struct Iterator: IteratorProtocol { - @usableFromInline - internal var iterator1: Base1.Iterator - - @usableFromInline - internal var iterator2: Base2.Iterator - - @inlinable - internal init(_ concatenation: Chain2Sequence) { - iterator1 = concatenation.base1.makeIterator() - iterator2 = concatenation.base2.makeIterator() - } - - @inlinable - public mutating func next() -> Base1.Element? { - return iterator1.next() ?? iterator2.next() - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(self) - } -} - -extension Chain2Sequence: Collection where Base1: Collection, Base2: Collection { - /// A position in a `Chain2Sequence` instance. - public struct Index: Comparable { - // The internal index representation, which can either be an index of the - // first collection or the second. The `endIndex` of the first collection - // is not to be used as a value - iterating over indices should go straight - // from the penultimate index of the first collection to the start of the - // second. - @usableFromInline - internal enum Representation: Equatable { - case first(Base1.Index) - case second(Base2.Index) - } - - @usableFromInline - internal let position: Representation - - /// Creates a new index into the first underlying collection. - @inlinable - internal init(first i: Base1.Index) { - position = .first(i) - } - - /// Creates a new index into the second underlying collection. - @inlinable - internal init(second i: Base2.Index) { - position = .second(i) - } - - @inlinable - public static func < (lhs: Index, rhs: Index) -> Bool { - switch (lhs.position, rhs.position) { - case (.first, .second): - return true - case (.second, .first): - return false - case let (.first(l), .first(r)): - return l < r - case let (.second(l), .second(r)): - return l < r - } - } - } - - /// Converts an index of `Base1` to the corresponding `Index` by mapping - /// `base1.endIndex` to `base2.startIndex`. - @inlinable - internal func normalizeIndex(_ i: Base1.Index) -> Index { - i == base1.endIndex ? Index(second: base2.startIndex) : Index(first: i) - } - - @inlinable - public var startIndex: Index { - // if `base1` is empty, this will return `base2.startIndex` - if `base2` is - // also empty, this will correctly equal `base2.endIndex` - normalizeIndex(base1.startIndex) - } - - @inlinable - public var endIndex: Index { - Index(second: base2.endIndex) - } - - @inlinable - public subscript(i: Index) -> Base1.Element { - switch i.position { - case let .first(i): - return base1[i] - case let .second(i): - return base2[i] - } - } - - @inlinable - public func index(after i: Index) -> Index { - switch i.position { - case let .first(i): - assert(i != base1.endIndex) - return normalizeIndex(base1.index(after: i)) - case let .second(i): - return Index(second: base2.index(after: i)) - } - } - - @inlinable - public func index(_ i: Index, offsetBy distance: Int) -> Index { - guard distance != 0 else { return i } - - return distance > 0 - ? offsetForward(i, by: distance) - : offsetBackward(i, by: -distance) - } - - @inlinable - public func index( - _ i: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - if distance >= 0 { - return limit >= i - ? offsetForward(i, by: distance, limitedBy: limit) - : offsetForward(i, by: distance) - } else { - return limit <= i - ? offsetBackward(i, by: -distance, limitedBy: limit) - : offsetBackward(i, by: -distance) - } - } - - @inlinable - internal func offsetForward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetForward(i, by: distance, limitedBy: endIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetBackward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetBackward(i, by: distance, limitedBy: startIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetForward( - _ i: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance >= 0) - assert(limit >= i) - - switch (i.position, limit.position) { - case let (.first(i), .first(limit)): - return base1.index(i, offsetBy: distance, limitedBy: limit) - .map(Index.init(first:)) - - case let (.first(i), .second(limit)): - if let j = base1.index(i, offsetBy: distance, limitedBy: base1.endIndex) { - // the offset stays within the bounds of `base1` - return normalizeIndex(j) - } else { - // the offset overflows the bounds of `base1` by `n - d` - let d = base1.distance(from: i, to: base1.endIndex) - return base2.index(base2.startIndex, offsetBy: distance - d, limitedBy: limit) - .map(Index.init(second:)) - } - - case (.second, .first): - // impossible because `limit >= i` - fatalError() - - case let (.second(i), .second(limit)): - return base2.index(i, offsetBy: distance, limitedBy: limit) - .map(Index.init(second:)) - } - } - - @inlinable - internal func offsetBackward( - _ i: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance >= 0) - assert(limit <= i) - - switch (i.position, limit.position) { - case let (.first(i), .first(limit)): - return base1.index(i, offsetBy: -distance, limitedBy: limit) - .map(Index.init(first:)) - - case (.first, .second): - // impossible because `limit <= i` - fatalError() - - case let (.second(i), .first(limit)): - if let j = base2.index(i, offsetBy: -distance, limitedBy: base2.startIndex) { - // the offset stays within the bounds of `base2` - return Index(second: j) - } else { - // the offset overflows the bounds of `base2` by `n - d` - let d = base2.distance(from: base2.startIndex, to: i) - return base1.index(base1.endIndex, offsetBy: -(distance - d), limitedBy: limit) - .map(Index.init(first:)) - } - - case let (.second(i), .second(limit)): - // `limit` is relevant, so `base1` cannot be reached - return base2.index(i, offsetBy: -distance, limitedBy: limit) - .map(Index.init(second:)) - } - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - switch (start.position, end.position) { - case let (.first(i), .first(j)): - return base1.distance(from: i, to: j) - case let (.second(i), .second(j)): - return base2.distance(from: i, to: j) - case let (.first(i), .second(j)): - return base1.distance(from: i, to: base1.endIndex) - + base2.distance(from: base2.startIndex, to: j) - case let (.second(i), .first(j)): - return base2.distance(from: i, to: base2.startIndex) - + base1.distance(from: base1.endIndex, to: j) - } - } -} - -extension Chain2Sequence: BidirectionalCollection - where Base1: BidirectionalCollection, Base2: BidirectionalCollection -{ - @inlinable - public func index(before i: Index) -> Index { - assert(i != startIndex, "Can't advance before startIndex") - switch i.position { - case let .first(i): - return Index(first: base1.index(before: i)) - case let .second(i): - return i == base2.startIndex - ? Index(first: base1.index(before: base1.endIndex)) - : Index(second: base2.index(before: i)) - } - } -} - -extension Chain2Sequence: RandomAccessCollection - where Base1: RandomAccessCollection, Base2: RandomAccessCollection {} - -//===----------------------------------------------------------------------===// -// chain(_:_:) -//===----------------------------------------------------------------------===// - -/// Returns a new sequence that iterates over the two given sequences, one -/// followed by the other. -/// -/// You can pass any two sequences or collections that have the same element -/// type as this sequence. This example chains a closed range of `Int` with an -/// array of `Int`: -/// -/// let small = 1...3 -/// let big = [100, 200, 300] -/// for num in chain(small, big) { -/// print(num) -/// } -/// // 1 -/// // 2 -/// // 3 -/// // 100 -/// // 200 -/// // 300 -/// -/// - Parameters: -/// - s1: The first sequence. -/// - s2: The second sequence. -/// - Returns: A sequence that iterates first over the elements of `s1`, and -/// then over the elements of `s2`. -/// -/// - Complexity: O(1) -@inlinable -public func chain(_ s1: S1, _ s2: S2) -> Chain2Sequence { - Chain2Sequence(base1: s1, base2: s2) -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Chunked.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Chunked.swift deleted file mode 100644 index 850ec25ff..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Chunked.swift +++ /dev/null @@ -1,587 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// A collection wrapper that breaks a collection into chunks based on a -/// predicate. -/// -/// Call `lazy.chunked(by:)` on a collection to create an instance of this type. -public struct ChunkedByCollection { - /// The collection that this instance provides a view onto. - @usableFromInline - internal let base: Base - - /// The projection function. - @usableFromInline - internal let projection: (Base.Element) -> Subject - - /// The predicate. - @usableFromInline - internal let belongInSameGroup: (Subject, Subject) -> Bool - - /// The end index of the first chunk. - @usableFromInline - internal var endOfFirstChunk: Base.Index - - @inlinable - internal init( - base: Base, - projection: @escaping (Base.Element) -> Subject, - belongInSameGroup: @escaping (Subject, Subject) -> Bool - ) { - self.base = base - self.projection = projection - self.belongInSameGroup = belongInSameGroup - self.endOfFirstChunk = base.startIndex - - if !base.isEmpty { - endOfFirstChunk = endOfChunk(startingAt: base.startIndex) - } - } -} - -extension ChunkedByCollection: Collection { - /// A position in a chunked collection. - public struct Index: Comparable { - /// The range corresponding to the chunk at this position. - @usableFromInline - internal var baseRange: Range - - @inlinable - internal init(_ baseRange: Range) { - self.baseRange = baseRange - } - - @inlinable - public static func == (lhs: Index, rhs: Index) -> Bool { - // Since each index represents the range of a disparate chunk, no two - // unique indices will have the same lower bound. - lhs.baseRange.lowerBound == rhs.baseRange.lowerBound - } - - @inlinable - public static func < (lhs: Index, rhs: Index) -> Bool { - // Only use the lower bound to test for ordering, as above. - lhs.baseRange.lowerBound < rhs.baseRange.lowerBound - } - } - - /// Returns the index in the base collection of the end of the chunk starting - /// at the given index. - @inlinable - internal func endOfChunk(startingAt start: Base.Index) -> Base.Index { - var subject = projection(base[start]) - - return base[base.index(after: start)...].endOfPrefix(while: { element in - let nextSubject = projection(element) - defer { subject = nextSubject } - return belongInSameGroup(subject, nextSubject) - }) - } - - @inlinable - public var startIndex: Index { - Index(base.startIndex.. Index { - precondition(i != endIndex, "Can't advance past endIndex") - let upperBound = i.baseRange.upperBound - guard upperBound != base.endIndex else { return endIndex } - let end = endOfChunk(startingAt: upperBound) - return Index(upperBound.. Base.SubSequence { - precondition(position != endIndex, "Can't subscript using endIndex") - return base[position.baseRange] - } -} - -extension ChunkedByCollection.Index: Hashable where Base.Index: Hashable {} - -extension ChunkedByCollection: BidirectionalCollection - where Base: BidirectionalCollection -{ - /// Returns the index in the base collection of the start of the chunk ending - /// at the given index. - @inlinable - internal func startOfChunk(endingAt end: Base.Index) -> Base.Index { - let indexBeforeEnd = base.index(before: end) - var subject = projection(base[indexBeforeEnd]) - - return base[.. Index { - precondition(i != startIndex, "Can't advance before startIndex") - let start = startOfChunk(endingAt: i.baseRange.lowerBound) - return Index(start.. { - @usableFromInline - internal var chunked: ChunkedByCollection - - @inlinable - internal init( - base: Base, - projection: @escaping (Base.Element) -> Subject - ) { - self.chunked = ChunkedByCollection( - base: base, - projection: projection, - belongInSameGroup: ==) - } -} - -extension ChunkedOnCollection: Collection { - public typealias Index = ChunkedByCollection.Index - - @inlinable - public var startIndex: Index { - chunked.startIndex - } - - @inlinable - public var endIndex: Index { - chunked.endIndex - } - - @inlinable - public subscript(position: Index) -> (Subject, Base.SubSequence) { - let subsequence = chunked[position] - let subject = chunked.projection(subsequence.first!) - return (subject, subsequence) - } - - @inlinable - public func index(after i: Index) -> Index { - chunked.index(after: i) - } -} - -extension ChunkedOnCollection: BidirectionalCollection - where Base: BidirectionalCollection -{ - @inlinable - public func index(before i: Index) -> Index { - chunked.index(before: i) - } -} - -extension ChunkedOnCollection: LazyCollectionProtocol {} - -//===----------------------------------------------------------------------===// -// lazy.chunked(by:) / lazy.chunked(on:) -//===----------------------------------------------------------------------===// - -extension LazySequenceProtocol where Self: Collection, Elements: Collection { - /// Returns a lazy collection of subsequences of this collection, chunked by - /// the given predicate. - /// - /// - Complexity: O(*n*), because the start index is pre-computed. - @inlinable - public func chunked( - by belongInSameGroup: @escaping (Element, Element) -> Bool - ) -> ChunkedByCollection { - ChunkedByCollection( - base: elements, - projection: { $0 }, - belongInSameGroup: belongInSameGroup) - } - - /// Returns a lazy collection of subsequences of this collection, chunked by - /// grouping elements that project to the same value. - /// - /// - Complexity: O(*n*), because the start index is pre-computed. - @inlinable - public func chunked( - on projection: @escaping (Element) -> Subject - ) -> ChunkedOnCollection { - ChunkedOnCollection( - base: elements, - projection: projection) - } -} - -//===----------------------------------------------------------------------===// -// chunked(by:) / chunked(on:) -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns a collection of subsequences of this collection, chunked by the - /// given predicate. - /// - /// - Complexity: O(*n*), where *n* is the length of this collection. - @inlinable - public func chunked( - by belongInSameGroup: (Element, Element) throws -> Bool - ) rethrows -> [SubSequence] { - guard !isEmpty else { return [] } - var result: [SubSequence] = [] - - var start = startIndex - var current = self[start] - - for (index, element) in indexed().dropFirst() { - if try !belongInSameGroup(current, element) { - result.append(self[start..( - on projection: (Element) throws -> Subject - ) rethrows -> [(Subject, SubSequence)] { - guard !isEmpty else { return [] } - var result: [(Subject, SubSequence)] = [] - - var start = startIndex - var subject = try projection(self[start]) - - for (index, element) in indexed().dropFirst() { - let nextSubject = try projection(element) - if subject != nextSubject { - result.append((subject, self[start.. { - public typealias Element = Base.SubSequence - - @usableFromInline - internal let base: Base - - @usableFromInline - internal let chunkCount: Int - - @usableFromInline - internal var endOfFirstChunk: Base.Index - - /// Creates a view instance that presents the elements of `base` in - /// `SubSequence` chunks of the given count. - /// - /// - Complexity: O(*n*), because the start index is pre-computed. - @inlinable - internal init(_base: Base, _chunkCount: Int) { - self.base = _base - self.chunkCount = _chunkCount - - // Compute the start index upfront in order to make start index a O(1) - // lookup. - self.endOfFirstChunk = _base.index( - _base.startIndex, offsetBy: _chunkCount, - limitedBy: _base.endIndex - ) ?? _base.endIndex - } -} - -extension ChunksOfCountCollection: Collection { - public struct Index { - @usableFromInline - internal let baseRange: Range - - @inlinable - internal init(_baseRange: Range) { - self.baseRange = _baseRange - } - } - - /// - Complexity: O(1) - @inlinable - public var startIndex: Index { - Index(_baseRange: base.startIndex.. Element { - precondition(i != endIndex, "Index out of range") - return base[i.baseRange] - } - - @inlinable - public func index(after i: Index) -> Index { - precondition(i != endIndex, "Advancing past end index") - let baseIdx = base.index( - i.baseRange.upperBound, offsetBy: chunkCount, - limitedBy: base.endIndex - ) ?? base.endIndex - return Index(_baseRange: i.baseRange.upperBound.. Bool { - lhs.baseRange.lowerBound == rhs.baseRange.lowerBound - } - - @inlinable - public static func < (lhs: ChunksOfCountCollection.Index, - rhs: ChunksOfCountCollection.Index) -> Bool { - lhs.baseRange.lowerBound < rhs.baseRange.lowerBound - } -} - -extension ChunksOfCountCollection: - BidirectionalCollection, RandomAccessCollection -where Base: RandomAccessCollection { - @inlinable - public func index(before i: Index) -> Index { - precondition(i != startIndex, "Advancing past start index") - - var offset = chunkCount - if i.baseRange.lowerBound == base.endIndex { - let remainder = base.count % chunkCount - if remainder != 0 { - offset = remainder - } - } - - let baseIdx = base.index( - i.baseRange.lowerBound, offsetBy: -offset, - limitedBy: base.startIndex - ) ?? base.startIndex - return Index(_baseRange: baseIdx.. Int { - let distance = - base.distance(from: start.baseRange.lowerBound, - to: end.baseRange.lowerBound) - let (quotient, remainder) = - distance.quotientAndRemainder(dividingBy: chunkCount) - return quotient + remainder.signum() - } - - @inlinable - public var count: Int { - let (quotient, remainder) = - base.count.quotientAndRemainder(dividingBy: chunkCount) - return quotient + remainder.signum() - } - - @inlinable - public func index( - _ i: Index, offsetBy offset: Int, limitedBy limit: Index - ) -> Index? { - guard offset != 0 else { return i } - guard limit != i else { return nil } - - if offset > 0 { - return limit > i - ? offsetForward(i, offsetBy: offset, limit: limit) - : offsetForward(i, offsetBy: offset) - } else { - return limit < i - ? offsetBackward(i, offsetBy: offset, limit: limit) - : offsetBackward(i, offsetBy: offset) - } - } - - @inlinable - public func index(_ i: Index, offsetBy distance: Int) -> Index { - guard distance != 0 else { return i } - - let idx = distance > 0 - ? offsetForward(i, offsetBy: distance) - : offsetBackward(i, offsetBy: distance) - guard let index = idx else { - fatalError("Out of bounds") - } - return index - } - - @inlinable - internal func offsetForward( - _ i: Index, offsetBy distance: Int, limit: Index? = nil - ) -> Index? { - assert(distance > 0) - - return makeOffsetIndex( - from: i, baseBound: base.endIndex, - distance: distance, baseDistance: distance * chunkCount, - limit: limit, by: > - ) - } - - // Convenience to compute offset backward base distance. - @inlinable - internal func computeOffsetBackwardBaseDistance( - _ i: Index, _ distance: Int - ) -> Int { - if i == endIndex { - let remainder = base.count%chunkCount - // We have to take it into account when calculating offsets. - if remainder != 0 { - // Distance "minus" one(at this point distance is negative) because we - // need to adjust for the last position that have a variadic(remainder) - // number of elements. - return ((distance + 1) * chunkCount) - remainder - } - } - return distance * chunkCount - } - - @inlinable - internal func offsetBackward( - _ i: Index, offsetBy distance: Int, limit: Index? = nil - ) -> Index? { - assert(distance < 0) - let baseDistance = - computeOffsetBackwardBaseDistance(i, distance) - return makeOffsetIndex( - from: i, baseBound: base.startIndex, - distance: distance, baseDistance: baseDistance, - limit: limit, by: < - ) - } - - // Helper to compute index(offsetBy:) index. - @inlinable - internal func makeOffsetIndex( - from i: Index, baseBound: Base.Index, distance: Int, baseDistance: Int, - limit: Index?, by limitFn: (Base.Index, Base.Index) -> Bool - ) -> Index? { - let baseIdx = base.index( - i.baseRange.lowerBound, offsetBy: baseDistance, - limitedBy: baseBound - ) - - if let limit = limit { - if baseIdx == nil { - // If we past the bounds while advancing forward and the limit is the - // `endIndex`, since the computation on base don't take into account the - // remainder, we have to make sure that passing the bound was because of - // the distance not just because of a remainder. Special casing is less - // expensive than always use count(which could be O(n) for non-random - // access collection base) to compute the base distance taking remainder - // into account. - if baseDistance > 0 && limit == endIndex { - if self.distance(from: i, to: limit) < distance { - return nil - } - } else { - return nil - } - } - - // Checks for the limit. - let baseStartIdx = baseIdx ?? baseBound - if limitFn(baseStartIdx, limit.baseRange.lowerBound) { - return nil - } - } - - let baseStartIdx = baseIdx ?? baseBound - let baseEndIdx = base.index( - baseStartIdx, offsetBy: chunkCount, limitedBy: base.endIndex - ) ?? base.endIndex - - return Index(_baseRange: baseStartIdx..` view presenting the elements in - /// chunks with count of the given count parameter. - /// - /// - Parameter count: The size of the chunks. If the count parameter is - /// evenly divided by the count of the base `Collection` all the chunks will - /// have the count equals to size. Otherwise, the last chunk will contain - /// the remaining elements. - /// - /// let c = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - /// print(c.chunks(ofCount: 5).map(Array.init)) - /// // [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]] - /// - /// print(c.chunks(ofCount: 3).map(Array.init)) - /// // [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]] - /// - /// - Complexity: O(*n*), because the start index is pre-computed. - @inlinable - public func chunks(ofCount count: Int) -> ChunksOfCountCollection { - precondition(count > 0, "Cannot chunk with count <= 0!") - return ChunksOfCountCollection(_base: self, _chunkCount: count) - } -} - -extension ChunksOfCountCollection.Index: Hashable where Base.Index: Hashable {} - -extension ChunksOfCountCollection: LazySequenceProtocol, LazyCollectionProtocol - where Base: LazySequenceProtocol {} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Combinations.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Combinations.swift deleted file mode 100644 index 2f26e9c72..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Combinations.swift +++ /dev/null @@ -1,299 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020-2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// A collection wrapper that generates combinations of a base collection. -public struct CombinationsSequence { - /// The collection to iterate over for combinations. - @usableFromInline - internal let base: Base - - @usableFromInline - internal let baseCount: Int - - /// The range of accepted sizes of combinations. - /// - /// - Note: This may be `nil` if the attempted range entirely exceeds the - /// upper bounds of the size of the `base` collection. - @usableFromInline - internal let kRange: Range? - - /// Initializes a `CombinationsSequence` for all combinations of `base` of - /// size `k`. - /// - /// - Parameters: - /// - base: The collection to iterate over for combinations. - /// - k: The expected size of each combination. - @inlinable - internal init(_ base: Base, k: Int) { - self.init(base, kRange: k...k) - } - - /// Initializes a `CombinationsSequence` for all combinations of `base` of - /// sizes within a given range. - /// - /// - Parameters: - /// - base: The collection to iterate over for combinations. - /// - kRange: The range of accepted sizes of combinations. - @inlinable - internal init( - _ base: Base, kRange: R - ) where R.Bound == Int { - let range = kRange.relative(to: 0 ..< .max) - self.base = base - let baseCount = base.count - self.baseCount = baseCount - let upperBound = baseCount + 1 - self.kRange = range.lowerBound < upperBound - ? range.clamped(to: 0 ..< upperBound) - : nil - } - - /// The total number of combinations. - @inlinable - public var count: Int { - guard let k = self.kRange else { return 0 } - let n = baseCount - if k == 0 ..< (n + 1) { - return 1 << n - } - - func binomial(n: Int, k: Int) -> Int { - switch k { - case n, 0: return 1 - case n...: return 0 - case (n / 2 + 1)...: return binomial(n: n, k: n - k) - default: return n * binomial(n: n - 1, k: k - 1) / k - } - } - - return k.map { - binomial(n: n, k: $0) - }.reduce(0, +) - } -} - -extension CombinationsSequence: Sequence { - /// The iterator for a `CombinationsSequence` instance. - public struct Iterator: IteratorProtocol { - @usableFromInline - internal let base: Base - - /// The current range of accepted sizes of combinations. - /// - /// - Note: The range is contracted until empty while iterating over - /// combinations of different sizes. When the range is empty, iteration is - /// finished. - @usableFromInline - internal var kRange: Range - - /// Whether or not iteration is finished (`kRange` is empty) - @inlinable - internal var isFinished: Bool { - return kRange.isEmpty - } - - @usableFromInline - internal var indexes: [Base.Index] - - @inlinable - internal init(_ combinations: CombinationsSequence) { - self.base = combinations.base - self.kRange = combinations.kRange ?? 0..<0 - self.indexes = Array(combinations.base.indices.prefix(kRange.lowerBound)) - } - - /// Advances the current indices to the next set of combinations. If - /// `indexes.count == 3` and `base.count == 5`, the indices advance like - /// this: - /// - /// [0, 1, 2] - /// [0, 1, 3] - /// [0, 1, 4] * - /// // * `base.endIndex` reached in `indexes.last` - /// // Advance penultimate index and propagate that change - /// [0, 2, 3] - /// [0, 2, 4] * - /// [0, 3, 4] * - /// [1, 2, 3] - /// [1, 2, 4] * - /// [1, 3, 4] * - /// [2, 3, 4] * - /// // Can't advance without needing to go past `base.endIndex`, - /// // so the iteration is finished. - @inlinable - internal mutating func advance() { - /// Advances `kRange` by incrementing its `lowerBound` until the range is - /// empty, when iteration is finished. - func advanceKRange() { - if kRange.lowerBound < kRange.upperBound { - let advancedLowerBound = kRange.lowerBound + 1 - kRange = advancedLowerBound ..< kRange.upperBound - indexes.removeAll(keepingCapacity: true) - indexes.append(contentsOf: base.indices.prefix(kRange.lowerBound)) - } - } - - guard !indexes.isEmpty else { - // Initial state for combinations of 0 elements is an empty array with - // `finished == false`. Even though no indexes are involved, advancing - // from that state means we are finished with iterating. - advanceKRange() - return - } - - let i = indexes.count - 1 - base.formIndex(after: &indexes[i]) - if indexes[i] != base.endIndex { return } - - var j = i - while indexes[i] == base.endIndex { - j -= 1 - guard j >= 0 else { - // Finished iterating over combinations of this size. - advanceKRange() - return - } - - base.formIndex(after: &indexes[j]) - for k in indexes.indices[(j + 1)...] { - indexes[k] = base.index(after: indexes[k - 1]) - if indexes[k] == base.endIndex { - break - } - } - } - } - - @inlinable - public mutating func next() -> [Base.Element]? { - guard !isFinished else { return nil } - defer { advance() } - return indexes.map { i in base[i] } - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(self) - } -} - -extension CombinationsSequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -//===----------------------------------------------------------------------===// -// combinations(ofCount:) -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns a collection of combinations of this collection's elements, with - /// each combination having the specified number of elements. - /// - /// This example prints the different combinations of 1 and 2 from an array of - /// four colors: - /// - /// let colors = ["fuchsia", "cyan", "mauve", "magenta"] - /// for combo in colors.combinations(ofCount: 1...2) { - /// print(combo.joined(separator: ", ")) - /// } - /// // fuchsia - /// // cyan - /// // mauve - /// // magenta - /// // fuchsia, cyan - /// // fuchsia, mauve - /// // fuchsia, magenta - /// // cyan, mauve - /// // cyan, magenta - /// // mauve, magenta - /// - /// The returned collection presents combinations in a consistent order, where - /// the indices in each combination are in ascending lexicographical order. - /// That is, in the example above, the combinations in order are the elements - /// at `[0]`, `[1]`, `[2]`, `[3]`, `[0, 1]`, `[0, 2]`, `[0, 3]`, `[1, 2]`, - /// `[1, 3]`, and finally `[2, 3]`. - /// - /// This example prints _all_ the combinations (including an empty array and - /// the original collection) from an array of numbers: - /// - /// let numbers = [10, 20, 30, 40] - /// for combo in numbers.combinations(ofCount: 0...) { - /// print(combo) - /// } - /// // [] - /// // [10] - /// // [20] - /// // [30] - /// // [40] - /// // [10, 20] - /// // [10, 30] - /// // [10, 40] - /// // [20, 30] - /// // [20, 40] - /// // [30, 40] - /// // [10, 20, 30] - /// // [10, 20, 40] - /// // [10, 30, 40] - /// // [20, 30, 40] - /// // [10, 20, 30, 40] - /// - /// If `kRange` is `0...0`, the resulting sequence has exactly one element, an - /// empty array. The given range is limited to `0...base.count`. If the - /// limited range is empty, the resulting sequence has no elements. - /// - /// - Parameter kRange: The range of numbers of elements to include in each - /// combination. - /// - /// - Complexity: O(1) for random-access base collections. O(*n*) where *n* - /// is the number of elements in the base collection, since - /// `CombinationsSequence` accesses the `count` of the base collection. - @inlinable - public func combinations( - ofCount kRange: R - ) -> CombinationsSequence where R.Bound == Int { - CombinationsSequence(self, kRange: kRange) - } - - /// Returns a collection of combinations of this collection's elements, with - /// each combination having the specified number of elements. - /// - /// This example prints the different combinations of three from an array of - /// four colors: - /// - /// let colors = ["fuchsia", "cyan", "mauve", "magenta"] - /// for combo in colors.combinations(ofCount: 3) { - /// print(combo.joined(separator: ", ")) - /// } - /// // fuchsia, cyan, mauve - /// // fuchsia, cyan, magenta - /// // fuchsia, mauve, magenta - /// // cyan, mauve, magenta - /// - /// The returned collection presents combinations in a consistent order, where - /// the indices in each combination are in ascending lexicographical order. - /// That is, in the example above, the combinations in order are the elements - /// at `[0, 1, 2]`, `[0, 1, 3]`, `[0, 2, 3]`, and finally `[1, 2, 3]`. - /// - /// If `k` is zero, the resulting sequence has exactly one element, an empty - /// array. If `k` is greater than the number of elements in this sequence, - /// the resulting sequence has no elements. - /// - /// - Parameter k: The number of elements to include in each combination. - /// - /// - Complexity: O(1) for random-access base collections. O(*n*) where *n* - /// is the number of elements in the base collection, since - /// `CombinationsSequence` accesses the `count` of the base collection. - @inlinable - public func combinations(ofCount k: Int) -> CombinationsSequence { - precondition(k >= 0, "Can't have combinations with a negative number of elements.") - return CombinationsSequence(self, k: k) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Compacted.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Compacted.swift deleted file mode 100644 index 1e7ba4891..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Compacted.swift +++ /dev/null @@ -1,184 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// A `Sequence` that iterates over every non-nil element from the original -/// `Sequence`. -public struct CompactedSequence: Sequence - where Base.Element == Element? { - - @usableFromInline - let base: Base - - @inlinable - init(base: Base) { - self.base = base - } - - public struct Iterator: IteratorProtocol { - @usableFromInline - var base: Base.Iterator - - @inlinable - init(base: Base.Iterator) { - self.base = base - } - - @inlinable - public mutating func next() -> Element? { - while let wrapped = base.next() { - guard let some = wrapped else { continue } - return some - } - return nil - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(base: base.makeIterator()) - } -} - -extension Sequence { - /// Returns a new `Sequence` that iterates over every non-nil element from the - /// original `Sequence`. - /// - /// Produces the same result as `c.compactMap { $0 }`. - /// - /// let c = [1, nil, 2, 3, nil] - /// for num in c.compacted() { - /// print(num) - /// } - /// // 1 - /// // 2 - /// // 3 - /// - /// - Returns: A `Sequence` where the element is the unwrapped original - /// element and iterates over every non-nil element from the original - /// `Sequence`. - /// - /// Complexity: O(1) - @inlinable - public func compacted() -> CompactedSequence - where Element == Unwrapped? { - CompactedSequence(base: self) - } -} - -/// A `Collection` that iterates over every non-nil element from the original -/// `Collection`. -public struct CompactedCollection: Collection - where Base.Element == Element? { - - @usableFromInline - let base: Base - - @inlinable - init(base: Base) { - self.base = base - let idx = base.firstIndex(where: { $0 != nil }) ?? base.endIndex - self.startIndex = Index(base: idx) - } - - public struct Index { - @usableFromInline - let base: Base.Index - - @inlinable - init(base: Base.Index) { - self.base = base - } - } - - public var startIndex: Index - - @inlinable - public var endIndex: Index { Index(base: base.endIndex) } - - @inlinable - public subscript(position: Index) -> Element { - base[position.base]! - } - - @inlinable - public func index(after i: Index) -> Index { - precondition(i != endIndex, "Index out of bounds") - - let baseIdx = base.index(after: i.base) - guard let idx = base[baseIdx...].firstIndex(where: { $0 != nil }) - else { return endIndex } - return Index(base: idx) - } -} - -extension CompactedCollection: BidirectionalCollection - where Base: BidirectionalCollection { - - @inlinable - public func index(before i: Index) -> Index { - precondition(i != startIndex, "Index out of bounds") - - guard let idx = - base[startIndex.base.. Bool { - lhs.base < rhs.base - } -} - -extension CompactedCollection.Index: Hashable - where Base.Index: Hashable {} - -extension Collection { - /// Returns a new `Collection` that iterates over every non-nil element from - /// the original `Collection`. - /// - /// Produces the same result as `c.compactMap { $0 }`. - /// - /// let c = [1, nil, 2, 3, nil] - /// for num in c.compacted() { - /// print(num) - /// } - /// // 1 - /// // 2 - /// // 3 - /// - /// - Returns: A `Collection` where the element is the unwrapped original - /// element and iterates over every non-nil element from the original - /// `Collection`. - /// - /// Complexity: O(*n*) where *n* is the number of elements in the - /// original `Collection`. - @inlinable - public func compacted() -> CompactedCollection - where Element == Unwrapped? - { - CompactedCollection(base: self) - } -} - -//===----------------------------------------------------------------------===// -// Protocol Conformances -//===----------------------------------------------------------------------===// - -extension CompactedSequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -extension CompactedCollection: LazySequenceProtocol, LazyCollectionProtocol - where Base: LazySequenceProtocol {} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Cycle.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Cycle.swift deleted file mode 100644 index 23d141646..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Cycle.swift +++ /dev/null @@ -1,222 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// A collection wrapper that repeats the elements of a base collection. -public struct CycledSequence { - /// The collection to repeat. - @usableFromInline - internal let base: Base - - @inlinable - internal init(base: Base) { - self.base = base - } -} - -extension CycledSequence: Sequence { - /// The iterator for a `CycledSequence` instance. - public struct Iterator: IteratorProtocol { - @usableFromInline - internal let base: Base - - @usableFromInline - internal var current: Base.Index - - @inlinable - internal init(base: Base) { - self.base = base - self.current = base.startIndex - } - - @inlinable - public mutating func next() -> Base.Element? { - guard !base.isEmpty else { return nil } - - if current == base.endIndex { - current = base.startIndex - } - - defer { base.formIndex(after: ¤t) } - return base[current] - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(base: base) - } -} - -extension CycledSequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -/// A collection wrapper that repeats the elements of a base collection for a -/// finite number of times. -public struct CycledTimesCollection { - /// A `Product2Sequence` instance for iterating the base collection. - @usableFromInline - internal let product: Product2Sequence, Base> - - @inlinable - internal init(base: Base, times: Int) { - self.product = Product2Sequence(0.., Base>.Index - - @inlinable - internal init(_ productIndex: Product2Sequence, Base>.Index) { - self.productIndex = productIndex - } - - @inlinable - public static func == (lhs: Index, rhs: Index) -> Bool { - lhs.productIndex == rhs.productIndex - } - - @inlinable - public static func < (lhs: Index, rhs: Index) -> Bool { - lhs.productIndex < rhs.productIndex - } - } - - @inlinable - public var startIndex: Index { - Index(product.startIndex) - } - - @inlinable - public var endIndex: Index { - Index(product.endIndex) - } - - @inlinable - public subscript(_ index: Index) -> Element { - product[index.productIndex].1 - } - - @inlinable - public func index(after i: Index) -> Index { - let productIndex = product.index(after: i.productIndex) - return Index(productIndex) - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - product.distance(from: start.productIndex, to: end.productIndex) - } - - @inlinable - public func index(_ i: Index, offsetBy distance: Int) -> Index { - let productIndex = product.index(i.productIndex, offsetBy: distance) - return Index(productIndex) - } - - @inlinable - public func index( - _ i: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - guard let productIndex = product.index( - i.productIndex, - offsetBy: distance, - limitedBy: limit.productIndex) - else { return nil } - return Index(productIndex) - } - - @inlinable - public var count: Int { - product.count - } -} - -extension CycledTimesCollection: BidirectionalCollection - where Base: BidirectionalCollection { - @inlinable - public func index(before i: Index) -> Index { - let productIndex = product.index(before: i.productIndex) - return Index(productIndex) - } -} - -extension CycledTimesCollection: RandomAccessCollection - where Base: RandomAccessCollection {} - -extension CycledTimesCollection: LazySequenceProtocol, LazyCollectionProtocol - where Base: LazySequenceProtocol {} - -//===----------------------------------------------------------------------===// -// cycled() -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns a sequence that repeats the elements of this collection forever. - /// - /// Use the `cycled()` method to repeat the elements of a sequence or - /// collection forever. You can combine `cycled()` with another, finite - /// sequence to iterate over the two together. - /// - /// for (evenOrOdd, number) in zip(["even", "odd"].cycled(), 0..<10) { - /// print("\(number) is \(evenOrOdd)") - /// } - /// // 0 is even - /// // 1 is odd - /// // 2 is even - /// // 3 is odd - /// // ... - /// // 9 is odd - /// - /// - Important: When called on a non-empty collection, the resulting sequence - /// is infinite. Do not directly call methods that require a finite - /// sequence, like `map` or `filter`, without first constraining the length - /// of the cycling sequence. - /// - /// - Returns: A sequence that repeats the elements of this collection - /// forever. - /// - /// - Complexity: O(1) - @inlinable - public func cycled() -> CycledSequence { - CycledSequence(base: self) - } - - /// Returns a sequence that repeats the elements of this collection the - /// specified number of times. - /// - /// Passing `1` as `times` results in this collection's elements being - /// provided a single time; passing `0` results in an empty sequence. The - /// `print(_:)` function in this example is never called: - /// - /// for x in [1, 2, 3].cycled(times: 0) { - /// print(x) - /// } - /// - /// - Parameter times: The number of times to repeat this sequence. `times` - /// must be zero or greater. - /// - Returns: A sequence that repeats the elements of this sequence `times` - /// times. - /// - /// - Complexity: O(1) - @inlinable - public func cycled(times: Int) -> CycledTimesCollection { - CycledTimesCollection(base: self, times: times) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/EitherSequence.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/EitherSequence.swift deleted file mode 100644 index d28c4a10b..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/EitherSequence.swift +++ /dev/null @@ -1,200 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// Either -//===----------------------------------------------------------------------===// - -/// A general-purpose sum type. -@usableFromInline -internal enum Either { - case left(Left) - case right(Right) -} - -extension Either: Equatable where Left: Equatable, Right: Equatable { - @usableFromInline - internal static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case let (.left(lhs), .left(rhs)): - return lhs == rhs - case let (.right(lhs), .right(rhs)): - return lhs == rhs - case (.left, .right), (.right, .left): - return false - } - } -} - -extension Either: Comparable where Left: Comparable, Right: Comparable { - @usableFromInline - internal static func < (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case let (.left(lhs), .left(rhs)): - return lhs < rhs - case let (.right(lhs), .right(rhs)): - return lhs < rhs - case (.left, .right): - return true - case (.right, .left): - return false - } - } -} - -//===----------------------------------------------------------------------===// -// EitherSequence -//===----------------------------------------------------------------------===// - -/// A sequence that has one of the two specified types. -@usableFromInline -internal enum EitherSequence - where Left.Element == Right.Element -{ - case left(Left) - case right(Right) -} - -extension EitherSequence: Sequence { - @usableFromInline - internal struct Iterator: IteratorProtocol { - @usableFromInline - internal var left: Left.Iterator? - - @usableFromInline - internal var right: Right.Iterator? - - @inlinable - internal mutating func next() -> Left.Element? { - left?.next() ?? right?.next() - } - } - - @usableFromInline - internal func makeIterator() -> Iterator { - switch self { - case .left(let left): - return Iterator(left: left.makeIterator(), right: nil) - case .right(let right): - return Iterator(left: nil, right: right.makeIterator()) - } - } -} - -extension EitherSequence: Collection - where Left: Collection, Right: Collection, Left.Element == Right.Element -{ - @usableFromInline - internal typealias Index = Either - - @inlinable - internal var startIndex: Index { - switch self { - case .left(let s): - return .left(s.startIndex) - case .right(let s): - return .right(s.startIndex) - } - } - - @inlinable - internal var endIndex: Index { - switch self { - case .left(let s): - return .left(s.endIndex) - case .right(let s): - return .right(s.endIndex) - } - } - - @inlinable - internal subscript(position: Index) -> Element { - switch (self, position) { - case let (.left(s), .left(i)): - return s[i] - case let (.right(s), .right(i)): - return s[i] - default: - fatalError() - } - } - - @inlinable - internal func index(after i: Index) -> Index { - switch (self,i) { - case let (.left(s), .left(i)): - return .left(s.index(after: i)) - case let (.right(s), .right(i)): - return .right(s.index(after: i)) - default: - fatalError() - } - } - - @inlinable - internal func index( - _ i: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - switch (self, i, limit) { - case let (.left(s), .left(i), .left(limit)): - return s.index(i, offsetBy: distance, limitedBy: limit).map { .left($0) } - case let (.right(s), .right(i), .right(limit)): - return s.index(i, offsetBy: distance, limitedBy: limit).map { .right($0) } - default: - fatalError() - } - } - - @inlinable - internal func index(_ i: Index, offsetBy distance: Int) -> Index { - switch (self, i) { - case let (.left(s), .left(i)): - return .left(s.index(i, offsetBy: distance)) - case let (.right(s), .right(i)): - return .right(s.index(i, offsetBy: distance)) - default: - fatalError() - } - } - - @inlinable - internal func distance(from start: Index, to end: Index) -> Int { - switch (self, start, end) { - case let (.left(s), .left(i), .left(j)): - return s.distance(from: i, to: j) - case let (.right(s), .right(i), .right(j)): - return s.distance(from: i, to: j) - default: - fatalError() - } - } -} - -extension EitherSequence: BidirectionalCollection - where Left: BidirectionalCollection, Right: BidirectionalCollection -{ - @inlinable - internal func index(before i: Index) -> Index { - switch (self, i) { - case let (.left(s), .left(i)): - return .left(s.index(before: i)) - case let (.right(s), .right(i)): - return .right(s.index(before: i)) - default: - fatalError() - } - } -} - -extension EitherSequence: RandomAccessCollection - where Left: RandomAccessCollection, Right: RandomAccessCollection {} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/FirstNonNil.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/FirstNonNil.swift deleted file mode 100644 index f9719f380..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/FirstNonNil.swift +++ /dev/null @@ -1,44 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// firstNonNil(_:) -//===----------------------------------------------------------------------===// - -extension Sequence { - /// Returns the first non-`nil` result obtained from applying the given - /// transformation to the elements of the sequence. - /// - /// let strings = ["three", "3.14", "-5", "2"] - /// if let firstInt = strings.firstNonNil({ Int($0) }) { - /// print(firstInt) - /// // -5 - /// } - /// - /// - Parameter transform: A closure that takes an element of the sequence as - /// its argument and returns an optional transformed value. - /// - Returns: The first non-`nil` return value of the transformation, or - /// `nil` if no transformation is successful. - /// - /// - Complexity: O(*n*), where *n* is the number of elements at the start of - /// the sequence that result in `nil` when applying the transformation. - @inlinable - public func firstNonNil( - _ transform: (Element) throws -> Result? - ) rethrows -> Result? { - for value in self { - if let value = try transform(value) { - return value - } - } - return nil - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/FlattenCollection.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/FlattenCollection.swift deleted file mode 100644 index 23b8a87f6..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/FlattenCollection.swift +++ /dev/null @@ -1,311 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// A collection consisting of all the elements contained in a collection of -/// collections. -@usableFromInline -internal struct FlattenCollection - where Base.Element: Collection -{ - @usableFromInline - internal let base: Base - - @usableFromInline - internal let indexOfFirstNonEmptyElement: Base.Index - - @inlinable - internal init(base: Base) { - self.base = base - self.indexOfFirstNonEmptyElement = base.endOfPrefix(while: { $0.isEmpty }) - } -} - -extension FlattenCollection: Collection { - @usableFromInline - internal struct Index: Comparable { - @usableFromInline - internal let outer: Base.Index - - @usableFromInline - internal let inner: Base.Element.Index? - - @inlinable - init(outer: Base.Index, inner: Base.Element.Index?) { - self.outer = outer - self.inner = inner - } - - @inlinable - internal static func < (lhs: Self, rhs: Self) -> Bool { - guard lhs.outer == rhs.outer else { return lhs.outer < rhs.outer } - return lhs.inner == nil ? false : lhs.inner! < rhs.inner! - } - } - - @inlinable - internal var startIndex: Index { - let outer = indexOfFirstNonEmptyElement - let inner = outer == base.endIndex ? nil : base[outer].startIndex - return Index(outer: outer, inner: inner) - } - - @inlinable - internal var endIndex: Index { - Index(outer: base.endIndex, inner: nil) - } - - /// Forms an index from a pair of base indices, normalizing - /// `(i, base2.endIndex)` to `(base1.index(after: i), base2.startIndex)` if - /// necessary. - @inlinable - internal func normalizeIndex( - outer: Base.Index, - inner: Base.Element.Index - ) -> Index { - if inner == base[outer].endIndex { - let outer = base[base.index(after: outer)...] - .endOfPrefix(while: { $0.isEmpty }) - let inner = outer == base.endIndex ? nil : base[outer].startIndex - return Index(outer: outer, inner: inner) - } else { - return Index(outer: outer, inner: inner) - } - } - - @inlinable - internal func index(after index: Index) -> Index { - let element = base[index.outer] - let nextInner = element.index(after: index.inner!) - return normalizeIndex(outer: index.outer, inner: nextInner) - } - - @inlinable - internal subscript(position: Index) -> Base.Element.Element { - base[position.outer][position.inner!] - } - - @inlinable - internal func distance(from start: Index, to end: Index) -> Int { - guard start.outer <= end.outer - else { return -distance(from: end, to: start) } - guard let startInner = start.inner - else { return 0 } - guard start.outer != end.outer - else { - return base[start.outer].distance(from: startInner, to: end.inner!) - } - - let firstPart = base[start.outer][startInner...].count - let middlePart = base[start.outer.. Index { - guard distance != 0 else { return index } - - return distance > 0 - ? offsetForward(index, by: distance) - : offsetBackward(index, by: -distance) - } - - @inlinable - internal func index( - _ index: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - guard distance != 0 else { return index } - - if distance > 0 { - return limit >= index - ? offsetForward(index, by: distance, limitedBy: limit) - : offsetForward(index, by: distance) - } else { - return limit <= index - ? offsetBackward(index, by: -distance, limitedBy: limit) - : offsetBackward(index, by: -distance) - } - } - - @inlinable - internal func offsetForward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetForward(i, by: distance, limitedBy: endIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetBackward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetBackward(i, by: distance, limitedBy: startIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetForward( - _ index: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance > 0) - assert(limit >= index) - - if index.outer == limit.outer { - if let indexInner = index.inner, let limitInner = limit.inner { - return base[index.outer] - .index(indexInner, offsetBy: distance, limitedBy: limitInner) - .map { inner in Index(outer: index.outer, inner: inner) } - } else { - // `index` and `limit` are both `endIndex` - return nil - } - } - - // `index <= limit` and `index.outer != limit.outer`, so `index != endIndex` - let indexInner = index.inner! - let element = base[index.outer] - - if let inner = element.index( - indexInner, - offsetBy: distance, - limitedBy: element.endIndex - ) { - return normalizeIndex(outer: index.outer, inner: inner) - } - - var remainder = distance - element[indexInner...].count - var outer = base.index(after: index.outer) - - while outer != limit.outer { - let element = base[outer] - - if let inner = element.index( - element.startIndex, - offsetBy: remainder, - limitedBy: element.endIndex - ) { - return normalizeIndex(outer: outer, inner: inner) - } - - remainder -= element.count - base.formIndex(after: &outer) - } - - if let limitInner = limit.inner { - let element = base[outer] - return element.index( - element.startIndex, - offsetBy: remainder, - limitedBy: limitInner) - .map { inner in Index(outer: outer, inner: inner) } - } else { - return nil - } - } - - @inlinable - internal func offsetBackward( - _ index: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance > 0) - assert(limit <= index) - - if index.outer == limit.outer { - if let indexInner = index.inner, let limitInner = limit.inner { - return base[index.outer] - .index(indexInner, offsetBy: -distance, limitedBy: limitInner) - .map { inner in Index(outer: index.outer, inner: inner) } - } else { - // `index` and `limit` are both `endIndex` - return nil - } - } - - var remainder = distance - - if let indexInner = index.inner { - let element = base[index.outer] - - if let inner = element.index( - indexInner, - offsetBy: -remainder, - limitedBy: element.startIndex - ) { - return Index(outer: index.outer, inner: inner) - } - - remainder -= element[.. Index { - if let inner = index.inner { - let element = base[index.outer] - - if inner != element.startIndex { - let previousInner = element.index(before: inner) - return Index(outer: index.outer, inner: previousInner) - } - } - - let previousOuter = base[.. FlattenCollection { - FlattenCollection(base: self) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Indexed.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Indexed.swift deleted file mode 100644 index ea5598a85..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Indexed.swift +++ /dev/null @@ -1,113 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// A collection wrapper that iterates over the indices and elements of a -/// collection together. -public struct IndexedCollection { - /// The base collection. - @usableFromInline - internal let base: Base - - @inlinable - internal init(base: Base) { - self.base = base - } -} - -extension IndexedCollection: Collection { - /// The element type for an `IndexedCollection` collection. - public typealias Element = (index: Base.Index, element: Base.Element) - - @inlinable - public var startIndex: Base.Index { - base.startIndex - } - - @inlinable - public var endIndex: Base.Index { - base.endIndex - } - - @inlinable - public subscript(position: Base.Index) -> Element { - (index: position, element: base[position]) - } - - @inlinable - public func index(after i: Base.Index) -> Base.Index { - base.index(after: i) - } - - @inlinable - public func index(_ i: Base.Index, offsetBy distance: Int) -> Base.Index { - base.index(i, offsetBy: distance) - } - - @inlinable - public func index( - _ i: Base.Index, - offsetBy distance: Int, - limitedBy limit: Base.Index - ) -> Base.Index? { - base.index(i, offsetBy: distance, limitedBy: limit) - } - - @inlinable - public func distance(from start: Base.Index, to end: Base.Index) -> Int { - base.distance(from: start, to: end) - } - - @inlinable - public var indices: Base.Indices { - base.indices - } -} - -extension IndexedCollection: BidirectionalCollection - where Base: BidirectionalCollection -{ - @inlinable - public func index(before i: Base.Index) -> Base.Index { - base.index(before: i) - } -} - -extension IndexedCollection: RandomAccessCollection - where Base: RandomAccessCollection {} - -extension IndexedCollection: LazySequenceProtocol, LazyCollectionProtocol - where Base: LazySequenceProtocol {} - -//===----------------------------------------------------------------------===// -// indexed() -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns a collection of pairs *(i, x)*, where *i* represents an index of - /// the collection, and *x* represents an element. - /// - /// This example iterates over the indices and elements of a set, building an - /// array consisting of indices of names with five or fewer letters. - /// - /// let names: Set = ["Sofia", "Camilla", "Martina", "Mateo", "Nicolás"] - /// var shorterIndices: [Set.Index] = [] - /// for (i, name) in names.indexed() { - /// if name.count <= 5 { - /// shorterIndices.append(i) - /// } - /// } - /// - /// Returns: A collection of paired indices and elements of this collection. - @inlinable - public func indexed() -> IndexedCollection { - IndexedCollection(base: self) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Intersperse.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Intersperse.swift deleted file mode 100644 index 706c4ea23..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Intersperse.swift +++ /dev/null @@ -1,669 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// Intersperse -//===----------------------------------------------------------------------===// - -/// A sequence that presents the elements of a base sequence of elements with a -/// separator between each of those elements. -public struct InterspersedSequence { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let separator: Base.Element - - @inlinable - internal init(base: Base, separator: Base.Element) { - self.base = base - self.separator = separator - } -} - -extension InterspersedSequence: Sequence { - /// The iterator for an `InterspersedSequence` sequence. - public struct Iterator: IteratorProtocol { - @usableFromInline - internal enum State { - case start - case element(Base.Element) - case separator - } - - @usableFromInline - internal var iterator: Base.Iterator - - @usableFromInline - internal let separator: Base.Element - - @usableFromInline - internal var state = State.start - - @inlinable - internal init(iterator: Base.Iterator, separator: Base.Element) { - self.iterator = iterator - self.separator = separator - } - - @inlinable - public mutating func next() -> Base.Element? { - // After the start, the state flips between element and separator. Before - // returning a separator, a check is made for the next element as a - // separator is only returned between two elements. The next element is - // stored to allow it to be returned in the next iteration. - switch state { - case .start: - state = .separator - return iterator.next() - case .separator: - guard let next = iterator.next() else { return nil } - state = .element(next) - return separator - case .element(let element): - state = .separator - return element - } - } - } - - @inlinable - public func makeIterator() -> InterspersedSequence.Iterator { - Iterator(iterator: base.makeIterator(), separator: separator) - } -} - -extension InterspersedSequence: Collection where Base: Collection { - /// A position in an `InterspersedSequence` instance. - public struct Index: Comparable { - @usableFromInline - internal enum Representation: Equatable { - case element(Base.Index) - case separator(next: Base.Index) - } - - @usableFromInline - internal let representation: Representation - - @inlinable - internal init(representation: Representation) { - self.representation = representation - } - - @inlinable - public static func < (lhs: Index, rhs: Index) -> Bool { - switch (lhs.representation, rhs.representation) { - case let (.element(li), .element(ri)), - let (.separator(next: li), .separator(next: ri)), - let (.element(li), .separator(next: ri)): - return li < ri - case let (.separator(next: li), .element(ri)): - return li <= ri - } - } - - @inlinable - internal static func element(_ index: Base.Index) -> Self { - Self(representation: .element(index)) - } - - @inlinable - internal static func separator(next: Base.Index) -> Self { - Self(representation: .separator(next: next)) - } - } - - @inlinable - public var startIndex: Index { - base.startIndex == base.endIndex ? endIndex : .element(base.startIndex) - } - - @inlinable - public var endIndex: Index { - .separator(next: base.endIndex) - } - - @inlinable - public func index(after i: Index) -> Index { - precondition(i != endIndex, "Can't advance past endIndex") - switch i.representation { - case let .element(index): - return .separator(next: base.index(after: index)) - case let .separator(next): - return .element(next) - } - } - - @inlinable - public subscript(position: Index) -> Element { - switch position.representation { - case .element(let index): return base[index] - case .separator: return separator - } - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - switch (start.representation, end.representation) { - case let (.element(element), .separator(next: separator)): - return 2 * base.distance(from: element, to: separator) - 1 - case let (.separator(next: separator), .element(element)): - return 2 * base.distance(from: separator, to: element) + 1 - case let (.element(start), .element(end)), - let (.separator(start), .separator(end)): - return 2 * base.distance(from: start, to: end) - } - } - - @inlinable - public func index(_ index: Index, offsetBy distance: Int) -> Index { - distance >= 0 - ? offsetForward(index, by: distance) - : offsetBackward(index, by: -distance) - } - - @inlinable - public func index( - _ index: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - if distance >= 0 { - return limit >= index - ? offsetForward(index, by: distance, limitedBy: limit) - : offsetForward(index, by: distance) - } else { - return limit <= index - ? offsetBackward(index, by: -distance, limitedBy: limit) - : offsetBackward(index, by: -distance) - } - } - - @inlinable - internal func offsetForward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetForward(i, by: distance, limitedBy: endIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetBackward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetBackward(i, by: distance, limitedBy: startIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetForward( - _ index: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance >= 0) - assert(limit >= index) - - switch (index.representation, limit.representation, distance.isMultiple(of: 2)) { - case let (.element(index), .element(limit), true), - let (.separator(next: index), .element(limit), false): - return base.index(index, offsetBy: distance / 2, limitedBy: limit) - .map { .element($0) } - - case let (.element(index), .element(limit), false), - let (.element(index), .separator(next: limit), false), - let (.separator(next: index), .element(limit), true), - let (.separator(next: index), .separator(next: limit), true): - return base.index(index, offsetBy: (distance + 1) / 2, limitedBy: limit) - .map { .separator(next: $0) } - - case let (.element(index), .separator(next: limit), true), - let (.separator(next: index), .separator(next: limit), false): - return base.index(index, offsetBy: distance / 2, limitedBy: limit) - .flatMap { $0 == limit ? nil : .element($0) } - } - } - - @inlinable - internal func offsetBackward( - _ index: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance >= 0) - assert(limit <= index) - - switch (index.representation, limit.representation, distance.isMultiple(of: 2)) { - case let (.element(index), .element(limit), true), - let (.element(index), .separator(next: limit), true), - let (.separator(next: index), .element(limit), false), - let (.separator(next: index), .separator(next: limit), false): - return base.index(index, offsetBy: -((distance + 1) / 2), limitedBy: limit) - .map { .element($0) } - - case let (.element(index), .separator(next: limit), false), - let (.separator(next: index), .separator(next: limit), true): - return base.index(index, offsetBy: -(distance / 2), limitedBy: limit) - .map { .separator(next: $0) } - - case let (.element(index), .element(limit), false), - let (.separator(next: index), .element(limit), true): - return base.index(index, offsetBy: -(distance / 2), limitedBy: limit) - .flatMap { $0 == limit ? nil : .separator(next: $0) } - } - } -} - -extension InterspersedSequence: BidirectionalCollection - where Base: BidirectionalCollection -{ - @inlinable - public func index(before i: Index) -> Index { - precondition(i != startIndex, "Can't move before startIndex") - switch i.representation { - case let .element(index): - return .separator(next: index) - case let .separator(next): - return .element(base.index(before: next)) - } - } -} - -extension InterspersedSequence: RandomAccessCollection - where Base: RandomAccessCollection {} - -extension InterspersedSequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -extension InterspersedSequence: LazyCollectionProtocol - where Base: LazySequenceProtocol & Collection {} - -//===----------------------------------------------------------------------===// -// InterspersedMap -//===----------------------------------------------------------------------===// - - -/// A sequence over the results of applying a closure to the sequence's -/// elements, with a separator that separates each pair of adjacent transformed -/// values. -@usableFromInline -internal struct InterspersedMapSequence { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let transform: (Base.Element) -> Result - - @usableFromInline - internal let separator: (Base.Element, Base.Element) -> Result -} - -extension InterspersedMapSequence: Sequence { - @usableFromInline - internal struct Iterator: IteratorProtocol { - @usableFromInline - internal enum State { - case start - case element(Base.Element) - case separator(previous: Base.Element) - } - - @usableFromInline - internal var base: Base.Iterator - - @usableFromInline - internal let transform: (Base.Element) -> Result - - @usableFromInline - internal let separator: (Base.Element, Base.Element) -> Result - - @usableFromInline - internal var state = State.start - - @inlinable - internal init( - base: Base.Iterator, - transform: @escaping (Base.Element) -> Result, - separator: @escaping (Base.Element, Base.Element) -> Result - ) { - self.base = base - self.transform = transform - self.separator = separator - } - - @inlinable - internal mutating func next() -> Result? { - switch state { - case .start: - guard let first = base.next() else { return nil } - state = .separator(previous: first) - return transform(first) - case .separator(let previous): - guard let next = base.next() else { return nil } - state = .element(next) - return separator(previous, next) - case .element(let element): - state = .separator(previous: element) - return transform(element) - } - } - } - - @inlinable - internal func makeIterator() -> Iterator { - Iterator( - base: base.makeIterator(), - transform: transform, - separator: separator) - } -} - -extension InterspersedMapSequence: Collection where Base: Collection { - @usableFromInline - internal struct Index: Comparable { - @usableFromInline - internal enum Representation { - case element(Base.Index) - case separator(previous: Base.Index, next: Base.Index) - } - - @usableFromInline - internal let representation: Representation - - @inlinable - internal init(representation: Representation) { - self.representation = representation - } - - @inlinable - internal static func element(_ index: Base.Index) -> Self { - Self(representation: .element(index)) - } - - @inlinable - internal static func separator(previous: Base.Index, next: Base.Index) -> Self { - Self(representation: .separator(previous: previous, next: next)) - } - - @inlinable - internal static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs.representation, rhs.representation) { - case let (.element(lhs), .element(rhs)), - let (.separator(_, next: lhs), .separator(_, next: rhs)): - return lhs == rhs - case (.element, .separator), (.separator, .element): - return false - } - } - - @inlinable - internal static func < (lhs: Self, rhs: Self) -> Bool { - switch (lhs.representation, rhs.representation) { - case let (.element(lhs), .element(rhs)), - let (.separator(_, next: lhs), .separator(_, next: rhs)), - let (.element(lhs), .separator(_, next: rhs)), - let (.separator(previous: lhs, _), .element(rhs)): - return lhs < rhs - } - } - } - - @inlinable - internal var startIndex: Index { - base.isEmpty ? endIndex : .element(base.startIndex) - } - - @inlinable - internal var endIndex: Index { - .separator(previous: base.endIndex, next: base.endIndex) - } - - @inlinable - internal func index(after index: Index) -> Index { - switch index.representation { - case .element(let index): - let next = base.index(after: index) - return .separator(previous: index, next: next) - case .separator(_, let next): - return .element(next) - } - } - - @inlinable - internal subscript(position: Index) -> Result { - switch position.representation { - case .element(let index): - return transform(base[index]) - case let .separator(previous, next): - return separator(base[previous], base[next]) - } - } - - @inlinable - internal func distance(from start: Index, to end: Index) -> Int { - switch (start.representation, end.representation) { - case let (.element(lhs), .element(rhs)), - let (.separator(_, next: lhs), .separator(_, next: rhs)): - return 2 * base.distance(from: lhs, to: rhs) - case let (.element(lhs), .separator(_, next: rhs)): - return 2 * base.distance(from: lhs, to: rhs) - 1 - case let (.separator(_, next: lhs), .element(rhs)): - return 2 * base.distance(from: lhs, to: rhs) + 1 - } - } - - @inlinable - internal func index(_ index: Index, offsetBy distance: Int) -> Index { - guard distance != 0 else { return index } - - return distance > 0 - ? offsetForward(index, by: distance) - : offsetBackward(index, by: -distance) - } - - @inlinable - internal func index( - _ index: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - guard distance != 0 else { return index } - - if distance > 0 { - return limit >= index - ? offsetForward(index, by: distance, limitedBy: limit) - : offsetForward(index, by: distance) - } else { - return limit <= index - ? offsetBackward(index, by: -distance, limitedBy: limit) - : offsetBackward(index, by: -distance) - } - } - - @inlinable - internal func offsetForward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetForward(i, by: distance, limitedBy: endIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetBackward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetBackward(i, by: distance, limitedBy: startIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetForward( - _ index: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance > 0) - assert(limit >= index) - - switch (index.representation, limit.representation, distance.isMultiple(of: 2)) { - case let (.element(index), .element(limit), true), - let (.separator(_, next: index), .element(limit), false): - return base.index(index, offsetBy: distance / 2, limitedBy: limit) - .map { .element($0) } - - case let (.element(index), .element(limit), false), - let (.element(index), .separator(_, next: limit), false), - let (.separator(_, next: index), .element(limit), true), - let (.separator(_, next: index), .separator(_, next: limit), true): - return base.index(index, offsetBy: (distance - 1) / 2, limitedBy: limit) - .flatMap { - guard $0 != limit else { return nil } - let next = base.index(after: $0) - return next == base.endIndex - ? endIndex - : .separator(previous: $0, next: next) - } - - case let (.element(index), .separator(_, next: limit), true), - let (.separator(_, next: index), .separator(_, next: limit), false): - return base.index(index, offsetBy: distance / 2, limitedBy: limit) - .flatMap { $0 == limit ? nil : .element($0) } - } - } - - @inlinable - internal func offsetBackward( - _ index: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance > 0) - assert(limit <= index) - - switch (index.representation, limit.representation, distance.isMultiple(of: 2)) { - case let (.element(index), .element(limit), true), - let (.element(index), .separator(_, next: limit), true), - let (.separator(_, next: index), .element(limit), false), - let (.separator(_, next: index), .separator(_, next: limit), false): - return base.index(index, offsetBy: -((distance + 1) / 2), limitedBy: limit) - .map { .element($0) } - - case let (.element(index), .separator(_, next: limit), false), - let (.separator(_, next: index), .separator(_, next: limit), true): - return base.index(index, offsetBy: -(distance / 2), limitedBy: limit) - .map { .separator(previous: base.index($0, offsetBy: -1), next: $0) } - - case let (.element(index), .element(limit), false), - let (.separator(_, next: index), .element(limit), true): - return base.index(index, offsetBy: -(distance / 2), limitedBy: limit) - .flatMap { - $0 == limit - ? nil - : .separator(previous: base.index($0, offsetBy: -1), next: $0) - } - } - } -} - -extension InterspersedMapSequence: BidirectionalCollection - where Base: BidirectionalCollection -{ - @inlinable - internal func index(before index: Index) -> Index { - switch index.representation { - case .element(let index): - let previous = base.index(before: index) - return .separator(previous: previous, next: index) - case let .separator(previous, next): - let index = next == base.endIndex ? base.index(before: next) : previous - return .element(index) - } - } -} - -extension InterspersedMapSequence.Index: Hashable - where Base.Index: Hashable -{ - @inlinable - internal func hash(into hasher: inout Hasher) { - switch representation { - case .element(let base): - hasher.combine(false) - hasher.combine(base) - case .separator(_, let next): - hasher.combine(true) - hasher.combine(next) - } - } -} - -extension InterspersedMapSequence: LazySequenceProtocol {} -extension InterspersedMapSequence: LazyCollectionProtocol - where Base: Collection {} - -//===----------------------------------------------------------------------===// -// interspersed(with:) -//===----------------------------------------------------------------------===// - -extension Sequence { - /// Returns a sequence containing elements of this sequence with the given - /// separator inserted in between each element. - /// - /// Any value of the sequence's element type can be used as the separator. - /// - /// ``` - /// for value in [1,2,3].interspersed(with: 0) { - /// print(value) - /// } - /// // 1 - /// // 0 - /// // 2 - /// // 0 - /// // 3 - /// ``` - /// - /// The following shows a String being interspersed with a Character: - /// ``` - /// let result = "ABCDE".interspersed(with: "-") - /// print(String(result)) - /// // "A-B-C-D-E" - /// ``` - /// - /// - Parameter separator: Value to insert in between each of this sequence’s - /// elements. - /// - Returns: The interspersed sequence of elements. - /// - /// - Complexity: O(1) - @inlinable - public func interspersed( - with separator: Element - ) -> InterspersedSequence { - InterspersedSequence(base: self, separator: separator) - } -} - -//===----------------------------------------------------------------------===// -// lazy.interspersedMap(_:with:) -//===----------------------------------------------------------------------===// - -extension LazySequenceProtocol { - /// Returns a sequence over the results of applying a closure to the - /// sequence's elements, with a separator that separates each pair of adjacent - /// transformed values. - /// - /// The transformation closure lets you intersperse a sequence using a - /// separator of a different type than the original's sequence's elements. - /// Each separator is produced by a closure that is given access to the - /// two elements in the original sequence right before and after it. - /// - /// let strings = [1, 2, 2].interspersedMap(String.init, - /// with: { $0 == $1 ? " == " : " != " }) - /// print(strings.joined()) // "1 != 2 == 2" - /// - @usableFromInline - internal func interspersedMap( - _ transform: @escaping (Element) -> Result, - with separator: @escaping (Element, Element) -> Result - ) -> InterspersedMapSequence { - InterspersedMapSequence( - base: elements, - transform: transform, - separator: separator) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Joined.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Joined.swift deleted file mode 100644 index 13f2f69b9..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Joined.swift +++ /dev/null @@ -1,489 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// JoinedBySequence -//===----------------------------------------------------------------------===// - -/// A sequence that presents the elements of a base sequence of sequences -/// concatenated using a given separator. -public struct JoinedBySequence - where Base.Element: Sequence, Base.Element.Element == Separator.Element -{ - @usableFromInline - internal typealias Inner = FlattenSequence>>> - - @usableFromInline - internal let inner: Inner - - @inlinable - internal init(base: Base, separator: Separator) { - self.inner = base.lazy - .map(EitherSequence.left) - .interspersed(with: .right(separator)) - .joined() - } -} - -extension JoinedBySequence: Sequence { - public struct Iterator: IteratorProtocol { - @usableFromInline - internal var inner: Inner.Iterator - - @inlinable - internal init(inner: Inner.Iterator) { - self.inner = inner - } - - @inlinable - public mutating func next() -> Base.Element.Element? { - inner.next() - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(inner: inner.makeIterator()) - } -} - -extension JoinedBySequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -//===----------------------------------------------------------------------===// -// JoinedByClosureSequence -//===----------------------------------------------------------------------===// - -/// A sequence that presents the elements of a base sequence of sequences -/// concatenated using a given separator that depends on the sequences right -/// before and after it. -public struct JoinedByClosureSequence - where Base.Element: Sequence, Base.Element.Element == Separator.Element -{ - @usableFromInline - internal typealias Inner = FlattenSequence>> - - @usableFromInline - internal let inner: Inner - - @inlinable - internal init( - base: Base, - separator: @escaping (Base.Element, Base.Element) -> Separator - ) { - self.inner = base.lazy - .interspersedMap( - EitherSequence.left, - with: { EitherSequence.right(separator($0, $1)) }) - .joined() - } -} - -extension JoinedByClosureSequence: Sequence { - public struct Iterator: IteratorProtocol { - @usableFromInline - internal var inner: Inner.Iterator - - @inlinable - internal init(inner: Inner.Iterator) { - self.inner = inner - } - - @inlinable - public mutating func next() -> Base.Element.Element? { - inner.next() - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(inner: inner.makeIterator()) - } -} - -extension JoinedByClosureSequence: LazySequenceProtocol {} - -//===----------------------------------------------------------------------===// -// JoinedByCollection -//===----------------------------------------------------------------------===// - -/// A collection that presents the elements of a base collection of collections -/// concatenated using a given separator. -public struct JoinedByCollection - where Base.Element: Collection, Base.Element.Element == Separator.Element -{ - @usableFromInline - internal typealias Inner = FlattenCollection>>> - - @usableFromInline - internal let inner: Inner - - @inlinable - internal init(base: Base, separator: Separator) { - self.inner = base.lazy - .map(EitherSequence.left) - .interspersed(with: .right(separator)) - .joined() - } -} - -extension JoinedByCollection: Collection { - public struct Index: Comparable { - @usableFromInline - internal let inner: Inner.Index - - @inlinable - internal init(_ inner: Inner.Index) { - self.inner = inner - } - - @inlinable - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.inner == rhs.inner - } - - @inlinable - public static func < (lhs: Self, rhs: Self) -> Bool { - lhs.inner < rhs.inner - } - } - - @inlinable - public var startIndex: Index { - Index(inner.startIndex) - } - - @inlinable - public var endIndex: Index { - Index(inner.endIndex) - } - - @inlinable - public func index(after index: Index) -> Index { - Index(inner.index(after: index.inner)) - } - - @inlinable - public subscript(position: Index) -> Base.Element.Element { - inner[position.inner] - } - - @inlinable - public func index(_ index: Index, offsetBy distance: Int) -> Index { - Index(inner.index(index.inner, offsetBy: distance)) - } - - @inlinable - public func index( - _ index: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - inner.index(index.inner, offsetBy: distance, limitedBy: limit.inner) - .map(Index.init) - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - inner.distance(from: start.inner, to: end.inner) - } -} - -extension JoinedByCollection: BidirectionalCollection - where Base: BidirectionalCollection, - Base.Element: BidirectionalCollection, - Separator: BidirectionalCollection -{ - @inlinable - public func index(before index: Index) -> Index { - Index(inner.index(before: index.inner)) - } -} - -extension JoinedByCollection: LazySequenceProtocol, LazyCollectionProtocol - where Base: LazySequenceProtocol {} - -//===----------------------------------------------------------------------===// -// JoinedByClosureCollection -//===----------------------------------------------------------------------===// - -/// A collection that presents the elements of a base collection of collections -/// concatenated using a given separator that depends on the collections right -/// before and after it. -public struct JoinedByClosureCollection - where Base.Element: Collection, Base.Element.Element == Separator.Element -{ - @usableFromInline - internal typealias Inner = FlattenCollection>> - - @usableFromInline - internal let inner: Inner - - @inlinable - internal init( - base: Base, - separator: @escaping (Base.Element, Base.Element) -> Separator - ) { - self.inner = base.lazy - .interspersedMap( - EitherSequence.left, - with: { EitherSequence.right(separator($0, $1)) }) - .joined() - } -} - -extension JoinedByClosureCollection: Collection { - public struct Index: Comparable { - @usableFromInline - internal let inner: Inner.Index - - @inlinable - internal init(_ inner: Inner.Index) { - self.inner = inner - } - - @inlinable - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.inner == rhs.inner - } - - @inlinable - public static func < (lhs: Self, rhs: Self) -> Bool { - lhs.inner < rhs.inner - } - } - - @inlinable - public var startIndex: Index { - Index(inner.startIndex) - } - - @inlinable - public var endIndex: Index { - Index(inner.endIndex) - } - - @inlinable - public func index(after index: Index) -> Index { - Index(inner.index(after: index.inner)) - } - - @inlinable - public subscript(position: Index) -> Base.Element.Element { - inner[position.inner] - } - - @inlinable - public func index(_ index: Index, offsetBy distance: Int) -> Index { - Index(inner.index(index.inner, offsetBy: distance)) - } - - @inlinable - public func index( - _ index: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - inner.index(index.inner, offsetBy: distance, limitedBy: limit.inner) - .map(Index.init) - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - inner.distance(from: start.inner, to: end.inner) - } -} - -extension JoinedByClosureCollection: BidirectionalCollection - where Base: BidirectionalCollection, - Base.Element: BidirectionalCollection, - Separator: BidirectionalCollection -{ - @inlinable - public func index(before index: Index) -> Index { - Index(inner.index(before: index.inner)) - } -} - -extension JoinedByClosureCollection: LazyCollectionProtocol {} - -//===----------------------------------------------------------------------===// -// Sequence.joined(by:) -//===----------------------------------------------------------------------===// - -extension Sequence where Element: Sequence { - /// Returns the concatenation of the elements in this sequence of sequences, - /// inserting the given separator between each sequence. - /// - /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: 100) { - /// print(x) - /// } - /// // 1, 2, 100, 3, 4, 100, 5, 6 - @inlinable - public func joined(by separator: Element.Element) - -> JoinedBySequence> - { - joined(by: CollectionOfOne(separator)) - } - - /// Returns the concatenation of the elements in this sequence of sequences, - /// inserting the given separator between each sequence. - /// - /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: [100, 200]) { - /// print(x) - /// } - /// // 1, 2, 100, 200, 3, 4, 100, 200, 5, 6 - @inlinable - public func joined( - by separator: Separator - ) -> JoinedBySequence - where Separator: Collection, Separator.Element == Element.Element - { - JoinedBySequence(base: self, separator: separator) - } - - @inlinable - internal func _joined( - by update: (inout [Element.Element], Element, Element) throws -> Void - ) rethrows -> [Element.Element] { - var iterator = makeIterator() - guard let first = iterator.next() else { return [] } - - var result = Array(first) - var previous = first - - while let next = iterator.next() { - try update(&result, previous, next) - result.append(contentsOf: next) - previous = next - } - - return result - } - - /// Returns the concatenation of the elements in this sequence of sequences, - /// inserting the separator produced by the closure between each sequence. - /// - /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: { $0.last! * $1.first! }) { - /// print(x) - /// } - /// // 1, 2, 6, 3, 4, 20, 5, 6 - @inlinable - public func joined( - by separator: (Element, Element) throws -> Element.Element - ) rethrows -> [Element.Element] { - try _joined(by: { $0.append(try separator($1, $2)) }) - } - - /// Returns the concatenation of the elements in this sequence of sequences, - /// inserting the separator produced by the closure between each sequence. - /// - /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: { [100 * $0.last!, 100 * $1.first!] }) { - /// print(x) - /// } - /// // 1, 2, 200, 300, 3, 4, 400, 500, 5, 6 - @inlinable - public func joined( - by separator: (Element, Element) throws -> Separator - ) rethrows -> [Element.Element] - where Separator: Sequence, Separator.Element == Element.Element - { - try _joined(by: { $0.append(contentsOf: try separator($1, $2)) }) - } -} - -//===----------------------------------------------------------------------===// -// LazySequenceProtocol.joined(by:) -//===----------------------------------------------------------------------===// - -extension LazySequenceProtocol where Element: Sequence { - /// Returns the concatenation of the elements in this sequence of sequences, - /// inserting the separator produced by the closure between each sequence. - @inlinable - public func joined( - by separator: @escaping (Element, Element) -> Element.Element - ) -> JoinedByClosureSequence> { - joined(by: { CollectionOfOne(separator($0, $1)) }) - } - - /// Returns the concatenation of the elements in this sequence of sequences, - /// inserting the separator produced by the closure between each sequence. - @inlinable - public func joined( - by separator: @escaping (Element, Element) -> Separator - ) -> JoinedByClosureSequence { - JoinedByClosureSequence(base: elements, separator: separator) - } -} - -//===----------------------------------------------------------------------===// -// Collection.joined(by:) -//===----------------------------------------------------------------------===// - -extension Collection where Element: Collection { - /// Returns the concatenation of the elements in this collection of - /// collections, inserting the given separator between each collection. - /// - /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: 100) { - /// print(x) - /// } - /// // 1, 2, 100, 3, 4, 100, 5, 6 - @inlinable - public func joined(by separator: Element.Element) - -> JoinedByCollection> - { - joined(by: CollectionOfOne(separator)) - } - - /// Returns the concatenation of the elements in this collection of - /// collections, inserting the given separator between each collection. - /// - /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: [100, 200]) { - /// print(x) - /// } - /// // 1, 2, 100, 200, 3, 4, 100, 200, 5, 6 - @inlinable - public func joined(by separator: Separator) - -> JoinedByCollection - { - JoinedByCollection(base: self, separator: separator) - } -} - -//===----------------------------------------------------------------------===// -// LazySequenceProtocol.joined(by:) where Self: Collection -//===----------------------------------------------------------------------===// - -extension LazySequenceProtocol where Elements: Collection, Element: Collection { - /// Returns the concatenation of the elements in this collection of - /// collections, inserting the separator produced by the closure between each - /// sequence. - @inlinable - public func joined( - by separator: @escaping (Element, Element) -> Element.Element - ) -> JoinedByClosureCollection> { - joined(by: { CollectionOfOne(separator($0, $1)) }) - } - - /// Returns the concatenation of the elements in this collection of - /// collections, inserting the separator produced by the closure between each - /// sequence. - @inlinable - public func joined( - by separator: @escaping (Element, Element) -> Separator - ) -> JoinedByClosureCollection { - JoinedByClosureCollection(base: elements, separator: separator) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/MinMax.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/MinMax.swift deleted file mode 100644 index 1ca66cb96..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/MinMax.swift +++ /dev/null @@ -1,484 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -extension Sequence { - /// Implementation for min(count:areInIncreasingOrder:) - @inlinable - internal func _minImplementation( - count: Int, - sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool - ) rethrows -> [Element] { - var iterator = makeIterator() - - var result: [Element] = [] - result.reserveCapacity(count) - while result.count < count, let e = iterator.next() { - result.append(e) - } - try result.sort(by: areInIncreasingOrder) - - while let e = iterator.next() { - // To be part of `result`, `e` must be strictly less than `result.last`. - guard try areInIncreasingOrder(e, result.last!) else { continue } - let insertionIndex = - try result.partitioningIndex { try areInIncreasingOrder(e, $0) } - - assert(insertionIndex != result.endIndex) - result.removeLast() - result.insert(e, at: insertionIndex) - } - - return result - } - - /// Implementation for max(count:areInIncreasingOrder:) - @inlinable - internal func _maxImplementation( - count: Int, - sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool - ) rethrows -> [Element] { - var iterator = makeIterator() - - var result: [Element] = [] - result.reserveCapacity(count) - while result.count < count, let e = iterator.next() { - result.append(e) - } - try result.sort(by: areInIncreasingOrder) - - while let e = iterator.next() { - // To be part of `result`, `e` must be greater/equal to `result.first`. - guard try !areInIncreasingOrder(e, result.first!) else { continue } - let insertionIndex = - try result.partitioningIndex { try areInIncreasingOrder(e, $0) } - - assert(insertionIndex > 0) - // Inserting `e` and then removing the first element (or vice versa) - // would perform a double shift, so we manually shift down the elements - // before dropping `e` in. - var i = 1 - while i < insertionIndex { - result[i - 1] = result[i] - i += 1 - } - result[insertionIndex - 1] = e - } - - return result - } - - /// Returns the smallest elements of this sequence, as sorted by the given - /// predicate. - /// - /// This example partially sorts an array of integers to retrieve its three - /// smallest values: - /// - /// let numbers = [7, 1, 6, 2, 8, 3, 9] - /// let smallestThree = numbers.min(count: 3, sortedBy: <) - /// // [1, 2, 3] - /// - /// If you need to sort a sequence but only need to access its smallest - /// elements, using this method can give you a performance boost over sorting - /// the entire sequence. The order of equal elements is guaranteed to be - /// preserved. - /// - /// - Parameters: - /// - count: The number of elements to return. If `count` is greater than - /// the number of elements in this sequence, all of the sequence's - /// elements are returned. - /// - areInIncreasingOrder: A predicate that returns `true` if its - /// first argument should be ordered before its second argument; - /// otherwise, `false`. - /// - Returns: An array of the smallest `count` elements of this sequence, - /// sorted according to `areInIncreasingOrder`. - /// - /// - Complexity: O(*k* log *k* + *nk*), where *n* is the length of the - /// sequence and *k* is `count`. - @inlinable - public func min( - count: Int, - sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool - ) rethrows -> [Element] { - precondition(count >= 0, """ - Cannot find a minimum with a negative count of elements! - """ - ) - - // Do nothing if we're prefixing nothing. - guard count > 0 else { - return [] - } - - return try _minImplementation(count: count, sortedBy: areInIncreasingOrder) - } - - /// Returns the largest elements of this sequence, as sorted by the given - /// predicate. - /// - /// This example partially sorts an array of integers to retrieve its three - /// largest values: - /// - /// let numbers = [7, 1, 6, 2, 8, 3, 9] - /// let smallestThree = numbers.max(count: 3, sortedBy: <) - /// // [7, 8, 9] - /// - /// If you need to sort a sequence but only need to access its largest - /// elements, using this method can give you a performance boost over sorting - /// the entire sequence. The order of equal elements is guaranteed to be - /// preserved. - /// - /// - Parameters: - /// - count: The number of elements to return. If `count` is greater than - /// the number of elements in this sequence, all of the sequence's - /// elements are returned. - /// - areInIncreasingOrder: A predicate that returns `true` if its - /// first argument should be ordered before its second argument; - /// otherwise, `false`. - /// - Returns: An array of the largest `count` elements of this sequence, - /// sorted according to `areInIncreasingOrder`. - /// - /// - Complexity: O(*k* log *k* + *nk*), where *n* is the length of the - /// sequence and *k* is `count`. - @inlinable - public func max( - count: Int, - sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool - ) rethrows -> [Element] { - precondition(count >= 0, """ - Cannot find a maximum with a negative count of elements! - """ - ) - - // Do nothing if we're suffixing nothing. - guard count > 0 else { - return [] - } - - return try _maxImplementation(count: count, sortedBy: areInIncreasingOrder) - } -} - -extension Sequence where Element: Comparable { - /// Returns the smallest elements of this sequence. - /// - /// This example partially sorts an array of integers to retrieve its three - /// smallest values: - /// - /// let numbers = [7, 1, 6, 2, 8, 3, 9] - /// let smallestThree = numbers.min(count: 3) - /// // [1, 2, 3] - /// - /// If you need to sort a sequence but only need to access its smallest - /// elements, using this method can give you a performance boost over sorting - /// the entire sequence. The order of equal elements is guaranteed to be - /// preserved. - /// - /// - Parameter count: The number of elements to return. If `count` is greater - /// than the number of elements in this sequence, all of the sequence's - /// elements are returned. - /// - Returns: An array of the smallest `count` elements of this sequence. - /// - /// - Complexity: O(*k* log *k* + *nk*), where *n* is the length of the - /// sequence and *k* is `count`. - @inlinable - public func min(count: Int) -> [Element] { - min(count: count, sortedBy: <) - } - - /// Returns the largest elements of this sequence. - /// - /// This example partially sorts an array of integers to retrieve its three - /// largest values: - /// - /// let numbers = [7, 1, 6, 2, 8, 3, 9] - /// let smallestThree = numbers.max(count: 3) - /// // [7, 8, 9] - /// - /// If you need to sort a sequence but only need to access its largest - /// elements, using this method can give you a performance boost over sorting - /// the entire sequence. The order of equal elements is guaranteed to be - /// preserved. - /// - /// - Parameter count: The number of elements to return. If `count` is greater - /// than the number of elements in this sequence, all of the sequence's - /// elements are returned. - /// - Returns: An array of the largest `count` elements of this sequence. - /// - /// - Complexity: O(*k* log *k* + *nk*), where *n* is the length of the - /// sequence and *k* is `count`. - @inlinable - public func max(count: Int) -> [Element] { - max(count: count, sortedBy: <) - } -} - -extension Collection { - /// Returns the smallest elements of this collection, as sorted by the given - /// predicate. - /// - /// This example partially sorts an array of integers to retrieve its three - /// smallest values: - /// - /// let numbers = [7, 1, 6, 2, 8, 3, 9] - /// let smallestThree = numbers.min(count: 3, sortedBy: <) - /// // [1, 2, 3] - /// - /// If you need to sort a collection but only need to access its smallest - /// elements, using this method can give you a performance boost over sorting - /// the entire collection. The order of equal elements is guaranteed to be - /// preserved. - /// - /// - Parameters: - /// - count: The number of elements to return. If `count` is greater than - /// the number of elements in this collection, all of the collection's - /// elements are returned. - /// - areInIncreasingOrder: A predicate that returns `true` if its - /// first argument should be ordered before its second argument; - /// otherwise, `false`. - /// - Returns: An array of the smallest `count` elements of this collection, - /// sorted according to `areInIncreasingOrder`. - /// - /// - Complexity: O(*k* log *k* + *nk*), where *n* is the length of the - /// collection and *k* is `count`. - @inlinable - public func min( - count: Int, - sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool - ) rethrows -> [Element] { - precondition(count >= 0, """ - Cannot find a minimum with a negative count of elements! - """ - ) - - // Do nothing if we're prefixing nothing. - guard count > 0 else { - return [] - } - - // Make sure we are within bounds. - let prefixCount = Swift.min(count, self.count) - - // If we're attempting to prefix more than 10% of the collection, it's - // faster to sort everything. - guard prefixCount < (self.count / 10) else { - return Array(try sorted(by: areInIncreasingOrder).prefix(prefixCount)) - } - - return try _minImplementation(count: count, sortedBy: areInIncreasingOrder) - } - - /// Returns the largest elements of this collection, as sorted by the given - /// predicate. - /// - /// This example partially sorts an array of integers to retrieve its three - /// largest values: - /// - /// let numbers = [7, 1, 6, 2, 8, 3, 9] - /// let smallestThree = numbers.max(count: 3, sortedBy: <) - /// // [7, 8, 9] - /// - /// If you need to sort a collection but only need to access its largest - /// elements, using this method can give you a performance boost over sorting - /// the entire collection. The order of equal elements is guaranteed to be - /// preserved. - /// - /// - Parameters: - /// - count: The number of elements to return. If `count` is greater than - /// the number of elements in this collection, all of the collection's - /// elements are returned. - /// - areInIncreasingOrder: A predicate that returns `true` if its - /// first argument should be ordered before its second argument; - /// otherwise, `false`. - /// - Returns: An array of the largest `count` elements of this collection, - /// sorted according to `areInIncreasingOrder`. - /// - /// - Complexity: O(*k* log *k* + *nk*), where *n* is the length of the - /// collection and *k* is `count`. - @inlinable - public func max( - count: Int, - sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool - ) rethrows -> [Element] { - precondition(count >= 0, """ - Cannot find a maximum with a negative count of elements! - """ - ) - - // Do nothing if we're suffixing nothing. - guard count > 0 else { - return [] - } - - // Make sure we are within bounds. - let suffixCount = Swift.min(count, self.count) - - // If we're attempting to prefix more than 10% of the collection, it's - // faster to sort everything. - guard suffixCount < (self.count / 10) else { - return Array(try sorted(by: areInIncreasingOrder).suffix(suffixCount)) - } - - return try _maxImplementation(count: count, sortedBy: areInIncreasingOrder) - } -} - -extension Collection where Element: Comparable { - /// Returns the smallest elements of this collection. - /// - /// This example partially sorts an array of integers to retrieve its three - /// smallest values: - /// - /// let numbers = [7, 1, 6, 2, 8, 3, 9] - /// let smallestThree = numbers.min(count: 3) - /// // [1, 2, 3] - /// - /// If you need to sort a collection but only need to access its smallest - /// elements, using this method can give you a performance boost over sorting - /// the entire collection. The order of equal elements is guaranteed to be - /// preserved. - /// - /// - Parameter count: The number of elements to return. If `count` is greater - /// than the number of elements in this collection, all of the collection's - /// elements are returned. - /// - Returns: An array of the smallest `count` elements of this collection. - /// - /// - Complexity: O(*k* log *k* + *nk*), where *n* is the length of the - /// collection and *k* is `count`. - @inlinable - public func min(count: Int) -> [Element] { - min(count: count, sortedBy: <) - } - - /// Returns the largest elements of this collection. - /// - /// This example partially sorts an array of integers to retrieve its three - /// largest values: - /// - /// let numbers = [7, 1, 6, 2, 8, 3, 9] - /// let smallestThree = numbers.max(count: 3) - /// // [7, 8, 9] - /// - /// If you need to sort a collection but only need to access its largest - /// elements, using this method can give you a performance boost over sorting - /// the entire collection. The order of equal elements is guaranteed to be - /// preserved. - /// - /// - Parameter count: The number of elements to return. If `count` is greater - /// than the number of elements in this collection, all of the collection's - /// elements are returned. - /// - Returns: An array of the largest `count` elements of this collection. - /// - /// - Complexity: O(*k* log *k* + *nk*), where *n* is the length of the - /// collection and *k* is `count`. - @inlinable - public func max(count: Int) -> [Element] { - max(count: count, sortedBy: <) - } -} - -//===----------------------------------------------------------------------===// -// Simultaneous minimum and maximum evaluation -//===----------------------------------------------------------------------===// - -extension Sequence { - /// Returns both the minimum and maximum elements in the sequence, using the - /// given predicate as the comparison between elements. - /// - /// The predicate must be a *strict weak ordering* over the elements. That is, - /// for any elements `a`, `b`, and `c`, the following conditions must hold: - /// - /// - `areInIncreasingOrder(a, a)` is always `false`. (Irreflexivity) - /// - If `areInIncreasingOrder(a, b)` and `areInIncreasingOrder(b, c)` are - /// both `true`, then `areInIncreasingOrder(a, c)` is also - /// `true`. (Transitive comparability) - /// - Two elements are *incomparable* if neither is ordered before the other - /// according to the predicate. If `a` and `b` are incomparable, and `b` - /// and `c` are incomparable, then `a` and `c` are also incomparable. - /// (Transitive incomparability) - /// - /// This example shows how to use the `minAndMax(by:)` method on a dictionary - /// to find the key-value pair with the lowest value and the pair with the - /// highest value. - /// - /// let hues = ["Heliotrope": 296, "Coral": 16, "Aquamarine": 156] - /// if let extremeHues = hues.minAndMax(by: {$0.value < $1.value}) { - /// print(extremeHues.min, extremeHues.max) - /// } else { - /// print("There are no hues") - /// } - /// // Prints: "(key: "Coral", value: 16) (key: "Heliotrope", value: 296)" - /// - /// - Precondition: The sequence is finite. - /// - /// - Parameter areInIncreasingOrder: A predicate that returns `true` if its - /// first argument should be ordered before its second argument; otherwise, - /// `false`. - /// - Returns: A tuple with the sequence's minimum element, followed by its - /// maximum element. If the sequence provides multiple qualifying minimum - /// elements, the first equivalent element is returned; of multiple maximum - /// elements, the last is returned. If the sequence has no elements, the - /// method returns `nil`. - /// - /// - Complexity: O(*n*), where *n* is the length of the sequence. - @inlinable - public func minAndMax( - by areInIncreasingOrder: (Element, Element) throws -> Bool - ) rethrows -> (min: Element, max: Element)? { - // Check short sequences. - var iterator = makeIterator() - guard var lowest = iterator.next() else { return nil } - guard var highest = iterator.next() else { return (lowest, lowest) } - - // Confirm the initial bounds. - if try areInIncreasingOrder(highest, lowest) { swap(&lowest, &highest) } - - // Read the elements in pairwise. Structuring the comparisons around this - // is actually faster than loops based on extracting and testing elements - // one-at-a-time. - while var low = iterator.next() { - var high = iterator.next() ?? low - if try areInIncreasingOrder(high, low) { swap(&low, &high) } - if try areInIncreasingOrder(low, lowest) { lowest = low } - if try !areInIncreasingOrder(high, highest) { highest = high } - } - - return (lowest, highest) - } -} - -extension Sequence where Element: Comparable { - /// Returns both the minimum and maximum elements in the sequence. - /// - /// This example finds the smallest and largest values in an array of height - /// measurements. - /// - /// let heights = [67.5, 65.7, 64.3, 61.1, 58.5, 60.3, 64.9] - /// if let (lowestHeight, greatestHeight) = heights.minAndMax() { - /// print(lowestHeight, greatestHeight) - /// } else { - /// print("The list of heights is empty") - /// } - /// // Prints: "58.5 67.5" - /// - /// - Precondition: The sequence is finite. - /// - /// - Returns: A tuple with the sequence's minimum element, followed by its - /// maximum element. If the sequence provides multiple qualifying minimum - /// elements, the first equivalent element is returned; of multiple maximum - /// elements, the last is returned. If the sequence has no elements, the - /// method returns `nil`. - /// - /// - Complexity: O(*n*), where *n* is the length of the sequence. - @inlinable - public func minAndMax() -> (min: Element, max: Element)? { - minAndMax(by: <) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Partition.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Partition.swift deleted file mode 100644 index 48f1b1ef1..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Partition.swift +++ /dev/null @@ -1,206 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// stablePartition(by:) -//===----------------------------------------------------------------------===// - -extension MutableCollection { - /// Moves all elements satisfying `belongsInSecondPartition` into a suffix of - /// the collection, preserving their relative order, and returns the start of - /// the resulting suffix. - /// - /// - Complexity: O(*n* log *n*), where *n* is the number of elements. - /// - Precondition: - /// `n == distance(from: range.lowerBound, to: range.upperBound)` - @inlinable - internal mutating func stablePartition( - count n: Int, - subrange: Range, - by belongsInSecondPartition: (Element) throws -> Bool - ) rethrows -> Index { - if n == 0 { return subrange.lowerBound } - if n == 1 { - return try belongsInSecondPartition(self[subrange.lowerBound]) - ? subrange.lowerBound - : subrange.upperBound - } - - let h = n / 2, i = index(subrange.lowerBound, offsetBy: h) - let j = try stablePartition( - count: h, - subrange: subrange.lowerBound.., - by belongsInSecondPartition: (Element) throws-> Bool - ) rethrows -> Index { - try stablePartition( - count: distance(from: subrange.lowerBound, to: subrange.upperBound), - subrange: subrange, - by: belongsInSecondPartition) - } - - /// Moves all elements satisfying the given predicate into a suffix of this - /// collection, preserving the relative order of the elements in both - /// partitions, and returns the start of the resulting suffix. - /// - /// - Parameter belongsInSecondPartition: A predicate used to partition the - /// collection. All elements satisfying this predicate are ordered after - /// all elements not satisfying it. - /// - /// - Complexity: O(*n* log *n*), where *n* is the length of this collection. - @inlinable - public mutating func stablePartition( - by belongsInSecondPartition: (Element) throws-> Bool - ) rethrows -> Index { - try stablePartition( - subrange: startIndex.., - by belongsInSecondPartition: (Element) throws -> Bool - ) rethrows -> Index { - // This version of `partition(subrange:)` is half stable; the elements in - // the first partition retain their original relative order. - guard var i = try self[subrange].firstIndex(where: belongsInSecondPartition) - else { return subrange.upperBound } - - var j = index(after: i) - while j != subrange.upperBound { - if try !belongsInSecondPartition(self[j]) { - swapAt(i, j) - formIndex(after: &i) - } - formIndex(after: &j) - } - - return i - } -} - -extension MutableCollection where Self: BidirectionalCollection { - /// Moves all elements satisfying `isSuffixElement` into a suffix of the - /// collection, returning the start position of the resulting suffix. - /// - /// - Complexity: O(*n*) where n is the length of the collection. - @inlinable - public mutating func partition( - subrange: Range, - by belongsInSecondPartition: (Element) throws -> Bool - ) rethrows -> Index { - var lo = subrange.lowerBound - var hi = subrange.upperBound - - // 'Loop' invariants (at start of Loop, all are true): - // * lo < hi - // * predicate(self[i]) == false, for i in startIndex ..< lo - // * predicate(self[i]) == true, for i in hi ..< endIndex - - Loop: while true { - FindLo: do { - while lo < hi { - if try belongsInSecondPartition(self[lo]) { break FindLo } - formIndex(after: &lo) - } - break Loop - } - - FindHi: do { - formIndex(before: &hi) - while lo < hi { - if try !belongsInSecondPartition(self[hi]) { break FindHi } - formIndex(before: &hi) - } - break Loop - } - - swapAt(lo, hi) - formIndex(after: &lo) - } - - return lo - } -} - -//===----------------------------------------------------------------------===// -// partitioningIndex(where:) -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns the index of the first element in the collection that matches - /// the predicate. - /// - /// The collection must already be partitioned according to the predicate. - /// That is, there should be an index `i` where for every element in - /// `collection[.. Bool - ) rethrows -> Index { - var n = count - var l = startIndex - - while n > 0 { - let half = n / 2 - let mid = index(l, offsetBy: half) - if try belongsInSecondPartition(self[mid]) { - n = half - } else { - l = index(after: mid) - n -= half + 1 - } - } - return l - } -} - diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Permutations.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Permutations.swift deleted file mode 100644 index 5b985cbd4..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Permutations.swift +++ /dev/null @@ -1,592 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// nextPermutation() -//===----------------------------------------------------------------------===// - -extension MutableCollection - where Self: BidirectionalCollection, Element: Comparable -{ - /// Permutes this collection's elements through all the lexical orderings. - /// - /// Call `nextPermutation()` repeatedly starting with the collection in sorted - /// order. When the full cycle of all permutations has been completed, the - /// collection will be back in sorted order and this method will return - /// `false`. - /// - /// - Returns: A Boolean value indicating whether the collection still has - /// remaining permutations. When this method returns `false`, the collection - /// is in ascending order according to `areInIncreasingOrder`. - /// - /// - Complexity: O(*n*), where *n* is the length of the collection. - @inlinable - internal mutating func nextPermutation(upperBound: Index? = nil) -> Bool { - // Ensure we have > 1 element in the collection. - guard !isEmpty else { return false } - var i = index(before: endIndex) - if i == startIndex { return false } - - let upperBound = upperBound ?? endIndex - - while true { - let ip1 = i - formIndex(before: &i) - - // Find the last ascending pair (ie. ..., a, b, ... where a < b) - if self[i] < self[ip1] { - // Find the last element greater than self[i] - // This is _always_ at most `ip1` due to if statement above - let j = lastIndex(where: { self[i] < $0 })! - - // At this point we have something like this: - // 0, 1, 4, 3, 2 - // ^ ^ - // i j - swapAt(i, j) - self.reverse(subrange: ip1 ..< endIndex) - - // Only return if we've made a change within .. -//===----------------------------------------------------------------------===// - -/// A sequence of all the permutations of a collection's elements. -public struct PermutationsSequence { - /// The base collection to iterate over for permutations. - @usableFromInline - internal let base: Base - - @usableFromInline - internal let baseCount: Int - - /// The range of accepted sizes of permutations. - /// - /// - Note: This may be empty if the attempted range entirely exceeds the - /// bounds of the size of the `base` collection. - @usableFromInline - internal let kRange: Range - - /// Initializes a `PermutationsSequence` for all permutations of `base` of - /// size `k`. - /// - /// - Parameters: - /// - base: The collection to iterate over for permutations - /// - k: The expected size of each permutation, or `nil` (default) to - /// iterate over all permutations of the same size as the base collection. - @inlinable - internal init(_ base: Base, k: Int? = nil) { - let kRange: ClosedRange? - if let countToChoose = k { - kRange = countToChoose ... countToChoose - } else { - kRange = nil - } - self.init(base, kRange: kRange) - } - - /// Initializes a `PermutationsSequence` for all combinations of `base` of - /// sizes within a given range. - /// - /// - Parameters: - /// - base: The collection to iterate over for permutations. - /// - kRange: The range of accepted sizes of permutations, or `nil` to - /// iterate over all permutations of the same size as the base collection. - @inlinable - internal init( - _ base: Base, kRange: R? - ) where R.Bound == Int { - self.base = base - let baseCount = base.count - self.baseCount = baseCount - let upperBound = baseCount + 1 - self.kRange = kRange?.relative(to: 0 ..< .max) - .clamped(to: 0 ..< upperBound) ?? - baseCount ..< upperBound - } - - /// The total number of permutations. - @inlinable - public var count: Int { - kRange.map { - stride(from: baseCount, to: baseCount - $0, by: -1).reduce(1, *) - }.reduce(0, +) - } -} - -extension PermutationsSequence: Sequence { - /// The iterator for a `PermutationsSequence` instance. - public struct Iterator: IteratorProtocol { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let baseCount: Int - - /// The current range of accepted sizes of permutations. - /// - Note: The range is contracted until empty while iterating over - /// permutations of different sizes. When the range is empty, iteration is - /// finished. - @usableFromInline - internal var kRange: Range - - /// Whether or not iteration is finished (`kRange` is empty) - @inlinable - internal var isFinished: Bool { - return kRange.isEmpty - } - - @usableFromInline - internal var indexes: [Base.Index] - - @inlinable - internal init(_ permutations: PermutationsSequence) { - self.base = permutations.base - self.baseCount = permutations.baseCount - self.kRange = permutations.kRange - self.indexes = Array(permutations.base.indices) - } - - /// Advances the `indexes` array such that the first `countToChoose` - /// elements contain the next lexicographic ordering of elements. - /// - /// Uses the SEP(n,k) algorithm, as described in: - /// https://alistairisrael.wordpress.com/2009/09/22/simple-efficient-pnk-algorithm/ - /// - /// - Returns: A Boolean value indicating whether `indexes` still has - /// remaining permutations. When this method returns `false`, `indexes` - /// is in ascending order. - /// - /// - Complexity: O(*n*), where *n* is the length of the collection. - @inlinable - internal mutating func nextState() -> Bool { - let countToChoose = self.kRange.lowerBound - let edge = countToChoose - 1 - - // Find first index greater than the one at `edge`. - if let i = indexes[countToChoose...].firstIndex(where: { indexes[edge] < $0 }) { - indexes.swapAt(edge, i) - } else { - indexes.reverse(subrange: countToChoose ..< indexes.endIndex) - - // Find last increasing pair below `edge`. - // TODO: This could be indexes[..= 0 && indexes[lastAscent] >= indexes[lastAscent + 1]) { - lastAscent -= 1 - } - if lastAscent < 0 { - return false - } - - // Find rightmost index less than that at `lastAscent`. - if let i = indexes[lastAscent...].lastIndex(where: { indexes[lastAscent] < $0 }) { - indexes.swapAt(lastAscent, i) - } - indexes.reverse(subrange: (lastAscent + 1) ..< indexes.endIndex) - } - - return true - } - - @inlinable - public mutating func next() -> [Base.Element]? { - guard !isFinished else { return nil } - - /// Advances `kRange` by incrementing its `lowerBound` until the range is - /// empty, when iteration is finished. - func advanceKRange() { - kRange.removeFirst() - indexes = Array(base.indices) - } - - let countToChoose = self.kRange.lowerBound - if countToChoose == 0 { - defer { - advanceKRange() - } - return [] - } - - let permutesFullCollection = (countToChoose == baseCount) - if permutesFullCollection { - // If we're permuting the full collection, each iteration is just a - // call to `nextPermutation` on `indexes`. - defer { - let hasMorePermutations = indexes.nextPermutation() - if !hasMorePermutations { - advanceKRange() - } - } - return indexes.map { base[$0] } - } else { - // Otherwise, return the items at the first `countToChoose` indices and - // advance the state. - defer { - let hasMorePermutations = nextState() - if !hasMorePermutations { - advanceKRange() - } - } - return indexes.prefix(countToChoose).map { base[$0] } - } - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(self) - } -} - -extension PermutationsSequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -//===----------------------------------------------------------------------===// -// permutations(ofCount:) -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns a collection of the permutations of this collection with lengths - /// in the specified range. - /// - /// This example prints the different permutations of one to two elements from - /// an array of three names: - /// - /// let names = ["Alex", "Celeste", "Davide"] - /// for perm in names.permutations(ofCount: 1...2) { - /// print(perm.joined(separator: ", ")) - /// } - /// // Alex - /// // Celeste - /// // Davide - /// // Alex, Celeste - /// // Alex, Davide - /// // Celeste, Alex - /// // Celeste, Davide - /// // Davide, Alex - /// // Davide, Celeste - /// - /// This example prints _all_ the permutations (including an empty array) from - /// an array of numbers: - /// - /// let numbers = [10, 20, 30] - /// for perm in numbers.permutations(ofCount: 0...) { - /// print(perm) - /// } - /// // [] - /// // [10] - /// // [20] - /// // [30] - /// // [10, 20] - /// // [10, 30] - /// // [20, 10] - /// // [20, 30] - /// // [30, 10] - /// // [30, 20] - /// // [10, 20, 30] - /// // [10, 30, 20] - /// // [20, 10, 30] - /// // [20, 30, 10] - /// // [30, 10, 20] - /// // [30, 20, 10] - /// - /// The returned permutations are in ascending order by length, and then - /// lexicographically within each group of the same length. - /// - /// - Parameter kRange: A range of the number of elements to include in each - /// permutation. `kRange` can be any integer range expression, and is - /// clamped to the number of elements in this collection. Passing a range - /// covering sizes greater than the number of elements in this collection - /// results in an empty sequence. - /// - /// - Complexity: O(1) for random-access base collections. O(*n*) where *n* - /// is the number of elements in the base collection, since - /// `PermutationsSequence` accesses the `count` of the base collection. - @inlinable - public func permutations( - ofCount kRange: R - ) -> PermutationsSequence where R.Bound == Int { - PermutationsSequence(self, kRange: kRange) - } - - /// Returns a collection of the permutations of this collection of the - /// specified length. - /// - /// This example prints the different permutations of two elements from an - /// array of three names: - /// - /// let names = ["Alex", "Celeste", "Davide"] - /// for perm in names.permutations(ofCount: 2) { - /// print(perm.joined(separator: ", ")) - /// } - /// // Alex, Celeste - /// // Alex, Davide - /// // Celeste, Alex - /// // Celeste, Davide - /// // Davide, Alex - /// // Davide, Celeste - /// - /// The permutations present the elements in increasing lexicographic order - /// of the collection's original ordering (rather than the order of the - /// elements themselves). The first permutation will always consist of - /// elements in their original order, and the last will have the elements in - /// the reverse of their original order. - /// - /// Values that are repeated in the original collection are always treated as - /// separate values in the resulting permutations: - /// - /// let numbers = [20, 10, 10] - /// for perm in numbers.permutations() { - /// print(perm) - /// } - /// // [20, 10, 10] - /// // [20, 10, 10] - /// // [10, 20, 10] - /// // [10, 10, 20] - /// // [10, 20, 10] - /// // [10, 10, 20] - /// - /// If `k` is zero, the resulting sequence has exactly one element, an - /// empty array. If `k` is greater than the number of elements in this - /// sequence, the resulting sequence has no elements. - /// - /// - Parameter k: The number of elements to include in each permutation. - /// If `k` is `nil`, the resulting sequence represents permutations of this - /// entire collection. If `k` is greater than the number of elements in - /// this collection, the resulting sequence is empty. - /// - /// - Complexity: O(1) for random-access base collections. O(*n*) where *n* - /// is the number of elements in the base collection, since - /// `PermutationsSequence` accesses the `count` of the base collection. - @inlinable - public func permutations(ofCount k: Int? = nil) -> PermutationsSequence { - precondition( - k ?? 0 >= 0, - "Can't have permutations with a negative number of elements.") - return PermutationsSequence(self, k: k) - } -} - -//===----------------------------------------------------------------------===// -// uniquePermutations() -//===----------------------------------------------------------------------===// - -/// A sequence of the unique permutations of the elements of a sequence or -/// collection. -/// -/// To create a `UniquePermutationsSequence` instance, call one of the -/// `uniquePermutations` methods on your collection. -public struct UniquePermutationsSequence { - /// The base collection to iterate over for permutations. - @usableFromInline - internal let base: Base - - @usableFromInline - internal var indexes: [Base.Index] - - @usableFromInline - internal let kRange: Range -} - -extension UniquePermutationsSequence where Base.Element: Hashable { - @inlinable - internal static func _indexes(_ base: Base) -> [Base.Index] { - let firstIndexesAndCountsByElement = Dictionary( - base.indices.lazy.map { (base[$0], ($0, 1)) }, - uniquingKeysWith: { indexAndCount, _ in (indexAndCount.0, indexAndCount.1 + 1) }) - - return firstIndexesAndCountsByElement - .values.sorted(by: { $0.0 < $1.0 }) - .flatMap { index, count in repeatElement(index, count: count) } - } - - @inlinable - internal init(_ elements: Base) { - self.indexes = Self._indexes(elements) - self.base = elements - self.kRange = self.indexes.count ..< (self.indexes.count + 1) - } - - @inlinable - internal init(_ base: Base, _ range: R) - where R.Bound == Int - { - self.indexes = Self._indexes(base) - self.base = base - - let upperBound = self.indexes.count + 1 - self.kRange = range.relative(to: 0 ..< .max) - .clamped(to: 0 ..< upperBound) - } -} - -extension UniquePermutationsSequence: Sequence { - /// The iterator for a `UniquePermutationsSequence` instance. - public struct Iterator: IteratorProtocol { - @usableFromInline - internal let base: Base - - @usableFromInline - internal var indexes: [Base.Index] - - @usableFromInline - internal var lengths: Range - - @usableFromInline - internal var initial = true - - @inlinable - internal init(_ elements: Base, indexes: [Base.Index], lengths: Range) { - self.base = elements - self.indexes = indexes - self.lengths = lengths - } - - @inlinable - public mutating func next() -> [Base.Element]? { - // In the end case, `lengths` is an empty range. - if lengths.isEmpty { - return nil - } - - // The first iteration must produce the original sorted array, before any - // permutations. We skip the permutation the first time so that we can - // always mutate the array _before_ returning a slice, which avoids - // copying when possible. - if initial { - initial = false - return indexes[.. Iterator { - Iterator(base, indexes: indexes, lengths: kRange) - } -} - -extension UniquePermutationsSequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -extension Collection where Element: Hashable { - /// Returns a sequence of the unique permutations of this sequence of the - /// specified length. - /// - /// Use this method to iterate over the unique permutations of a sequence - /// with repeating elements. This example prints every unique two-element - /// permutation of an array of numbers: - /// - /// let numbers = [1, 1, 2] - /// for perm in numbers.uniquePermutations(ofCount: 2) { - /// print(perm) - /// } - /// // [1, 1] - /// // [1, 2] - /// // [2, 1] - /// - /// By contrast, the `permutations(ofCount:)` method permutes a collection's - /// elements by position, and can include permutations with equal elements - /// in each permutation: - /// - /// for perm in numbers.permutations(ofCount: 2) - /// print(perm) - /// } - /// // [1, 1] - /// // [1, 1] - /// // [1, 2] - /// // [1, 2] - /// // [2, 1] - /// // [2, 1] - /// - /// The returned permutations are in lexicographically sorted order. - /// - /// - Parameter k: The number of elements to include in each permutation. - /// If `k` is `nil`, the resulting sequence represents permutations of this - /// entire collection. If `k` is greater than the number of elements in - /// this collection, the resulting sequence is empty. - /// - /// - Complexity: O(*n*), where *n* is the number of elements in this - /// collection. - @inlinable - public func uniquePermutations(ofCount k: Int? = nil) - -> UniquePermutationsSequence - { - if let k = k { - return UniquePermutationsSequence(self, k ..< (k + 1)) - } else { - return UniquePermutationsSequence(self) - } - } - - /// Returns a collection of the unique permutations of this sequence with - /// lengths in the specified range. - /// - /// Use this method to iterate over the unique permutations of a sequence - /// with repeating elements. This example prints every unique permutation - /// of an array of numbers with lengths through 2 elements: - /// - /// let numbers = [1, 1, 2] - /// for perm in numbers.uniquePermutations(ofCount: ...2) { - /// print(perm) - /// } - /// // [] - /// // [1] - /// // [2] - /// // [1, 1] - /// // [1, 2] - /// // [2, 1] - /// - /// The returned permutations are in ascending order by length, and then - /// lexicographically within each group of the same length. - /// - /// - Parameter kRange: A range of the number of elements to include in each - /// permutation. `kRange` can be any integer range expression, and is - /// clamped to the number of elements in this collection. Passing a range - /// covering sizes greater than the number of elements in this collection - /// results in an empty sequence. - /// - /// - Complexity: O(*n*), where *n* is the number of elements in this - /// collection. - @inlinable - public func uniquePermutations( - ofCount kRange: R - ) -> UniquePermutationsSequence where R.Bound == Int { - UniquePermutationsSequence(self, kRange) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Product.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Product.swift deleted file mode 100644 index d5611e99d..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Product.swift +++ /dev/null @@ -1,488 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// A sequence that represents the product of two sequences' elements. -public struct Product2Sequence { - /// The outer sequence in the product. - @usableFromInline - internal let base1: Base1 - - /// The inner sequence in the product. - @usableFromInline - internal let base2: Base2 - - @inlinable - internal init(_ base1: Base1, _ base2: Base2) { - self.base1 = base1 - self.base2 = base2 - } -} - -extension Product2Sequence: Sequence { - public typealias Element = (Base1.Element, Base2.Element) - - /// The iterator for a `Product2Sequence` sequence. - public struct Iterator: IteratorProtocol { - @usableFromInline - internal var i1: Base1.Iterator - @usableFromInline - internal var i2: Base2.Iterator - @usableFromInline - internal var element1: Base1.Element? - @usableFromInline - internal let base2: Base2 - - @inlinable - internal init(_ c: Product2Sequence) { - self.base2 = c.base2 - self.i1 = c.base1.makeIterator() - self.i2 = c.base2.makeIterator() - self.element1 = nil - } - - @inlinable - public mutating func next() -> (Base1.Element, - Base2.Element)? { - // This is the initial state, where i1.next() has never - // been called, or the final state, where i1.next() has - // already returned nil. - if element1 == nil { - element1 = i1.next() - // once Base1 is exhausted, return `nil` forever - if element1 == nil { return nil } - } - - // Get the next element from the second sequence, if not - // at end. - if let element2 = i2.next() { - return (element1!, element2) - } - - // We've reached the end of the second sequence, so: - // 1) Get the next element of the first sequence, if exists - // 2) Restart iteration of the second sequence - // 3) Get the first element of the second sequence, if exists - element1 = i1.next() - guard let element1 = element1 - else { return nil } - - i2 = base2.makeIterator() - if let element2 = i2.next() { - return (element1, element2) - } else { - return nil - } - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(self) - } -} - -extension Product2Sequence: Collection where Base1: Collection { - /// The index type for a `Product2Sequence` collection. - public struct Index: Comparable { - @usableFromInline - internal var i1: Base1.Index - @usableFromInline - internal var i2: Base2.Index - - @inlinable - internal init(i1: Base1.Index, i2: Base2.Index) { - self.i1 = i1 - self.i2 = i2 - } - - @inlinable - public static func < (lhs: Index, rhs: Index) -> Bool { - (lhs.i1, lhs.i2) < (rhs.i1, rhs.i2) - } - } - - @inlinable - public var count: Int { - base1.count * base2.count - } - - @inlinable - public var startIndex: Index { - Index( - i1: base2.isEmpty ? base1.endIndex : base1.startIndex, - i2: base2.startIndex) - } - - @inlinable - public var endIndex: Index { - // `base2.startIndex` simplifies index calculations. - Index(i1: base1.endIndex, i2: base2.startIndex) - } - - @inlinable - public subscript(position: Index) -> (Base1.Element, - Base2.Element) { - (base1[position.i1], base2[position.i2]) - } - - /// Forms an index from a pair of base indices, normalizing - /// `(i, base2.endIndex)` to `(base1.index(after: i), base2.startIndex)` if - /// necessary. - @inlinable - internal func normalizeIndex(_ i1: Base1.Index, _ i2: Base2.Index) -> Index { - i2 == base2.endIndex - ? Index(i1: base1.index(after: i1), i2: base2.startIndex) - : Index(i1: i1, i2: i2) - } - - @inlinable - public func index(after i: Index) -> Index { - precondition(i.i1 != base1.endIndex, "Can't advance past endIndex") - return normalizeIndex(i.i1, base2.index(after: i.i2)) - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - guard start.i1 <= end.i1 - else { return -distance(from: end, to: start) } - guard start.i1 != end.i1 - else { return base2.distance(from: start.i2, to: end.i2) } - - // The number of full cycles through `base2` between `start` and `end`, - // excluding the cycles that `start` and `end` are on. - let fullBase2Cycles = base1[start.i1.. [l l l|c c c c c c r r r] - // [l l l c c c c c c r r r] > - // ... > `fullBase2Cycles` times - // [l l l c c c c c c r r r] > - // end.i1 > [l l l c c c c c c|r r r] - // ^ - // end.i2 - - let left = base2[.. [l l l c c c c c c|r r r] - // [l l l c c c c c c r r r] > - // ... > `fullBase2Cycles` times - // [l l l c c c c c c r r r] > - // end.i1 > [l l l|c c c c c c r r r] - // ^ - // end.i2 - - let left = base2[.. 0 else { return right + left } - - let center = base2[end.i2.. Index { - guard distance != 0 else { return i } - - return distance > 0 - ? offsetForward(i, by: distance) - : offsetBackward(i, by: -distance) - } - - @inlinable - public func index( - _ i: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - if distance >= 0 { - return limit >= i - ? offsetForward(i, by: distance, limitedBy: limit) - : offsetForward(i, by: distance) - } else { - return limit <= i - ? offsetBackward(i, by: -distance, limitedBy: limit) - : offsetBackward(i, by: -distance) - } - } - - @inlinable - internal func offsetForward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetForward(i, by: distance, limitedBy: endIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetBackward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetBackward(i, by: distance, limitedBy: startIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetForward( - _ i: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance >= 0) - assert(limit >= i) - - if limit.i1 == i.i1 { - // Delegate to `base2` if the offset is limited to `i.i1`. - // - // i.i2 limit.i2 - // v v - // i.i1 > [x x x|x x x x x x|x x x] - - return base2.index(i.i2, offsetBy: distance, limitedBy: limit.i2) - .map { i2 in Index(i1: i.i1, i2: i2) } - } - - - if let i2 = base2.index(i.i2, offsetBy: distance, limitedBy: base2.endIndex) { - // `distance` does not overflow `base2[i.i2...]`. - // - // i.i2 i2 - // v v - // i.i1 > [x x x|x x x x x x|x x x] - // [ |> > > > > >| ] (`distance`) - - return normalizeIndex(i.i1, i2) - } - - let suffixCount = base2[i.i2...].count - let remaining = distance - suffixCount - let nextI1 = base1.index(after: i.i1) - - if limit.i1 == nextI1 { - // Delegate to `base2` if the offset is limited to `nextI1`. - // - // i.i2 - // v - // i.i1 > [x x x|x x x x x x x x x] - // nextI1 > [x x x x x x x x x|x x x] - // ^ - // limit.i2 - - return base2.index(base2.startIndex, offsetBy: remaining, limitedBy: limit.i2) - .map { i2 in Index(i1: nextI1, i2: i2) } - } - - if let i2 = base2.index(base2.startIndex, offsetBy: remaining, limitedBy: i.i2) { - // `remaining` does not overflow `base2[.. [x x x x x x x x x|x x x] - // [ |> > >] (`suffixCount`) - // [> > >| ] (`remaining`) - // nextI1 > [x x x|x x x x x x x x x] - // ^ - // i2 - - return Index(i1: nextI1, i2: i2) - } - - let prefixCount = base2[.. Index? { - assert(distance >= 0) - assert(limit <= i) - - if limit.i1 == i.i1 { - // Delegate to `base2` if the offset is limited to `i.i1`. - // - // limit.i2 i.i2 - // v v - // i.i1 > [x x x|x x x x x x|x x x] - - return base2.index(i.i2, offsetBy: -distance, limitedBy: limit.i2) - .map { i2 in Index(i1: i.i1, i2: i2) } - } - - if let i2 = base2.index(i.i2, offsetBy: -distance, limitedBy: base2.startIndex) { - // `distance` does not underflow `base2[.. [x x x|x x x x x x|x x x] - // [ |< < < < < <| ] (`distance`) - - return Index(i1: i.i1, i2: i2) - } - - let prefixCount = base2[.. [x x x|x x x x x x x x x] - // i.i1 > [x x x x x x x x x|x x x] - // ^ - // i.i2 - - return base2.index(base2.endIndex, offsetBy: -remaining, limitedBy: limit.i2) - .map { i2 in Index(i1: previousI1, i2: i2) } - } - - if let i2 = base2.index(base2.endIndex, offsetBy: -remaining, limitedBy: i.i2) { - // `remaining` does not underflow `base2[i.i2...]`. - // - // i2 - // v - // previousI1 > [x x x x x x x x x|x x x] - // [ |< < <] (`remaining`) - // [< < <| ] (`prefixCount`) - // i.i1 > [x x x|x x x x x x x x x] - // ^ - // i.i2 - - return Index(i1: previousI1, i2: i2) - } - - let suffixCount = base2[i.i2...].count - let base2Count = prefixCount + suffixCount - let base1Distance = remaining / base2Count - - // The distance from `base2.endIndex` to the target. - let base2Distance = remaining % base2Count - - if base2Distance == 0 { - // We end up exactly between two cycles, so `base1Distance` would - // overshoot the target by 1. - // - // base2.startIndex - // v - // i1 > |x x x x x x x x x x x x] > - // ... > `base1Distance` times - // previousI1 > [x x x x x x x x x x x x] > - // i.i1 > [x x x|x x x x x x x x x] - // ^ - // i.i2 - - if let i1 = base1.index(previousI1, offsetBy: -(base1Distance - 1), limitedBy: limit.i1) { - let index = Index(i1: i1, i2: base2.startIndex) - return index < limit ? nil : index - } else { - return nil - } - } - - guard let i1 = base1.index(previousI1, offsetBy: -base1Distance, limitedBy: limit.i1) - else { return nil } - - let base2Limit = limit.i1 == i1 ? limit.i2 : base2.startIndex - return base2.index(base2.endIndex, offsetBy: -base2Distance, limitedBy: base2Limit) - .map { i2 in Index(i1: i1, i2: i2) } - } -} - -extension Product2Sequence: BidirectionalCollection - where Base1: BidirectionalCollection, Base2: BidirectionalCollection -{ - @inlinable - public func index(before i: Index) -> Index { - precondition(i != startIndex, - "Can't move before startIndex") - if i.i2 == base2.startIndex { - return Index( - i1: base1.index(before: i.i1), - i2: base2.index(before: base2.endIndex)) - } else { - return Index(i1: i.i1, i2: base2.index(before: i.i2)) - } - } -} - -extension Product2Sequence: RandomAccessCollection - where Base1: RandomAccessCollection, Base2: RandomAccessCollection {} - -extension Product2Sequence.Index: Hashable - where Base1.Index: Hashable, Base2.Index: Hashable {} - -//===----------------------------------------------------------------------===// -// product(_:_:) -//===----------------------------------------------------------------------===// - -/// Creates a sequence of each pair of elements of two underlying sequences. -/// -/// Use this function to iterate over every pair of elements in two different -/// collections. The returned sequence yields 2-element tuples, where the first -/// element of the tuple is from the first collection and the second element is -/// from the second collection. -/// -/// -/// let numbers = 1...3 -/// let colors = ["cerise", "puce", "heliotrope"] -/// for (number, color) in product(numbers, colors) { -/// print("\(number): \(color)") -/// } -/// // 1: cerise -/// // 1: puce -/// // 1: heliotrope -/// // 2: cerise -/// // 2: puce -/// // 2: heliotrope -/// // 3: cerise -/// // 3: puce -/// // 3: heliotrope -/// -/// The order of tuples in the returned sequence is consistent. The first -/// element of the first collection is paired with each element of the second -/// collection, then the second element of the first collection is paired with -/// each element of the second collection, and so on. -/// -/// - Parameters: -/// - s1: The first sequence to iterate over. -/// - s2: The second sequence to iterate over. -/// -/// - Complexity: O(1) -@inlinable -public func product( - _ s1: Base1, _ s2: Base2 -) -> Product2Sequence { - Product2Sequence(s1, s2) -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Reductions.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Reductions.swift deleted file mode 100644 index d45ca0e04..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Reductions.swift +++ /dev/null @@ -1,620 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -// MARK: - Exclusive Reductions - -extension LazySequenceProtocol { - /// Returns a sequence containing the accumulated results of combining the - /// elements of the sequence using the given closure. - /// - /// This can be seen as applying the reduce function to each element and - /// providing the initial value followed by these results as a sequence. - /// - /// ``` - /// let runningTotal = [1, 2, 3, 4].lazy.reductions(0, +) - /// print(Array(runningTotal)) - /// - /// // prints [0, 1, 3, 6, 10] - /// ``` - /// - /// - Parameters: - /// - initial: The value to use as the initial value. - /// - transform: A closure that combines the previously reduced result and - /// the next element in the receiving sequence. - /// - Returns: A sequence of the initial value followed by the reduced - /// elements. - /// - /// - Complexity: O(1) - @inlinable - public func reductions( - _ initial: Result, - _ transform: @escaping (Result, Element) -> Result - ) -> ExclusiveReductionsSequence { - reductions(into: initial) { result, element in - result = transform(result, element) - } - } - - /// Returns a sequence containing the accumulated results of combining the - /// elements of the sequence using the given closure. - /// - /// This can be seen as applying the reduce function to each element and - /// providing the initial value followed by these results as a sequence. - /// - /// ``` - /// let runningTotal = [1, 2, 3, 4].lazy.reductions(into: 0, +) - /// print(Array(runningTotal)) - /// - /// // prints [0, 1, 3, 6, 10] - /// ``` - /// - /// - Parameters: - /// - initial: The value to use as the initial value. - /// - transform: A closure that combines the previously reduced result and - /// the next element in the receiving sequence. - /// - Returns: A sequence of the initial value followed by the reduced - /// elements. - /// - /// - Complexity: O(1) - @inlinable - public func reductions( - into initial: Result, - _ transform: @escaping (inout Result, Element) -> Void - ) -> ExclusiveReductionsSequence { - ExclusiveReductionsSequence( - base: elements, - initial: initial, - transform: transform) - } -} - -extension Sequence { - /// Returns an array containing the accumulated results of combining the - /// elements of the sequence using the given closure. - /// - /// This can be seen as applying the reduce function to each element and - /// providing the initial value followed by these results as a sequence. - /// - /// ``` - /// let runningTotal = [1, 2, 3, 4].reductions(0, +) - /// print(runningTotal) - /// - /// // prints [0, 1, 3, 6, 10] - /// ``` - /// - /// When `reductions(_:_:)` is called, the following steps occur: - /// - /// 1. The `initial` result is added to an array of results. - /// 2. The `transform` closure is called with the `initial` result and the - /// first element of the sequence, appending the result to the array. - /// 3. The closure is called again repeatedly with the updated accumulating - /// result and each element of the sequence, adding each result to the - /// array. - /// 4. When the sequence is exhausted, the results array is returned to the - /// caller. - /// - /// If the sequence has no elements, `transform` is never executed and - /// an array containing only the `initial` result is returned. - /// - /// - Parameters: - /// - initial: The value to use as the initial value. - /// - transform: A closure that combines the previously reduced result and - /// the next element in the receiving sequence. - /// - Returns: An array of the initial value followed by the reduced elements. - /// - /// - Complexity: O(_n_), where _n_ is the length of the sequence. - @inlinable - public func reductions( - _ initial: Result, - _ transform: (Result, Element) throws -> Result - ) rethrows -> [Result] { - try reductions(into: initial) { result, element in - result = try transform(result, element) - } - } - - /// Returns an array containing the accumulated results of combining the - /// elements of the sequence using the given closure. - /// - /// This can be seen as applying the reduce function to each element and - /// providing the initial value followed by these results as a sequence. - /// - /// ``` - /// let runningTotal = [1, 2, 3, 4].reductions(into: 0, +) - /// print(runningTotal) - /// - /// // prints [0, 1, 3, 6, 10] - /// ``` - /// - /// When `reductions(into:_:_)` is called, the following steps occur: - /// - /// 1. The `initial` result is added to an array of results. - /// 2. The `transform` closure is called with the `initial` result and the - /// first element of the sequence, appending the result to the array. - /// 3. The closure is called again repeatedly with the updated accumulating - /// result and each element of the sequence, adding each result to the - /// array. - /// 4. When the sequence is exhausted, the results array is returned to the - /// caller. - /// - /// If the sequence has no elements, `transform` is never executed and - /// an array containing only the `initial` result is returned. - /// - /// - Parameters: - /// - initial: The value to use as the initial value. - /// - transform: A closure that combines the previously reduced result and - /// the next element in the receiving sequence. - /// - Returns: An array of the initial value followed by the reduced elements. - /// - /// - Complexity: O(_n_), where _n_ is the length of the sequence. - @inlinable - public func reductions( - into initial: Result, - _ transform: (inout Result, Element) throws -> Void - ) rethrows -> [Result] { - - var output = [Result]() - output.reserveCapacity(underestimatedCount + 1) - output.append(initial) - - var initial = initial - for element in self { - try transform(&initial, element) - output.append(initial) - } - - return output - } -} - -/// A sequence of applying a transform to the element of a sequence and the -/// previously transformed result. -public struct ExclusiveReductionsSequence { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let initial: Result - - @usableFromInline - internal let transform: (inout Result, Base.Element) -> Void - - @inlinable - internal init( - base: Base, - initial: Result, - transform: @escaping (inout Result, Base.Element) -> Void - ) { - self.base = base - self.initial = initial - self.transform = transform - } -} - -extension ExclusiveReductionsSequence: Sequence { - public struct Iterator: IteratorProtocol { - @usableFromInline - internal var iterator: Base.Iterator - - @usableFromInline - internal var current: Result? - - @usableFromInline - internal let transform: (inout Result, Base.Element) -> Void - - @inlinable - internal init( - iterator: Base.Iterator, - current: Result? = nil, - transform: @escaping (inout Result, Base.Element) -> Void - ) { - self.iterator = iterator - self.current = current - self.transform = transform - } - - @inlinable - public mutating func next() -> Result? { - guard let result = current else { return nil } - current = iterator.next().map { element in - var result = result - transform(&result, element) - return result - } - return result - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator( - iterator: base.makeIterator(), - current: initial, - transform: transform) - } -} - -extension ExclusiveReductionsSequence: Collection where Base: Collection { - public struct Index: Comparable { - @usableFromInline - internal enum Representation { - case base(Base.Index, Result) - case end - } - - @usableFromInline - internal let representation: Representation - - @inlinable - internal init(_ representation: Representation) { - self.representation = representation - } - - @inlinable - public static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs.representation, rhs.representation) { - case (.base(let lhs, _), .base(let rhs, _)): - return lhs == rhs - case (.end, .end): - return true - case (.base, .end), (.end, .base): - return false - } - } - - @inlinable - public static func < (lhs: Self, rhs: Self) -> Bool { - switch (lhs.representation, rhs.representation) { - case (.end, _): - return false - case (_, .end): - return true - case (.base(let lhs, _), .base(let rhs, _)): - return lhs < rhs - } - } - } - - @inlinable - public var startIndex: Index { - Index(.base(base.startIndex, initial)) - } - - @inlinable - public var endIndex: Index { - Index(.end) - } - - @inlinable - public subscript(position: Index) -> Result { - switch position.representation { - case .base(_, let result): - return result - case .end: - fatalError("Cannot get element of end index.") - } - } - - @inlinable - public func index(after index: Index) -> Index { - switch index.representation { - case .base(base.endIndex, _): - return Index(.end) - case .base(let index, var result): - transform(&result, base[index]) - return Index(.base(base.index(after: index), result)) - case .end: - fatalError("Cannot get index after end index.") - } - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - switch (start.representation, end.representation) { - case let (.base(start, _), .base(end, _)): - return base.distance(from: start, to: end) - case let (.base(index, _), .end): - return base.distance(from: index, to: base.endIndex) + 1 - case let (.end, .base(index, _)): - return base.distance(from: base.endIndex, to: index) - 1 - case (.end, .end): - return 0 - } - } -} - -extension ExclusiveReductionsSequence: LazySequenceProtocol {} - -extension ExclusiveReductionsSequence: LazyCollectionProtocol - where Base: Collection {} - -extension ExclusiveReductionsSequence.Index: Hashable - where Base.Index: Hashable -{ - @inlinable - public func hash(into hasher: inout Hasher) { - switch representation { - case .base(let base, _): - hasher.combine(base) - case .end: - break - } - } -} - -// MARK: - Inclusive Reductions - -extension LazySequenceProtocol { - /// Returns a sequence containing the accumulated results of combining the - /// elements of the sequence using the given closure. - /// - /// This can be seen as applying the reduce function to each element and - /// providing the initial value followed by these results as a sequence. - /// - /// ``` - /// let runningTotal = [1, 2, 3, 4].lazy.reductions(+) - /// print(Array(runningTotal)) - /// - /// // prints [1, 3, 6, 10] - /// ``` - /// - /// - Parameter transform: A closure that combines the previously reduced - /// result and the next element in the receiving sequence. - /// - Returns: A sequence of the reduced elements. - /// - /// - Complexity: O(1) - @inlinable - public func reductions( - _ transform: @escaping (Element, Element) -> Element - ) -> InclusiveReductionsSequence { - InclusiveReductionsSequence(base: elements, transform: transform) - } -} - -extension Sequence { - /// Returns an array containing the accumulated results of combining the - /// elements of the sequence using the given closure. - /// - /// This can be seen as applying the reduce function to each element and - /// providing the initial value followed by these results as a sequence. - /// - /// ``` - /// let runningTotal = [1, 2, 3, 4].reductions(+) - /// print(runningTotal) - /// - /// // prints [1, 3, 6, 10] - /// ``` - /// - /// When `reductions(_:)` is called, the following steps occur: - /// - /// 1. The `transform` closure is called with the first and second elements - /// of the sequence, appending the result to an array of results. - /// 2. The closure is called again repeatedly with the updated accumulating - /// result and the next element of the sequence, adding each result to the - /// array. - /// 3. When the sequence is exhausted, the results array is returned to the - /// caller. - /// - /// If the sequence has no elements, `transform` is never executed and - /// an empty array is returned. - /// - /// If the sequence has one element, `transform` is never executed and - /// an array containing only that first element is returned. - /// - /// - Parameter transform: A closure that combines the previously reduced - /// result and the next element in the receiving sequence. - /// - Returns: An array of the reduced elements. - /// - /// - Complexity: O(_n_), where _n_ is the length of the sequence. - @inlinable - public func reductions( - _ transform: (Element, Element) throws -> Element - ) rethrows -> [Element] { - var iterator = makeIterator() - guard let initial = iterator.next() else { return [] } - return try IteratorSequence(iterator).reductions(initial, transform) - } -} - -public struct InclusiveReductionsSequence { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let transform: (Base.Element, Base.Element) -> Base.Element - - @inlinable - internal init( - base: Base, - transform: @escaping (Base.Element, Base.Element) -> Base.Element - ) { - self.base = base - self.transform = transform - } -} - -extension InclusiveReductionsSequence: Sequence { - public struct Iterator: IteratorProtocol { - @usableFromInline - internal var iterator: Base.Iterator - - @usableFromInline - internal var element: Base.Element? - - @usableFromInline - internal let transform: (Base.Element, Base.Element) -> Base.Element - - @inlinable - internal init( - iterator: Base.Iterator, - transform: @escaping (Base.Element, Base.Element) -> Base.Element - ) { - self.iterator = iterator - self.transform = transform - } - - @inlinable - public mutating func next() -> Base.Element? { - guard let previous = element else { - element = iterator.next() - return element - } - guard let next = iterator.next() else { return nil } - element = transform(previous, next) - return element - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(iterator: base.makeIterator(), transform: transform) - } -} - -extension InclusiveReductionsSequence: Collection where Base: Collection { - public struct Index: Comparable { - @usableFromInline - internal let base: Base.Index - - @usableFromInline - internal let result: Element? - - @inlinable - internal init(base: Base.Index, result: Element?) { - self.base = base - self.result = result - } - - @inlinable - public static func < (lhs: Self, rhs: Self) -> Bool { - lhs.base < rhs.base - } - - @inlinable - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.base == rhs.base - } - } - - @inlinable - public var startIndex: Index { - Index(base: base.startIndex, result: base.first) - } - - @inlinable - public var endIndex: Index { - Index(base: base.endIndex, result: nil) - } - - @inlinable - public subscript(index: Index) -> Base.Element { - guard let result = index.result else { - fatalError("Can't subscript using endIndex") - } - - return result - } - - @inlinable - public func index(after index: Index) -> Index { - guard let result = index.result else { - fatalError("Can't advance past endIndex") - } - - let index = base.index(after: index.base) - let nextResult = index == base.endIndex - ? nil - : transform(result, base[index]) - - return Index(base: index, result: nextResult) - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - base.distance(from: start.base, to: end.base) - } -} - -extension InclusiveReductionsSequence: LazySequenceProtocol {} - -extension InclusiveReductionsSequence: LazyCollectionProtocol - where Base: Collection {} - -extension InclusiveReductionsSequence.Index: Hashable - where Base.Index: Hashable -{ - @inlinable - public func hash(into hasher: inout Hasher) { - hasher.combine(base) - } -} - -// MARK: - Scan - -extension LazySequenceProtocol { - @available(*, deprecated, message: "Use reductions(_:_:) instead.") - @inlinable - public func scan( - _ initial: Result, - _ transform: @escaping (Result, Element) -> Result - ) -> ExclusiveReductionsSequence { - reductions(initial, transform) - } - - @available(*, deprecated, message: "Use reductions(into:_:) instead.") - @inlinable - public func scan( - into initial: Result, - _ transform: @escaping (inout Result, Element) -> Void - ) -> ExclusiveReductionsSequence { - reductions(into: initial, transform) - } -} - -extension Sequence { - @available(*, deprecated, message: "Use reductions(_:_:) instead.") - @inlinable - public func scan( - _ initial: Result, - _ transform: (Result, Element) throws -> Result - ) rethrows -> [Result] { - try reductions(initial, transform) - } - - @available(*, deprecated, message: "Use reductions(into:_:) instead.") - @inlinable - public func scan( - into initial: Result, - _ transform: (inout Result, Element) throws -> Void - ) rethrows -> [Result] { - try reductions(into: initial, transform) - } -} - -extension LazySequenceProtocol { - @available(*, deprecated, message: "Use reductions(_:) instead.") - @inlinable - public func scan( - _ transform: @escaping (Element, Element) -> Element - ) -> InclusiveReductionsSequence { - reductions(transform) - } -} - -extension Sequence { - @available(*, deprecated, message: "Use reductions(_:) instead.") - @inlinable - public func scan( - _ transform: (Element, Element) throws -> Element - ) rethrows -> [Element] { - try reductions(transform) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Rotate.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Rotate.swift deleted file mode 100644 index 4d0fa0c0b..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Rotate.swift +++ /dev/null @@ -1,279 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// reverse(subrange:) -//===----------------------------------------------------------------------===// - -extension MutableCollection where Self: BidirectionalCollection { - /// Reverses the elements of the collection, moving from each end until - /// `limit` is reached from either direction. The returned indices are the - /// start and end of the range of unreversed elements. - /// - /// Input: - /// [a b c d e f g h i j k l m n o p] - /// ^ - /// limit - /// Output: - /// [p o n m e f g h i j k l d c b a] - /// ^ ^ - /// lower upper - /// - /// - Postcondition: For returned indices `(lower, upper)`: - /// `lower == limit || upper == limit` - @inlinable - @discardableResult - internal mutating func _reverse( - subrange: Range, until limit: Index - ) -> (Index, Index) { - var lower = subrange.lowerBound - var upper = subrange.upperBound - while lower != limit && upper != limit { - formIndex(before: &upper) - swapAt(lower, upper) - formIndex(after: &lower) - } - return (lower, upper) - } - - /// Reverses the elements within the given subrange. - /// - /// This example reverses the numbers within the subrange at the start of the - /// `numbers` array: - /// - /// var numbers = [10, 20, 30, 40, 50, 60, 70, 80] - /// numbers.reverse(subrange: 0..<4) - /// // numbers == [40, 30, 20, 10, 50, 60, 70, 80] - /// - /// - Parameter subrange: The subrange of this collection to reverse. - /// - /// - Complexity: O(*n*), where *n* is the length of `subrange`. - @inlinable - public mutating func reverse(subrange: Range) { - if subrange.isEmpty { return } - var lower = subrange.lowerBound - var upper = subrange.upperBound - while lower < upper { - formIndex(before: &upper) - swapAt(lower, upper) - formIndex(after: &lower) - } - } -} - -//===----------------------------------------------------------------------===// -// rotate(toStartAt:) / rotate(subrange:toStartAt:) -//===----------------------------------------------------------------------===// - -extension MutableCollection { - /// Swaps the elements of the two given subranges, up to the upper bound of - /// the smaller subrange. The returned indices are the ends of the two ranges - /// that were actually swapped. - /// - /// Input: - /// [a b c d e f g h i j k l m n o p] - /// ^^^^^^^ ^^^^^^^^^^^^^ - /// lhs rhs - /// - /// Output: - /// [i j k l e f g h a b c d m n o p] - /// ^ ^ - /// p q - /// - /// - Precondition: !lhs.isEmpty && !rhs.isEmpty - /// - Postcondition: For returned indices `(p, q)`: - /// - /// - distance(from: lhs.lowerBound, to: p) == distance(from: - /// rhs.lowerBound, to: q) - /// - p == lhs.upperBound || q == rhs.upperBound - @inlinable - internal mutating func _swapNonemptySubrangePrefixes( - _ lhs: Range, _ rhs: Range - ) -> (Index, Index) { - assert(!lhs.isEmpty) - assert(!rhs.isEmpty) - - var p = lhs.lowerBound - var q = rhs.lowerBound - repeat { - swapAt(p, q) - formIndex(after: &p) - formIndex(after: &q) - } - while p != lhs.upperBound && q != rhs.upperBound - return (p, q) - } - - /// Rotates the elements within the given subrange so that the element at the - /// specified index becomes the start of the subrange. - /// - /// Rotating a collection is equivalent to breaking the collection into two - /// sections at the index `newStart`, and then swapping those two sections. - /// In this example, the `numbers` array is rotated so that the element at - /// index `3` (`40`) is first: - /// - /// var numbers = [10, 20, 30, 40, 50, 60, 70, 80] - /// let oldStart = numbers.rotate(subrange: 0..<4, toStartAt: 2) - /// // numbers == [30, 40, 10, 20, 50, 60, 70, 80] - /// // numbers[oldStart] == 10 - /// - /// - Parameters: - /// - subrange: The subrange of this collection to rotate. - /// - newStart: The index of the element that should be at the start of - /// `subrange` after rotating. - /// - Returns: The new index of the element that was at the start of - /// `subrange` pre-rotation. - /// - /// - Complexity: O(*n*), where *n* is the length of `subrange`. - @inlinable - @discardableResult - public mutating func rotate( - subrange: Range, - toStartAt newStart: Index - ) -> Index { - var m = newStart, s = subrange.lowerBound - let e = subrange.upperBound - - // Handle the trivial cases - if s == m { return e } - if m == e { return s } - - // We have two regions of possibly-unequal length that need to be exchanged. - // The return value of this method is going to be the position following - // that of the element that is currently last (element j). - // - // [a b c d e f g|h i j] or [a b c|d e f g h i j] - // ^ ^ ^ ^ ^ ^ - // s m e s m e - // - var ret = e // start with a known incorrect result. - while true { - // Exchange the leading elements of each region (up to the length of the - // shorter region). - // - // [a b c d e f g|h i j] or [a b c|d e f g h i j] - // ^^^^^ ^^^^^ ^^^^^ ^^^^^ - // [h i j d e f g|a b c] or [d e f|a b c g h i j] - // ^ ^ ^ ^ ^ ^ ^ ^ - // s s1 m m1/e s s1/m m1 e - // - let (s1, m1) = _swapNonemptySubrangePrefixes(s.. Index { - rotate(subrange: startIndex.., - toStartAt newStart: Index - ) -> Index { - reverse(subrange: subrange.lowerBound.. Index { - rotate(subrange: startIndex.. { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let isSeparator: (Base.Element) -> Bool - - @usableFromInline - internal let maxSplits: Int - - @usableFromInline - internal let omittingEmptySubsequences: Bool - - @inlinable - internal init( - base: Base, - isSeparator: @escaping (Base.Element) -> Bool, - maxSplits: Int, - omittingEmptySubsequences: Bool - ) { - self.base = base - self.isSeparator = isSeparator - self.maxSplits = maxSplits - self.omittingEmptySubsequences = omittingEmptySubsequences - } -} - -extension SplitSequence: Sequence { - public struct Iterator { - public typealias Element = [Base.Element] - - @usableFromInline - internal var base: Base.Iterator - - @usableFromInline - internal let isSeparator: (Base.Element) -> Bool - - @usableFromInline - internal let maxSplits: Int - - @usableFromInline - internal let omittingEmptySubsequences: Bool - - /// The number of splits performed. - @usableFromInline - internal var splitCount = 0 - - /// The number of subsequences returned. - @usableFromInline - internal var sequenceLength = 0 - - @inlinable - internal init( - base: Base.Iterator, - whereSeparator: @escaping (Base.Element) -> Bool, - maxSplits: Int, - omittingEmptySubsequences: Bool - ) { - self.base = base - self.isSeparator = whereSeparator - self.maxSplits = maxSplits - self.omittingEmptySubsequences = omittingEmptySubsequences - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator( - base: base.makeIterator(), - whereSeparator: self.isSeparator, - maxSplits: self.maxSplits, - omittingEmptySubsequences: self.omittingEmptySubsequences - ) - } -} - -extension SplitSequence.Iterator: IteratorProtocol { - @inlinable - public mutating func next() -> Element? { - var currentElement = base.next() - var subsequence: Element = [] - - // Add the next elements of the base sequence to this subsequence, until we - // reach a separator, unless we've already split the maximum number of - // times. In all cases, stop at the end of the base sequence. - while currentElement != nil { - if splitCount < maxSplits && isSeparator(currentElement!) { - if omittingEmptySubsequences && subsequence.isEmpty { - // Keep going if we don't want to return an empty subsequence. - currentElement = base.next() - continue - } else { - splitCount += 1 - break - } - } else { - subsequence.append(currentElement!) - currentElement = base.next() - } - } - - // We're done iterating when we've reached the end of the base sequence, - // and we've either returned the maximum number of subsequences (one more - // than the number of separators), or the only subsequence left to return is - // empty and we're omitting those. - if currentElement == nil - && (sequenceLength == splitCount + 1 - || omittingEmptySubsequences && subsequence.isEmpty) - { - return nil - } else { - sequenceLength += 1 - return subsequence - } - } -} - -extension SplitSequence: LazySequenceProtocol {} - -extension LazySequenceProtocol { - /// Lazily returns the longest possible subsequences of the sequence, in - /// order, that don't contain elements satisfying the given predicate. - /// - /// The resulting lazy sequence consists of at most `maxSplits + 1` - /// subsequences. Elements that are used to split the sequence are not - /// returned as part of any subsequence (except possibly the last one, in the - /// case where `maxSplits` is less than the number of separators in the - /// sequence). - /// - /// The following examples show the effects of the `maxSplits` and - /// `omittingEmptySubsequences` parameters when lazily splitting a sequence of - /// integers using a closure that matches numbers evenly divisible by 3 or 5. - /// The first use of `split` returns each subsequence that was originally - /// separated by one or more such numbers. - /// - /// let numbers = stride(from: 1, through: 16, by: 1) - /// for subsequence in numbers.lazy.split( - /// whereSeparator: { $0 % 3 == 0 || $0 % 5 == 0 } - /// ) { - /// print(subsequence) - /// } - /// /* Prints: - /// [1, 2] - /// [4] - /// [7, 8] - /// [11] - /// [13, 14] - /// [16] - /// */ - /// - /// The second example passes `1` for the `maxSplits` parameter, so the - /// original sequence is split just once, into two subsequences. - /// - /// for subsequence in numbers.lazy.split( - /// maxSplits: 1, - /// whereSeparator: { $0 % 3 == 0 || $0 % 5 == 0 } - /// ) { - /// print(subsequence) - /// } - /// /* Prints: - /// [1, 2] - /// [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] - /// */ - /// - /// The final example passes `false` for the `omittingEmptySubsequences` - /// parameter, so the sequence of returned subsequences contains empty - /// subsequences where numbers evenly divisible by 3 or 5 were repeated. - /// - /// for subsequence in numbers.lazy.split( - /// omittingEmptySubsequences: false, - /// whereSeparator: { $0 % 3 == 0 || $0 % 5 == 0 } - /// ) { - /// print(subsequence) - /// } - /// /* Prints: - /// [1, 2] - /// [4] - /// [] - /// [7, 8] - /// [] - /// [11] - /// [13, 14] - /// [16] - /// */ - /// - /// - Parameters: - /// - maxSplits: The maximum number of times to split the sequence, or - /// one less than the number of subsequences to return. If - /// `maxSplits + 1` subsequences are returned, the last one is a suffix - /// of the original sequence containing the remaining elements. - /// `maxSplits` must be greater than or equal to zero. The default value - /// is `Int.max`. - /// - omittingEmptySubsequences: If `false`, an empty subsequence is - /// returned in the result for each pair of consecutive elements - /// satisfying the `isSeparator` predicate and for each element at the - /// start or end of the sequence satisfying the `isSeparator` - /// predicate. The default value is `true`. - /// - whereSeparator: A closure that takes an element as an argument and - /// returns a Boolean value indicating whether the sequence should be - /// split at that element. - /// - Returns: A lazy sequence of subsequences, split from this sequence's - /// elements. - /// - /// - Complexity: O(*n*), where *n* is the length of the sequence. - @inlinable - public func split( - maxSplits: Int = Int.max, - omittingEmptySubsequences: Bool = true, - whereSeparator isSeparator: @escaping (Element) -> Bool - ) -> SplitSequence { - precondition(maxSplits >= 0, "Must take zero or more splits") - - return SplitSequence( - base: elements, - isSeparator: isSeparator, - maxSplits: maxSplits, - omittingEmptySubsequences: omittingEmptySubsequences - ) - } -} - -extension LazySequenceProtocol where Element: Equatable { - /// Lazily returns the longest possible subsequences of the sequence, in - /// order, around elements equal to the given element. - /// - /// The resulting lazy sequence consists of at most `maxSplits + 1` - /// subsequences. Elements that are used to split the sequence are not - /// returned as part of any subsequence (except possibly the last one, in the - /// case where `maxSplits` is less than the number of separators in the - /// sequence). - /// - /// The following examples show the effects of the `maxSplits` and - /// `omittingEmptySubsequences` parameters when splitting a sequence of - /// integers at each zero (`0`). The first use of `split` returns each - /// subsequence that was originally separated by one or more zeros. - /// - /// let numbers = AnySequence([1, 2, 0, 3, 4, 0, 0, 5]) - /// for subsequence in numbers.lazy.split(separator: 0) { - /// print(subsequence) - /// } - /// /* Prints: - /// [1, 2] - /// [3, 4] - /// [5] - /// */ - /// - /// The second example passes `1` for the `maxSplits` parameter, so the - /// original sequence is split just once, into two subsequences. - /// - /// for subsequence in numbers.lazy.split( - /// separator: 0, - /// maxSplits: 1 - /// ) { - /// print(subsequence) - /// } - /// /* Prints: - /// [1, 2] - /// [3, 4, 0, 0, 5] - /// */ - /// - /// The final example passes `false` for the `omittingEmptySubsequences` - /// parameter, so the sequence of returned subsequences contains empty - /// subsequences where zeros were repeated. - /// - /// for subsequence in numbers.lazy.split( - /// separator: 0, - /// omittingEmptySubsequences: false - /// ) { - /// print(subsequence) - /// } - /// /* Prints: - /// [1, 2] - /// [3, 4] - /// [] - /// [5] - /// */ - /// - /// - Parameters: - /// - separator: The element that should be split upon. - /// - maxSplits: The maximum number of times to split the sequence, or - /// one less than the number of subsequences to return. If - /// `maxSplits + 1` subsequences are returned, the last one is a suffix - /// of the original sequence containing the remaining elements. - /// `maxSplits` must be greater than or equal to zero. The default value - /// is `Int.max`. - /// - omittingEmptySubsequences: If `false`, an empty subsequence is - /// returned in the result for each consecutive pair of `separator` - /// elements in the sequence and for each instance of `separator` at - /// the start or end of the sequence. If `true`, only nonempty - /// subsequences are returned. The default value is `true`. - /// - Returns: A lazy sequence of subsequences, split from this sequence's - /// elements. - /// - /// - Complexity: O(*n*), where *n* is the length of the sequence. - @inlinable - public func split( - separator: Element, - maxSplits: Int = Int.max, - omittingEmptySubsequences: Bool = true - ) -> SplitSequence { - precondition(maxSplits >= 0, "Must take zero or more splits") - - return SplitSequence( - base: elements, - isSeparator: { $0 == separator }, - maxSplits: maxSplits, - omittingEmptySubsequences: omittingEmptySubsequences - ) - } -} - -//===----------------------------------------------------------------------===// -// SplitCollection -//===----------------------------------------------------------------------===// - -/// A collection that lazily splits a base collection into subsequences -/// separated by elements that satisfy the given `whereSeparator` predicate. -/// -/// - Note: This type is the result of -/// -/// x.split(maxSplits:omittingEmptySubsequences:whereSeparator) -/// x.split(separator:maxSplits:omittingEmptySubsequences) -/// -/// where `x` conforms to `LazySequenceProtocol` and `Collection`. -public struct SplitCollection { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let isSeparator: (Base.Element) -> Bool - - @usableFromInline - internal let maxSplits: Int - - @usableFromInline - internal let omittingEmptySubsequences: Bool - - @usableFromInline - internal var _startIndex: Index - - @inlinable - internal init( - base: Base, - isSeparator: @escaping (Base.Element) -> Bool, - maxSplits: Int, - omittingEmptySubsequences: Bool - ) { - self.base = base - self.isSeparator = isSeparator - self.maxSplits = maxSplits - self.omittingEmptySubsequences = omittingEmptySubsequences - - // We precalculate `startIndex`. There are three possibilities: - // 1. `base` is empty and we're _not_ omitting empty subsequences, in which - // case the following index describes the sole element of this collection; - self._startIndex = Index( - baseRange: base.startIndex.. - - /// The number of subsequences up to and including this position in the - /// collection. - @usableFromInline - internal let sequenceLength: Int - - /// The number splits performed up to and including this position in the - /// collection. - @usableFromInline - internal let splitCount: Int - - @inlinable - internal init( - baseRange: Range, - sequenceLength: Int, - splitCount: Int - ) { - self.baseRange = baseRange - self.sequenceLength = sequenceLength - self.splitCount = splitCount - } - - @inlinable - public static func == (lhs: Index, rhs: Index) -> Bool { - // `sequenceLength` is equivalent to the index's 1-based position in the - // collection of indices. - lhs.sequenceLength == rhs.sequenceLength - } - - @inlinable - public static func < (lhs: Index, rhs: Index) -> Bool { - lhs.sequenceLength < rhs.sequenceLength - } - } - - /// Returns the index of the subsequence starting at or after the given base - /// collection index. - @inlinable - internal func indexForSubsequence( - atOrAfter lowerBound: Base.Index, - sequenceLength: Int, - splitCount: Int - ) -> Index { - var start = lowerBound - // If we don't have any more splits to do (which we'll determine shortly), - // the end of this subsequence will be the end of the base collection. - var end = base.endIndex - - if splitCount < maxSplits { - // The non-inclusive end of this subsequence is marked by the next - // separator, or the end of the base collection. - end = - base[start...].firstIndex(where: isSeparator) - ?? base.endIndex - - if base[start.. Index { - precondition(i != endIndex, "Can't advance past endIndex") - - var subsequenceStart = i.baseRange.upperBound - if subsequenceStart < base.endIndex { - // If we're not already at the end of the base collection, the previous - // susequence ended with a separator. Start searching for the next - // subsequence at the following element. - subsequenceStart = base.index(after: i.baseRange.upperBound) - } - - guard subsequenceStart != base.endIndex else { - if !omittingEmptySubsequences - && i.sequenceLength < i.splitCount + 1 - { - // The base collection ended with a separator, so we need to emit one - // more empty subsequence. This one differs from `endIndex` in its - // `sequenceLength` (except in an extreme edge case!), which is the - // sole property tested for equality and comparison. - return Index( - baseRange: base.endIndex.. Base.SubSequence { - precondition(position != endIndex, "Can't subscript using endIndex") - return base[position.baseRange] - } -} - -extension SplitCollection.Index: Hashable { - @inlinable - public func hash(into hasher: inout Hasher) { - hasher.combine(sequenceLength) - } -} - -extension SplitCollection: LazyCollectionProtocol {} - -extension LazySequenceProtocol where Self: Collection, Elements: Collection { - /// Lazily returns the longest possible subsequences of the collection, in - /// order, that don't contain elements satisfying the given predicate. - /// - /// The resulting lazy collection consists of at most `maxSplits + 1` - /// subsequences. Elements that are used to split the collection are not - /// returned as part of any subsequence (except possibly the last one, in the - /// case where `maxSplits` is less than the number of separators in the - /// collection). - /// - /// The following examples show the effects of the `maxSplits` and - /// `omittingEmptySubsequences` parameters when lazily splitting a string - /// using a closure that matches spaces. The first use of `split` returns each - /// word that was originally separated by one or more spaces. - /// - /// let line = "BLANCHE: I don't want realism. I want magic!" - /// for spaceless in line.lazy.split(whereSeparator: { $0 == " " }) { - /// print(spaceless) - /// } - /// // Prints - /// // BLANCHE: - /// // I - /// // don't - /// // want - /// // realism. - /// // I - /// // want - /// // magic! - /// - /// The second example passes `1` for the `maxSplits` parameter, so the - /// original string is split just once, into two new strings. - /// - /// for spaceless in line.lazy.split( - /// maxSplits: 1, - /// whereSeparator: { $0 == " " } - /// ) { - /// print(spaceless) - /// } - /// // Prints - /// // BLANCHE: - /// // I don't want realism. I want magic! - /// - /// The final example passes `false` for the `omittingEmptySubsequences` - /// parameter, so the returned array contains empty strings where spaces - /// were repeated. - /// - /// for spaceless in line.lazy.split( - /// omittingEmptySubsequences: false, - /// whereSeparator: { $0 == " " } - /// ) { - /// print(spaceless) - /// } - /// // Prints - /// // BLANCHE: - /// // - /// // - /// // I - /// // don't - /// // want - /// // realism. - /// // I - /// // want - /// // magic! - /// - /// - Parameters: - /// - maxSplits: The maximum number of times to split the collection, or - /// one less than the number of subsequences to return. If - /// `maxSplits + 1` subsequences are returned, the last one is a suffix - /// of the original collection containing the remaining elements. - /// `maxSplits` must be greater than or equal to zero. The default value - /// is `Int.max`. - /// - omittingEmptySubsequences: If `false`, an empty subsequence is - /// returned in the result for each pair of consecutive elements - /// satisfying the `isSeparator` predicate and for each element at the - /// start or end of the collection satisfying the `isSeparator` - /// predicate. The default value is `true`. - /// - whereSeparator: A closure that takes an element as an argument and - /// returns a Boolean value indicating whether the collection should be - /// split at that element. - /// - Returns: A lazy collection of subsequences, split from this collection's - /// elements. - /// - /// - Complexity: O(*n*), where *n* is the length of the collection. - @inlinable - public func split( - maxSplits: Int = Int.max, - omittingEmptySubsequences: Bool = true, - whereSeparator isSeparator: @escaping (Element) -> Bool - ) -> SplitCollection { - precondition(maxSplits >= 0, "Must take zero or more splits") - - return SplitCollection( - base: elements, - isSeparator: isSeparator, - maxSplits: maxSplits, - omittingEmptySubsequences: omittingEmptySubsequences - ) - } -} - -extension LazySequenceProtocol - where Self: Collection, Elements: Collection, Element: Equatable -{ - /// Lazily returns the longest possible subsequences of the collection, in - /// order, around elements equal to the given element. - /// - /// The resulting lazy collection consists of at most `maxSplits + 1` - /// subsequences. Elements that are used to split the collection are not - /// returned as part of any subsequence (except possibly the last one, in the - /// case where `maxSplits` is less than the number of separators in the - /// collection). - /// - /// The following examples show the effects of the `maxSplits` and - /// `omittingEmptySubsequences` parameters when splitting a string at each - /// space character (" "). The first use of `split` returns each word that - /// was originally separated by one or more spaces. - /// - /// let line = "BLANCHE: I don't want realism. I want magic!" - /// for spaceless in line.lazy.split(separator: " ") { - /// print(spaceless) - /// } - /// // Prints - /// // BLANCHE: - /// // I - /// // don't - /// // want - /// // realism. - /// // I - /// // want - /// // magic! - /// - /// The second example passes `1` for the `maxSplits` parameter, so the - /// original string is split just once, into two new strings. - /// - /// for spaceless in line.lazy.split(separator: " ", maxSplits: 1) { - /// print(spaceless) - /// } - /// // Prints - /// // BLANCHE: - /// // I don't want realism. I want magic! - /// - /// The final example passes `false` for the `omittingEmptySubsequences` - /// parameter, so the returned array contains empty strings where spaces - /// were repeated. - /// - /// for spaceless in line.lazy.split( - /// separator: " ", - /// omittingEmptySubsequences: false - /// ) { - /// print(spaceless) - /// } - /// // Prints - /// // BLANCHE: - /// // - /// // - /// // I - /// // don't - /// // want - /// // realism. - /// // I - /// // want - /// // magic! - /// - /// - Parameters: - /// - separator: The element that should be split upon. - /// - maxSplits: The maximum number of times to split the collection, or - /// one less than the number of subsequences to return. If - /// `maxSplits + 1` subsequences are returned, the last one is a suffix - /// of the original collection containing the remaining elements. - /// `maxSplits` must be greater than or equal to zero. The default value - /// is `Int.max`. - /// - omittingEmptySubsequences: If `false`, an empty subsequence is - /// returned in the result for each consecutive pair of `separator` - /// elements in the collection and for each instance of `separator` at - /// the start or end of the collection. If `true`, only nonempty - /// subsequences are returned. The default value is `true`. - /// - Returns: A lazy collection of subsequences split from this collection's - /// elements. - /// - /// - Complexity: O(*n*), where *n* is the length of the collection. - @inlinable - public func split( - separator: Element, - maxSplits: Int = Int.max, - omittingEmptySubsequences: Bool = true - ) -> SplitCollection { - precondition(maxSplits >= 0, "Must take zero or more splits") - - return SplitCollection( - base: elements, - isSeparator: { $0 == separator }, - maxSplits: maxSplits, - omittingEmptySubsequences: omittingEmptySubsequences - ) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Stride.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Stride.swift deleted file mode 100644 index bf91612b5..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Stride.swift +++ /dev/null @@ -1,283 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// striding(by:) -//===----------------------------------------------------------------------===// - -extension Sequence { - /// Returns a sequence stepping through the elements every `step` starting at - /// the first value. Any remainders of the stride will be trimmed. - /// - /// (0...10).striding(by: 2) // == [0, 2, 4, 6, 8, 10] - /// (0...10).striding(by: 3) // == [0, 3, 6, 9] - /// - /// - Complexity: O(1). Access to successive values is O(k) where _k_ is the - /// striding `step`. - /// - /// - Parameter step: The amount to step with each iteration. - /// - Returns: Returns a sequence for stepping through the elements by the - /// specified amount. - @inlinable - public func striding(by step: Int) -> StridingSequence { - StridingSequence(base: self, stride: step) - } -} - -extension Collection { - /// Returns a sequence stepping through the elements every `step` starting at - /// the first value. Any remainders of the stride will be trimmed. - /// - /// (0...10).striding(by: 2) // == [0, 2, 4, 6, 8, 10] - /// (0...10).striding(by: 3) // == [0, 3, 6, 9] - /// - /// - Complexity: O(1). Access to successive values is O(1) if the collection - /// conforms to `RandomAccessCollection`; otherwise, O(_k_), where _k_ is - /// the striding `step`. - /// - /// - Parameter step: The amount to step with each iteration. - /// - Returns: Returns a collection for stepping through the elements by the - /// specified amount. - @inlinable - public func striding(by step: Int) -> StridingCollection { - StridingCollection(base: self, stride: step) - } -} - -/// A wrapper that strides over a base sequence. -public struct StridingSequence { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let stride: Int - - @inlinable - internal init(base: Base, stride: Int) { - precondition(stride > 0, "Stride must be greater than zero") - self.base = base - self.stride = stride - } -} - -extension StridingSequence { - @inlinable - public func striding(by step: Int) -> Self { - Self(base: base, stride: stride * step) - } -} - -extension StridingSequence: Sequence { - /// An iterator over a `StridingSequence` instance. - public struct Iterator: IteratorProtocol { - @usableFromInline - internal var iterator: Base.Iterator - - @usableFromInline - internal let stride: Int - - @usableFromInline - internal var striding: Bool = false - - @inlinable - internal init(iterator: Base.Iterator, stride: Int) { - self.iterator = iterator - self.stride = stride - } - - @inlinable - public mutating func next() -> Base.Element? { - guard striding else { - striding = true - return iterator.next() - } - for _ in 0.. Iterator { - Iterator(iterator: base.makeIterator(), stride: stride) - } -} - -extension StridingSequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -/// A wrapper that strides over a base collection. -public struct StridingCollection { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let stride: Int - - @inlinable - internal init(base: Base, stride: Int) { - precondition(stride > 0, "striding must be greater than zero") - self.base = base - self.stride = stride - } -} - -extension StridingCollection { - @inlinable - public func striding(by step: Int) -> Self { - Self(base: base, stride: stride * step) - } -} - -extension StridingCollection: Collection { - /// A position in a `StridingCollection` instance. - public struct Index: Comparable { - @usableFromInline - internal let base: Base.Index - - @usableFromInline - internal init(_ base: Base.Index) { - self.base = base - } - - @inlinable - public static func < (lhs: Index, rhs: Index) -> Bool { - lhs.base < rhs.base - } - } - - @inlinable - public var startIndex: Index { - Index(base.startIndex) - } - - @inlinable - public var endIndex: Index { - Index(base.endIndex) - } - - @inlinable - public subscript(i: Index) -> Base.Element { - base[i.base] - } - - @inlinable - public func index(after i: Index) -> Index { - precondition(i.base != base.endIndex, "Advancing past end index") - return index(i, offsetBy: 1) - } - - @inlinable - public func index( - _ i: Index, - offsetBy n: Int, - limitedBy limit: Index - ) -> Index? { - guard n != 0 else { return i } - guard limit != i else { return nil } - - return n > 0 - ? offsetForward(i, offsetBy: n, limitedBy: limit) - : offsetBackward(i, offsetBy: -n, limitedBy: limit) - } - - @inlinable - internal func offsetForward( - _ i: Index, - offsetBy n: Int, - limitedBy limit: Index - ) -> Index? { - if limit < i { - if let idx = base.index( - i.base, - offsetBy: n * stride, - limitedBy: base.endIndex - ) { - return Index(idx) - } else { - assert(distance(from: i, to: endIndex) == n, "Advancing past end index") - return endIndex - } - } else if let idx = base.index( - i.base, - offsetBy: n * stride, - limitedBy: limit.base - ) { - return Index(idx) - } else { - return distance(from: i, to: limit) == n - ? endIndex - : nil - } - } - - @inlinable - internal func offsetBackward( - _ i: Index, - offsetBy n: Int, - limitedBy limit: Index - ) -> Index? { - // We typically use the ternary operator but this significantly increases - // compile times when using Swift 5.3.2 - // https://github.com/apple/swift-algorithms/issues/146 - let distance: Int - if i == endIndex { - distance = -((base.count - 1) % stride + 1) + (n - 1) * -stride - } else { - distance = n * -stride - } - return base.index( - i.base, - offsetBy: distance, - limitedBy: limit.base - ).map(Index.init) - } - - @inlinable - public var count: Int { - base.isEmpty ? 0 : (base.count - 1) / stride + 1 - } - - @inlinable - public func distance(from start: Index, to end: Index) -> Int { - let distance = base.distance(from: start.base, to: end.base) - return distance / stride + (distance % stride).signum() - } - - @inlinable - public func index(_ i: Index, offsetBy distance: Int) -> Index { - precondition(distance <= 0 || i.base != base.endIndex, "Advancing past end index") - precondition(distance >= 0 || i.base != base.startIndex, "Incrementing past start index") - let limit = distance > 0 ? endIndex : startIndex - let idx = index(i, offsetBy: distance, limitedBy: limit) - precondition(idx != nil, "The distance \(distance) is not valid for this collection") - return idx! - } -} - -extension StridingCollection: BidirectionalCollection - where Base: RandomAccessCollection { - - @inlinable - public func index(before i: Index) -> Index { - precondition(i.base != base.startIndex, "Incrementing past start index") - return index(i, offsetBy: -1) - } -} - -extension StridingCollection: RandomAccessCollection - where Base: RandomAccessCollection {} - -extension StridingCollection: LazySequenceProtocol, LazyCollectionProtocol - where Base: LazySequenceProtocol {} - -extension StridingCollection.Index: Hashable where Base.Index: Hashable {} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Suffix.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Suffix.swift deleted file mode 100644 index c091b82de..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Suffix.swift +++ /dev/null @@ -1,88 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// suffix(while:) -//===----------------------------------------------------------------------===// - -extension BidirectionalCollection { - /// Returns a subsequence containing the elements from the end until - /// `predicate` returns `false` and skipping the remaining elements. - /// - /// - Parameter predicate: A closure that takes an element of the sequence as - /// its argument and returns `true` if the element should be included or - /// `false` if it should be excluded. Once the predicate returns `false` it - /// will not be called again. - /// - /// - Complexity: O(*n*), where *n* is the length of the collection. - @inlinable - public func suffix( - while predicate: (Element) throws -> Bool - ) rethrows -> SubSequence { - try self[startOfSuffix(while: predicate)...] - } -} - -//===----------------------------------------------------------------------===// -// endOfPrefix(while:) -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns the exclusive upper bound of the prefix of elements that satisfy - /// the predicate. - /// - /// - Parameter predicate: A closure that takes an element of the collection - /// as its argument and returns `true` if the element is part of the prefix - /// or `false` if it is not. Once the predicate returns `false` it will not - /// be called again. - /// - /// - Complexity: O(*n*), where *n* is the length of the collection. - @inlinable - internal func endOfPrefix( - while predicate: (Element) throws -> Bool - ) rethrows -> Index { - var index = startIndex - while try index != endIndex && predicate(self[index]) { - formIndex(after: &index) - } - return index - } -} - -//===----------------------------------------------------------------------===// -// startOfSuffix(while:) -//===----------------------------------------------------------------------===// - -extension BidirectionalCollection { - /// Returns the inclusive lower bound of the suffix of elements that satisfy - /// the predicate. - /// - /// - Parameter predicate: A closure that takes an element of the collection - /// as its argument and returns `true` if the element is part of the suffix - /// or `false` if it is not. Once the predicate returns `false` it will not - /// be called again. - /// - /// - Complexity: O(*n*), where *n* is the length of the collection. - @inlinable - internal func startOfSuffix( - while predicate: (Element) throws -> Bool - ) rethrows -> Index { - var index = endIndex - while index != startIndex { - let after = index - formIndex(before: &index) - if try !predicate(self[index]) { - return after - } - } - return index - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Trim.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Trim.swift deleted file mode 100644 index 832216bab..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Trim.swift +++ /dev/null @@ -1,240 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// - -//===----------------------------------------------------------------------===// -// trimmingPrefix(while:) -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns a `SubSequence` formed by discarding all elements at the start of - /// the collection which satisfy the given predicate. - /// - /// This example uses `trimmingPrefix(while:)` to get a substring without the - /// white space at the beginning of the string: - /// - /// let myString = " hello, world " - /// print(myString.trimmingPrefix(while: \.isWhitespace)) // "hello, world " - /// - /// - Parameter predicate: A closure which determines if the element should be - /// omitted from the resulting slice. - /// - /// - Complexity: O(*n*), where *n* is the length of this collection. - /// - @inlinable - public func trimmingPrefix( - while predicate: (Element) throws -> Bool - ) rethrows -> SubSequence { - let start = try endOfPrefix(while: predicate) - return self[start...] - } -} - -//===----------------------------------------------------------------------===// -// trimPrefix(while:) -//===----------------------------------------------------------------------===// - -extension Collection where Self: RangeReplaceableCollection { - /// Mutates a `Collection` by discarding all elements at the start of it which - /// satisfy the given predicate. - /// - /// This example uses `trimPrefix(while:)` to remove the white space at the - /// beginning of the string: - /// - /// let myString = " hello, world " - /// myString.trimPrefix(while: \.isWhitespace) - /// print(myString) // "hello, world " - /// - /// - Parameter predicate: A closure which determines if the element should be - /// removed from the string. - /// - /// - Complexity: O(*n*), where *n* is the length of this collection. - /// - @inlinable - @_disfavoredOverload - public mutating func trimPrefix( - while predicate: (Element) throws -> Bool - ) rethrows { - let end = try endOfPrefix(while: predicate) - removeSubrange(startIndex.. Bool - ) rethrows { - self = try trimmingPrefix(while: predicate) - } -} - -//===----------------------------------------------------------------------===// -// trimming(while:) / trimmingSuffix(while:) -//===----------------------------------------------------------------------===// - -extension BidirectionalCollection { - /// Returns a `SubSequence` formed by discarding all elements at the start and - /// end of the collection which satisfy the given predicate. - /// - /// This example uses `trimming(while:)` to get a substring without the white - /// space at the beginning and end of the string: - /// - /// let myString = " hello, world " - /// print(myString.trimming(while: \.isWhitespace)) // "hello, world" - /// - /// - Parameter predicate: A closure which determines if the element should be - /// omitted from the resulting slice. - /// - /// - Complexity: O(*n*), where *n* is the length of this collection. - /// - @inlinable - public func trimming( - while predicate: (Element) throws -> Bool - ) rethrows -> SubSequence { - try trimmingPrefix(while: predicate).trimmingSuffix(while: predicate) - } - - /// Returns a `SubSequence` formed by discarding all elements at the end of - /// the collection which satisfy the given predicate. - /// - /// This example uses `trimmingSuffix(while:)` to get a substring without the - /// white space at the end of the string: - /// - /// let myString = " hello, world " - /// print(myString.trimmingSuffix(while: \.isWhitespace)) // " hello, world" - /// - /// - Parameter predicate: A closure which determines if the element should be - /// omitted from the resulting slice. - /// - /// - Complexity: O(*n*), where *n* is the length of this collection. - /// - @inlinable - public func trimmingSuffix( - while predicate: (Element) throws -> Bool - ) rethrows -> SubSequence { - let end = try startOfSuffix(while: predicate) - return self[.. Bool - ) rethrows { - try trimSuffix(while: predicate) - try trimPrefix(while: predicate) - } - - /// Mutates a `BidirectionalCollection` by discarding all elements at the end - /// of it which satisfy the given predicate. - /// - /// This example uses `trimSuffix(while:)` to remove the white space at the - /// beginning of the string: - /// - /// let myString = " hello, world " - /// myString.trimSuffix(while: \.isWhitespace) - /// print(myString) // " hello, world" - /// - /// - Parameter predicate: A closure which determines if the element should be - /// removed from the string. - /// - /// - Complexity: O(*n*), where *n* is the length of this collection. - /// - @inlinable - @_disfavoredOverload - public mutating func trimSuffix( - while predicate: (Element) throws -> Bool - ) rethrows { - let start = try startOfSuffix(while: predicate) - removeSubrange(start.. Bool - ) rethrows { - self = try trimming(while: predicate) - } - - /// Mutates a `BidirectionalCollection` by discarding all elements at the end - /// of it which satisfy the given predicate. - /// - /// This example uses `trimSuffix(while:)` to remove the white space at the - /// beginning of the string: - /// - /// let myString = " hello, world " - /// myString.trimSuffix(while: \.isWhitespace) - /// print(myString) // " hello, world" - /// - /// - Parameter predicate: A closure which determines if the element should be - /// removed from the string. - /// - /// - Complexity: O(*n*), where *n* is the length of this collection. - /// - @inlinable - public mutating func trimSuffix( - while predicate: (Element) throws -> Bool - ) rethrows { - self = try trimmingSuffix(while: predicate) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Unique.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Unique.swift deleted file mode 100644 index 074f8d05b..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Unique.swift +++ /dev/null @@ -1,145 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// A sequence wrapper that leaves out duplicate elements of a base sequence. -public struct UniquedSequence { - /// The base collection. - @usableFromInline - internal let base: Base - - /// The projection function. - @usableFromInline - internal let projection: (Base.Element) -> Subject - - @usableFromInline - internal init(base: Base, projection: @escaping (Base.Element) -> Subject) { - self.base = base - self.projection = projection - } -} - -extension UniquedSequence: Sequence { - /// The iterator for a `UniquedSequence` instance. - public struct Iterator: IteratorProtocol { - @usableFromInline - internal var base: Base.Iterator - - @usableFromInline - internal let projection: (Base.Element) -> Subject - - @usableFromInline - internal var seen: Set = [] - - @usableFromInline - internal init( - base: Base.Iterator, - projection: @escaping (Base.Element) -> Subject - ) { - self.base = base - self.projection = projection - } - - @inlinable - public mutating func next() -> Base.Element? { - while let element = base.next() { - if seen.insert(projection(element)).inserted { - return element - } - } - return nil - } - } - - @inlinable - public func makeIterator() -> Iterator { - Iterator(base: base.makeIterator(), projection: projection) - } -} - -extension UniquedSequence: LazySequenceProtocol - where Base: LazySequenceProtocol {} - -//===----------------------------------------------------------------------===// -// uniqued() -//===----------------------------------------------------------------------===// - -extension Sequence where Element: Hashable { - /// Returns a sequence with only the unique elements of this sequence, in the - /// order of the first occurrence of each unique element. - /// - /// let animals = ["dog", "pig", "cat", "ox", "dog", "cat"] - /// let uniqued = animals.uniqued() - /// print(Array(uniqued)) - /// // Prints '["dog", "pig", "cat", "ox"]' - /// - /// - Returns: A sequence with only the unique elements of this sequence. - /// . - /// - Complexity: O(1). - @inlinable - public func uniqued() -> UniquedSequence { - UniquedSequence(base: self, projection: { $0 }) - } -} - -extension Sequence { - /// Returns an array with the unique elements of this sequence (as determined - /// by the given projection), in the order of the first occurrence of each - /// unique element. - /// - /// This example finds the elements of the `animals` array with unique - /// first characters: - /// - /// let animals = ["dog", "pig", "cat", "ox", "cow", "owl"] - /// let uniqued = animals.uniqued(on: { $0.first }) - /// print(uniqued) - /// // Prints '["dog", "pig", "cat", "ox"]' - /// - /// - Parameter projection: A closure that transforms an element into the - /// value to use for uniqueness. If `projection` returns the same value for - /// two different elements, the second element will be excluded from the - /// resulting array. - /// - /// - Returns: An array with only the unique elements of this sequence, as - /// determined by the result of `projection` for each element. - /// - /// - Complexity: O(*n*), where *n* is the length of the sequence. - @inlinable - public func uniqued( - on projection: (Element) throws -> Subject - ) rethrows -> [Element] { - var seen: Set = [] - var result: [Element] = [] - for element in self { - if seen.insert(try projection(element)).inserted { - result.append(element) - } - } - return result - } -} - -//===----------------------------------------------------------------------===// -// lazy.uniqued() -//===----------------------------------------------------------------------===// - -extension LazySequenceProtocol { - /// Returns a lazy sequence with the unique elements of this sequence (as - /// determined by the given projection), in the order of the first occurrence - /// of each unique element. - /// - /// - Complexity: O(1). - @inlinable - public func uniqued( - on projection: @escaping (Element) -> Subject - ) -> UniquedSequence { - UniquedSequence(base: self, projection: projection) - } -} diff --git a/Pods/SwiftAlgorithms/Sources/Algorithms/Windows.swift b/Pods/SwiftAlgorithms/Sources/Algorithms/Windows.swift deleted file mode 100644 index bee7b8642..000000000 --- a/Pods/SwiftAlgorithms/Sources/Algorithms/Windows.swift +++ /dev/null @@ -1,360 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Algorithms open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// windows(ofCount:) -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns a collection of all the overlapping slices of a given size. - /// - /// Use this method to iterate over overlapping subsequences of this - /// collection. This example prints every five character substring of `str`: - /// - /// let str = "Hello, world!" - /// for substring in str.windows(ofCount: 5) { - /// print(substring) - /// } - /// // "Hello" - /// // "ello," - /// // "llo, " - /// // "lo, W" - /// // ... - /// // "orld!" - /// - /// - Parameter count: The number of elements in each window subsequence. - /// - Returns: A collection of subsequences of this collection, each with - /// length `count`. If this collection is shorter than `count`, the - /// resulting collection is empty. - /// - /// - Complexity: O(1) if the collection conforms to - /// `RandomAccessCollection`, otherwise O(*k*) where `k` is `count`. - /// Access to successive windows is O(1). - @inlinable - public func windows(ofCount count: Int) -> WindowsOfCountCollection { - WindowsOfCountCollection(base: self, windowSize: count) - } -} - -/// A collection wrapper that presents a sliding window over the elements of -/// a collection. -public struct WindowsOfCountCollection { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let windowSize: Int - - @usableFromInline - internal var endOfFirstWindow: Base.Index? - - @inlinable - internal init(base: Base, windowSize: Int) { - precondition(windowSize > 0, "Windows size must be greater than zero") - self.base = base - self.windowSize = windowSize - self.endOfFirstWindow = - base.index(base.startIndex, offsetBy: windowSize, limitedBy: base.endIndex) - } -} - -extension WindowsOfCountCollection: Collection { - /// A position in a `WindowsOfCountCollection` instance. - public struct Index: Comparable { - @usableFromInline - internal var lowerBound: Base.Index - - @usableFromInline - internal var upperBound: Base.Index - - @inlinable - internal init(lowerBound: Base.Index, upperBound: Base.Index) { - self.lowerBound = lowerBound - self.upperBound = upperBound - } - - @inlinable - public static func == (lhs: Index, rhs: Index) -> Bool { - lhs.lowerBound == rhs.lowerBound - } - - @inlinable - public static func < (lhs: Index, rhs: Index) -> Bool { - lhs.lowerBound < rhs.lowerBound - } - } - - @inlinable - public var startIndex: Index { - if let upperBound = endOfFirstWindow { - return Index(lowerBound: base.startIndex, upperBound: upperBound) - } else { - return endIndex - } - } - - @inlinable - public var endIndex: Index { - Index(lowerBound: base.endIndex, upperBound: base.endIndex) - } - - @inlinable - public subscript(index: Index) -> Base.SubSequence { - precondition( - index.lowerBound != index.upperBound, - "Windows index is out of range") - return base[index.lowerBound.. Index { - precondition(index != endIndex, "Advancing past end index") - guard index.upperBound < base.endIndex else { return endIndex } - - let lowerBound = windowSize == 1 - ? index.upperBound - : base.index(after: index.lowerBound) - let upperBound = base.index(after: index.upperBound) - return Index(lowerBound: lowerBound, upperBound: upperBound) - } - - @inlinable - public func index(_ i: Index, offsetBy distance: Int) -> Index { - guard distance != 0 else { return i } - - return distance > 0 - ? offsetForward(i, by: distance) - : offsetBackward(i, by: -distance) - } - - @inlinable - public func index( - _ i: Index, - offsetBy distance: Int, - limitedBy limit: Index - ) -> Index? { - guard distance != 0 else { return i } - guard limit != i else { return nil } - - if distance > 0 { - return limit > i - ? offsetForward(i, by: distance, limitedBy: limit) - : offsetForward(i, by: distance) - } else { - return limit < i - ? offsetBackward(i, by: -distance, limitedBy: limit) - : offsetBackward(i, by: -distance) - } - } - - @inlinable - internal func offsetForward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetForward(i, by: distance, limitedBy: endIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetBackward(_ i: Index, by distance: Int) -> Index { - guard let index = offsetBackward(i, by: distance, limitedBy: startIndex) - else { fatalError("Index is out of bounds") } - return index - } - - @inlinable - internal func offsetForward( - _ i: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance > 0) - assert(limit > i) - - // `endIndex` and the index before it both have `base.endIndex` as their - // upper bound, so we first advance to the base index _before_ the upper - // bound of the output, in order to avoid advancing past the end of `base` - // when advancing to `endIndex`. - // - // Advancing by 4: - // - // input: [x|x x x x x|x x x x] [x x|x x x x x|x x x] - // |> > >|>| or |> > >| - // output: [x x x x x|x x x x x] [x x x x x x x x x x] (`endIndex`) - - if distance >= windowSize { - // Avoid traversing `self[i.lowerBound.. >|> > >|>| - // output: [x x x x x x x|x x x x|x] - - guard limit.lowerBound >= i.upperBound, - let lowerBound = base.index( - i.upperBound, - offsetBy: distance - windowSize, - limitedBy: limit.lowerBound), - let indexBeforeUpperBound = base.index( - lowerBound, - offsetBy: windowSize - 1, - limitedBy: limit.upperBound) - else { return nil } - - // If `indexBeforeUpperBound` equals `base.endIndex`, we're advancing to - // `endIndex`. - guard indexBeforeUpperBound != base.endIndex else { return endIndex } - - return Index( - lowerBound: lowerBound, - upperBound: base.index(after: indexBeforeUpperBound)) - } else { - // input: [x|x x x x x x|x x x x x] - // |> > > >| |> > >|>| - // output: [x x x x x|x x x x x x|x] - - guard let indexBeforeUpperBound = base.index( - i.upperBound, - offsetBy: distance - 1, - limitedBy: limit.upperBound) - else { return nil } - - // If `indexBeforeUpperBound` equals the limit, the upper bound itself - // exceeds it. - guard indexBeforeUpperBound != limit.upperBound || limit == endIndex - else { return nil } - - // If `indexBeforeUpperBound` equals `base.endIndex`, we're advancing to - // `endIndex`. - guard indexBeforeUpperBound != base.endIndex else { return endIndex } - - return Index( - lowerBound: base.index(i.lowerBound, offsetBy: distance), - upperBound: base.index(after: indexBeforeUpperBound)) - } - } - - @inlinable - internal func offsetBackward( - _ i: Index, by distance: Int, limitedBy limit: Index - ) -> Index? { - assert(distance > 0) - assert(limit < i) - - if i == endIndex { - // Advance `base.endIndex` by `distance - 1`, because the index before - // `endIndex` also has `base.endIndex` as its upper bound. - // - // Advancing by 4: - // - // input: [x x x x x x x x x x] (`endIndex`) - // |< < < < <|< < <| - // output: [x x|x x x x x|x x x] - - guard let upperBound = base.index( - base.endIndex, - offsetBy: -(distance - 1), - limitedBy: limit.upperBound) - else { return nil } - - return Index( - lowerBound: base.index(upperBound, offsetBy: -windowSize), - upperBound: upperBound) - } else if distance >= windowSize { - // Avoid traversing `self[i.lowerBound.. Int { - guard start <= end else { return -distance(from: end, to: start) } - guard start != end else { return 0 } - guard end != endIndex else { - // We add 1 here because the index before `endIndex` also has - // `base.endIndex` as its upper bound. - return base[start.upperBound...].count + 1 - } - - if start.upperBound <= end.lowerBound { - // The distance between `start.lowerBound` and `start.upperBound` is - // already known. - // - // start: [x|x x x x|x x x x x x x] - // |- - - -|> >| - // end: [x x x x x x x|x x x x|x] - - return windowSize + base[start.upperBound.. > > >| - // end: [x x x x x|x x x x x x|x] - - return base[start.lowerBound.. Index { - precondition(index != startIndex, "Incrementing past start index") - if index == endIndex { - return Index( - lowerBound: base.index(index.lowerBound, offsetBy: -windowSize), - upperBound: index.upperBound - ) - } else { - return Index( - lowerBound: base.index(before: index.lowerBound), - upperBound: base.index(before: index.upperBound) - ) - } - } -} - -extension WindowsOfCountCollection: RandomAccessCollection - where Base: RandomAccessCollection {} - -extension WindowsOfCountCollection: LazySequenceProtocol, LazyCollectionProtocol - where Base: LazySequenceProtocol {} - -extension WindowsOfCountCollection.Index: Hashable where Base.Index: Hashable {} diff --git a/Pods/Target Support Files/Charts/Charts-Info.plist b/Pods/Target Support Files/Charts/Charts-Info.plist deleted file mode 100644 index c8bd5a0e5..000000000 --- a/Pods/Target Support Files/Charts/Charts-Info.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - ${PODS_DEVELOPMENT_LANGUAGE} - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 4.1.0 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - - - diff --git a/Pods/Target Support Files/Charts/Charts-dummy.m b/Pods/Target Support Files/Charts/Charts-dummy.m deleted file mode 100644 index 0d0640ce9..000000000 --- a/Pods/Target Support Files/Charts/Charts-dummy.m +++ /dev/null @@ -1,5 +0,0 @@ -#import -@interface PodsDummy_Charts : NSObject -@end -@implementation PodsDummy_Charts -@end diff --git a/Pods/Target Support Files/Charts/Charts-prefix.pch b/Pods/Target Support Files/Charts/Charts-prefix.pch deleted file mode 100644 index beb2a2441..000000000 --- a/Pods/Target Support Files/Charts/Charts-prefix.pch +++ /dev/null @@ -1,12 +0,0 @@ -#ifdef __OBJC__ -#import -#else -#ifndef FOUNDATION_EXPORT -#if defined(__cplusplus) -#define FOUNDATION_EXPORT extern "C" -#else -#define FOUNDATION_EXPORT extern -#endif -#endif -#endif - diff --git a/Pods/Target Support Files/Charts/Charts-umbrella.h b/Pods/Target Support Files/Charts/Charts-umbrella.h deleted file mode 100644 index 006a8a229..000000000 --- a/Pods/Target Support Files/Charts/Charts-umbrella.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifdef __OBJC__ -#import -#else -#ifndef FOUNDATION_EXPORT -#if defined(__cplusplus) -#define FOUNDATION_EXPORT extern "C" -#else -#define FOUNDATION_EXPORT extern -#endif -#endif -#endif - - -FOUNDATION_EXPORT double ChartsVersionNumber; -FOUNDATION_EXPORT const unsigned char ChartsVersionString[]; - diff --git a/Pods/Target Support Files/Charts/Charts.debug.xcconfig b/Pods/Target Support Files/Charts/Charts.debug.xcconfig deleted file mode 100644 index 476d81f32..000000000 --- a/Pods/Target Support Files/Charts/Charts.debug.xcconfig +++ /dev/null @@ -1,16 +0,0 @@ -CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Charts -FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms" -GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Algorithms" -OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -PODS_BUILD_DIR = ${BUILD_DIR} -PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) -PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} -PODS_ROOT = ${SRCROOT} -PODS_TARGET_SRCROOT = ${PODS_ROOT}/Charts -PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates -PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} -SKIP_INSTALL = YES -USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Charts/Charts.modulemap b/Pods/Target Support Files/Charts/Charts.modulemap deleted file mode 100644 index 663dec7af..000000000 --- a/Pods/Target Support Files/Charts/Charts.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module Charts { - umbrella header "Charts-umbrella.h" - - export * - module * { export * } -} diff --git a/Pods/Target Support Files/Charts/Charts.release.xcconfig b/Pods/Target Support Files/Charts/Charts.release.xcconfig deleted file mode 100644 index 476d81f32..000000000 --- a/Pods/Target Support Files/Charts/Charts.release.xcconfig +++ /dev/null @@ -1,16 +0,0 @@ -CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Charts -FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms" -GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Algorithms" -OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -PODS_BUILD_DIR = ${BUILD_DIR} -PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) -PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} -PODS_ROOT = ${SRCROOT} -PODS_TARGET_SRCROOT = ${PODS_ROOT}/Charts -PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates -PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} -SKIP_INSTALL = YES -USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.markdown b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.markdown index 22a3d16f8..50e32d8ba 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.markdown +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.markdown @@ -1,212 +1,6 @@ # Acknowledgements This application makes use of the following third party libraries: -## Charts - -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2016-2019 Daniel Cohen Gindi & Philipp Jahoda - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - ## ShareClient The MIT License (MIT) @@ -231,219 +25,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -## SwiftAlgorithms - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - -## Runtime Library Exception to the Apache 2.0 License: ## - - - As an exception, if you use this Software to compile your source code and - portions of this Software are embedded into the binary product as a result, - you may redistribute such product without providing attribution as would - otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. - Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.plist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.plist index 3b4731ff9..c8d33e3b1 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.plist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.plist @@ -12,223 +12,11 @@ Type PSGroupSpecifier - - FooterText - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2016-2019 Daniel Cohen Gindi & Philipp Jahoda - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - License - Apache License, Version 2.0 - Title - Charts - Type - PSGroupSpecifier - FooterText The MIT License (MIT) -Copyright (c) 2019 XMARTLABS +Copyright (c) 2016 Mark Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -255,227 +43,6 @@ SOFTWARE. Type PSGroupSpecifier - - FooterText - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - -## Runtime Library Exception to the Apache 2.0 License: ## - - - As an exception, if you use this Software to compile your source code and - portions of this Software are embedded into the binary product as a result, - you may redistribute such product without providing attribution as would - otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. - - License - Apache License, Version 2.0 - Title - SwiftAlgorithms - Type - PSGroupSpecifier - FooterText Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-input-files.xcfilelist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-input-files.xcfilelist index 674c1e10e..a11fac43b 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-input-files.xcfilelist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-input-files.xcfilelist @@ -1,4 +1,2 @@ ${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh -${BUILT_PRODUCTS_DIR}/Charts/Charts.framework -${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework -${BUILT_PRODUCTS_DIR}/SwiftAlgorithms/Algorithms.framework \ No newline at end of file +${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-output-files.xcfilelist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-output-files.xcfilelist index 7ef5b39a5..3a25c971d 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-output-files.xcfilelist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-output-files.xcfilelist @@ -1,3 +1 @@ -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ShareClient.framework -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Algorithms.framework \ No newline at end of file +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ShareClient.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-input-files.xcfilelist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-input-files.xcfilelist index 674c1e10e..a11fac43b 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-input-files.xcfilelist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-input-files.xcfilelist @@ -1,4 +1,2 @@ ${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh -${BUILT_PRODUCTS_DIR}/Charts/Charts.framework -${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework -${BUILT_PRODUCTS_DIR}/SwiftAlgorithms/Algorithms.framework \ No newline at end of file +${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-output-files.xcfilelist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-output-files.xcfilelist index 7ef5b39a5..3a25c971d 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-output-files.xcfilelist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-output-files.xcfilelist @@ -1,3 +1 @@ -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ShareClient.framework -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Algorithms.framework \ No newline at end of file +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ShareClient.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh index a25e47c80..10761f31b 100755 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh @@ -18,7 +18,7 @@ echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" -SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" BCSYMBOLMAP_DIR="BCSymbolMaps" @@ -176,14 +176,10 @@ code_sign_if_enabled() { } if [[ "$CONFIGURATION" == "Debug" ]]; then - install_framework "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework" install_framework "${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework" - install_framework "${BUILT_PRODUCTS_DIR}/SwiftAlgorithms/Algorithms.framework" fi if [[ "$CONFIGURATION" == "Release" ]]; then - install_framework "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework" install_framework "${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework" - install_framework "${BUILT_PRODUCTS_DIR}/SwiftAlgorithms/Algorithms.framework" fi if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then wait diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig index 88d7b4e2a..67e4b3f2f 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig @@ -1,11 +1,12 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms" +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts/Charts.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient/ShareClient.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms/Algorithms.framework/Headers" +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient/ShareClient.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Algorithms" -framework "Charts" -framework "Foundation" -framework "ShareClient" -framework "UIKit" +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "ShareClient" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig index 88d7b4e2a..67e4b3f2f 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig @@ -1,11 +1,12 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms" +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts/Charts.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient/ShareClient.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms/Algorithms.framework/Headers" +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient/ShareClient.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Algorithms" -framework "Charts" -framework "Foundation" -framework "ShareClient" -framework "UIKit" +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "ShareClient" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) diff --git a/Pods/Target Support Files/ShareClient/ShareClient.debug.xcconfig b/Pods/Target Support Files/ShareClient/ShareClient.debug.xcconfig index 3268b8abb..a12b0ae09 100644 --- a/Pods/Target Support Files/ShareClient/ShareClient.debug.xcconfig +++ b/Pods/Target Support Files/ShareClient/ShareClient.debug.xcconfig @@ -1,7 +1,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/ShareClient GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) diff --git a/Pods/Target Support Files/ShareClient/ShareClient.release.xcconfig b/Pods/Target Support Files/ShareClient/ShareClient.release.xcconfig index 3268b8abb..a12b0ae09 100644 --- a/Pods/Target Support Files/ShareClient/ShareClient.release.xcconfig +++ b/Pods/Target Support Files/ShareClient/ShareClient.release.xcconfig @@ -1,7 +1,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/ShareClient GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) diff --git a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-Info.plist b/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-Info.plist deleted file mode 100644 index 19cf209d2..000000000 --- a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-Info.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - ${PODS_DEVELOPMENT_LANGUAGE} - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0.0 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - - - diff --git a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-dummy.m b/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-dummy.m deleted file mode 100644 index b649e360b..000000000 --- a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-dummy.m +++ /dev/null @@ -1,5 +0,0 @@ -#import -@interface PodsDummy_SwiftAlgorithms : NSObject -@end -@implementation PodsDummy_SwiftAlgorithms -@end diff --git a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-prefix.pch b/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-prefix.pch deleted file mode 100644 index beb2a2441..000000000 --- a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-prefix.pch +++ /dev/null @@ -1,12 +0,0 @@ -#ifdef __OBJC__ -#import -#else -#ifndef FOUNDATION_EXPORT -#if defined(__cplusplus) -#define FOUNDATION_EXPORT extern "C" -#else -#define FOUNDATION_EXPORT extern -#endif -#endif -#endif - diff --git a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-umbrella.h b/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-umbrella.h deleted file mode 100644 index 5ce2116fd..000000000 --- a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms-umbrella.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifdef __OBJC__ -#import -#else -#ifndef FOUNDATION_EXPORT -#if defined(__cplusplus) -#define FOUNDATION_EXPORT extern "C" -#else -#define FOUNDATION_EXPORT extern -#endif -#endif -#endif - - -FOUNDATION_EXPORT double AlgorithmsVersionNumber; -FOUNDATION_EXPORT const unsigned char AlgorithmsVersionString[]; - diff --git a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms.debug.xcconfig b/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms.debug.xcconfig deleted file mode 100644 index da5f012f6..000000000 --- a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms.debug.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms -GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -PODS_BUILD_DIR = ${BUILD_DIR} -PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) -PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} -PODS_ROOT = ${SRCROOT} -PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftAlgorithms -PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates -PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} -SKIP_INSTALL = YES -USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms.modulemap b/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms.modulemap deleted file mode 100644 index 8d150244a..000000000 --- a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module Algorithms { - umbrella header "SwiftAlgorithms-umbrella.h" - - export * - module * { export * } -} diff --git a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms.release.xcconfig b/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms.release.xcconfig deleted file mode 100644 index da5f012f6..000000000 --- a/Pods/Target Support Files/SwiftAlgorithms/SwiftAlgorithms.release.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms -GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -PODS_BUILD_DIR = ${BUILD_DIR} -PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) -PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} -PODS_ROOT = ${SRCROOT} -PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftAlgorithms -PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates -PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} -SKIP_INSTALL = YES -USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/RestartLiveActivityIntent.swift b/RestartLiveActivityIntent.swift deleted file mode 100644 index 95563dafc..000000000 --- a/RestartLiveActivityIntent.swift +++ /dev/null @@ -1,47 +0,0 @@ -// LoopFollow -// RestartLiveActivityIntent.swift - -#if !targetEnvironment(macCatalyst) - import AppIntents - import UIKit - - @available(iOS 16.4, *) - struct RestartLiveActivityIntent: AppIntent, ForegroundContinuableIntent { - static var title: LocalizedStringResource = "Restart Live Activity" - static var description = IntentDescription("Starts or restarts the LoopFollow Live Activity.") - - func perform() async throws -> some IntentResult & ProvidesDialog { - Storage.shared.laEnabled.value = true - - let keyId = Storage.shared.lfKeyId.value - let apnsKey = Storage.shared.lfApnsKey.value - - if keyId.isEmpty || apnsKey.isEmpty { - if let url = URL(string: "loopfollow://settings/live-activity") { - await MainActor.run { UIApplication.shared.open(url) } - } - return .result(dialog: "Please enter your APNs credentials in LoopFollow settings to use the Live Activity.") - } - - if #available(iOS 26.0, *) { - try await continueInForeground() - } - - await MainActor.run { LiveActivityManager.shared.forceRestart() } - - return .result(dialog: "Live Activity restarted.") - } - } - - @available(iOS 16.4, *) - struct LoopFollowAppShortcuts: AppShortcutsProvider { - static var appShortcuts: [AppShortcut] { - AppShortcut( - intent: RestartLiveActivityIntent(), - phrases: ["Restart Live Activity in \(.applicationName)"], - shortTitle: "Restart Live Activity", - systemImageName: "dot.radiowaves.left.and.right" - ) - } - } -#endif diff --git a/LoopFollow/LiveActivity/AppGroupID.swift b/Shared/AppGroupID.swift similarity index 100% rename from LoopFollow/LiveActivity/AppGroupID.swift rename to Shared/AppGroupID.swift diff --git a/LoopFollow/Helpers/GlucoseConversion.swift b/Shared/GlucoseConversion.swift similarity index 100% rename from LoopFollow/Helpers/GlucoseConversion.swift rename to Shared/GlucoseConversion.swift diff --git a/LoopFollow/LiveActivity/GlucoseLiveActivityAttributes.swift b/Shared/GlucoseLiveActivityAttributes.swift similarity index 100% rename from LoopFollow/LiveActivity/GlucoseLiveActivityAttributes.swift rename to Shared/GlucoseLiveActivityAttributes.swift diff --git a/LoopFollow/LiveActivity/GlucoseSnapshot.swift b/Shared/GlucoseSnapshot.swift similarity index 89% rename from LoopFollow/LiveActivity/GlucoseSnapshot.swift rename to Shared/GlucoseSnapshot.swift index c5f5fffcc..6800ccd3b 100644 --- a/LoopFollow/LiveActivity/GlucoseSnapshot.swift +++ b/Shared/GlucoseSnapshot.swift @@ -52,6 +52,17 @@ struct GlucoseSnapshot: Codable, Equatable, Hashable { /// Active override name (nil if no active override) let override: String? + /// End of the active override as Unix epoch seconds. Only meaningful while + /// `override` is non-nil; nil with a non-nil name means the override is + /// indefinite (no countdown). + let overrideEndAt: TimeInterval? + + /// Active temporary target in mg/dL (nil if no active temp target) + let tempTargetMgdl: Double? + + /// End of the active temporary target as Unix epoch seconds (nil if no active temp target) + let tempTargetEndAt: TimeInterval? + /// Recommended bolus in units (nil if not available) let recBolus: Double? @@ -134,6 +145,9 @@ struct GlucoseSnapshot: Codable, Equatable, Hashable { cob: Double?, projected: Double?, override: String? = nil, + overrideEndAt: TimeInterval? = nil, + tempTargetMgdl: Double? = nil, + tempTargetEndAt: TimeInterval? = nil, recBolus: Double? = nil, battery: Double? = nil, pumpBattery: Double? = nil, @@ -164,6 +178,9 @@ struct GlucoseSnapshot: Codable, Equatable, Hashable { self.cob = cob self.projected = projected self.override = override + self.overrideEndAt = overrideEndAt + self.tempTargetMgdl = tempTargetMgdl + self.tempTargetEndAt = tempTargetEndAt self.recBolus = recBolus self.battery = battery self.pumpBattery = pumpBattery @@ -207,6 +224,9 @@ struct GlucoseSnapshot: Codable, Equatable, Hashable { cob: cob, projected: projected, override: override, + overrideEndAt: overrideEndAt, + tempTargetMgdl: tempTargetMgdl, + tempTargetEndAt: tempTargetEndAt, recBolus: recBolus, battery: battery, pumpBattery: pumpBattery, @@ -243,6 +263,9 @@ struct GlucoseSnapshot: Codable, Equatable, Hashable { try container.encodeIfPresent(cob, forKey: .cob) try container.encodeIfPresent(projected, forKey: .projected) try container.encodeIfPresent(override, forKey: .override) + try container.encodeIfPresent(overrideEndAt, forKey: .overrideEndAt) + try container.encodeIfPresent(tempTargetMgdl, forKey: .tempTargetMgdl) + try container.encodeIfPresent(tempTargetEndAt, forKey: .tempTargetEndAt) try container.encodeIfPresent(recBolus, forKey: .recBolus) try container.encodeIfPresent(battery, forKey: .battery) try container.encodeIfPresent(pumpBattery, forKey: .pumpBattery) @@ -276,6 +299,9 @@ struct GlucoseSnapshot: Codable, Equatable, Hashable { cob = try container.decodeIfPresent(Double.self, forKey: .cob) projected = try container.decodeIfPresent(Double.self, forKey: .projected) override = try container.decodeIfPresent(String.self, forKey: .override) + overrideEndAt = try container.decodeIfPresent(Double.self, forKey: .overrideEndAt) + tempTargetMgdl = try container.decodeIfPresent(Double.self, forKey: .tempTargetMgdl) + tempTargetEndAt = try container.decodeIfPresent(Double.self, forKey: .tempTargetEndAt) recBolus = try container.decodeIfPresent(Double.self, forKey: .recBolus) battery = try container.decodeIfPresent(Double.self, forKey: .battery) pumpBattery = try container.decodeIfPresent(Double.self, forKey: .pumpBattery) @@ -302,7 +328,8 @@ struct GlucoseSnapshot: Codable, Equatable, Hashable { private enum CodingKeys: String, CodingKey { case glucose, delta, trend, updatedAt case iob, cob, projected - case override, recBolus, battery, pumpBattery, basalRate, pumpReservoirU + case override, overrideEndAt, tempTargetMgdl, tempTargetEndAt + case recBolus, battery, pumpBattery, basalRate, pumpReservoirU case autosens, tdd, targetLowMgdl, targetHighMgdl, isfMgdlPerU, carbRatio, carbsToday case profileName, sageInsertTime, cageInsertTime, iageInsertTime, minBgMgdl, maxBgMgdl case unit, isNotLooping, showRenewalOverlay diff --git a/LoopFollow/LiveActivity/LAAppGroupSettings.swift b/Shared/LAAppGroupSettings.swift similarity index 92% rename from LoopFollow/LiveActivity/LAAppGroupSettings.swift rename to Shared/LAAppGroupSettings.swift index 6359fe55e..bdf93d56b 100644 --- a/LoopFollow/LiveActivity/LAAppGroupSettings.swift +++ b/Shared/LAAppGroupSettings.swift @@ -106,6 +106,14 @@ enum LiveActivitySlotOption: String, CaseIterable, Codable { } } + /// Options selectable for the lock-screen 2×2 grid. The active override is + /// excluded — the card has a dedicated override/temp-target countdown row. + /// It remains selectable for the single CarPlay/Watch slot, which has no + /// such row. + static var gridCases: [LiveActivitySlotOption] { + allCases.filter { $0 != .override } + } + /// True when the underlying value may be nil (e.g. Dexcom-only users who have /// no Loop data). The widget renders "—" in those cases. var isOptional: Bool { @@ -188,7 +196,11 @@ enum LAAppGroupSettings { guard let raw = defaults?.stringArray(forKey: Keys.slots), raw.count == 4 else { return LiveActivitySlotDefaults.all } + // .override is no longer a grid option (the card has a dedicated + // override/temp-target row); selections stored by older versions + // render as empty cells. return raw.map { LiveActivitySlotOption(rawValue: $0) ?? .none } + .map { $0 == .override ? .none : $0 } } // MARK: - Small widget slot (Write) diff --git a/LoopFollow/LiveActivity/LALivenessMarker.swift b/Shared/LALivenessMarker.swift similarity index 100% rename from LoopFollow/LiveActivity/LALivenessMarker.swift rename to Shared/LALivenessMarker.swift diff --git a/LoopFollow/LiveActivity/LALivenessStore.swift b/Shared/LALivenessStore.swift similarity index 100% rename from LoopFollow/LiveActivity/LALivenessStore.swift rename to Shared/LALivenessStore.swift diff --git a/Tests/AlarmConditions/BatteryConditionTests.swift b/Tests/AlarmConditions/BatteryConditionTests.swift index 804a7a728..580b51b2d 100644 --- a/Tests/AlarmConditions/BatteryConditionTests.swift +++ b/Tests/AlarmConditions/BatteryConditionTests.swift @@ -1,6 +1,7 @@ // LoopFollow // BatteryConditionTests.swift +import Foundation @testable import LoopFollow import Testing diff --git a/Tests/AlarmConditions/FastDropConditionTests.swift b/Tests/AlarmConditions/FastDropConditionTests.swift new file mode 100644 index 000000000..33b332702 --- /dev/null +++ b/Tests/AlarmConditions/FastDropConditionTests.swift @@ -0,0 +1,94 @@ +// LoopFollow +// FastDropConditionTests.swift + +import Foundation +@testable import LoopFollow +import Testing + +/// Fast drop fires only when **each** of the last `monitoringWindow` consecutive +/// intervals dropped by at least `delta` mg/dL. It is per-reading, not a single +/// cumulative drop — these tests pin that behaviour down. +struct FastDropConditionTests { + let cond = FastDropCondition() + + // MARK: - Fires + + @Test("fires when every one of N readings drops by at least delta") + func firesOnConsecutiveDrops() { + // 3 intervals, each falling exactly 15 → about 45 over ~15 minutes + let alarm = Alarm.fastDrop(delta: 15, window: 3) + let data = AlarmData.withReadings([150, 135, 120, 105]) + #expect(cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("a drop exactly equal to delta still counts") + func firesAtExactThreshold() { + let alarm = Alarm.fastDrop(delta: 15, window: 1) + let data = AlarmData.withReadings([120, 105]) + #expect(cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("only the most recent window+1 readings matter") + func ignoresOlderReadings() { + // Earlier readings rise sharply; the last 4 fall 15 each → fires + let alarm = Alarm.fastDrop(delta: 15, window: 3) + let data = AlarmData.withReadings([100, 200, 165, 150, 135, 120]) + #expect(cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + // MARK: - Does not fire + + @Test("does NOT fire when one interval falls short of delta") + func doesNotFireWhenOneIntervalTooSmall() { + let alarm = Alarm.fastDrop(delta: 15, window: 3) + // middle interval is only 10 + let data = AlarmData.withReadings([150, 135, 125, 110]) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("per-reading, not cumulative: 40+5+5 = 50 total but does NOT fire") + func doesNotFireOnCumulativeOnly() { + let alarm = Alarm.fastDrop(delta: 15, window: 3) + // total fall is 50 (> 45) but two intervals are only 5 each + let data = AlarmData.withReadings([150, 110, 105, 100]) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("does NOT fire when glucose is rising") + func doesNotFireWhenRising() { + let alarm = Alarm.fastDrop(delta: 15, window: 3) + let data = AlarmData.withReadings([100, 115, 130, 145]) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("does NOT fire without enough readings") + func doesNotFireWithTooFewReadings() { + // window 3 needs 4 readings; only 3 supplied + let alarm = Alarm.fastDrop(delta: 15, window: 3) + let data = AlarmData.withReadings([135, 120, 105]) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + // MARK: - Guard cases + + @Test("does NOT fire when delta is nil") + func ignoresNilDelta() { + let alarm = Alarm.fastDrop(delta: nil, window: 3) + let data = AlarmData.withReadings([150, 135, 120, 105]) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("does NOT fire when window is nil") + func ignoresNilWindow() { + let alarm = Alarm.fastDrop(delta: 15, window: nil) + let data = AlarmData.withReadings([150, 135, 120, 105]) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("does NOT fire when window is zero") + func ignoresZeroWindow() { + let alarm = Alarm.fastDrop(delta: 15, window: 0) + let data = AlarmData.withReadings([150, 135, 120, 105]) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } +} diff --git a/Tests/AlarmConditions/Helpers.swift b/Tests/AlarmConditions/Helpers.swift index d6b05630c..b4287504e 100644 --- a/Tests/AlarmConditions/Helpers.swift +++ b/Tests/AlarmConditions/Helpers.swift @@ -28,6 +28,21 @@ extension Alarm { alarm.delta = delta return alarm } + + static func low(belowBG: Double?, predictiveMinutes: Int? = nil, persistentMinutes: Int? = nil) -> Self { + var alarm = Alarm(type: .low) + alarm.belowBG = belowBG + alarm.predictiveMinutes = predictiveMinutes + alarm.persistentMinutes = persistentMinutes + return alarm + } + + static func fastDrop(delta: Double?, window: Int?) -> Self { + var alarm = Alarm(type: .fastDrop) + alarm.delta = delta + alarm.monitoringWindow = window + return alarm + } } // MARK: - AlarmData helpers @@ -51,9 +66,11 @@ extension AlarmData { IOB: nil, recentBoluses: [], latestBattery: level, + latestBatteryIsCharging: nil, latestPumpBattery: nil, batteryHistory: [], - recentCarbs: [] + recentCarbs: [], + dbSizePercentage: nil ) } @@ -75,9 +92,71 @@ extension AlarmData { IOB: nil, recentBoluses: [], latestBattery: nil, + latestBatteryIsCharging: nil, + latestPumpBattery: nil, + batteryHistory: [], + recentCarbs: [], + dbSizePercentage: nil + ) + } + + /// Builds an `AlarmData` from a list of glucose values ordered **oldest → + /// newest** (the same order the app stores them; `bgReadings.last` is the most + /// recent reading). Readings are spaced 5 minutes apart ending now. + static func withReadings(_ sgvs: [Int]) -> Self { + let now = Date() + let last = sgvs.count - 1 + let readings = sgvs.enumerated().map { offset, sgv in + GlucoseValue(sgv: sgv, date: now.addingTimeInterval(Double(offset - last) * 300)) + } + return AlarmData( + bgReadings: readings, + predictionData: [], + expireDate: nil, + lastLoopTime: nil, + latestOverrideStart: nil, + latestOverrideEnd: nil, + latestTempTargetStart: nil, + latestTempTargetEnd: nil, + recBolus: nil, + COB: nil, + sageInsertTime: nil, + pumpInsertTime: nil, + latestPumpVolume: nil, + IOB: nil, + recentBoluses: [], + latestBattery: nil, + latestBatteryIsCharging: nil, + latestPumpBattery: nil, + batteryHistory: [], + recentCarbs: [], + dbSizePercentage: nil + ) + } + + static func withGlucose(readings: [GlucoseValue] = [], prediction: [GlucoseValue] = []) -> Self { + AlarmData( + bgReadings: readings, + predictionData: prediction, + expireDate: nil, + lastLoopTime: nil, + latestOverrideStart: nil, + latestOverrideEnd: nil, + latestTempTargetStart: nil, + latestTempTargetEnd: nil, + recBolus: nil, + COB: nil, + sageInsertTime: nil, + pumpInsertTime: nil, + latestPumpVolume: nil, + IOB: nil, + recentBoluses: [], + latestBattery: nil, + latestBatteryIsCharging: nil, latestPumpBattery: nil, batteryHistory: [], - recentCarbs: [] + recentCarbs: [], + dbSizePercentage: nil ) } @@ -99,9 +178,11 @@ extension AlarmData { IOB: nil, recentBoluses: [], latestBattery: nil, + latestBatteryIsCharging: nil, latestPumpBattery: nil, batteryHistory: [], - recentCarbs: carbs + recentCarbs: carbs, + dbSizePercentage: nil ) } } diff --git a/Tests/AlarmConditions/LowBGConditionTests.swift b/Tests/AlarmConditions/LowBGConditionTests.swift new file mode 100644 index 000000000..287799d82 --- /dev/null +++ b/Tests/AlarmConditions/LowBGConditionTests.swift @@ -0,0 +1,211 @@ +// LoopFollow +// LowBGConditionTests.swift + +import Foundation +@testable import LoopFollow +import Testing + +@Suite(.serialized) +struct LowBGConditionTests { + let cond = LowBGCondition() + + /// Builds a forward prediction series at 5-minute spacing. + private func pred(_ values: [Int], from start: Date = Date()) -> [GlucoseValue] { + values.enumerated().map { i, v in + GlucoseValue(sgv: v, date: start.addingTimeInterval(Double(i) * 300)) + } + } + + /// Builds a recent BG history (oldest .. newest) at 5-minute spacing ending now. + private func history(_ values: [Int], endingAt now: Date = Date()) -> [GlucoseValue] { + values.enumerated().map { i, v in + let offset = Double(values.count - 1 - i) * 300 + return GlucoseValue(sgv: v, date: now.addingTimeInterval(-offset)) + } + } + + /// Recent readings that are clearly above any low threshold. Used by the + /// predictive tests so the persistence branch evaluates to `false` and the + /// result reflects the predictive look-ahead alone. + private var recentHigh: [GlucoseValue] { history([120, 120, 120]) } + + // MARK: - Loop (single forecast) + + @Test("#loop — predictive low within window fires") + func loopPredictiveLowFires() { + let alarm = Alarm.low(belowBG: 80, predictiveMinutes: 30, persistentMinutes: 15) + // ceil(30/5) = 6 points looked at; index 5 dips to 75 + let data = AlarmData.withGlucose(readings: recentHigh, prediction: pred([120, 110, 100, 90, 85, 75])) + + #expect(cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#loop — forecast low beyond window does not fire") + func loopPredictiveLowBeyondWindow() { + let alarm = Alarm.low(belowBG: 80, predictiveMinutes: 15, persistentMinutes: 15) + // ceil(15/5) = 3 points looked at (120, 110, 100); the low only appears later + let data = AlarmData.withGlucose(readings: recentHigh, prediction: pred([120, 110, 100, 90, 85, 75])) + + #expect(!cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#loop — forecast staying above threshold does not fire") + func loopForecastAboveThreshold() { + let alarm = Alarm.low(belowBG: 80, predictiveMinutes: 60, persistentMinutes: 15) + let data = AlarmData.withGlucose(readings: recentHigh, prediction: pred([120, 110, 100, 95, 90, 85])) + + #expect(!cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#loop — predictiveMinutes 0 disables look-ahead") + func loopPredictiveDisabled() { + let alarm = Alarm.low(belowBG: 80, predictiveMinutes: 0, persistentMinutes: 15) + let data = AlarmData.withGlucose(readings: recentHigh, prediction: pred([60, 60, 60])) + + #expect(!cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + // MARK: - Trio (lowest of four forecasts) + + @Test("#trio — combined forecast fires when one forecast dips low") + func trioCombinedForecastFires() { + let alarm = Alarm.low(belowBG: 80, predictiveMinutes: 30, persistentMinutes: 15) + // ZT stays high, but the IOB forecast dips to 70 — the per-point minimum + // must surface that dip so the alarm fires. + let forecasts: [[Double]] = [ + [150, 150, 150, 150, 150, 150], // ZT + [120, 110, 100, 90, 80, 70], // IOB + [130, 125, 120, 118, 116, 115], // COB + [140, 138, 136, 134, 132, 130], // UAM + ] + let combined = MainViewController.lowestForecast(forecasts: forecasts, start: Date().timeIntervalSince1970) + let data = AlarmData.withGlucose(readings: recentHigh, prediction: combined) + + #expect(cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#trio — combined forecast does not fire when all forecasts stay high") + func trioCombinedForecastNoFire() { + let alarm = Alarm.low(belowBG: 80, predictiveMinutes: 60, persistentMinutes: 15) + let forecasts: [[Double]] = [ + [150, 150, 150, 150, 150, 150], + [120, 110, 100, 95, 92, 90], + [130, 125, 120, 118, 116, 115], + [140, 138, 136, 134, 132, 130], + ] + let combined = MainViewController.lowestForecast(forecasts: forecasts, start: Date().timeIntervalSince1970) + let data = AlarmData.withGlucose(readings: recentHigh, prediction: combined) + + #expect(!cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#trio — deep forecast below display floor still fires (not masked)") + func trioDeepLowNotMasked() { + let alarm = Alarm.low(belowBG: 70, predictiveMinutes: 30, persistentMinutes: 15) + // A forecast dipping to 30 (below the 39 display floor) is passed through + // raw, so the predictive-low alarm still fires on a genuine deep low. + let forecasts: [[Double]] = [ + [150, 150, 150, 150], + [120, 100, 60, 30], + ] + let combined = MainViewController.lowestForecast(forecasts: forecasts, start: Date().timeIntervalSince1970) + let data = AlarmData.withGlucose(readings: recentHigh, prediction: combined) + + #expect(cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + // MARK: - Persistent / immediate low (device-agnostic) + + @Test("#persistent — all readings in window low fires") + func persistentLowFires() { + let alarm = Alarm.low(belowBG: 80, persistentMinutes: 15) // window = 3 readings + let data = AlarmData.withGlucose(readings: history([75, 72, 70])) + + #expect(cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#persistent — one reading above threshold does not fire") + func persistentLowOneAbove() { + let alarm = Alarm.low(belowBG: 80, persistentMinutes: 15) + let data = AlarmData.withGlucose(readings: history([75, 95, 70])) + + #expect(!cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#persistent — not enough samples does not fire") + func persistentNotEnoughSamples() { + let alarm = Alarm.low(belowBG: 80, persistentMinutes: 15) // needs 3 + let data = AlarmData.withGlucose(readings: history([70, 70])) + + #expect(!cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#no belowBG threshold never fires") + func noThresholdNoFire() { + let alarm = Alarm.low(belowBG: nil, predictiveMinutes: 30, persistentMinutes: 15) + let data = AlarmData.withGlucose(readings: history([40, 40, 40]), prediction: pred([40, 40, 40])) + + #expect(!cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + // MARK: - Immediate low (no persistence, no prediction) + + @Test("#immediate — latest reading at/below threshold fires") + func immediateLowFires() { + let alarm = Alarm.low(belowBG: 80) + let data = AlarmData.withGlucose(readings: history([100, 90, 78])) + + #expect(cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#immediate — latest reading above threshold does not fire") + func immediateAboveThreshold() { + let alarm = Alarm.low(belowBG: 80) + let data = AlarmData.withGlucose(readings: history([85, 83, 82])) + + #expect(!cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + @Test("#immediate — no readings does not fire even with a low forecast") + func noReadingsNoFire() { + let alarm = Alarm.low(belowBG: 80, predictiveMinutes: 30) + let data = AlarmData.withGlucose(readings: [], prediction: pred([60, 60])) + + #expect(!cond.evaluate(alarm: alarm, data: data, now: Date())) + } + + // MARK: - Full pipeline (shouldFire, including the BG-limit gate) + + @Test("#pipeline — predictive low fires while BG is still above the threshold") + func predictiveFiresThroughGate() { + let alarm = Alarm.low(belowBG: 108, predictiveMinutes: 30, persistentMinutes: 0) + // The scenario predictive low exists for: BG hovering above the + // threshold while the forecast dips below it. + let data = AlarmData.withGlucose( + readings: history([110, 109, 110]), + prediction: pred([110, 101, 92, 82]) + ) + + #expect(cond.shouldFire(alarm: alarm, data: data, now: Date(), config: .default)) + } + + @Test("#pipeline — BG above threshold with a high forecast does not fire") + func aboveThresholdHighForecastNoFire() { + let alarm = Alarm.low(belowBG: 108, predictiveMinutes: 30, persistentMinutes: 0) + let data = AlarmData.withGlucose( + readings: history([110, 109, 110]), + prediction: pred([110, 112, 115, 118]) + ) + + #expect(!cond.shouldFire(alarm: alarm, data: data, now: Date(), config: .default)) + } + + @Test("#pipeline — BG-limit gate blocks other alarm types") + func gateAppliesToOtherTypes() { + var alarm = Alarm(type: .temporary) + alarm.belowBG = 108 + let data = AlarmData.withGlucose(readings: history([110, 109, 110])) + + #expect(!TemporaryCondition().shouldFire(alarm: alarm, data: data, now: Date(), config: .default)) + } +} diff --git a/Tests/AlarmConditions/LowestForecastTests.swift b/Tests/AlarmConditions/LowestForecastTests.swift new file mode 100644 index 000000000..3e821aa7d --- /dev/null +++ b/Tests/AlarmConditions/LowestForecastTests.swift @@ -0,0 +1,98 @@ +// LoopFollow +// LowestForecastTests.swift + +import Foundation +@testable import LoopFollow +import Testing + +@Suite(.serialized) +struct LowestForecastTests { + private let start: TimeInterval = 1_000_000 + + // MARK: - Combining + + @Test("#takes the per-point minimum across forecasts") + func perPointMinimum() { + let forecasts: [[Double]] = [ + [150, 140, 130, 120], + [120, 130, 100, 160], + [130, 110, 140, 90], + ] + let result = MainViewController.lowestForecast(forecasts: forecasts, start: start) + + #expect(result.map(\.sgv) == [120, 110, 100, 90]) + } + + @Test("#empty forecasts are ignored") + func emptyForecastsIgnored() { + let forecasts: [[Double]] = [ + [], + [120, 110, 100], + [], + ] + let result = MainViewController.lowestForecast(forecasts: forecasts, start: start) + + #expect(result.map(\.sgv) == [120, 110, 100]) + } + + @Test("#all empty yields no points") + func allEmpty() { + let result = MainViewController.lowestForecast(forecasts: [[], []], start: start) + #expect(result.isEmpty) + } + + @Test("#no forecasts yields no points") + func noForecasts() { + let result = MainViewController.lowestForecast(forecasts: [], start: start) + #expect(result.isEmpty) + } + + @Test("#uneven lengths use whichever forecasts still extend") + func unevenLengths() { + let forecasts: [[Double]] = [ + [100, 90], // shorter + [110, 95, 80, 70], // longer + ] + let result = MainViewController.lowestForecast(forecasts: forecasts, start: start) + + // Points 0-1 use the min of both; points 2-3 use only the longer forecast. + #expect(result.map(\.sgv) == [100, 90, 80, 70]) + } + + // MARK: - Capping + + @Test("#caps the number of points") + func capsPoints() { + let long = Array(repeating: 100.0, count: 30) + let result = MainViewController.lowestForecast(forecasts: [long], start: start, cap: 12) + + #expect(result.count == 12) + } + + @Test("#default cap is 12 points") + func defaultCap() { + let long = Array(repeating: 100.0, count: 30) + let result = MainViewController.lowestForecast(forecasts: [long], start: start) + + #expect(result.count == MainViewController.alarmForecastPointCap) + #expect(result.count == 12) + } + + // MARK: - Timestamps & rounding + + @Test("#points are spaced 5 minutes from start") + func timestampSpacing() { + let forecasts: [[Double]] = [[100, 100, 100]] + let result = MainViewController.lowestForecast(forecasts: forecasts, start: start) + + #expect(result.map { $0.date.timeIntervalSince1970 } == [start, start + 300, start + 600]) + } + + @Test("#values are rounded to the nearest integer") + func rounding() { + let forecasts: [[Double]] = [[100.4, 100.6, 99.5]] + let result = MainViewController.lowestForecast(forecasts: forecasts, start: start) + + #expect(result.map(\.sgv) == [100, 101, 100]) + } +} diff --git a/Tests/AlarmConditions/SensorAgeConditionTests.swift b/Tests/AlarmConditions/SensorAgeConditionTests.swift index ec407a916..dba9c66b3 100644 --- a/Tests/AlarmConditions/SensorAgeConditionTests.swift +++ b/Tests/AlarmConditions/SensorAgeConditionTests.swift @@ -1,6 +1,7 @@ // LoopFollow // SensorAgeConditionTests.swift +import Foundation @testable import LoopFollow import Testing diff --git a/Tests/InfoColoringTests.swift b/Tests/InfoColoringTests.swift new file mode 100644 index 000000000..1009764d8 --- /dev/null +++ b/Tests/InfoColoringTests.swift @@ -0,0 +1,71 @@ +// LoopFollow +// InfoColoringTests.swift + +@testable import LoopFollow +import SwiftUI +import Testing + +struct InfoColoringTests { + @Test("disabled coloring returns nil regardless of value") + func disabledReturnsNil() { + let coloring = InfoColoring(enabled: false, warning: 30, urgent: 15) + #expect(coloring.color(for: 5, direction: .below) == nil) + } + + @Test("below: battery warn 30 / urgent 15, in-range is green") + func belowDirection() { + let coloring = InfoColoring(enabled: true, warning: 30, urgent: 15) + #expect(coloring.color(for: 40, direction: .below) == .green) + #expect(coloring.color(for: 30, direction: .below) == .yellow) + #expect(coloring.color(for: 25, direction: .below) == .yellow) + #expect(coloring.color(for: 15, direction: .below) == .red) + #expect(coloring.color(for: 10, direction: .below) == .red) + } + + @Test("above: SAGE warn 8 / urgent 9.5, in-range is green") + func aboveDirection() { + let coloring = InfoColoring(enabled: true, warning: 8, urgent: 9.5) + #expect(coloring.color(for: 6, direction: .above) == .green) + #expect(coloring.color(for: 8, direction: .above) == .yellow) + #expect(coloring.color(for: 8.5, direction: .above) == .yellow) + #expect(coloring.color(for: 9.5, direction: .above) == .red) + #expect(coloring.color(for: 12, direction: .above) == .red) + } + + @Test("nil thresholds are green when enabled") + func nilThresholds() { + let coloring = InfoColoring(enabled: true, warning: nil, urgent: nil) + #expect(coloring.color(for: 999, direction: .above) == .green) + } + + @Test("only urgent set still colors red, otherwise green") + func onlyUrgent() { + let coloring = InfoColoring(enabled: true, warning: nil, urgent: 15) + #expect(coloring.color(for: 20, direction: .below) == .green) + #expect(coloring.color(for: 15, direction: .below) == .red) + } + + @Test("pump reservoir colors on the way down, and a capped 50+ reads green") + func pumpReservoir() throws { + let config = try #require(InfoType.pump.colorConfig) + let coloring = InfoColoring(enabled: true, warning: config.defaultWarning, urgent: config.defaultUrgent) + #expect(coloring.color(for: 50, direction: config.direction) == .green) + #expect(coloring.color(for: 20, direction: config.direction) == .yellow) + #expect(coloring.color(for: 10, direction: config.direction) == .red) + } + + @Test("thresholds that need decimals get a fractional step") + func fractionalSteps() { + #expect(InfoType.recBolus.colorConfig?.step == 0.1) + #expect(InfoType.iob.colorConfig?.step == 0.5) + #expect(InfoType.sage.colorConfig?.fractionDigits == 1) + #expect(InfoType.cob.colorConfig?.fractionDigits == 0) + } + + @Test("non-numeric rows are not colorable") + func notColorable() { + #expect(InfoType.basal.isColorable == false) + #expect(InfoType.minMax.isColorable == false) + #expect(InfoType.pump.isColorable) + } +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 120b9f061..8f46645c5 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -23,6 +23,27 @@ DEVICE_NAME = ENV["DEVICE_NAME"] DEVICE_ID = ENV["DEVICE_ID"] ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120" +# Reads the per-app suffix from LoopFollowDisplayNameConfig.xcconfig so the same +# Fastfile produces the correct bundle identifiers in every sister repository +# (LoopFollow, LoopFollow_Second, LoopFollow_Third) without per-repo edits. +def loopfollow_app_suffix + root = GITHUB_WORKSPACE || File.expand_path("..", __dir__) + path = File.join(root, "LoopFollowDisplayNameConfig.xcconfig") + return "" unless File.exist?(path) + + File.foreach(path) do |line| + next if line.strip.start_with?("//") + if (match = line.match(/^\s*app_suffix\s*=\s*(.*)$/)) + return match[1].strip + end + end + "" +end + +APP_SUFFIX = loopfollow_app_suffix +APP_IDENTIFIER = "com.#{TEAMID}.LoopFollow#{APP_SUFFIX}" +LA_IDENTIFIER = "#{APP_IDENTIFIER}.LoopFollowLAExtension" + platform :ios do desc "Build Loop Follow" lane :build_LoopFollow do @@ -40,7 +61,7 @@ platform :ios do ) previous_build_number = latest_testflight_build_number( - app_identifier: "com.#{TEAMID}.LoopFollow", + app_identifier: APP_IDENTIFIER, api_key: api_key, ) @@ -55,8 +76,8 @@ platform :ios do type: "appstore", git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), app_identifier: [ - "com.#{TEAMID}.LoopFollow", - "com.#{TEAMID}.LoopFollow.LoopFollowLAExtension" + APP_IDENTIFIER, + LA_IDENTIFIER ] ) @@ -66,14 +87,14 @@ platform :ios do update_code_signing_settings( path: "#{GITHUB_WORKSPACE}/LoopFollow.xcodeproj", - profile_name: mapping["com.#{TEAMID}.LoopFollow"], + profile_name: mapping[APP_IDENTIFIER], code_sign_identity: "iPhone Distribution", targets: ["LoopFollow"] ) update_code_signing_settings( - path: "#{GITHUB_WORKSPACE}/LoopFollow.xcodeproj", - profile_name: mapping["com.#{TEAMID}.LoopFollow.LoopFollowLAExtension"], + path: "#{GITHUB_WORKSPACE}/LoopFollow.xcodeproj", + profile_name: mapping[LA_IDENTIFIER], code_sign_identity: "iPhone Distribution", targets: ["LoopFollowLAExtensionExtension"] ) @@ -87,10 +108,10 @@ platform :ios do buildlog_path: 'buildlog', export_options: { provisioningProfiles: { - "com.#{TEAMID}.LoopFollow" => mapping["com.#{TEAMID}.LoopFollow"], - "com.#{TEAMID}.LoopFollow.LoopFollowLAExtension" => mapping["com.#{TEAMID}.LoopFollow.LoopFollowLAExtension"] + APP_IDENTIFIER => mapping[APP_IDENTIFIER], + LA_IDENTIFIER => mapping[LA_IDENTIFIER] } - } + } ) copy_artifacts( @@ -138,12 +159,12 @@ platform :ios do end end - configure_bundle_id("LoopFollow", "com.#{TEAMID}.LoopFollow", [ + configure_bundle_id("LoopFollow", APP_IDENTIFIER, [ Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS, Spaceship::ConnectAPI::BundleIdCapability::Type::PUSH_NOTIFICATIONS ]) - - configure_bundle_id("LoopFollow Live Activity Extension", "com.#{TEAMID}.LoopFollow.LoopFollowLAExtension", [ + + configure_bundle_id("LoopFollow Live Activity Extension", LA_IDENTIFIER, [ Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS ]) @@ -166,8 +187,8 @@ platform :ios do verbose: true, git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), app_identifier: [ - "com.#{TEAMID}.LoopFollow", - "com.#{TEAMID}.LoopFollow.LoopFollowLAExtension" + APP_IDENTIFIER, + LA_IDENTIFIER ] ) end diff --git a/fastlane/testflight.md b/fastlane/testflight.md index 860049576..31f5cb5c0 100644 --- a/fastlane/testflight.md +++ b/fastlane/testflight.md @@ -11,11 +11,9 @@ These instructions allow you to build this Open-Source app without having access > > The browser build defaults to automatically updating and building a new version of the app according to this schedule: > -> * automatically checks for updates weekly on Wednesdays and if updates are found, it will build a new version of the app -> * automatically builds once a month regardless of whether there are updates on the first of the month -> * with each scheduled run (weekly or monthly), a successful Build log appears - if the time is very short, it did not need to build - only the long actions (>5 minutes) built a new app -> -> It also creates an alive branch, if you don't already have one. See [Why do I have an alive branch?](#why-do-i-have-an-alive-branch). +> * automatically checks for updates weekly on Sundays and if updates are found, it will build a new version of the app +> * automatically builds once a month regardless of whether there are updates, on the second Sunday of the month +> * if a scheduled run finds nothing new to build, the run is cancelled and shows as cancelled (grey) in the Actions list - a green check always means a new build was made and uploaded to TestFlight > > The [**Optional**](#optional) section provides instructions to modify the default behavior if desired. @@ -23,13 +21,13 @@ These instructions allow you to build this Open-Source app without having access The setup steps are somewhat involved, but nearly all are one time steps. Subsequent builds are trivial. Your app must be updated once every 90 days, but it's a simple click to make a new build and can be done from anywhere. -Note that installing with TestFlight requires the Apple ID account holder for the phone be 13 years or older (age varies with country). This can be circumvented by logging into Media & Purchase on the child's phone with an adult's account. More details on this can be found in [LoopDocs](https://loopkit.github.io/loopdocs/gh-actions/gh-deploy/#install-testflight-loop-for-child). +Note that installing with TestFlight requires the Apple ID account holder for the phone be 13 years or older (age varies with country). This can be circumvented by logging into Media & Purchase on the child's phone with an adult's account. More details on this can be found in [LoopDocs](https://loopkit.github.io/loopdocs/browser/phone-install/#testflight-for-a-child). This method for building without a Mac was ported from Loop. If you have used this method for Loop or one of the other DIY apps (LoopCaregiver, Trio, xDrip4iOS), some of the steps can be re-used and the full set of instructions does not need to be repeated. This will be mentioned in relevant sections below. -There are more detailed instructions in LoopDocs for doing Browser Builds of Loop and other apps, including troubleshooting and build errors. Please refer to [LoopDocs](https://loopkit.github.io/loopdocs/gh-actions/gh-other-apps/) for more details. +There are more detailed instructions in LoopDocs for doing Browser Builds of Loop and other apps, including troubleshooting and build errors. Please refer to [LoopDocs](https://loopkit.github.io/loopdocs/browser/other-apps/) for more details. -If you build multiple apps, it is strongly recommended that you configure a free *GitHub* organization and do all your building in the organization. This means you enter items one time for the organization (6 SECRETS required to build and 1 VARIABLE required to automatically update your certificates annually). Otherwise, those 6 SECRETS must be entered for every repository. Please refer to [LoopDocs: Use a *GitHub* Organization Account](https://loopkit.github.io/loopdocs/gh-actions/gh-other-apps/#use-a-github-organization-account). +If you build multiple apps, it is strongly recommended that you configure a free *GitHub* organization and do all your building in the organization. This means you enter items one time for the organization (6 SECRETS required to build and 1 VARIABLE required to automatically update your certificates annually). Otherwise, those 6 SECRETS must be entered for every repository. Please refer to [LoopDocs: Create a Free *GitHub* Organization](https://loopkit.github.io/loopdocs/browser/secrets/#create-a-free-github-organization). ## Prerequisites @@ -49,7 +47,7 @@ Each secret is indentified below by `ALL_CAPITAL_LETTER_NAMES`. * Be sure to save the 6 Secrets in a text file using a text editor * Do **NOT** use a smart editor, which might auto-correct and change case, because these Secrets are case sensitive -Refer to [LoopDocs: Make a Secrets Reference File](https://loopkit.github.io/loopdocs/gh-actions/gh-first-time/#make-a-secrets-reference-file) for a handy template to use when saving your Secrets. +Refer to [LoopDocs: Make a Secrets Reference File](https://loopkit.github.io/loopdocs/browser/intro-summary/#make-a-secrets-reference-file) for a handy template to use when saving your Secrets. ## Generate App Store Connect API Key @@ -155,17 +153,7 @@ Once a year, you will get an email from Apple indicating your certificate will e ## TestFlight and Deployment Details -For more details, please refer to [LoopDocs: Set Up Users](https://loopkit.github.io/loopdocs/gh-actions/gh-first-time/#set-up-users-and-access-testflight) and [LoopDocs: Deploy](https://loopkit.github.io/loopdocs/gh-actions/gh-deploy/) - -## Automatic Build FAQs - -### Why do I have an `alive` branch? - -If a GitHub repository has no activity (no commits are made) in 60 days, then GitHub disables the ability to use automated actions for that repository. We need to take action more frequently than that or the automated build process won't work. - -The `build_LoopFollow.yml` file uses a special branch called `alive` and adds a dummy commit to the `alive` branch at regular intervals. This "trick" keeps the Actions enabled so the automated build works. - -The branch `alive` is created automatically for you. Do not delete or rename it! Do not modify `alive` yourself; it is not used for building the app. (There are actually 2 alive branches: `alive-main` and `alive-dev`; they both serve a similar purpose and should be left alone.) +For more details, please refer to [LoopDocs: TestFlight Overview](https://loopkit.github.io/loopdocs/browser/tf-users) and [LoopDocs: Install on Phone](https://loopkit.github.io/loopdocs/browser/phone-install/) ## OPTIONAL @@ -195,18 +183,18 @@ You can modify the automation by creating and using some variables. To configure the automated build more granularly involves creating up to two environment variables: `SCHEDULED_BUILD` and/or `SCHEDULED_SYNC`. See [How to configure a variable](#how-to-configure-a-variable). -Note that the weekly and monthly Build Trio actions will continue, but the actions are modified if one or more of these variables is set to false. **A successful Action Log will still appear, even if no automatic activity happens**. +Note that the weekly and monthly scheduled actions will continue, but the actions are modified if one or more of these variables is set to false. **When a scheduled run ends without building, it shows as cancelled (grey) instead of successful - a green check always means a new build was made**. -* If you want to manually decide when to update your repository to the latest commit, but you want the monthly builds and keep-alive to continue: set `SCHEDULED_SYNC` to false and either do not create `SCHEDULED_BUILD` or set it to true +* If you want to manually decide when to update your repository to the latest commit, but you want the monthly builds to continue: set `SCHEDULED_SYNC` to false and either do not create `SCHEDULED_BUILD` or set it to true * If you want to only build when an update has been found: set `SCHEDULED_BUILD` to false and either do not create `SCHEDULED_SYNC` or set it to true * **Warning**: if no updates to your default branch are detected within 90 days, your previous TestFlight build may expire requiring a manual build |`SCHEDULED_SYNC`|`SCHEDULED_BUILD`|Automatic Actions| |---|---|---| -| `true` (or NA) | `true` (or NA) | keep-alive, weekly update check (auto update/build), monthly build with auto update | -| `true` (or NA) | `false` | keep-alive, weekly update check with auto update, only builds if update detected | -| `false` | `true` (or NA) | keep-alive, monthly build, no auto update | -| `false` | `false` | no automatic activity, no keep-alive | +| `true` (or NA) | `true` (or NA) | weekly update check (auto update/build), monthly build with auto update | +| `true` (or NA) | `false` | weekly update check with auto update, only builds if update detected | +| `false` | `true` (or NA) | monthly build, no auto update | +| `false` | `false` | no automatic activity | ### How to configure a variable @@ -228,12 +216,12 @@ Note that the weekly and monthly Build Trio actions will continue, but the actio Your build will run on the following conditions: * Default behaviour: - * Run weekly, every Wednesday to check for changes; if there are changes, it will update your repository and build - * Run monthly, every first of the month, if there are changes, it will update your repository; regardless of changes, it will build - * Each time the action runs, it makes a keep-alive commit to the `alive` branch if necessary -* If you disable any automation (both variables set to `false`), no updates, keep-alive or building happens when Build Trio runs -* If you disabled just scheduled synchronization (`SCHEDULED_SYNC` set to`false`), it will only run once a month, on the first of the month, no update will happen; keep-alive will run -* If you disabled just scheduled build (`SCHEDULED_BUILD` set to `false`), it will run once weekly, every Wednesday, to check for changes; if there are changes, it will update and build; keep-alive will run + * Run weekly, every Sunday to check for changes; if there are changes, it will update your repository and build + * Run monthly, on the second Sunday of the month, if there are changes, it will update your repository; regardless of changes, it will build + * If a scheduled run ends without building, it shows as cancelled (grey) instead of successful +* If you disable any automation (both variables set to `false`), no updates or building happens when the scheduled action runs; the run shows as cancelled +* If you disabled just scheduled synchronization (`SCHEDULED_SYNC` set to`false`), it will only build once a month, on the second Sunday of the month, no update will happen +* If you disabled just scheduled build (`SCHEDULED_BUILD` set to `false`), it will run once weekly, every Sunday, to check for changes; if there are changes, it will update and build ## What if I build using more than one GitHub username diff --git a/release.sh b/release.sh index 3ba67c2fb..059b826b1 100755 --- a/release.sh +++ b/release.sh @@ -14,7 +14,6 @@ VERSION_FILE="Config.xcconfig" MARKETING_KEY="LOOP_FOLLOW_MARKETING_VERSION" DEV_BRANCH="dev" MAIN_BRANCH="main" -PATCH_DIR="../${APP_NAME}_update_patches" # --------------------------------------- # --- functions here --- @@ -26,38 +25,49 @@ queue_push() { push_cmds+=("git -C \"$(pwd)\" $*"); echo "+ [queued] (in $(pwd)) update_follower () { local DIR="$1" + local build_minute="$2" # staggered Sunday-build minute (see calls below) + local suffix=".${DIR#${APP_NAME}_}" # LoopFollow_Second -> .Second + local display="$DIR" # LoopFollow_Second + local upstream="loopandlearn/${DIR}" # loopandlearn/LoopFollow_Second + echo; echo "🔄 Updating $DIR …" cd "$DIR" - echo; echo "If there are custom changes needed to the patch, make the change before continuing" - pause - # 1 · Make sure we’re on a clean, up-to-date main echo_run git switch "$MAIN_BRANCH" echo_run git fetch echo_run git pull - # 2 · Apply the patch - if ! git apply --whitespace=nowarn "$PATCH_FILE"; then - echo "‼️ Some changes could not be applied, so no changes were made." - echo "The command used was: git apply --whitespace=nowarn $PATCH_FILE" - echo; echo "Use a different terminal to fix and apply the patch before continuing" - pause + # 2 · Full mirror of the release tree from the primary repo. + # Every tracked file (including the overlay files) is synced; only git + # metadata and local/build dirs are protected. --delete makes the tree an + # exact mirror, auto-correcting any drift. + echo_run rsync -a --delete \ + --exclude='.git/' \ + --exclude='.claude/' \ + --exclude='build/' \ + "$PRIMARY_ABS_PATH"/ ./ + + # 3 · Re-apply this instance's overlay on top of the mirror + perl -i -pe "s|^app_suffix\s*=.*|app_suffix = ${suffix}|" LoopFollowDisplayNameConfig.xcconfig + perl -i -pe "s|^display_name\s*=.*|display_name = ${display}|" LoopFollowDisplayNameConfig.xcconfig + perl -i -pe "s|^(\s*)UPSTREAM_REPO:.*|\${1}UPSTREAM_REPO: ${upstream}|" .github/workflows/build_LoopFollow.yml + perl -i -pe "s|^(\s*)- cron:.*|\${1}- cron: \"${build_minute} 10 * * 0\" # Sunday at UTC 10:${build_minute}|" .github/workflows/build_LoopFollow.yml + + # 4 · Rename the synced workspace to this instance's name + rm -rf "${DIR}.xcworkspace" + if [ -d "${APP_NAME}.xcworkspace" ]; then + mv "${APP_NAME}.xcworkspace" "${DIR}.xcworkspace" fi - # 3 · Pause if any conflict markers remain - if git ls-files -u | grep -q .; then - echo "⚠️ Conflicts detected." - echo " If Fastfile or build_LoopFollow.yml were modified, these are expected." - echo " Open your merge tool, resolve, then press Enter." - pause + # 5 · Single commit capturing the mirror + overlay + git add -A + if git diff --cached --quiet; then + echo "✓ $DIR already up to date — nothing to commit." + else + echo_run git commit -m "transfer v${new_ver} updates from LF to ${DIR}" fi - # 4 · Single commit capturing all staged changes - git add -u - git add $(git ls-files --others --exclude-standard) 2>/dev/null || true - git commit -m "transfer v${new_ver} updates from LF to ${DIR}" - echo_run git status echo "💻 Build & test $DIR now."; pause # build & test checkpoint queue_push push origin "$MAIN_BRANCH" @@ -68,6 +78,18 @@ update_follower () { PRIMARY_ABS_PATH="$(pwd -P)" echo "🏁 Working in $PRIMARY_ABS_PATH …" +# --- guard: make sure we're running the latest release.sh from origin/dev ---- +# The release flow assumes the newest script. If this copy differs from the one +# on origin/dev, we're likely running a stale version — abort and tell the user. +SCRIPT_PATH="${BASH_SOURCE[0]}" +echo_run git fetch origin "$DEV_BRANCH" +if ! diff -q <(git show "origin/${DEV_BRANCH}:release.sh") "$SCRIPT_PATH" >/dev/null 2>&1; then + echo "❌ This release.sh does not match origin/${DEV_BRANCH}:release.sh." + echo " You're likely running an outdated copy of the release script." + echo " Switch to the latest '${DEV_BRANCH}' (git switch ${DEV_BRANCH} && git pull) and re-run. Aborting." + exit 1 +fi + # --- start out in main to capture old_ver ---- echo_run git switch "$MAIN_BRANCH" echo_run git fetch @@ -104,21 +126,21 @@ echo_run git switch -c "$RELEASE_BRANCH" # --- bump version on the release branch ---- sed -i '' "s/${MARKETING_KEY}[[:space:]]*=.*/${MARKETING_KEY} = ${new_ver}/" "$VERSION_FILE" echo_run git diff "$VERSION_FILE"; pause -echo_run git commit -m "update version to ${new_ver} [skip ci]" "$VERSION_FILE" +# No [skip ci] here on purpose: it would skip the required SwiftFormat lint check +# (leaving the release PRs blocked) and the tag_on_main workflow (no auto-tag). +# auto_version_dev already avoids re-bumping via its "Config.xcconfig changed" guard. +echo_run git commit -m "update version to ${new_ver}" "$VERSION_FILE" echo "💻 Build & test release branch now."; pause queue_push push origin "$RELEASE_BRANCH" -# --- create a patch from main..release branch (includes the bump) ----- -mkdir -p "$PATCH_DIR" -PATCH_FILE="${PATCH_DIR}/LF_diff_${old_ver}_to_${new_ver}.patch" - -git diff -M --binary "$MAIN_BRANCH" "$RELEASE_BRANCH" \ - > "$PATCH_FILE" - +# --- mirror the release tree into the sister repos ---- +# Second arg = each app's scheduled browser-build minute (Sundays at 10:xx UTC), +# staggered from the main app (:17) so a user who forked all three apps doesn't +# trigger three simultaneous builds. cd .. -update_follower "$SECOND_DIR" -update_follower "$THIRD_DIR" +update_follower "$SECOND_DIR" "27" +update_follower "$THIRD_DIR" "40" # ---------- GitHub Actions Test --------- echo; @@ -137,8 +159,15 @@ if [[ $confirm =~ ^[Yy]$ ]]; then for cmd in "${push_cmds[@]}"; do echo "+ $cmd"; bash -c "$cmd"; done echo "🎉 All pushes completed." + # --- resolve the GitHub repo explicitly so gh doesn't depend on a default ---- + # Clones with multiple remotes have no inferred default repo, which makes + # `gh pr create` fail. Derive owner/repo from the origin remote URL. + # Handles both https://github.com/owner/repo(.git) and git@github.com:owner/repo(.git) + REPO_SLUG="$(git remote get-url origin | sed -E -e 's#\.git$##' -e 's#^.*github\.com[:/]##')" + echo; echo "📝 Opening sync PR ${RELEASE_BRANCH} → ${DEV_BRANCH} …" gh pr create \ + --repo "$REPO_SLUG" \ --base "$DEV_BRANCH" \ --head "$RELEASE_BRANCH" \ --title "Sync v${new_ver} version bump to dev" \ @@ -146,10 +175,11 @@ if [[ $confirm =~ ^[Yy]$ ]]; then \`auto_version_dev\` detects that \`Config.xcconfig\` was changed in this push and skips re-bumping. -⚠️ **Use rebase-merge** (not squash or merge-commit) so \`dev\` and \`main\` end up at the same commit SHA after the release." +⚠️ **Use \"Create a merge commit\"** (not squash or rebase) so \`dev\` and \`main\` share the same release lineage." echo; echo "📝 Opening release PR ${RELEASE_BRANCH} → ${MAIN_BRANCH} …" gh pr create \ + --repo "$REPO_SLUG" \ --base "$MAIN_BRANCH" \ --head "$RELEASE_BRANCH" \ --title "Release v${new_ver}" \ @@ -157,7 +187,7 @@ if [[ $confirm =~ ^[Yy]$ ]]; then Merging this PR triggers the tagging workflow, which creates tag \`v${new_ver}\` from \`LOOP_FOLLOW_MARKETING_VERSION\` in \`Config.xcconfig\`. -⚠️ **Use rebase-merge** (not squash or merge-commit) so \`dev\` and \`main\` end up at the same commit SHA after the release." +⚠️ **Use \"Create a merge commit\"** (not squash or rebase) so \`main\` keeps the original feature-PR commits." echo; echo "🎉 All repos updated to v${new_ver} (local). Release PRs opened (sync → dev, release → main)." echo "👉 Review and merge both PRs — the tag will be created automatically by .github/workflows/tag_on_main.yml."