├── .gitignore ├── BatteryHelper ├── BatteryHelper.h ├── BatteryHelper.m ├── Makefile └── control ├── BatteryInfo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── BatteryInfo.xcscheme ├── BatteryInfo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── icon.png │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── BatteryHelper.a ├── BatteryHelper.h ├── BatteryInfo-Bridging-Header.h ├── BatteryManufacturer.xcstrings ├── Controller │ ├── ApplicationLanguageController.swift │ ├── BatteryDataController.swift │ ├── BatteryRecordDatabaseManager.swift │ └── SettingsBatteryDataController.swift ├── DeviceController.h ├── DeviceController.m ├── Entity │ ├── BatteryDataRecord.swift │ ├── BatteryInfoItem.swift │ ├── BatteryRAWInfo.swift │ ├── InfoItem.swift │ ├── InfoItemGroup.swift │ ├── RAWData │ │ ├── AccessoryDetails.swift │ │ ├── AdapterDetails.swift │ │ ├── BatteryData.swift │ │ ├── ChargerData.swift │ │ ├── KioskMode.swift │ │ └── LifetimeData.swift │ └── SettingsBatteryData.swift ├── Info.plist ├── InfoPlist.xcstrings ├── Localizable.xcstrings ├── Protocol │ └── BatteryDataProviderProtocol.swift ├── Provider │ └── IOKitBatteryDataProvider.swift ├── Utils │ ├── BatteryFormatUtils.swift │ ├── PlistManagerUtils.swift │ ├── SettingsUtils.swift │ └── SystemInfoUtils.swift └── ViewController │ ├── AllBatteryDataViewController.swift │ ├── DataRecordSettingsViewController.swift │ ├── DisplaySettingsViewController.swift │ ├── HistoryRecordViewController.swift │ ├── HistoryStatisticsViewController.swift │ ├── HomeViewController.swift │ ├── LanguageSettingsViewController.swift │ ├── MainUITabBarController.swift │ ├── RawDataViewController.swift │ └── SettingsViewController.swift ├── LICENSE ├── Makefile ├── README.md ├── SettingsBatteryHelper ├── Makefile ├── SettingsBatteryHelper ├── entitlements.plist └── main.m ├── control └── entitlements.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | # .swiftpm 31 | 32 | .build/ 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | # 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | -------------------------------------------------------------------------------- /BatteryHelper/BatteryHelper.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /// 获取电池信息的函数声明 4 | NSDictionary *getBatteryInfo(void); 5 | -------------------------------------------------------------------------------- /BatteryHelper/BatteryHelper.m: -------------------------------------------------------------------------------- 1 | #import "BatteryHelper.h" 2 | #import 3 | 4 | NSDictionary *getBatteryInfo() { 5 | mach_port_t masterPort; 6 | CFMutableDictionaryRef matchingDict; 7 | io_service_t service; 8 | CFMutableDictionaryRef properties = NULL; 9 | 10 | // 获取主端口 11 | IOMasterPort(MACH_PORT_NULL, &masterPort); 12 | 13 | // 匹配电池服务 14 | matchingDict = IOServiceMatching("IOPMPowerSource"); 15 | service = IOServiceGetMatchingService(masterPort, matchingDict); 16 | 17 | if (service) { 18 | // 获取电池属性 19 | IORegistryEntryCreateCFProperties(service, &properties, kCFAllocatorDefault, 0); 20 | IOObjectRelease(service); 21 | } 22 | 23 | if (properties) { 24 | NSDictionary *result = [NSDictionary dictionaryWithDictionary:(__bridge NSDictionary *)properties]; 25 | CFRelease(properties); // 手动释放 26 | return result; 27 | } 28 | 29 | return nil; 30 | } 31 | -------------------------------------------------------------------------------- /BatteryHelper/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = iphone:clang:latest:12.0 2 | ARCHS = arm64 arm64e 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | LIBRARY_NAME = BatteryHelper 7 | $(LIBRARY_NAME)_FILES = BatteryHelper.m 8 | $(LIBRARY_NAME)_CFLAGS = -fobjc-arc 9 | $(LIBRARY_NAME)_LINKAGE_TYPE = static 10 | $(LIBRARY_NAME)_FRAMEWORKS = IOKit 11 | 12 | include $(THEOS_MAKE_PATH)/library.mk 13 | -------------------------------------------------------------------------------- /BatteryHelper/control: -------------------------------------------------------------------------------- 1 | Package: com.developlab.batteryinfo.helper 2 | Name: BatteryHelper 3 | Version: 1.0 4 | Architecture: iphoneos-arm 5 | Description: An awesome library of some sort!! 6 | Maintainer: developlab 7 | Author: developlab 8 | Section: System 9 | Tag: role::developer 10 | -------------------------------------------------------------------------------- /BatteryInfo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 70; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1731F9BB2D50D02600AA1FD5 /* DeviceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1731F9BA2D50D02600AA1FD5 /* DeviceController.m */; }; 11 | 179E78992D1D5E49009C29C9 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 179E78982D1D5E49009C29C9 /* UIKit.framework */; }; 12 | 179E789B2D1D5E4F009C29C9 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 179E789A2D1D5E4F009C29C9 /* CoreGraphics.framework */; }; 13 | 179E789D2D1D5E55009C29C9 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 179E789C2D1D5E55009C29C9 /* IOKit.framework */; }; 14 | 17A0DB252DCBD3B6006DEDA6 /* SettingsBatteryDataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A0DB242DCBD3B6006DEDA6 /* SettingsBatteryDataController.swift */; }; 15 | 17B0EDC02D49C07B00F77781 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 17B0EDBF2D49C07B00F77781 /* Localizable.xcstrings */; }; 16 | 17B0EDC22D49C0A600F77781 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 17B0EDC12D49C0A600F77781 /* InfoPlist.xcstrings */; }; 17 | 17BC113C2D521A79009924BF /* BatteryManufacturer.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 17BC113B2D521A79009924BF /* BatteryManufacturer.xcstrings */; }; 18 | 17BC11CE2D5506F9009924BF /* BatteryRecordDatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC11CD2D5506F9009924BF /* BatteryRecordDatabaseManager.swift */; }; 19 | 17D996D32D1D50A60056394C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D996D22D1D50A60056394C /* AppDelegate.swift */; }; 20 | 17D996DC2D1D50A60056394C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17D996DB2D1D50A60056394C /* Assets.xcassets */; }; 21 | 17D996DF2D1D50A60056394C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17D996DD2D1D50A60056394C /* LaunchScreen.storyboard */; }; 22 | 17E83C792DD093B00093DAE7 /* ApplicationLanguageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E83C782DD093B00093DAE7 /* ApplicationLanguageController.swift */; }; 23 | 17F66BE62D2A1C7A0097A29A /* BatteryHelper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17F66BE52D2A1C7A0097A29A /* BatteryHelper.a */; }; 24 | 17F66BEE2D2A52580097A29A /* BatteryDataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F66BED2D2A52580097A29A /* BatteryDataController.swift */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXFileReference section */ 28 | 1731F9B92D50D02600AA1FD5 /* DeviceController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeviceController.h; sourceTree = ""; }; 29 | 1731F9BA2D50D02600AA1FD5 /* DeviceController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DeviceController.m; sourceTree = ""; }; 30 | 179E78902D1D52A0009C29C9 /* BatteryInfo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BatteryInfo-Bridging-Header.h"; sourceTree = ""; }; 31 | 179E78912D1D52A0009C29C9 /* BatteryHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BatteryHelper.h; sourceTree = ""; }; 32 | 179E78982D1D5E49009C29C9 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 33 | 179E789A2D1D5E4F009C29C9 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 34 | 179E789C2D1D5E55009C29C9 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 35 | 17A0DB242DCBD3B6006DEDA6 /* SettingsBatteryDataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBatteryDataController.swift; sourceTree = ""; }; 36 | 17B0EDBF2D49C07B00F77781 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 37 | 17B0EDC12D49C0A600F77781 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = InfoPlist.xcstrings; path = BatteryInfo/InfoPlist.xcstrings; sourceTree = SOURCE_ROOT; }; 38 | 17BC113B2D521A79009924BF /* BatteryManufacturer.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = BatteryManufacturer.xcstrings; sourceTree = ""; }; 39 | 17BC11CD2D5506F9009924BF /* BatteryRecordDatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryRecordDatabaseManager.swift; sourceTree = ""; }; 40 | 17D996CF2D1D50A60056394C /* BatteryInfo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BatteryInfo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 17D996D22D1D50A60056394C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 42 | 17D996DB2D1D50A60056394C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 17D996DE2D1D50A60056394C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 17D996E02D1D50A60056394C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | 17E83C782DD093B00093DAE7 /* ApplicationLanguageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationLanguageController.swift; sourceTree = ""; }; 46 | 17F66BE52D2A1C7A0097A29A /* BatteryHelper.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = BatteryHelper.a; sourceTree = ""; }; 47 | 17F66BED2D2A52580097A29A /* BatteryDataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDataController.swift; sourceTree = ""; }; 48 | /* End PBXFileReference section */ 49 | 50 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 51 | 17BC10FD2D51F6E4009924BF /* Utils */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Utils; sourceTree = ""; }; 52 | 17DA4E2A2DAB91D80024FE8C /* Protocol */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Protocol; sourceTree = ""; }; 53 | 17DA4E2B2DAB91EC0024FE8C /* Provider */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Provider; sourceTree = ""; }; 54 | 17F66BE92D2A24BC0097A29A /* ViewController */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ViewController; sourceTree = ""; }; 55 | 17F66BEB2D2A25840097A29A /* Entity */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Entity; sourceTree = ""; }; 56 | /* End PBXFileSystemSynchronizedRootGroup section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | 17D996CC2D1D50A60056394C /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | 179E789D2D1D5E55009C29C9 /* IOKit.framework in Frameworks */, 64 | 17F66BE62D2A1C7A0097A29A /* BatteryHelper.a in Frameworks */, 65 | 179E789B2D1D5E4F009C29C9 /* CoreGraphics.framework in Frameworks */, 66 | 179E78992D1D5E49009C29C9 /* UIKit.framework in Frameworks */, 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | /* End PBXFrameworksBuildPhase section */ 71 | 72 | /* Begin PBXGroup section */ 73 | 179E78972D1D5E49009C29C9 /* Frameworks */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 179E789C2D1D5E55009C29C9 /* IOKit.framework */, 77 | 179E789A2D1D5E4F009C29C9 /* CoreGraphics.framework */, 78 | 179E78982D1D5E49009C29C9 /* UIKit.framework */, 79 | ); 80 | name = Frameworks; 81 | sourceTree = ""; 82 | }; 83 | 17D996C62D1D50A50056394C = { 84 | isa = PBXGroup; 85 | children = ( 86 | 17D996D12D1D50A60056394C /* BatteryInfo */, 87 | 17D996D02D1D50A60056394C /* Products */, 88 | 179E78972D1D5E49009C29C9 /* Frameworks */, 89 | ); 90 | sourceTree = ""; 91 | }; 92 | 17D996D02D1D50A60056394C /* Products */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 17D996CF2D1D50A60056394C /* BatteryInfo.app */, 96 | ); 97 | name = Products; 98 | sourceTree = ""; 99 | }; 100 | 17D996D12D1D50A60056394C /* BatteryInfo */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 17DA4E2B2DAB91EC0024FE8C /* Provider */, 104 | 17DA4E2A2DAB91D80024FE8C /* Protocol */, 105 | 17BC10FD2D51F6E4009924BF /* Utils */, 106 | 1731F9B92D50D02600AA1FD5 /* DeviceController.h */, 107 | 1731F9BA2D50D02600AA1FD5 /* DeviceController.m */, 108 | 17B0EDBF2D49C07B00F77781 /* Localizable.xcstrings */, 109 | 17B0EDC12D49C0A600F77781 /* InfoPlist.xcstrings */, 110 | 17BC113B2D521A79009924BF /* BatteryManufacturer.xcstrings */, 111 | 17F66BEC2D2A52460097A29A /* Controller */, 112 | 17F66BEB2D2A25840097A29A /* Entity */, 113 | 17F66BE92D2A24BC0097A29A /* ViewController */, 114 | 17F66BE52D2A1C7A0097A29A /* BatteryHelper.a */, 115 | 17D996D22D1D50A60056394C /* AppDelegate.swift */, 116 | 17D996DB2D1D50A60056394C /* Assets.xcassets */, 117 | 17D996DD2D1D50A60056394C /* LaunchScreen.storyboard */, 118 | 17D996E02D1D50A60056394C /* Info.plist */, 119 | 179E78912D1D52A0009C29C9 /* BatteryHelper.h */, 120 | 179E78902D1D52A0009C29C9 /* BatteryInfo-Bridging-Header.h */, 121 | ); 122 | path = BatteryInfo; 123 | sourceTree = ""; 124 | }; 125 | 17F66BEC2D2A52460097A29A /* Controller */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 17F66BED2D2A52580097A29A /* BatteryDataController.swift */, 129 | 17BC11CD2D5506F9009924BF /* BatteryRecordDatabaseManager.swift */, 130 | 17A0DB242DCBD3B6006DEDA6 /* SettingsBatteryDataController.swift */, 131 | 17E83C782DD093B00093DAE7 /* ApplicationLanguageController.swift */, 132 | ); 133 | path = Controller; 134 | sourceTree = ""; 135 | }; 136 | /* End PBXGroup section */ 137 | 138 | /* Begin PBXNativeTarget section */ 139 | 17D996CE2D1D50A60056394C /* BatteryInfo */ = { 140 | isa = PBXNativeTarget; 141 | buildConfigurationList = 17D996E32D1D50A60056394C /* Build configuration list for PBXNativeTarget "BatteryInfo" */; 142 | buildPhases = ( 143 | 17D996CB2D1D50A60056394C /* Sources */, 144 | 17D996CC2D1D50A60056394C /* Frameworks */, 145 | 17D996CD2D1D50A60056394C /* Resources */, 146 | ); 147 | buildRules = ( 148 | ); 149 | dependencies = ( 150 | ); 151 | fileSystemSynchronizedGroups = ( 152 | 17BC10FD2D51F6E4009924BF /* Utils */, 153 | 17DA4E2A2DAB91D80024FE8C /* Protocol */, 154 | 17DA4E2B2DAB91EC0024FE8C /* Provider */, 155 | 17F66BE92D2A24BC0097A29A /* ViewController */, 156 | 17F66BEB2D2A25840097A29A /* Entity */, 157 | ); 158 | name = BatteryInfo; 159 | productName = BatteryInfo; 160 | productReference = 17D996CF2D1D50A60056394C /* BatteryInfo.app */; 161 | productType = "com.apple.product-type.application"; 162 | }; 163 | /* End PBXNativeTarget section */ 164 | 165 | /* Begin PBXProject section */ 166 | 17D996C72D1D50A50056394C /* Project object */ = { 167 | isa = PBXProject; 168 | attributes = { 169 | BuildIndependentTargetsInParallel = 1; 170 | LastSwiftUpdateCheck = 1520; 171 | LastUpgradeCheck = 1620; 172 | TargetAttributes = { 173 | 17D996CE2D1D50A60056394C = { 174 | CreatedOnToolsVersion = 15.2; 175 | LastSwiftMigration = 1520; 176 | }; 177 | }; 178 | }; 179 | buildConfigurationList = 17D996CA2D1D50A50056394C /* Build configuration list for PBXProject "BatteryInfo" */; 180 | compatibilityVersion = "Xcode 14.0"; 181 | developmentRegion = en; 182 | hasScannedForEncodings = 0; 183 | knownRegions = ( 184 | en, 185 | Base, 186 | "zh-Hans", 187 | "es-ES", 188 | ); 189 | mainGroup = 17D996C62D1D50A50056394C; 190 | productRefGroup = 17D996D02D1D50A60056394C /* Products */; 191 | projectDirPath = ""; 192 | projectRoot = ""; 193 | targets = ( 194 | 17D996CE2D1D50A60056394C /* BatteryInfo */, 195 | ); 196 | }; 197 | /* End PBXProject section */ 198 | 199 | /* Begin PBXResourcesBuildPhase section */ 200 | 17D996CD2D1D50A60056394C /* Resources */ = { 201 | isa = PBXResourcesBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | 17D996DF2D1D50A60056394C /* LaunchScreen.storyboard in Resources */, 205 | 17B0EDC02D49C07B00F77781 /* Localizable.xcstrings in Resources */, 206 | 17BC113C2D521A79009924BF /* BatteryManufacturer.xcstrings in Resources */, 207 | 17B0EDC22D49C0A600F77781 /* InfoPlist.xcstrings in Resources */, 208 | 17D996DC2D1D50A60056394C /* Assets.xcassets in Resources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXResourcesBuildPhase section */ 213 | 214 | /* Begin PBXSourcesBuildPhase section */ 215 | 17D996CB2D1D50A60056394C /* Sources */ = { 216 | isa = PBXSourcesBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | 17D996D32D1D50A60056394C /* AppDelegate.swift in Sources */, 220 | 17A0DB252DCBD3B6006DEDA6 /* SettingsBatteryDataController.swift in Sources */, 221 | 17F66BEE2D2A52580097A29A /* BatteryDataController.swift in Sources */, 222 | 17E83C792DD093B00093DAE7 /* ApplicationLanguageController.swift in Sources */, 223 | 1731F9BB2D50D02600AA1FD5 /* DeviceController.m in Sources */, 224 | 17BC11CE2D5506F9009924BF /* BatteryRecordDatabaseManager.swift in Sources */, 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | /* End PBXSourcesBuildPhase section */ 229 | 230 | /* Begin PBXVariantGroup section */ 231 | 17D996DD2D1D50A60056394C /* LaunchScreen.storyboard */ = { 232 | isa = PBXVariantGroup; 233 | children = ( 234 | 17D996DE2D1D50A60056394C /* Base */, 235 | ); 236 | name = LaunchScreen.storyboard; 237 | sourceTree = ""; 238 | }; 239 | /* End PBXVariantGroup section */ 240 | 241 | /* Begin XCBuildConfiguration section */ 242 | 17D996E12D1D50A60056394C /* Debug */ = { 243 | isa = XCBuildConfiguration; 244 | buildSettings = { 245 | ALWAYS_SEARCH_USER_PATHS = NO; 246 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 247 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 248 | CLANG_ANALYZER_NONNULL = YES; 249 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 250 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 251 | CLANG_ENABLE_MODULES = YES; 252 | CLANG_ENABLE_OBJC_ARC = YES; 253 | CLANG_ENABLE_OBJC_WEAK = YES; 254 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 255 | CLANG_WARN_BOOL_CONVERSION = YES; 256 | CLANG_WARN_COMMA = YES; 257 | CLANG_WARN_CONSTANT_CONVERSION = YES; 258 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 259 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 260 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 261 | CLANG_WARN_EMPTY_BODY = YES; 262 | CLANG_WARN_ENUM_CONVERSION = YES; 263 | CLANG_WARN_INFINITE_RECURSION = YES; 264 | CLANG_WARN_INT_CONVERSION = YES; 265 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 267 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 268 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 269 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 270 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 271 | CLANG_WARN_STRICT_PROTOTYPES = YES; 272 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 273 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 274 | CLANG_WARN_UNREACHABLE_CODE = YES; 275 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 276 | COPY_PHASE_STRIP = NO; 277 | DEBUG_INFORMATION_FORMAT = dwarf; 278 | ENABLE_STRICT_OBJC_MSGSEND = YES; 279 | ENABLE_TESTABILITY = YES; 280 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 281 | GCC_C_LANGUAGE_STANDARD = gnu17; 282 | GCC_DYNAMIC_NO_PIC = NO; 283 | GCC_NO_COMMON_BLOCKS = YES; 284 | GCC_OPTIMIZATION_LEVEL = 0; 285 | GCC_PREPROCESSOR_DEFINITIONS = ( 286 | "DEBUG=1", 287 | "$(inherited)", 288 | ); 289 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 290 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 291 | GCC_WARN_UNDECLARED_SELECTOR = YES; 292 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 293 | GCC_WARN_UNUSED_FUNCTION = YES; 294 | GCC_WARN_UNUSED_VARIABLE = YES; 295 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 296 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 297 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 298 | MTL_FAST_MATH = YES; 299 | ONLY_ACTIVE_ARCH = YES; 300 | SDKROOT = iphoneos; 301 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 302 | SWIFT_EMIT_LOC_STRINGS = YES; 303 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 304 | }; 305 | name = Debug; 306 | }; 307 | 17D996E22D1D50A60056394C /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 312 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 313 | CLANG_ANALYZER_NONNULL = YES; 314 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 315 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 316 | CLANG_ENABLE_MODULES = YES; 317 | CLANG_ENABLE_OBJC_ARC = YES; 318 | CLANG_ENABLE_OBJC_WEAK = YES; 319 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 320 | CLANG_WARN_BOOL_CONVERSION = YES; 321 | CLANG_WARN_COMMA = YES; 322 | CLANG_WARN_CONSTANT_CONVERSION = YES; 323 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 324 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 325 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 326 | CLANG_WARN_EMPTY_BODY = YES; 327 | CLANG_WARN_ENUM_CONVERSION = YES; 328 | CLANG_WARN_INFINITE_RECURSION = YES; 329 | CLANG_WARN_INT_CONVERSION = YES; 330 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 331 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 332 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 333 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 334 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 335 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 336 | CLANG_WARN_STRICT_PROTOTYPES = YES; 337 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 338 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 339 | CLANG_WARN_UNREACHABLE_CODE = YES; 340 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 341 | COPY_PHASE_STRIP = NO; 342 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 343 | ENABLE_NS_ASSERTIONS = NO; 344 | ENABLE_STRICT_OBJC_MSGSEND = YES; 345 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 346 | GCC_C_LANGUAGE_STANDARD = gnu17; 347 | GCC_NO_COMMON_BLOCKS = YES; 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 355 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 356 | MTL_ENABLE_DEBUG_INFO = NO; 357 | MTL_FAST_MATH = YES; 358 | SDKROOT = iphoneos; 359 | SWIFT_COMPILATION_MODE = wholemodule; 360 | SWIFT_EMIT_LOC_STRINGS = YES; 361 | VALIDATE_PRODUCT = YES; 362 | }; 363 | name = Release; 364 | }; 365 | 17D996E42D1D50A60056394C /* Debug */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 369 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 370 | CLANG_ENABLE_MODULES = YES; 371 | CODE_SIGN_IDENTITY = "Apple Development"; 372 | CODE_SIGN_STYLE = Automatic; 373 | CURRENT_PROJECT_VERSION = 15; 374 | DEVELOPMENT_TEAM = ""; 375 | FRAMEWORK_SEARCH_PATHS = ( 376 | /System/Library/Frameworks, 377 | /System/Library/PrivateFrameworks, 378 | ); 379 | GENERATE_INFOPLIST_FILE = YES; 380 | INFOPLIST_FILE = BatteryInfo/Info.plist; 381 | INFOPLIST_KEY_CFBundleDisplayName = "Battery Info"; 382 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 383 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 384 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 385 | INFOPLIST_KEY_UISupportedInterfaceOrientations = ""; 386 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 387 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 388 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 389 | LD_RUNPATH_SEARCH_PATHS = ( 390 | "$(inherited)", 391 | "@executable_path/Frameworks", 392 | ); 393 | LIBRARY_SEARCH_PATHS = ( 394 | "$(inherited)", 395 | "$(PROJECT_DIR)/BatteryInfo", 396 | ); 397 | MARKETING_VERSION = 1.1.9; 398 | OTHER_CFLAGS = "-Wno-error=unguarded-availability-new"; 399 | OTHER_LDFLAGS = ""; 400 | PRODUCT_BUNDLE_IDENTIFIER = com.developlab.BatteryInfo; 401 | PRODUCT_NAME = "$(TARGET_NAME)"; 402 | PROVISIONING_PROFILE_SPECIFIER = ""; 403 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 404 | SUPPORTS_MACCATALYST = NO; 405 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; 406 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 407 | SWIFT_EMIT_LOC_STRINGS = YES; 408 | SWIFT_OBJC_BRIDGING_HEADER = "BatteryInfo/BatteryInfo-Bridging-Header.h"; 409 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 410 | SWIFT_VERSION = 5.0; 411 | TARGETED_DEVICE_FAMILY = "1,2"; 412 | }; 413 | name = Debug; 414 | }; 415 | 17D996E52D1D50A60056394C /* Release */ = { 416 | isa = XCBuildConfiguration; 417 | buildSettings = { 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 420 | CLANG_ENABLE_MODULES = YES; 421 | CODE_SIGN_IDENTITY = "Apple Development"; 422 | CODE_SIGN_STYLE = Automatic; 423 | CURRENT_PROJECT_VERSION = 15; 424 | DEVELOPMENT_TEAM = ""; 425 | FRAMEWORK_SEARCH_PATHS = ( 426 | /System/Library/Frameworks, 427 | /System/Library/PrivateFrameworks, 428 | ); 429 | GENERATE_INFOPLIST_FILE = YES; 430 | INFOPLIST_FILE = BatteryInfo/Info.plist; 431 | INFOPLIST_KEY_CFBundleDisplayName = "Battery Info"; 432 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 433 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 434 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 435 | INFOPLIST_KEY_UISupportedInterfaceOrientations = ""; 436 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 437 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 438 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 439 | LD_RUNPATH_SEARCH_PATHS = ( 440 | "$(inherited)", 441 | "@executable_path/Frameworks", 442 | ); 443 | LIBRARY_SEARCH_PATHS = ( 444 | "$(inherited)", 445 | "$(PROJECT_DIR)/BatteryInfo", 446 | ); 447 | MARKETING_VERSION = 1.1.9; 448 | OTHER_CFLAGS = "-Wno-error=unguarded-availability-new"; 449 | OTHER_LDFLAGS = ""; 450 | PRODUCT_BUNDLE_IDENTIFIER = com.developlab.BatteryInfo; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | PROVISIONING_PROFILE_SPECIFIER = ""; 453 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 454 | SUPPORTS_MACCATALYST = NO; 455 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; 456 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 457 | SWIFT_EMIT_LOC_STRINGS = YES; 458 | SWIFT_OBJC_BRIDGING_HEADER = "BatteryInfo/BatteryInfo-Bridging-Header.h"; 459 | SWIFT_VERSION = 5.0; 460 | TARGETED_DEVICE_FAMILY = "1,2"; 461 | }; 462 | name = Release; 463 | }; 464 | /* End XCBuildConfiguration section */ 465 | 466 | /* Begin XCConfigurationList section */ 467 | 17D996CA2D1D50A50056394C /* Build configuration list for PBXProject "BatteryInfo" */ = { 468 | isa = XCConfigurationList; 469 | buildConfigurations = ( 470 | 17D996E12D1D50A60056394C /* Debug */, 471 | 17D996E22D1D50A60056394C /* Release */, 472 | ); 473 | defaultConfigurationIsVisible = 0; 474 | defaultConfigurationName = Release; 475 | }; 476 | 17D996E32D1D50A60056394C /* Build configuration list for PBXNativeTarget "BatteryInfo" */ = { 477 | isa = XCConfigurationList; 478 | buildConfigurations = ( 479 | 17D996E42D1D50A60056394C /* Debug */, 480 | 17D996E52D1D50A60056394C /* Release */, 481 | ); 482 | defaultConfigurationIsVisible = 0; 483 | defaultConfigurationName = Release; 484 | }; 485 | /* End XCConfigurationList section */ 486 | }; 487 | rootObject = 17D996C72D1D50A50056394C /* Project object */; 488 | } 489 | -------------------------------------------------------------------------------- /BatteryInfo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BatteryInfo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BatteryInfo.xcodeproj/xcshareddata/xcschemes/BatteryInfo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 56 | 57 | 58 | 64 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /BatteryInfo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 9 | 10 | // 设置App语言(必须在加载 UI 之前) 11 | ApplicationLanguageController.loadLanguageFromSettings() 12 | 13 | // 初始化数据提供者 14 | BatteryDataController.configureInstance(provider: IOKitBatteryDataProvider()) 15 | // 加载root view 16 | window = UIWindow(frame: UIScreen.main.bounds) 17 | window!.rootViewController = MainUITabBarController() 18 | window!.makeKeyAndVisible() 19 | return true 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /BatteryInfo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BatteryInfo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BatteryInfo/Assets.xcassets/AppIcon.appiconset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevelopCubeLab/BatteryInfo/ba48f11000d8bf514a5ffbe421cc920f9a7811c1/BatteryInfo/Assets.xcassets/AppIcon.appiconset/icon.png -------------------------------------------------------------------------------- /BatteryInfo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BatteryInfo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /BatteryInfo/BatteryHelper.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevelopCubeLab/BatteryInfo/ba48f11000d8bf514a5ffbe421cc920f9a7811c1/BatteryInfo/BatteryHelper.a -------------------------------------------------------------------------------- /BatteryInfo/BatteryHelper.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | // 获取电池信息的函数声明 4 | NSDictionary *getBatteryInfo(void); 5 | -------------------------------------------------------------------------------- /BatteryInfo/BatteryInfo-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | #import "BatteryHelper.h" 5 | #import "DeviceController.h" 6 | -------------------------------------------------------------------------------- /BatteryInfo/BatteryManufacturer.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "ATL" : { 5 | "extractionState" : "manual", 6 | "localizations" : { 7 | "en" : { 8 | "stringUnit" : { 9 | "state" : "translated", 10 | "value" : "ATL" 11 | } 12 | }, 13 | "zh-Hans" : { 14 | "stringUnit" : { 15 | "state" : "translated", 16 | "value" : "ATL新能源科技" 17 | } 18 | }, 19 | "es-ES" : { 20 | "stringUnit" : { 21 | "state" : "translated", 22 | "value" : "ATL" 23 | } 24 | } 25 | } 26 | }, 27 | "Desay" : { 28 | "extractionState" : "manual", 29 | "localizations" : { 30 | "en" : { 31 | "stringUnit" : { 32 | "state" : "translated", 33 | "value" : "Desay" 34 | } 35 | }, 36 | "zh-Hans" : { 37 | "stringUnit" : { 38 | "state" : "translated", 39 | "value" : "德赛" 40 | } 41 | }, 42 | "es-ES" : { 43 | "stringUnit" : { 44 | "state" : "translated", 45 | "value" : "Desay" 46 | } 47 | } 48 | } 49 | }, 50 | "LG" : { 51 | "extractionState" : "manual", 52 | "localizations" : { 53 | "en" : { 54 | "stringUnit" : { 55 | "state" : "translated", 56 | "value" : "LG" 57 | } 58 | }, 59 | "zh-Hans" : { 60 | "stringUnit" : { 61 | "state" : "translated", 62 | "value" : "LG" 63 | } 64 | }, 65 | "es-ES" : { 66 | "stringUnit" : { 67 | "state" : "translated", 68 | "value" : "LG" 69 | } 70 | } 71 | } 72 | }, 73 | "Simplo" : { 74 | "extractionState" : "manual", 75 | "localizations" : { 76 | "en" : { 77 | "stringUnit" : { 78 | "state" : "translated", 79 | "value" : "Simplo" 80 | } 81 | }, 82 | "zh-Hans" : { 83 | "stringUnit" : { 84 | "state" : "translated", 85 | "value" : "新普" 86 | } 87 | }, 88 | "es-ES" : { 89 | "stringUnit" : { 90 | "state" : "translated", 91 | "value" : "Simplo" 92 | } 93 | } 94 | } 95 | }, 96 | "Sony" : { 97 | "extractionState" : "manual", 98 | "localizations" : { 99 | "en" : { 100 | "stringUnit" : { 101 | "state" : "translated", 102 | "value" : "Sony" 103 | } 104 | }, 105 | "zh-Hans" : { 106 | "stringUnit" : { 107 | "state" : "translated", 108 | "value" : "索尼" 109 | } 110 | }, 111 | "es-ES" : { 112 | "stringUnit" : { 113 | "state" : "translated", 114 | "value" : "Sony" 115 | } 116 | } 117 | } 118 | }, 119 | "Sunwoda" : { 120 | "extractionState" : "manual", 121 | "localizations" : { 122 | "en" : { 123 | "stringUnit" : { 124 | "state" : "translated", 125 | "value" : "Sunwoda" 126 | } 127 | }, 128 | "zh-Hans" : { 129 | "stringUnit" : { 130 | "state" : "translated", 131 | "value" : "欣旺达" 132 | } 133 | }, 134 | "es-ES" : { 135 | "stringUnit" : { 136 | "state" : "translated", 137 | "value" : "Sunwoda" 138 | } 139 | } 140 | } 141 | }, 142 | "Unknown" : { 143 | "extractionState" : "manual", 144 | "localizations" : { 145 | "en" : { 146 | "stringUnit" : { 147 | "state" : "translated", 148 | "value" : "Unknown Manufacturer" 149 | } 150 | }, 151 | "zh-Hans" : { 152 | "stringUnit" : { 153 | "state" : "translated", 154 | "value" : "未知制造商" 155 | } 156 | }, 157 | "es-ES" : { 158 | "stringUnit" : { 159 | "state" : "translated", 160 | "value" : "Fabricante desconocido" 161 | } 162 | } 163 | } 164 | } 165 | }, 166 | "version" : "1.0" 167 | } 168 | -------------------------------------------------------------------------------- /BatteryInfo/Controller/ApplicationLanguageController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class ApplicationLanguageController { 4 | 5 | private static var bundle: Bundle? 6 | 7 | // 应用启动或语言切换时调用 8 | static func setLanguage(_ languageCode: String?) { 9 | guard let languageCode = languageCode, 10 | let path = Bundle.main.path(forResource: languageCode, ofType: "lproj"), 11 | let langBundle = Bundle(path: path) else { 12 | bundle = nil 13 | return 14 | } 15 | bundle = langBundle 16 | } 17 | 18 | // 从语言文件中获取文本 19 | static func localizedString(forKey key: String, comment: String = "") -> String { 20 | return bundle?.localizedString(forKey: key, value: nil, table: nil) 21 | ?? Bundle.main.localizedString(forKey: key, value: nil, table: nil) 22 | } 23 | 24 | static func localizedString(forKey key: String, table: String?, comment: String = "") -> String { 25 | return bundle?.localizedString(forKey: key, value: nil, table: table) 26 | ?? Bundle.main.localizedString(forKey: key, value: nil, table: table) 27 | } 28 | 29 | // 应用启动时自动加载设置 30 | static func loadLanguageFromSettings() { 31 | let settingLanguage = SettingsUtils.instance.getApplicationLanguage() 32 | switch settingLanguage { 33 | case .English: 34 | setLanguage("en") 35 | case .SimplifiedChinese: 36 | setLanguage("zh-Hans") 37 | case .System: 38 | setLanguage(nil) 39 | } 40 | 41 | } 42 | } 43 | 44 | /// 替代 NSLocalizedString 的封装 45 | func NSLocalizedString(_ key: String, comment: String = "") -> String { 46 | return ApplicationLanguageController.localizedString(forKey: key, comment: comment) 47 | } 48 | 49 | func NSLocalizedString(_ key: String, tableName: String?, comment: String = "") -> String { 50 | return ApplicationLanguageController.localizedString(forKey: key, table: tableName, comment: comment) 51 | } 52 | 53 | -------------------------------------------------------------------------------- /BatteryInfo/Controller/BatteryRecordDatabaseManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SQLite3 3 | 4 | class BatteryRecordDatabaseManager { 5 | 6 | static let shared = BatteryRecordDatabaseManager() 7 | 8 | private let dbName = "BatteryData.sqlite" 9 | private let recordTableName = "BatteryDataRecords" 10 | private var db: OpaquePointer? 11 | 12 | private init() { 13 | openDatabase() 14 | createTable() 15 | } 16 | 17 | /// 打开数据库 18 | private func openDatabase() { 19 | let fileURL = FileManager.default 20 | .urls(for: .documentDirectory, in: .userDomainMask) 21 | .first! 22 | .appendingPathComponent(dbName) 23 | 24 | if sqlite3_open(fileURL.path, &db) != SQLITE_OK { 25 | // 26 | } 27 | } 28 | 29 | /// 创建表 30 | private func createTable() { 31 | let createTableQuery = """ 32 | CREATE TABLE IF NOT EXISTS \(recordTableName) ( 33 | id INTEGER PRIMARY KEY AUTOINCREMENT, 34 | createDate INTEGER NOT NULL, 35 | recordType INTEGER NOT NULL, 36 | cycleCount INTEGER NOT NULL, 37 | nominalChargeCapacity INTEGER, 38 | designCapacity INTEGER, 39 | maximumCapacity TEXT 40 | ); 41 | """ 42 | 43 | if sqlite3_exec(db, createTableQuery, nil, nil, nil) != SQLITE_OK { 44 | // 45 | } 46 | } 47 | 48 | /// 查询所有记录 49 | func fetchAllRecords() -> [BatteryDataRecord] { 50 | let fetchQuery = "SELECT * FROM \(recordTableName) ORDER BY createDate DESC;" 51 | 52 | var statement: OpaquePointer? 53 | var records: [BatteryDataRecord] = [] 54 | 55 | if sqlite3_prepare_v2(db, fetchQuery, -1, &statement, nil) == SQLITE_OK { 56 | 57 | while sqlite3_step(statement) == SQLITE_ROW { 58 | let id = Int(sqlite3_column_int(statement, 0)) 59 | let createDate = Int(sqlite3_column_int(statement, 1)) 60 | let recordType = BatteryDataRecord.BatteryDataRecordType(rawValue: Int(sqlite3_column_int(statement, 2))) ?? .Automatic 61 | let cycleCount = Int(sqlite3_column_int(statement, 3)) 62 | 63 | let nominalChargeCapacity = sqlite3_column_type(statement, 4) != SQLITE_NULL ? Int(sqlite3_column_int(statement, 4)) : nil 64 | let designCapacity = sqlite3_column_type(statement, 5) != SQLITE_NULL ? Int(sqlite3_column_int(statement, 5)) : nil 65 | 66 | var maximumCapacity: String? 67 | if let rawText = sqlite3_column_text(statement, 6) { 68 | maximumCapacity = String(cString: rawText) 69 | } 70 | 71 | let record = BatteryDataRecord(id: id, createDate: createDate, recordType: recordType, cycleCount: cycleCount, nominalChargeCapacity: nominalChargeCapacity, designCapacity: designCapacity, maximumCapacity: maximumCapacity) 72 | 73 | records.append(record) 74 | } 75 | 76 | } else { 77 | print("查询失败: \(String(cString: sqlite3_errmsg(db)))") 78 | } 79 | 80 | sqlite3_finalize(statement) 81 | return records 82 | } 83 | 84 | func getRecordCount() -> Int { 85 | let countQuery = "SELECT COUNT(*) FROM \(recordTableName);" 86 | var statement: OpaquePointer? 87 | var count: Int = 0 88 | 89 | if sqlite3_prepare_v2(db, countQuery, -1, &statement, nil) == SQLITE_OK { 90 | if sqlite3_step(statement) == SQLITE_ROW { 91 | count = Int(sqlite3_column_int(statement, 0)) 92 | } 93 | } else { 94 | print("查询记录数失败: \(String(cString: sqlite3_errmsg(db)))") 95 | } 96 | 97 | sqlite3_finalize(statement) 98 | return count 99 | } 100 | 101 | 102 | func insertRecord(_ record: BatteryDataRecord) -> Bool { 103 | let insertQuery = """ 104 | INSERT INTO \(recordTableName) (createDate, recordType, cycleCount, nominalChargeCapacity, designCapacity, maximumCapacity) 105 | VALUES (?, ?, ?, ?, ?, ?); 106 | """ 107 | 108 | var statement: OpaquePointer? 109 | 110 | if sqlite3_prepare_v2(db, insertQuery, -1, &statement, nil) == SQLITE_OK { 111 | 112 | sqlite3_bind_int(statement, 1, Int32(Date().timeIntervalSince1970)) 113 | sqlite3_bind_int(statement, 2, Int32(record.recordType.rawValue)) 114 | sqlite3_bind_int(statement, 3, Int32(record.cycleCount)) 115 | sqlite3_bind_int(statement, 4, Int32(record.nominalChargeCapacity ?? 0)) 116 | sqlite3_bind_int(statement, 5, Int32(record.designCapacity ?? 0)) 117 | 118 | // if let maximumCapacity = record.maximumCapacity { 119 | // sqlite3_bind_text(statement, 6, (maximumCapacity as NSString).utf8String, -1, nil) 120 | // } else { 121 | // sqlite3_bind_null(statement, 6) 122 | // } 123 | 124 | if sqlite3_step(statement) == SQLITE_DONE { 125 | sqlite3_finalize(statement) 126 | return true 127 | } else { 128 | sqlite3_finalize(statement) 129 | return false 130 | } 131 | 132 | } else { 133 | sqlite3_finalize(statement) 134 | return false 135 | } 136 | 137 | } 138 | 139 | /// 删除一条记录 140 | func deleteRecord(byID id: Int) -> Bool { 141 | let deleteQuery = "DELETE FROM \(recordTableName) WHERE id = ?;" 142 | 143 | var statement: OpaquePointer? 144 | 145 | if sqlite3_prepare_v2(db, deleteQuery, -1, &statement, nil) == SQLITE_OK { 146 | sqlite3_bind_int(statement, 1, Int32(id)) 147 | 148 | if sqlite3_step(statement) == SQLITE_DONE { 149 | sqlite3_finalize(statement) 150 | return true 151 | } else { 152 | sqlite3_finalize(statement) 153 | return false 154 | } 155 | } else { 156 | sqlite3_finalize(statement) 157 | return false 158 | } 159 | 160 | } 161 | 162 | func getLatestRecord() -> BatteryDataRecord? { 163 | let query = """ 164 | SELECT id, createDate, recordType, cycleCount, nominalChargeCapacity, designCapacity, maximumCapacity 165 | FROM \(recordTableName) 166 | ORDER BY createDate DESC 167 | LIMIT 1; 168 | """ 169 | 170 | var statement: OpaquePointer? 171 | var latestRecord: BatteryDataRecord? = nil 172 | 173 | if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK { 174 | if sqlite3_step(statement) == SQLITE_ROW { 175 | let id = Int(sqlite3_column_int(statement, 0)) 176 | let createDate = Int(sqlite3_column_int(statement, 1)) 177 | let recordType = BatteryDataRecord.BatteryDataRecordType(rawValue: Int(sqlite3_column_int(statement, 2))) ?? .Automatic 178 | let cycleCount = Int(sqlite3_column_int(statement, 3)) 179 | 180 | let nominalChargeCapacity = sqlite3_column_type(statement, 4) != SQLITE_NULL ? Int(sqlite3_column_int(statement, 4)) : nil 181 | let designCapacity = sqlite3_column_type(statement, 5) != SQLITE_NULL ? Int(sqlite3_column_int(statement, 5)) : nil 182 | let maximumCapacity = sqlite3_column_type(statement, 6) != SQLITE_NULL ? String(cString: sqlite3_column_text(statement, 6)) : nil 183 | 184 | latestRecord = BatteryDataRecord(id: id, createDate: createDate, recordType: recordType, cycleCount: cycleCount, nominalChargeCapacity: nominalChargeCapacity, designCapacity: designCapacity, maximumCapacity: maximumCapacity) 185 | } 186 | } else { 187 | print("没有查询到: \(String(cString: sqlite3_errmsg(db)))") 188 | } 189 | 190 | sqlite3_finalize(statement) 191 | return latestRecord 192 | } 193 | 194 | /// 查询指定循环次数的最新记录 195 | func getRecord(byCycleCount cycleCount: Int) -> BatteryDataRecord? { 196 | let query = """ 197 | SELECT id, createDate, recordType, cycleCount, nominalChargeCapacity, designCapacity, maximumCapacity 198 | FROM \(recordTableName) 199 | WHERE cycleCount = ? 200 | ORDER BY createDate DESC 201 | LIMIT 1; 202 | """ 203 | 204 | var statement: OpaquePointer? 205 | var record: BatteryDataRecord? = nil 206 | 207 | if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK { 208 | sqlite3_bind_int(statement, 1, Int32(cycleCount)) 209 | 210 | if sqlite3_step(statement) == SQLITE_ROW { 211 | let id = Int(sqlite3_column_int(statement, 0)) 212 | let createDate = Int(sqlite3_column_int(statement, 1)) 213 | let recordType = BatteryDataRecord.BatteryDataRecordType(rawValue: Int(sqlite3_column_int(statement, 2))) ?? .Automatic 214 | let cycleCount = Int(sqlite3_column_int(statement, 3)) 215 | 216 | let nominalChargeCapacity = sqlite3_column_type(statement, 4) != SQLITE_NULL ? Int(sqlite3_column_int(statement, 4)) : nil 217 | let designCapacity = sqlite3_column_type(statement, 5) != SQLITE_NULL ? Int(sqlite3_column_int(statement, 5)) : nil 218 | let maximumCapacity = sqlite3_column_type(statement, 6) != SQLITE_NULL ? String(cString: sqlite3_column_text(statement, 6)) : nil 219 | 220 | record = BatteryDataRecord(id: id, createDate: createDate, recordType: recordType, cycleCount: cycleCount, nominalChargeCapacity: nominalChargeCapacity, designCapacity: designCapacity, maximumCapacity: maximumCapacity) 221 | } 222 | } else { 223 | print("查询指定循环次数的记录失败: \(String(cString: sqlite3_errmsg(db)))") 224 | } 225 | 226 | sqlite3_finalize(statement) 227 | return record 228 | } 229 | 230 | // 导出全部记录为CSV 231 | func exportToCSV() -> URL? { 232 | let records = fetchAllRecords() 233 | guard !records.isEmpty else { 234 | NSLog("No records found to export.") 235 | return nil 236 | } 237 | 238 | let fileName = NSLocalizedString("BatteryDataRecordsCSVName", comment: "BatteryDataRecords").appending(".csv") 239 | let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) 240 | 241 | var csvText = "ID,CreateDate,CycleCount,NominalChargeCapacity,DesignCapacity,MaximumCapacity\n" 242 | 243 | let dateFormatter = DateFormatter() 244 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 245 | 246 | for record in records { 247 | let createDateStr = dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(record.createDate))) 248 | let line = "\(record.id),\(createDateStr),\(record.cycleCount),\(record.nominalChargeCapacity ?? 0),\(record.designCapacity ?? 0),\(record.maximumCapacity ?? "N/A")\n" 249 | csvText.append(line) 250 | } 251 | 252 | do { 253 | try csvText.write(to: fileURL, atomically: true, encoding: .utf8) 254 | NSLog("CSV file created at: \(fileURL.path)") 255 | return fileURL 256 | } catch { 257 | NSLog("Failed to write CSV file: \(error.localizedDescription)") 258 | return nil 259 | } 260 | } 261 | 262 | // 删除全部数据 263 | func deleteAllRecords() { 264 | let deleteQuery = "DELETE FROM \(recordTableName);" 265 | 266 | if sqlite3_exec(db, deleteQuery, nil, nil, nil) == SQLITE_OK { 267 | print("All records deleted successfully.") 268 | } 269 | } 270 | 271 | } 272 | -------------------------------------------------------------------------------- /BatteryInfo/Controller/SettingsBatteryDataController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class SettingsBatteryDataController { 4 | 5 | 6 | private static var lastUpdate: Date? // 内部缓存,每6小时重新填充一次性电池信息以避免频繁读取 7 | private static var cachedData: SettingsBatteryData? 8 | 9 | static func getSettingsBatteryInfoData(forceRefresh: Bool = false) -> SettingsBatteryData? { 10 | // 缓存逻辑:如果在6小时内已有数据且非强制刷新,直接返回缓存 11 | if let last = lastUpdate, let cached = cachedData, 12 | !forceRefresh, Date().timeIntervalSince(last) < 6 * 60 * 60 { 13 | return cached 14 | } 15 | 16 | // 获取当前包内 `SettingsBatteryHelper` 可执行文件的路径 17 | let executablePath = Bundle.main.url(forAuxiliaryExecutable: "SettingsBatteryHelper")?.path ?? "/" 18 | 19 | var stdOut: NSString? 20 | // 调用 `spawnRoot` 21 | spawnRoot(executablePath, nil, &stdOut, nil) 22 | 23 | if let stdOutString = stdOut as String?, let plistData = stdOutString.data(using: .utf8) { 24 | do { 25 | // 使用 Codable 解析 JSON 26 | let batteryData = try JSONDecoder().decode(SettingsBatteryData.self, from: plistData) 27 | 28 | // 更新缓存 29 | lastUpdate = Date() 30 | cachedData = batteryData 31 | 32 | return batteryData 33 | 34 | } catch { 35 | print("BatteryInfo------> Error converting string to plist: \(error.localizedDescription)") 36 | } 37 | 38 | } else { 39 | NSLog("BatteryInfo------> RootHelper工作失败") 40 | } 41 | // 如果失败则返回旧数据(可能为 nil) 42 | return cachedData 43 | } 44 | 45 | static func clearCache() { 46 | lastUpdate = nil 47 | cachedData = nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BatteryInfo/DeviceController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface DeviceController : NSObject 4 | 5 | //- (BOOL) RebootDevice; 6 | - (void) Respring; 7 | 8 | @end 9 | 10 | int spawnRoot(NSString *path, NSArray *args, NSString **stdOut, NSString **stdErr); 11 | -------------------------------------------------------------------------------- /BatteryInfo/DeviceController.m: -------------------------------------------------------------------------------- 1 | #import "DeviceController.h" 2 | #include 3 | #import 4 | #import 5 | 6 | @implementation DeviceController 7 | 8 | #define POSIX_SPAWN_PERSONA_FLAGS_OVERRIDE 1 9 | extern int posix_spawnattr_set_persona_np(const posix_spawnattr_t* __restrict, uid_t, uint32_t); 10 | extern int posix_spawnattr_set_persona_uid_np(const posix_spawnattr_t* __restrict, uid_t); 11 | extern int posix_spawnattr_set_persona_gid_np(const posix_spawnattr_t* __restrict, uid_t); 12 | 13 | 14 | 15 | // @See https://github.com/opa334/TrollStore/blob/main/Shared/TSUtil.m#L297 16 | - (void) Respring 17 | { 18 | killall(@"SpringBoard", YES); 19 | exit(0); 20 | } 21 | 22 | // @See https://github.com/opa334/TrollStore/blob/main/Shared/TSUtil.m#L79 23 | int spawnRoot(NSString* path, NSArray* args, NSString** stdOut, NSString** stdErr) 24 | { 25 | NSMutableArray* argsM = args.mutableCopy ?: [NSMutableArray new]; 26 | [argsM insertObject:path atIndex:0]; 27 | 28 | NSUInteger argCount = [argsM count]; 29 | char **argsC = (char **)malloc((argCount + 1) * sizeof(char*)); 30 | 31 | for (NSUInteger i = 0; i < argCount; i++) 32 | { 33 | argsC[i] = strdup([[argsM objectAtIndex:i] UTF8String]); 34 | } 35 | argsC[argCount] = NULL; 36 | 37 | posix_spawnattr_t attr; 38 | posix_spawnattr_init(&attr); 39 | 40 | posix_spawnattr_set_persona_np(&attr, 99, POSIX_SPAWN_PERSONA_FLAGS_OVERRIDE); 41 | posix_spawnattr_set_persona_uid_np(&attr, 0); 42 | posix_spawnattr_set_persona_gid_np(&attr, 0); 43 | 44 | posix_spawn_file_actions_t action; 45 | posix_spawn_file_actions_init(&action); 46 | 47 | int outErr[2]; 48 | if(stdErr) 49 | { 50 | pipe(outErr); 51 | posix_spawn_file_actions_adddup2(&action, outErr[1], STDERR_FILENO); 52 | posix_spawn_file_actions_addclose(&action, outErr[0]); 53 | } 54 | 55 | int out[2]; 56 | if(stdOut) 57 | { 58 | pipe(out); 59 | posix_spawn_file_actions_adddup2(&action, out[1], STDOUT_FILENO); 60 | posix_spawn_file_actions_addclose(&action, out[0]); 61 | } 62 | 63 | pid_t task_pid; 64 | int status = -200; 65 | int spawnError = posix_spawn(&task_pid, [path UTF8String], &action, &attr, (char* const*)argsC, NULL); 66 | posix_spawnattr_destroy(&attr); 67 | for (NSUInteger i = 0; i < argCount; i++) 68 | { 69 | free(argsC[i]); 70 | } 71 | free(argsC); 72 | 73 | if(spawnError != 0) 74 | { 75 | NSLog(@"posix_spawn error %d\n", spawnError); 76 | return spawnError; 77 | } 78 | 79 | __block volatile BOOL _isRunning = YES; 80 | NSMutableString* outString = [NSMutableString new]; 81 | NSMutableString* errString = [NSMutableString new]; 82 | dispatch_semaphore_t sema = 0; 83 | dispatch_queue_t logQueue; 84 | if(stdOut || stdErr) 85 | { 86 | logQueue = dispatch_queue_create("com.opa334.TrollStore.LogCollector", NULL); 87 | sema = dispatch_semaphore_create(0); 88 | 89 | int outPipe = out[0]; 90 | int outErrPipe = outErr[0]; 91 | 92 | __block BOOL outEnabled = (BOOL)stdOut; 93 | __block BOOL errEnabled = (BOOL)stdErr; 94 | dispatch_async(logQueue, ^ 95 | { 96 | while(_isRunning) 97 | { 98 | @autoreleasepool 99 | { 100 | if(outEnabled) 101 | { 102 | [outString appendString:getNSStringFromFile(outPipe)]; 103 | } 104 | if(errEnabled) 105 | { 106 | [errString appendString:getNSStringFromFile(outErrPipe)]; 107 | } 108 | } 109 | } 110 | dispatch_semaphore_signal(sema); 111 | }); 112 | } 113 | 114 | do 115 | { 116 | if (waitpid(task_pid, &status, 0) != -1) { 117 | NSLog(@"Child status %d", WEXITSTATUS(status)); 118 | } else 119 | { 120 | perror("waitpid"); 121 | _isRunning = NO; 122 | return -222; 123 | } 124 | } while (!WIFEXITED(status) && !WIFSIGNALED(status)); 125 | 126 | _isRunning = NO; 127 | if(stdOut || stdErr) 128 | { 129 | if(stdOut) 130 | { 131 | close(out[1]); 132 | } 133 | if(stdErr) 134 | { 135 | close(outErr[1]); 136 | } 137 | 138 | // wait for logging queue to finish 139 | dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); 140 | 141 | if(stdOut) 142 | { 143 | *stdOut = outString.copy; 144 | } 145 | if(stdErr) 146 | { 147 | *stdErr = errString.copy; 148 | } 149 | } 150 | 151 | return WEXITSTATUS(status); 152 | } 153 | 154 | NSString* getNSStringFromFile(int fd) 155 | { 156 | NSMutableString* ms = [NSMutableString new]; 157 | ssize_t num_read; 158 | char c; 159 | if(!fd_is_valid(fd)) return @""; 160 | while((num_read = read(fd, &c, sizeof(c)))) 161 | { 162 | [ms appendString:[NSString stringWithFormat:@"%c", c]]; 163 | if(c == '\n') break; 164 | } 165 | return ms.copy; 166 | } 167 | 168 | int fd_is_valid(int fd) 169 | { 170 | return fcntl(fd, F_GETFD) != -1 || errno != EBADF; 171 | } 172 | 173 | // @See https://github.com/opa334/TrollStore/blob/main/Shared/TSUtil.m#L279 174 | void killall(NSString* processName, BOOL softly) 175 | { 176 | enumerateProcessesUsingBlock(^(pid_t pid, NSString* executablePath, BOOL* stop) 177 | { 178 | if([executablePath.lastPathComponent isEqualToString:processName]) 179 | { 180 | if(softly) 181 | { 182 | kill(pid, SIGTERM); 183 | } 184 | else 185 | { 186 | kill(pid, SIGKILL); 187 | } 188 | } 189 | }); 190 | } 191 | 192 | void enumerateProcessesUsingBlock(void (^enumerator)(pid_t pid, NSString* executablePath, BOOL* stop)) 193 | { 194 | static int maxArgumentSize = 0; 195 | if (maxArgumentSize == 0) { 196 | size_t size = sizeof(maxArgumentSize); 197 | if (sysctl((int[]){ CTL_KERN, KERN_ARGMAX }, 2, &maxArgumentSize, &size, NULL, 0) == -1) { 198 | perror("sysctl argument size"); 199 | maxArgumentSize = 4096; // Default 200 | } 201 | } 202 | int mib[3] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL}; 203 | struct kinfo_proc *info; 204 | size_t length; 205 | int count; 206 | 207 | if (sysctl(mib, 3, NULL, &length, NULL, 0) < 0) 208 | return; 209 | if (!(info = malloc(length))) 210 | return; 211 | if (sysctl(mib, 3, info, &length, NULL, 0) < 0) { 212 | free(info); 213 | return; 214 | } 215 | count = length / sizeof(struct kinfo_proc); 216 | for (int i = 0; i < count; i++) { 217 | @autoreleasepool { 218 | pid_t pid = info[i].kp_proc.p_pid; 219 | if (pid == 0) { 220 | continue; 221 | } 222 | size_t size = maxArgumentSize; 223 | char* buffer = (char *)malloc(length); 224 | if (sysctl((int[]){ CTL_KERN, KERN_PROCARGS2, pid }, 3, buffer, &size, NULL, 0) == 0) { 225 | NSString* executablePath = [NSString stringWithCString:(buffer+sizeof(int)) encoding:NSUTF8StringEncoding]; 226 | 227 | BOOL stop = NO; 228 | enumerator(pid, executablePath, &stop); 229 | if(stop) 230 | { 231 | free(buffer); 232 | break; 233 | } 234 | } 235 | free(buffer); 236 | } 237 | } 238 | free(info); 239 | } 240 | 241 | @end 242 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/BatteryDataRecord.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class BatteryDataRecord { 4 | 5 | enum BatteryDataRecordType: Int { 6 | case Automatic = 0 // 自动记录 7 | case ManualAdd = 1 // 手动记录,但是数据是API的 8 | case AutomaticOCR = 2 // 自动记录,是OCR导入的 9 | case ManualRecord = 3 // 手动记录,但是数据是自己填写的 10 | } 11 | 12 | // 数据库表的 13 | private let dbTableVersion = 1 14 | 15 | // ID 16 | let id: Int 17 | 18 | // 记录的日期 19 | let createDate: Int 20 | 21 | // 记录的类型 22 | let recordType: BatteryDataRecordType 23 | 24 | let cycleCount: Int 25 | 26 | var nominalChargeCapacity: Int? 27 | 28 | var designCapacity: Int? 29 | 30 | var maximumCapacity: String? 31 | 32 | init( cycleCount: Int, nominalChargeCapacity: Int, designCapacity: Int) { 33 | self.id = 0 34 | self.createDate = 0 35 | self.recordType = .Automatic 36 | 37 | self.cycleCount = cycleCount 38 | self.nominalChargeCapacity = nominalChargeCapacity 39 | self.designCapacity = designCapacity 40 | } 41 | 42 | init(createDate: Int, cycleCount: Int, nominalChargeCapacity: Int, designCapacity: Int) { 43 | self.id = 0 44 | self.recordType = .Automatic 45 | 46 | self.createDate = createDate 47 | 48 | self.cycleCount = cycleCount 49 | self.nominalChargeCapacity = nominalChargeCapacity 50 | self.designCapacity = designCapacity 51 | } 52 | 53 | init(id: Int, createDate: Int, recordType: BatteryDataRecordType, cycleCount: Int, nominalChargeCapacity: Int, designCapacity: Int) { 54 | self.id = id 55 | self.createDate = createDate 56 | self.nominalChargeCapacity = nominalChargeCapacity 57 | self.designCapacity = designCapacity 58 | self.cycleCount = cycleCount 59 | self.recordType = recordType 60 | } 61 | 62 | init(id: Int, createDate: Int, recordType: BatteryDataRecordType, cycleCount: Int, nominalChargeCapacity: Int? = nil, designCapacity: Int? = nil, maximumCapacity: String? = nil) { 63 | self.id = id 64 | self.createDate = createDate 65 | self.recordType = recordType 66 | self.cycleCount = cycleCount 67 | self.nominalChargeCapacity = nominalChargeCapacity 68 | self.designCapacity = designCapacity 69 | self.maximumCapacity = maximumCapacity 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/BatteryInfoItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // 电池信息组的ID集合 4 | enum BatteryInfoGroupID: Int { 5 | case basic = 1 6 | case charge = 2 7 | case settingsBatteryInfo = 3 8 | case batterySerialNumber = 4 9 | case batteryQmax = 5 10 | case charger = 6 11 | case batteryVoltage = 7 12 | case batteryLifeTime = 8 13 | case notChargeReason = 9 14 | case chargingPowerAndNotChargeReason = 10 15 | case accessoryDetails = 11 16 | 17 | static var allCases: [BatteryInfoGroupID] { 18 | return [ 19 | .basic, 20 | .charge, 21 | .settingsBatteryInfo, 22 | .batterySerialNumber, 23 | .batteryQmax, 24 | .charger, 25 | .batteryVoltage, 26 | .batteryLifeTime, 27 | .notChargeReason, 28 | .chargingPowerAndNotChargeReason, 29 | .accessoryDetails 30 | ] 31 | } 32 | } 33 | 34 | // 电池信息组 35 | enum BatteryInfoGroupName { 36 | static func getName(for id: Int) -> String { 37 | switch id { 38 | case BatteryInfoGroupID.basic.rawValue: 39 | return NSLocalizedString("GroupBasic", comment: "电池基础信息组") 40 | case BatteryInfoGroupID.charge.rawValue: 41 | return NSLocalizedString("GroupCharge", comment: "充电信息组") 42 | case BatteryInfoGroupID.settingsBatteryInfo.rawValue: 43 | return NSLocalizedString("GroupSettingsBatteryInfo", comment: "设置中的电池信息组") 44 | case BatteryInfoGroupID.batterySerialNumber.rawValue: 45 | return NSLocalizedString("GroupBatterySerial", comment: "电池序列号信息组") 46 | case BatteryInfoGroupID.batteryQmax.rawValue: 47 | return NSLocalizedString("GroupBatteryQmax", comment: "电池Qmax信息组") 48 | case BatteryInfoGroupID.charger.rawValue: 49 | return NSLocalizedString("GroupCharger", comment: "充电器信息组") 50 | case BatteryInfoGroupID.batteryVoltage.rawValue: 51 | return NSLocalizedString("GroupBatteryVoltage", comment: "电池电压信息组") 52 | case BatteryInfoGroupID.batteryLifeTime.rawValue: 53 | return NSLocalizedString("GroupBatteryLifeTime", comment: "电池生命周期信息组") 54 | case BatteryInfoGroupID.notChargeReason.rawValue: 55 | return NSLocalizedString("GroupNotChargeReason", comment: "不充电原因组") 56 | case BatteryInfoGroupID.chargingPowerAndNotChargeReason.rawValue: 57 | return NSLocalizedString("GroupChargingPowerNotChargeReason", comment: "充电功率和不充电原因组") 58 | case BatteryInfoGroupID.accessoryDetails.rawValue: 59 | return NSLocalizedString("GroupAccessoryDetails", comment: "外接配件信息组") 60 | default: 61 | return NSLocalizedString("GroupUnknown", comment: "未知") 62 | } 63 | } 64 | } 65 | 66 | // 电池信息每一项的ID集合 67 | enum BatteryInfoItemID { 68 | static let maximumCapacity = 101 69 | static let cycleCount = 102 70 | static let designCapacity = 103 71 | static let nominalChargeCapacity = 104 72 | static let temperature = 105 73 | static let currentCapacity = 106 74 | static let currentRAWCapacity = 107 75 | static let currentVoltage = 108 76 | static let instantAmperage = 109 77 | 78 | static let isCharging = 201 79 | static let chargeDescription = 202 80 | static let isWirelessCharger = 203 81 | static let maximumChargingHandshakeWatts = 204 82 | static let powerOptionDetail = 205 83 | static let powerOptions = 206 84 | static let chargingLimitVoltage = 207 85 | static let chargingVoltage = 208 86 | static let chargingCurrent = 209 87 | static let calculatedChargingPower = 210 88 | static let notChargingReason = 211 89 | 90 | static let possibleRefreshDate = 303 91 | 92 | static let batterySerialNumber = 401 93 | static let batteryManufacturer = 402 94 | 95 | static let maximumQmax = 501 96 | static let minimumQmax = 502 97 | 98 | static let chargerName = 601 99 | static let chargerModel = 602 100 | static let chargerManufacturer = 603 101 | static let chargerSerialNumber = 604 102 | static let chargerHardwareVersion = 605 103 | static let chargerFirmwareVersion = 606 104 | 105 | static let batteryInstalled = 701 106 | static let bootVoltage = 702 107 | static let limitVoltage = 703 108 | 109 | static let averageTemperature = 801 110 | static let maximumTemperature = 802 111 | static let minimumTemperature = 803 112 | static let maximumChargeCurrent = 804 113 | static let maximumDischargeCurrent = 805 114 | static let maximumPackVoltage = 806 115 | static let minimumPackVoltage = 807 116 | 117 | static let accessoryCurrentCapacity = 1101 118 | static let accessoryIsCharging = 1102 119 | static let accessoryExternalConnected = 1103 120 | 121 | } 122 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/BatteryRAWInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // 定义一个结构体来存储IO接口提供的电池原始信息 4 | struct BatteryRAWInfo { 5 | var updateTime: Int? // 数据刷新时间 6 | var batteryInstalled: Int? // 电池已安装 7 | var bootPathUpdated: Int? // 8 | var bootVoltage: Int? // 开机电压 9 | var serialNumber: String? // 电池序列号 10 | var voltage: Int? // 当前电压 11 | var instantAmperage: Int? // 当前电流 12 | var currentCapacity: Int? // 当前电量百分比 13 | var appleRawCurrentCapacity: Int? // 当前电池剩余的毫安数 14 | var designCapacity: Int? // 电池设计容量 15 | var nominalChargeCapacity: Int? // 电池当前的最大容量 16 | var isCharging: Bool? // 是否充电 17 | var cycleCount: Int? // 循环次数 18 | var temperature: Int? // 电池温度 19 | var batteryData: BatteryData? // 嵌套 BatteryData 20 | var kioskMode: KioskMode? // 嵌套 KioskMode 21 | var bestAdapterIndex: Int? // 最合适的充电器序号 22 | var adapterDetails: AdapterDetails? // 充电器信息 23 | var accessoryDetails: AccessoryDetails? // 扩展配件的电量,例如MagSafe外接电池 24 | var appleRawAdapterDetails: [AdapterDetails] // 充电器原始信息 25 | var chargerData: ChargerData? // 嵌套 ChargerData 26 | var maximumCapacity: String? // 最大可充电的百分比,默认是100 27 | } 28 | 29 | extension BatteryRAWInfo { 30 | init(dict: [String: Any]) { 31 | self.updateTime = dict["UpdateTime"] as? Int 32 | self.batteryInstalled = dict["BatteryInstalled"] as? Int 33 | self.bootVoltage = dict["BootVoltage"] as? Int 34 | self.bootPathUpdated = dict["BootPathUpdated"] as? Int 35 | self.serialNumber = dict["Serial"] as? String 36 | self.voltage = dict["Voltage"] as? Int 37 | self.instantAmperage = dict["InstantAmperage"] as? Int 38 | self.currentCapacity = dict["CurrentCapacity"] as? Int 39 | self.appleRawCurrentCapacity = dict["AppleRawCurrentCapacity"] as? Int 40 | self.designCapacity = dict["DesignCapacity"] as? Int 41 | self.nominalChargeCapacity = dict["NominalChargeCapacity"] as? Int 42 | self.isCharging = (dict["IsCharging"] as? Int) == 1 43 | self.cycleCount = dict["CycleCount"] as? Int 44 | self.temperature = dict["Temperature"] as? Int 45 | self.bestAdapterIndex = dict["BestAdapterIndex"] as? Int 46 | 47 | if let batteryDataDict = dict["BatteryData"] as? [String: Any] { 48 | self.batteryData = BatteryData(dict: batteryDataDict) 49 | } 50 | 51 | if let kioskModeDict = dict["KioskMode"] as? [String: Any] { 52 | self.kioskMode = KioskMode(dict: kioskModeDict) 53 | } 54 | 55 | if let chargerDataDict = dict["ChargerData"] as? [String: Any] { 56 | self.chargerData = ChargerData(dict: chargerDataDict) 57 | } 58 | 59 | if let adapterDataDict = dict["AdapterDetails"] as? [String: Any] { 60 | self.adapterDetails = AdapterDetails(dict: adapterDataDict) 61 | } 62 | 63 | if let accessoryArray = dict["AccessoryDetails"] as? [[String: Any]], 64 | let accessoryDict = accessoryArray.first { 65 | self.accessoryDetails = AccessoryDetails(dict: accessoryDict) 66 | } 67 | 68 | self.appleRawAdapterDetails = [] 69 | if let rawAdapterArray = dict["AppleRawAdapterDetails"] as? [[String: Any]] { 70 | self.appleRawAdapterDetails = rawAdapterArray.map { AdapterDetails(dict: $0) } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/InfoItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // 每一个item的具体数据模型 4 | class InfoItem { 5 | 6 | let id: Int // id 7 | var text: String // 文本 8 | var detailText: String? // 说明文本 9 | var sort: Int // 预留排序标记 10 | var haveData: Bool // 是否可用 11 | 12 | init(id: Int, text: String) { 13 | self.id = id 14 | self.text = text 15 | self.sort = id 16 | self.haveData = true 17 | } 18 | 19 | init(id: Int, text: String, haveData: Bool) { 20 | self.id = id 21 | self.text = text 22 | self.sort = id 23 | self.haveData = haveData 24 | } 25 | 26 | init(id: Int, text: String, sort: Int) { 27 | self.id = id 28 | self.text = text 29 | self.sort = sort 30 | self.haveData = true 31 | } 32 | 33 | init(id: Int, text: String, detailText: String? = nil, sort: Int) { 34 | self.id = id 35 | self.text = text 36 | self.detailText = detailText 37 | self.sort = sort 38 | self.haveData = true 39 | } 40 | 41 | init(id: Int, text: String, detailText: String? = nil, sort: Int, haveData: Bool) { 42 | self.id = id 43 | self.text = text 44 | self.detailText = detailText 45 | self.sort = sort 46 | self.haveData = haveData 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/InfoItemGroup.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class InfoItemGroup { 4 | 5 | let id: Int // 组id 6 | var titleText: String? // 顶部文本 7 | private(set) var items: [InfoItem] // 这一组的内容 8 | var footerText: String? // 底部文本 9 | 10 | init(id: Int) { 11 | self.id = id 12 | self.items = [] 13 | } 14 | 15 | init(id: Int, items: [InfoItem]) { 16 | self.id = id 17 | self.items = items 18 | } 19 | 20 | init(id: Int, titleText: String, items: [InfoItem]) { 21 | self.id = id 22 | self.titleText = titleText 23 | self.items = items 24 | } 25 | 26 | init(id: Int, items: [InfoItem], footerText: String) { 27 | self.id = id 28 | self.items = items 29 | self.footerText = footerText 30 | } 31 | 32 | init(id: Int, titleText: String, items: [InfoItem], footerText: String) { 33 | self.id = id 34 | self.titleText = titleText 35 | self.items = items 36 | self.footerText = footerText 37 | } 38 | 39 | // 添加单个条目 40 | func addItem(_ item: InfoItem) { 41 | self.items.append(item) 42 | } 43 | 44 | // 添加多个条目 45 | func addItems(_ newItems: [InfoItem]) { 46 | self.items.append(contentsOf: newItems) 47 | } 48 | 49 | // 清空所有条目 50 | func clearItems() { 51 | self.items.removeAll() 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/RAWData/AccessoryDetails.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AccessoryDetails { // 扩展配件的信息 4 | var currentCapacity: Int? 5 | var isCharging: Bool? 6 | var externalConnected: Bool? 7 | } 8 | 9 | extension AccessoryDetails { 10 | init(dict: [String: Any]) { 11 | self.currentCapacity = dict["CurrentCapacity"] as? Int 12 | self.isCharging = (dict["IsCharging"] as? Int) == 1 13 | self.externalConnected = (dict["ExternalConnected"] as? Int) == 1 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/RAWData/AdapterDetails.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // 适配器详情 4 | struct AdapterDetails { 5 | var adapterID: Int? 6 | var adapterVoltage: Int? 7 | var current: Int? 8 | var description: String? 9 | var familyCode: String? 10 | var isWireless: Bool? 11 | var pmuConfiguration: Int? 12 | var sharedSource: Bool? 13 | var source: Int? 14 | var usbHvcHvcIndex: Int? 15 | var usbHvcMenu: [UsbHvcOption] = [] 16 | var voltage: Int? 17 | var watts: Int? 18 | var name: String? // 充电器名字 19 | var manufacturer: String? // 充电器的制造厂家 20 | var model: String? // 充电器型号 21 | var serialString: String? // 充电器的序列号 22 | var hwVersion: String? // 充电器的硬件版本 23 | var fwVersion: String? // 充电器的软件版本 24 | 25 | // 解析字典数据 26 | init(dict: [String: Any]) { 27 | self.adapterID = dict["AdapterID"] as? Int 28 | self.adapterVoltage = dict["AdapterVoltage"] as? Int 29 | self.current = dict["Current"] as? Int 30 | self.description = dict["Description"] as? String 31 | self.familyCode = dict["FamilyCode"] as? String 32 | self.isWireless = (dict["IsWireless"] as? Int) == 1 33 | self.pmuConfiguration = dict["PMUConfiguration"] as? Int 34 | self.sharedSource = (dict["SharedSource"] as? Int) == 1 35 | self.source = dict["Source"] as? Int 36 | self.usbHvcHvcIndex = dict["UsbHvcHvcIndex"] as? Int 37 | self.voltage = dict["Voltage"] as? Int 38 | self.watts = dict["Watts"] as? Int 39 | 40 | self.name = dict["Name"] as? String 41 | self.model = dict["Model"] as? String 42 | self.manufacturer = dict["Manufacturer"] as? String 43 | self.serialString = dict["SerialString"] as? String 44 | self.hwVersion = dict["HwVersion"] as? String 45 | self.fwVersion = dict["FwVersion"] as? String 46 | 47 | // 解析 USB HVC 选项 48 | if let usbHvcMenuArray = dict["UsbHvcMenu"] as? [[String: Any]] { 49 | self.usbHvcMenu = usbHvcMenuArray.map { UsbHvcOption(dict: $0) } 50 | } 51 | } 52 | } 53 | 54 | // 适配器的 USB HVC 选项 55 | struct UsbHvcOption { 56 | var index: Int 57 | var maxCurrent: Int 58 | var maxVoltage: Int 59 | } 60 | 61 | extension UsbHvcOption { 62 | init(dict: [String: Any]) { 63 | self.index = dict["Index"] as? Int ?? 0 64 | self.maxCurrent = dict["MaxCurrent"] as? Int ?? 0 65 | self.maxVoltage = dict["MaxVoltage"] as? Int ?? 0 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/RAWData/BatteryData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct BatteryData { 4 | var algoChemID: Int? 5 | var cycleCount: Int? 6 | var designCapacity: Int? 7 | var dynamicSoc1Vcut: Int? 8 | var maximumFCC: Int? 9 | var minimumFCC: Int? 10 | var temperatureSamples: Int? 11 | var stateOfCharge: Int? 12 | var lifetimeData: LifetimeData? // 嵌套 LifetimeData 13 | } 14 | 15 | extension BatteryData { 16 | init(dict: [String: Any]) { 17 | self.algoChemID = dict["AlgoChemID"] as? Int 18 | self.cycleCount = dict["CycleCount"] as? Int 19 | self.designCapacity = dict["DesignCapacity"] as? Int 20 | self.dynamicSoc1Vcut = dict["DynamicSoc1Vcut"] as? Int 21 | self.maximumFCC = dict["MaximumFCC"] as? Int 22 | self.minimumFCC = dict["MinimumFCC"] as? Int 23 | self.temperatureSamples = dict["TemperatureSamples"] as? Int 24 | self.stateOfCharge = dict["StateOfCharge"] as? Int 25 | 26 | if let lifetimeDataDict = dict["LifetimeData"] as? [String: Any] { 27 | self.lifetimeData = LifetimeData(dict: lifetimeDataDict) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/RAWData/ChargerData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ChargerData { 4 | var chargerID: String? 5 | var chargingCurrent: Int? // 充电电流 6 | var chargingVoltage: Int? // 充电电压 7 | // 未充电的原因 0 = 正常状态 8 | // 1 = 电池已充满电 9 | // 128 = 电池未在充电 10 | // 256 = 电池温度过高导致停止充电 11 | // 272 = 电池温度过高导致停止充电 12 | // 8192 = (可能是正在握手) 13 | // 1024 = (可能是正在握手) 14 | var notChargingReason: Int? 15 | var vacVoltageLimit: Int? // 限制电压 16 | } 17 | 18 | extension ChargerData { 19 | init(dict: [String: Any]) { 20 | self.chargerID = dict["ChargerID"] as? String 21 | self.chargingCurrent = dict["ChargingCurrent"] as? Int 22 | self.chargingVoltage = dict["ChargingVoltage"] as? Int 23 | self.notChargingReason = dict["NotChargingReason"] as? Int 24 | self.vacVoltageLimit = dict["VacVoltageLimit"] as? Int 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/RAWData/KioskMode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct KioskMode { 4 | var fullChargeVoltage: Int? 5 | var highSocDays: Int? 6 | var lastHighSocHours: Int? 7 | var mode: Int? 8 | } 9 | 10 | extension KioskMode { 11 | init(dict: [String: Any]) { 12 | self.fullChargeVoltage = dict["KioskModeFullChargeVoltage"] as? Int 13 | self.highSocDays = dict["KioskModeHighSocDays"] as? Int 14 | self.lastHighSocHours = dict["KioskModeLastHighSocHours"] as? Int 15 | self.mode = dict["KioskModeMode"] as? Int 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/RAWData/LifetimeData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // 电池生命周期内数据 4 | struct LifetimeData { 5 | var averageTemperature: Int? // 平均温度 6 | var maximumTemperature: Int? // 最高温度 7 | var minimumTemperature: Int? // 最低温度 8 | var cycleCountLastQmax: Int? // 9 | var maximumChargeCurrent: Int? // 最大充电电流 10 | var maximumDischargeCurrent: Int? // 最大放电电流 11 | var maximumPackVoltage: Int? // 电池组最高电压 12 | var minimumPackVoltage: Int? // 电池组最低电压 13 | var maximumQmax: Int? // 最大QMax 14 | var minimumQmax: Int? // 最小QMax 15 | var totalOperatingTime: Int? // 总工作时间 单位推测是小时 16 | } 17 | 18 | extension LifetimeData { 19 | init(dict: [String: Any]) { 20 | self.averageTemperature = dict["AverageTemperature"] as? Int 21 | self.maximumTemperature = dict["MaximumTemperature"] as? Int 22 | self.minimumTemperature = dict["MinimumTemperature"] as? Int 23 | self.cycleCountLastQmax = dict["CycleCountLastQmax"] as? Int 24 | self.maximumChargeCurrent = dict["MaximumChargeCurrent"] as? Int 25 | self.maximumDischargeCurrent = dict["MaximumDischargeCurrent"] as? Int 26 | self.maximumPackVoltage = dict["MaximumPackVoltage"] as? Int 27 | self.minimumPackVoltage = dict["MinimumPackVoltage"] as? Int 28 | self.maximumQmax = dict["MaximumQmax"] as? Int 29 | self.minimumQmax = dict["MinimumQmax"] as? Int 30 | self.totalOperatingTime = dict["TotalOperatingTime"] as? Int 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BatteryInfo/Entity/SettingsBatteryData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// 设置中的电池健康数据 4 | struct SettingsBatteryData: Codable { 5 | var cycleCount: Int? 6 | var maximumCapacityPercent: Int? 7 | 8 | // 自定义 key 对应 JSON 字段 9 | enum CodingKeys: String, CodingKey { 10 | case cycleCount = "CycleCount" 11 | case maximumCapacityPercent = "Maximum Capacity Percent" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BatteryInfo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BatteryInfo/InfoPlist.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "CFBundleDisplayName" : { 5 | "extractionState" : "manual", 6 | "localizations" : { 7 | "en" : { 8 | "stringUnit" : { 9 | "state" : "translated", 10 | "value" : "Battery Info" 11 | } 12 | }, 13 | "zh-Hans" : { 14 | "stringUnit" : { 15 | "state" : "translated", 16 | "value" : "电池数据" 17 | } 18 | }, 19 | "es-ES" : { 20 | "stringUnit" : { 21 | "state" : "translated", 22 | "value" : "Info Batería" 23 | } 24 | } 25 | } 26 | }, 27 | "CFBundleName" : { 28 | "comment" : "Bundle name", 29 | "extractionState" : "extracted_with_value", 30 | "localizations" : { 31 | "en" : { 32 | "stringUnit" : { 33 | "state" : "new", 34 | "value" : "BatteryInfo" 35 | } 36 | }, 37 | "zh-Hans" : { 38 | "stringUnit" : { 39 | "state" : "translated", 40 | "value" : "电池数据" 41 | } 42 | }, 43 | "es-ES" : { 44 | "stringUnit" : { 45 | "state" : "translated", 46 | "value" : "InfoBateria" 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "version" : "1.0" 53 | } 54 | -------------------------------------------------------------------------------- /BatteryInfo/Protocol/BatteryDataProviderProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol BatteryDataProviderProtocol { 4 | // 数据提供者的名称 5 | var providerName: String { get } 6 | // 获取是否包含设置中的电池健康信息 7 | var isIncludeSettingsBatteryInfo: Bool { get } 8 | // 获取电池原始数据 9 | func fetchBatteryRAWInfo() -> [String: Any]? 10 | // 获取电池格式化后的数据 11 | func fetchBatteryInfo() -> BatteryRAWInfo? 12 | } 13 | -------------------------------------------------------------------------------- /BatteryInfo/Provider/IOKitBatteryDataProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class IOKitBatteryDataProvider: BatteryDataProviderProtocol { 4 | 5 | 6 | // 注册数据来源 7 | let providerName: String = NSLocalizedString("ProviderIOKit", comment: "") 8 | 9 | // 获取是否包含设置中的电池健康信息 10 | let isIncludeSettingsBatteryInfo: Bool = true 11 | 12 | // 获取电池数据的原始数据 13 | func fetchBatteryRAWInfo() -> [String : Any]? { 14 | return getBatteryInfo() as? [String: Any] 15 | } 16 | 17 | // 获取数据来源 18 | func fetchBatteryInfo() -> BatteryRAWInfo? { 19 | guard let batteryInfoDict = fetchBatteryRAWInfo() else { 20 | print("Failed to fetch IOKit battery info") 21 | return nil 22 | } 23 | // 返回数据 24 | return BatteryRAWInfo(dict: batteryInfoDict) 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /BatteryInfo/Utils/BatteryFormatUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class BatteryFormatUtils { 4 | 5 | /// 通过配置文件来格式化显示电池的健康度 6 | static func getFormatMaximumCapacity(nominalChargeCapacity: Int, designCapacity: Int, accuracy: SettingsUtils.MaximumCapacityAccuracy) -> String { 7 | let rawValue = Double(nominalChargeCapacity) / Double(designCapacity) * 100.0 8 | 9 | switch accuracy { 10 | case .Keep: 11 | return String(Double(String(format: "%.2f", rawValue)) ?? rawValue) // 保留两位小数 12 | case .Ceiling: 13 | return String(Int(ceil(rawValue))) // 直接进1,解决用户强迫症问题 [Doge] 14 | case .Round: 15 | return String(Int(round(rawValue))) // 四舍五入 16 | case .Floor: 17 | return String(Int(floor(rawValue))) // 直接去掉小数 18 | } 19 | } 20 | 21 | /// 比较是否是同一天 22 | static func isSameDay(timestamp1: Int, timestamp2: Int) -> Bool { 23 | let date1 = Date(timeIntervalSince1970: TimeInterval(timestamp1)) 24 | let date2 = Date(timeIntervalSince1970: TimeInterval(timestamp2)) 25 | 26 | return Calendar.current.isDate(date1, inSameDayAs: date2) 27 | } 28 | 29 | /// 格式化时间 30 | static func formatTimestamp(_ timestamp: Int) -> String { 31 | let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) // 时间戳转换为 Date 32 | let formatter = DateFormatter() 33 | formatter.dateStyle = .medium // 按用户地区自动适配年月日格式 34 | formatter.timeStyle = .short // 按用户地区自动适配时分格式 35 | formatter.locale = Locale.autoupdatingCurrent // 自动适配用户的地区和语言 36 | 37 | return formatter.string(from: date) 38 | } 39 | 40 | /// 格式化为仅包含年月日的字符串 41 | static func formatDateOnly(_ timestamp: Int) -> String { 42 | let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) // 时间戳转换为 Date 43 | let formatter = DateFormatter() 44 | formatter.dateStyle = .medium // 显示年月日,自动适配用户地区 45 | formatter.timeStyle = .none // 不显示时间 46 | formatter.locale = Locale.autoupdatingCurrent // 自动适配用户的地区和语言 47 | 48 | return formatter.string(from: date) 49 | } 50 | 51 | /// 给序列号打上*号* 52 | static func maskSerialNumber(_ serial: String) -> String { 53 | if serial.contains("UNKNOWN") { // 序列号是UNKNOWN就直接返回 54 | return serial 55 | } 56 | 57 | guard serial.count >= 5 else { 58 | return serial // 如果长度小于 5,则直接返回 59 | } 60 | 61 | let prefix = serial.prefix(5) // 获取前 5 位 62 | let maskedPart = String(repeating: "*", count: serial.count - 5) // 剩余部分用 * 替代 63 | 64 | return prefix + maskedPart 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /BatteryInfo/Utils/PlistManagerUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class PlistManagerUtils { 4 | 5 | // 单例实例 6 | private static var instances: [String: PlistManagerUtils] = [:] 7 | 8 | private var plistFileURL: URL 9 | private var plistExist: Bool = false 10 | private var preferences: [String: Any] 11 | private var cachedChanges: [String: Any] = [:] 12 | private var isDirty = false 13 | 14 | // 获取实例方法(支持多实例) 15 | static func instance(for plistName: String) -> PlistManagerUtils { 16 | // 如果实例已存在,则返回现有实例 17 | if let instance = instances[plistName] { 18 | return instance 19 | } 20 | 21 | // 否则创建新的实例并存储 22 | let instance = PlistManagerUtils(plistName: plistName) 23 | instances[plistName] = instance 24 | return instance 25 | } 26 | 27 | // 初始化 PlistManager,指定 plist 文件名 28 | private init(plistName: String) { 29 | // 获取沙盒中的 Preferences 目录路径 30 | let preferencesDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first! 31 | let preferencesPath = preferencesDirectory.appendingPathComponent("Preferences") 32 | self.plistFileURL = preferencesPath.appendingPathComponent("\(plistName).plist") 33 | 34 | // 如果 plist 文件不存在,则创建一个空的文件 35 | if !FileManager.default.fileExists(atPath: plistFileURL.path) { 36 | preferences = [:] 37 | save() 38 | plistExist = false //增加一个标识,让外界知道这个配置文件是新创建的 39 | } else { 40 | self.preferences = PlistManagerUtils.loadPreferences(from: plistFileURL) 41 | plistExist = true 42 | } 43 | } 44 | 45 | // 加载 plist 文件中的数据 46 | private static func loadPreferences(from url: URL) -> [String: Any] { 47 | guard let data = try? Data(contentsOf: url), 48 | let preferences = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] else { 49 | return [:] 50 | } 51 | return preferences 52 | } 53 | 54 | // 保存数据到 plist 文件 55 | private func save() { 56 | do { 57 | let data = try PropertyListSerialization.data(fromPropertyList: preferences, format: .xml, options: 0) 58 | try data.write(to: plistFileURL) 59 | } catch { 60 | print("Error saving preferences to plist: \(error.localizedDescription)") 61 | } 62 | } 63 | 64 | 65 | // 获取 plist 文件是否存在 66 | func isPlistExist() -> Bool { 67 | return plistExist 68 | } 69 | 70 | // 获取指定 key 对应的 Int 值 71 | func getInt(key: String, defaultValue: Int) -> Int { 72 | return preferences[key] as? Int ?? defaultValue 73 | } 74 | 75 | // 获取指定 key 对应的 Bool 值 76 | func getBool(key: String, defaultValue: Bool) -> Bool { 77 | return preferences[key] as? Bool ?? defaultValue 78 | } 79 | 80 | // 获取指定 key 对应的 String 值 81 | func getString(key: String, defaultValue: String) -> String { 82 | return preferences[key] as? String ?? defaultValue 83 | } 84 | 85 | // 获取指定 key 对应的 Float 值 86 | func getFloat(key: String, defaultValue: Float) -> Float { 87 | return preferences[key] as? Float ?? defaultValue 88 | } 89 | 90 | // 获取指定 key 对应的 Double 值 91 | func getDouble(key: String, defaultValue: Double) -> Double { 92 | return preferences[key] as? Double ?? defaultValue 93 | } 94 | 95 | // 获取指定 key 对应的 Data 值 96 | func getData(key: String, defaultValue: Data) -> Data { 97 | return preferences[key] as? Data ?? defaultValue 98 | } 99 | 100 | // 获取指定 key 对应的 URL 值 101 | func getURL(key: String, defaultValue: URL) -> URL { 102 | return preferences[key] as? URL ?? defaultValue 103 | } 104 | 105 | // 获取指定 key 对应的 Array 值 106 | func getArray(key: String, defaultValue: [Any]) -> [Any] { 107 | return preferences[key] as? [Any] ?? defaultValue 108 | } 109 | 110 | // 获取指定 key 对应的 Dictionary 值 111 | func getDictionary(key: String, defaultValue: [String: Any]) -> [String: Any] { 112 | return preferences[key] as? [String: Any] ?? defaultValue 113 | } 114 | 115 | // 设置 Int 值 116 | func setInt(key: String, value: Int) { 117 | cachedChanges[key] = value 118 | isDirty = true 119 | } 120 | 121 | // 设置 Bool 值 122 | func setBool(key: String, value: Bool) { 123 | cachedChanges[key] = value 124 | isDirty = true 125 | } 126 | 127 | // 设置 String 值 128 | func setString(key: String, value: String) { 129 | cachedChanges[key] = value 130 | isDirty = true 131 | } 132 | 133 | // 设置 Float 值 134 | func setFloat(key: String, value: Float) { 135 | cachedChanges[key] = value 136 | isDirty = true 137 | } 138 | 139 | // 设置 Double 值 140 | func setDouble(key: String, value: Double) { 141 | cachedChanges[key] = value 142 | isDirty = true 143 | } 144 | 145 | // 设置 Data 值 146 | func setData(key: String, value: Data) { 147 | cachedChanges[key] = value 148 | isDirty = true 149 | } 150 | 151 | // 设置 URL 值 152 | func setURL(key: String, value: URL) { 153 | cachedChanges[key] = value 154 | isDirty = true 155 | } 156 | 157 | // 设置 Array 值 158 | func setArray(key: String, value: [Any]) { 159 | cachedChanges[key] = value 160 | isDirty = true 161 | } 162 | 163 | // 设置 Dictionary 值 164 | func setDictionary(key: String, value: [String: Any]) { 165 | cachedChanges[key] = value 166 | isDirty = true 167 | } 168 | 169 | // 删除指定 key 的数据 170 | func remove(key: String) { 171 | cachedChanges[key] = NSNull() 172 | isDirty = true 173 | } 174 | 175 | // 清除 plist 文件(删除整个文件) 176 | func clear() { 177 | do { 178 | try FileManager.default.removeItem(at: plistFileURL) 179 | preferences = [:] 180 | } catch { 181 | print("Error clearing plist file: \(error.localizedDescription)") 182 | } 183 | } 184 | 185 | func commit() { 186 | self.apply() 187 | } 188 | 189 | // 将更改保存到 plist 190 | func apply() { 191 | // 只有在有修改的情况下才进行保存 192 | if isDirty { 193 | // 将所有更改合并到 preferences 中,覆盖掉已存在的相同键 194 | for (key, value) in cachedChanges { 195 | if value is NSNull { 196 | preferences.removeValue(forKey: key) 197 | } else { 198 | preferences[key] = value 199 | } 200 | } 201 | 202 | // 执行保存操作 203 | save() 204 | 205 | // 重置缓存和脏标记 206 | cachedChanges.removeAll() 207 | isDirty = false 208 | } 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /BatteryInfo/Utils/SettingsUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class SettingsUtils { 4 | 5 | // 单例实例 6 | static let instance = SettingsUtils() 7 | 8 | // 私有的 PlistManagerUtils 实例,用于管理特定的 plist 文件 9 | private let plistManager: PlistManagerUtils 10 | 11 | // 语言设置 12 | enum ApplicationLanguage: Int { 13 | case System = 0 14 | case English = 1 15 | case SimplifiedChinese = 2 16 | } 17 | 18 | enum MaximumCapacityAccuracy: Int { 19 | case Keep = 0 // 保留原始数据 20 | case Ceiling = 1 // 向上取整 21 | case Round = 2 // 四舍五入 22 | case Floor = 3 // 向下取整 23 | } 24 | 25 | enum RecordFrequency: Int { 26 | case Toggle = 0 // 禁用的时候下面的值变成负数,启用的时候是正数 27 | case Automatic = 1 // 自动,每天或者电池剩余容量发生变化或者电池循环次数变化时保存 28 | case DataChanged = 2 // 数据发生改变时记录,电池剩余容量发生变化或者电池循环次数变化时保存 29 | case EveryDay = 3 // 每天打开App的时候记录 30 | case Manual = 4 // 手动 31 | } 32 | 33 | private init() { 34 | // 初始化 35 | self.plistManager = PlistManagerUtils.instance(for: "Settings") 36 | } 37 | 38 | private func setDefaultSettings() { 39 | 40 | if self.plistManager.isPlistExist() { 41 | return 42 | } 43 | 44 | } 45 | 46 | /// 获取App语言设置 47 | func getApplicationLanguage() -> ApplicationLanguage { 48 | let value = plistManager.getInt(key: "ApplicationLanguage", defaultValue: ApplicationLanguage.System.rawValue) 49 | return ApplicationLanguage(rawValue: value) ?? ApplicationLanguage.System 50 | } 51 | 52 | /// 设置App语言设置 53 | func setApplicationLanguage(value: ApplicationLanguage) { 54 | setApplicationLanguage(value: value.rawValue) 55 | } 56 | 57 | /// 设置App语言设置 58 | func setApplicationLanguage(value: Int) { 59 | plistManager.setInt(key: "ApplicationLanguage", value: value) 60 | plistManager.apply() 61 | } 62 | 63 | func getAutoRefreshDataView() -> Bool { 64 | return plistManager.getBool(key: "AutoRefreshDataView", defaultValue: true) 65 | } 66 | 67 | func setAutoRefreshDataView(value: Bool) { 68 | plistManager.setBool(key: "AutoRefreshDataView", value: value) 69 | plistManager.apply() 70 | } 71 | 72 | func getForceShowChargingData() -> Bool { 73 | return plistManager.getBool(key: "ForceShowChargingData", defaultValue: false) 74 | } 75 | 76 | func setForceShowChargingData(value: Bool) { 77 | plistManager.setBool(key: "ForceShowChargingData", value: value) 78 | plistManager.apply() 79 | } 80 | 81 | /// 获取是否显示设置中的电池健康度信息 82 | func getShowSettingsBatteryInfo() -> Bool { 83 | return plistManager.getBool(key: "ShowSettingsBatteryInfo", defaultValue: false) 84 | } 85 | 86 | func setShowSettingsBatteryInfo(value: Bool) { 87 | plistManager.setBool(key: "ShowSettingsBatteryInfo", value: value) 88 | plistManager.apply() 89 | } 90 | 91 | /// 获取是否使用历史记录中的数据推算设置中的电池健康度的刷新日期 92 | func getUseHistoryRecordToCalculateSettingsBatteryInfoRefreshDate() -> Bool { 93 | // 必须开启历史记录功能才能获取 94 | return getEnableRecordBatteryData() && plistManager.getBool(key: "UseHistoryRecordToCalculate", defaultValue: true) 95 | } 96 | 97 | func setUseHistoryRecordToCalculateSettingsBatteryInfoRefreshDate(value: Bool) { 98 | plistManager.setBool(key: "UseHistoryRecordToCalculate", value: value) 99 | plistManager.apply() 100 | } 101 | 102 | /// 获取是否允许双击首页TabBar按钮来让列表滚动到顶部 103 | func getDoubleClickTabBarButtonToScrollToTop() -> Bool { 104 | return plistManager.getBool(key: "DoubleClickTabBarButtonToScrollToTop", defaultValue: true) 105 | } 106 | 107 | func setDoubleClickTabBarButtonToScrollToTop(value: Bool) { 108 | plistManager.setBool(key: "DoubleClickTabBarButtonToScrollToTop", value: value) 109 | plistManager.apply() 110 | } 111 | 112 | /// 获取健康度准确度设置 113 | /// - return 返回选项 默认值向上取整,减少用户对电池健康的焦虑 [Doge] 114 | func getMaximumCapacityAccuracy() -> MaximumCapacityAccuracy { 115 | let value = plistManager.getInt(key: "MaximumCapacityAccuracy", defaultValue: MaximumCapacityAccuracy.Ceiling.rawValue) 116 | return MaximumCapacityAccuracy(rawValue: value) ?? MaximumCapacityAccuracy.Ceiling 117 | } 118 | 119 | /// 设置健康度准确度设置 120 | func setMaximumCapacityAccuracy(value: MaximumCapacityAccuracy) { 121 | setMaximumCapacityAccuracy(value: value.rawValue) 122 | } 123 | 124 | /// 设置健康度准确度设置 125 | func setMaximumCapacityAccuracy(value: Int) { 126 | plistManager.setInt(key: "MaximumCapacityAccuracy", value: value) 127 | plistManager.apply() 128 | } 129 | 130 | private func getRecordFrequencyRawValue() -> Int { 131 | return plistManager.getInt(key: "RecordFrequency", defaultValue: RecordFrequency.Automatic.rawValue) 132 | } 133 | 134 | func getEnableRecordBatteryData() -> Bool { 135 | return getRecordFrequencyRawValue() > 0 136 | } 137 | 138 | // 获取在主界面显示历史记录界面的设置 139 | func getShowHistoryRecordViewInHomeView() -> Bool { 140 | return plistManager.getBool(key: "ShowHistoryRecordViewInHomeView", defaultValue: true) 141 | } 142 | 143 | func setShowHistoryRecordViewInHomeView(value: Bool) { 144 | plistManager.setBool(key: "ShowHistoryRecordViewInHomeView", value: value) 145 | plistManager.apply() 146 | } 147 | 148 | // 获取是否在历史记录中显示设计容量 149 | func getRecordShowDesignCapacity() -> Bool { 150 | return plistManager.getBool(key: "RecordShowDesignCapacity", defaultValue: true) 151 | } 152 | 153 | func setRecordShowDesignCapacity(value: Bool) { 154 | plistManager.setBool(key: "RecordShowDesignCapacity", value: value) 155 | plistManager.apply() 156 | } 157 | 158 | // 获取启用历史数据统计功能 159 | func getEnableHistoryStatistics() -> Bool { 160 | return plistManager.getBool(key: "EnableHistoryStatistics", defaultValue: true) 161 | } 162 | 163 | func setEnableHistoryStatistics(value: Bool) { 164 | plistManager.setBool(key: "EnableHistoryStatistics", value: value) 165 | plistManager.apply() 166 | } 167 | 168 | /// 获取记录电池记录频率设置 169 | func getRecordFrequency() -> RecordFrequency { 170 | var value = getRecordFrequencyRawValue() 171 | if value == 0 { 172 | return .Automatic 173 | } 174 | if value < 0 { // 判断下是不是关闭记录了 175 | value = -value 176 | } 177 | return RecordFrequency(rawValue: value) ?? RecordFrequency.Automatic 178 | } 179 | 180 | /// 设置记录电池记录频率设置 181 | func setRecordFrequency(value: RecordFrequency) { 182 | setRecordFrequency(value: value.rawValue) 183 | } 184 | 185 | /// 设置记录电池记录频率设置 186 | func setRecordFrequency(value: Int) { 187 | let originalValue = getRecordFrequencyRawValue() // 获取原始值 188 | var changedValue = value 189 | if changedValue > 0 { // 已启用 190 | if originalValue < 0 { // 如果小于0就是禁用状态下,但是更改了记录频率 191 | changedValue = -changedValue 192 | } 193 | } else { // = 0 就是切换状态,因为提供的参数不可能小于0 194 | changedValue = -originalValue 195 | } 196 | // 保存数据 197 | plistManager.setInt(key: "RecordFrequency", value: changedValue) 198 | plistManager.apply() 199 | } 200 | 201 | // 获取首页显示的信息组的顺序 202 | func getHomeItemGroupSequence() -> [Int] { 203 | let raw = plistManager.getArray(key: "HomeItemGroupSequence", defaultValue: []) 204 | let intArray = raw.compactMap { $0 as? Int } 205 | 206 | // 默认顺序(用 rawValue 返回 Int) 207 | let defaultSequence = [ 208 | BatteryInfoGroupID.basic.rawValue, 209 | BatteryInfoGroupID.charge.rawValue, 210 | BatteryInfoGroupID.settingsBatteryInfo.rawValue 211 | ] 212 | 213 | // 如果为空,直接返回默认顺序 214 | if intArray.isEmpty { 215 | return defaultSequence 216 | } 217 | 218 | // 如果有重复或非法项,则清除设置并返回默认 219 | if Set(intArray).count != intArray.count { 220 | plistManager.setArray(key: "HomeItemGroupSequence", value: []) 221 | plistManager.apply() 222 | return defaultSequence 223 | } 224 | 225 | return intArray 226 | } 227 | 228 | // 保存首页显示的信息组的顺序 229 | func setHomeItemGroupSequence(_ sequence: [Int]) { 230 | let set = Set(sequence) 231 | // 必须非空且无重复项 232 | guard !sequence.isEmpty, set.count == sequence.count else { 233 | return 234 | } 235 | plistManager.setArray(key: "HomeItemGroupSequence", value: sequence) 236 | plistManager.apply() 237 | } 238 | 239 | // 重设首页显示的信息组顺序 240 | func resetHomeItemGroupSequence() { 241 | plistManager.remove(key: "HomeItemGroupSequence") 242 | plistManager.apply() 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /BatteryInfo/Utils/SystemInfoUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class SystemInfoUtils { 5 | 6 | static func getDeviceModel() -> String { 7 | var systemInfo = utsname() 8 | uname(&systemInfo) 9 | let machineMirror = Mirror(reflecting: systemInfo.machine) 10 | let identifier = machineMirror.children.reduce("") { identifier, element in 11 | guard let value = element.value as? Int8, value != 0 else { return identifier } 12 | return identifier + String(UnicodeScalar(UInt8(value))) 13 | } 14 | return identifier 15 | } 16 | 17 | static func isRunningOniPadOS() -> Bool { 18 | // 判断设备是否为 iPad 19 | if UIDevice.current.userInterfaceIdiom == .pad { 20 | // 判断系统版本是否大于等于 13.0 21 | if #available(iOS 13.0, *) { 22 | return true 23 | } 24 | } 25 | return false 26 | } 27 | 28 | static func getSystemBuildVersion() -> String? { 29 | var size = 0 30 | sysctlbyname("kern.osversion", nil, &size, nil, 0) 31 | 32 | var build = [CChar](repeating: 0, count: size) 33 | sysctlbyname("kern.osversion", &build, &size, nil, 0) 34 | 35 | return String(cString: build) 36 | } 37 | 38 | static func getDeviceUptime() -> String { 39 | let uptimeInSeconds = Int(ProcessInfo.processInfo.systemUptime) 40 | 41 | let days = Int(Double(uptimeInSeconds / (24 * 3600))) // 计算天数 42 | let hours = Int(Double((uptimeInSeconds % (24 * 3600)) / 3600)) // 计算小时数 43 | let minutes = Int(Double((uptimeInSeconds % 3600) / 60)) // 计算分钟数 44 | 45 | return String.localizedStringWithFormat(NSLocalizedString("DeviceUptime", comment: ""),days, hours, minutes) 46 | } 47 | 48 | static func getDeviceUptimeUsingSysctl() -> String { 49 | var tv = timeval() 50 | var size = MemoryLayout.stride 51 | var mib: [Int32] = [CTL_KERN, KERN_BOOTTIME] 52 | 53 | // **使用 withUnsafeMutablePointer 解决指针转换问题** 54 | _ = mib.withUnsafeMutableBufferPointer { mibPointer -> Bool in 55 | guard let baseAddress = mibPointer.baseAddress else { return false } 56 | return sysctl(baseAddress, 2, &tv, &size, nil, 0) == 0 57 | } 58 | 59 | // 计算设备已运行的秒数 60 | let bootTime = Date(timeIntervalSince1970: TimeInterval(tv.tv_sec)) 61 | let uptimeInSeconds = Int(Date().timeIntervalSince(bootTime)) 62 | 63 | // **计算天、小时、分钟** 64 | let days = uptimeInSeconds / (24 * 3600) 65 | let hours = (uptimeInSeconds % (24 * 3600)) / 3600 66 | let minutes = (uptimeInSeconds % 3600) / 60 67 | 68 | // **格式化字符串** 69 | return String.localizedStringWithFormat( 70 | NSLocalizedString("DeviceUptime", comment: "设备已运行时间"), 71 | days, hours, minutes 72 | ) 73 | } 74 | 75 | // 设备是否正在充电,非Root设备也可以用 76 | static func isDeviceCharging() -> Bool { 77 | UIDevice.current.isBatteryMonitoringEnabled = true 78 | return UIDevice.current.batteryState == .charging || UIDevice.current.batteryState == .full 79 | } 80 | 81 | /// 获取当前设备的充电状态 82 | static func getBatteryState() -> UIDevice.BatteryState { 83 | UIDevice.current.isBatteryMonitoringEnabled = true 84 | return UIDevice.current.batteryState 85 | } 86 | 87 | // 获取电量百分比,非Root设备也可以用 88 | static func getBatteryPercentage() -> Int? { 89 | UIDevice.current.isBatteryMonitoringEnabled = true 90 | let batteryLevel = UIDevice.current.batteryLevel 91 | 92 | if batteryLevel < 0 { 93 | return nil // -1 表示无法获取电池电量 94 | } else { 95 | return Int(batteryLevel * 100) // 转换为百分比 96 | } 97 | } 98 | 99 | // 获取设备总容量 100 | static func getTotalDiskSpace() -> Int64 { 101 | if let attributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()), 102 | let totalSize = attributes[.systemSize] as? Int64 { 103 | return totalSize 104 | } 105 | return 0 106 | } 107 | 108 | // 获取设备总容量的近似 109 | static func getDiskTotalSpace() -> String { 110 | let totalSize = getTotalDiskSpace() 111 | let sizeInGB = Double(totalSize) / 1_000_000_000 // 转换为GB(基于10^9) 112 | 113 | // iOS 设备常见存储规格(按官方设备容量) 114 | let storageSizes: [Double] = [16, 32, 64, 128, 256, 512, 1024, 2048] // 单位 GB 115 | 116 | // 找到最接近的存储规格 117 | let closestSize = storageSizes.min(by: { abs($0 - sizeInGB) < abs($1 - sizeInGB) }) ?? sizeInGB 118 | 119 | return closestSize >= 1024 ? "\(Int(closestSize / 1024)) TB" : "\(Int(closestSize)) GB" 120 | } 121 | 122 | // 获取设备的型号代码 123 | static func getDeviceRegionCode() -> String? { 124 | let path = "/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist" 125 | 126 | // 读取 plist 文件 127 | guard let dict = NSDictionary(contentsOfFile: path) as? [String: Any] else { 128 | NSLog("Battery Info----> 无法加载 MobileGestalt.plist") 129 | return nil 130 | } 131 | 132 | // 获取 `CacheExtra` 字典 133 | guard let cacheExtra = dict["CacheExtra"] as? [String: Any] else { 134 | NSLog("Battery Info----> MobileGestalt无法找到 `CacheExtra` 字典") 135 | return nil 136 | } 137 | 138 | // 先尝试直接获取 `zHeENZu+wbg7PUprwNwBWg` 139 | if let regionCode = cacheExtra["zHeENZu+wbg7PUprwNwBWg"] as? String { 140 | if isValidRegionCode(regionCode) { 141 | return regionCode 142 | } 143 | } 144 | 145 | // 遍历 `CacheExtra` 并匹配设备型号代码格式 146 | for (key, value) in cacheExtra { 147 | if let regionCode = value as? String, isValidRegionCode(regionCode) { 148 | NSLog("Battery Info----> MobileGestalt未找到默认 key,使用遍历找到的 key: \(key) -> \(regionCode)") 149 | return regionCode 150 | } 151 | } 152 | return nil 153 | } 154 | 155 | // 匹配设备发售型号格式 156 | private static func isValidRegionCode(_ code: String) -> Bool { 157 | let pattern = "^[A-Z]{2}/A$" // 匹配 XX/A 格式,例如 CH/A、LL/A、ZA/A 158 | return code.range(of: pattern, options: .regularExpression) != nil 159 | } 160 | 161 | static func getDeviceName() -> String { 162 | switch getDeviceModel() { 163 | 164 | case "iPhone1,1": return "iPhone" 165 | case "iPhone1,2": return "iPhone 3G" 166 | case "iPhone2,1": return "iPhone 3GS" 167 | case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4" 168 | case "iPhone4,1": return "iPhone 4S" 169 | case "iPhone5,1": return "iPhone 5 (GSM)" 170 | case "iPhone5,2": return "iPhone 5 (GSM+CDMA)" 171 | case "iPhone5,3": return "iPhone 5C (GSM)" 172 | case "iPhone5,4": return "iPhone 5C (Global)" 173 | case "iPhone6,1": return "iPhone 5S (GSM)" 174 | case "iPhone6,2": return "iPhone 5S (Global)" 175 | case "iPhone7,1": return "iPhone 6 Plus" 176 | case "iPhone7,2": return "iPhone 6" 177 | case "iPhone8,1": return "iPhone 6s" 178 | case "iPhone8,2": return "iPhone 6s Plus" 179 | case "iPhone8,4": return "iPhone SE (1st Gen)" 180 | case "iPhone9,1", "iPhone9,3": return "iPhone 7" 181 | case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus" 182 | case "iPhone10,1", "iPhone10,4": return "iPhone 8" 183 | case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus" 184 | 185 | case "iPhone10,3", "iPhone10,6": return "iPhone X" 186 | case "iPhone11,2": return "iPhone XS" 187 | case "iPhone11,4", "iPhone11,6": return "iPhone XS Max" 188 | case "iPhone11,8": return "iPhone XR" 189 | case "iPhone12,1": return "iPhone 11" 190 | case "iPhone12,3": return "iPhone 11 Pro" 191 | case "iPhone12,5": return "iPhone 11 Pro Max" 192 | case "iPhone12,8": return "iPhone SE (2nd Gen)" 193 | case "iPhone13,1": return "iPhone 12 mini" 194 | case "iPhone13,2": return "iPhone 12" 195 | case "iPhone13,3": return "iPhone 12 Pro" 196 | case "iPhone13,4": return "iPhone 12 Pro Max" 197 | case "iPhone14,2": return "iPhone 13 Pro" 198 | case "iPhone14,3": return "iPhone 13 Pro Max" 199 | case "iPhone14,4": return "iPhone 13 mini" 200 | case "iPhone14,5": return "iPhone 13" 201 | case "iPhone14,6": return "iPhone SE (3rd Gen)" 202 | case "iPhone14,7": return "iPhone 14" 203 | case "iPhone14,8": return "iPhone 14 Plus" 204 | case "iPhone15,2": return "iPhone 14 Pro" 205 | case "iPhone15,3": return "iPhone 14 Pro Max" 206 | case "iPhone15,4": return "iPhone 15" 207 | case "iPhone15,5": return "iPhone 15 Plus" 208 | case "iPhone16,1": return "iPhone 15 Pro" 209 | case "iPhone16,2": return "iPhone 15 Pro Max" 210 | case "iPhone17,1": return "iPhone 16 Pro" 211 | case "iPhone17,2": return "iPhone 16 Pro Max" 212 | case "iPhone17,3": return "iPhone 16" 213 | case "iPhone17,4": return "iPhone 16 Plus" 214 | case "iPhone17,5": return "iPhone 16e" // 新2025.2.19 新增iPhone 16e 215 | 216 | // iPod 217 | case "iPod1,1": return "iPod Touch (1st Gen)" 218 | case "iPod2,1": return "iPod Touch (2nd Gen)" 219 | case "iPod3,1": return "iPod Touch (3rd Gen)" 220 | case "iPod4,1": return "iPod Touch (4th Gen)" 221 | case "iPod5,1": return "iPod Touch (5th Gen)" 222 | case "iPod7,1": return "iPod Touch (6th Gen)" 223 | case "iPod9,1": return "iPod Touch (7th Gen)" 224 | 225 | // iPad 226 | case "iPad1,1": return "iPad (1st Gen)" 227 | case "iPad1,2": return "iPad (1st Gen, 3G)" 228 | case "iPad2,1": return "iPad 2 (WiFi)" 229 | case "iPad2,2": return "iPad 2 (GSM)" 230 | case "iPad2,3": return "iPad 2 (CDMA)" 231 | case "iPad2,4": return "iPad 2 (Rev A)" 232 | case "iPad2,5": return "iPad Mini (1st Gen)" 233 | case "iPad2,6": return "iPad Mini (1st Gen, GSM+LTE)" 234 | case "iPad2,7": return "iPad Mini (1st Gen, CDMA+LTE)" 235 | case "iPad3,1": return "iPad (3rd Gen, WiFi)" 236 | case "iPad3,2": return "iPad (3rd Gen, CDMA)" 237 | case "iPad3,3": return "iPad (3rd Gen, GSM)" 238 | case "iPad3,4": return "iPad (4th Gen, WiFi)" 239 | case "iPad3,5": return "iPad (4th Gen, GSM+LTE)" 240 | case "iPad3,6": return "iPad (4th Gen, CDMA+LTE)" 241 | case "iPad4,1": return "iPad Air (WiFi)" 242 | case "iPad4,2": return "iPad Air (GSM+CDMA)" 243 | case "iPad4,3": return "iPad Air (China)" 244 | case "iPad4,4": return "iPad Mini 2 (WiFi)" 245 | case "iPad4,5": return "iPad Mini 2 (GSM+CDMA)" 246 | case "iPad4,6": return "iPad Mini 2 (China)" 247 | case "iPad4,7": return "iPad Mini 3 (WiFi)" 248 | case "iPad4,8": return "iPad Mini 3 (GSM+CDMA)" 249 | case "iPad4,9": return "iPad Mini 3 (China)" 250 | case "iPad5,1": return "iPad Mini 4 (WiFi)" 251 | case "iPad5,2": return "iPad Mini 4 (WiFi+Cellular)" 252 | case "iPad5,3": return "iPad Air 2 (WiFi)" 253 | case "iPad5,4": return "iPad Air 2 (Cellular)" 254 | case "iPad6,3": return "iPad Pro (9.7 inch, WiFi)" 255 | case "iPad6,4": return "iPad Pro (9.7 inch, WiFi+LTE)" 256 | case "iPad6,7": return "iPad Pro (12.9 inch, WiFi)" 257 | case "iPad6,8": return "iPad Pro (12.9 inch, WiFi+LTE)" 258 | case "iPad6,11": return "iPad (5th Gen, WiFi)" 259 | case "iPad6,12": return "iPad (5th Gen, WiFi+Cellular)" 260 | case "iPad7,1": return "iPad Pro 2nd Gen (12.9 inch, WiFi)" 261 | case "iPad7,2": return "iPad Pro 2nd Gen (12.9 inch, WiFi+Cellular)" 262 | case "iPad7,3": return "iPad Pro 10.5-inch (WiFi)" 263 | case "iPad7,4": return "iPad Pro 10.5-inch (WiFi+Cellular)" 264 | case "iPad7,5": return "iPad (6th Gen, WiFi)" 265 | case "iPad7,6": return "iPad (6th Gen, WiFi+Cellular)" 266 | case "iPad7,11": return "iPad (7th Gen, 10.2 inch, WiFi)" 267 | case "iPad7,12": return "iPad (7th Gen, 10.2 inch, WiFi+Cellular)" 268 | case "iPad8,1": return "iPad Pro 11 inch (3rd Gen, WiFi)" 269 | case "iPad8,2": return "iPad Pro 11 inch (3rd Gen, 1TB, WiFi)" 270 | case "iPad8,3": return "iPad Pro 11 inch (3rd Gen, WiFi+Cellular)" 271 | case "iPad8,4": return "iPad Pro 11 inch (3rd Gen, 1TB, WiFi+Cellular)" 272 | case "iPad8,5": return "iPad Pro 12.9 inch (3rd Gen, WiFi)" 273 | case "iPad8,6": return "iPad Pro 12.9 inch (3rd Gen, 1TB, WiFi)" 274 | case "iPad8,7": return "iPad Pro 12.9 inch (3rd Gen, WiFi+Cellular)" 275 | case "iPad8,8": return "iPad Pro 12.9 inch (3rd Gen, 1TB, WiFi+Cellular)" 276 | case "iPad8,9": return "iPad Pro 11 inch (4th Gen, WiFi)" 277 | case "iPad8,10": return "iPad Pro 11 inch (4th Gen, WiFi+Cellular)" 278 | case "iPad8,11": return "iPad Pro 12.9 inch (4th Gen, WiFi)" 279 | case "iPad8,12": return "iPad Pro 12.9 inch (4th Gen, WiFi+Cellular)" 280 | case "iPad11,1": return "iPad Mini (5th Gen, WiFi)" 281 | case "iPad11,2": return "iPad Mini (5th Gen, WiFi+Cellular)" 282 | case "iPad11,3": return "iPad Air (3rd Gen, WiFi)" 283 | case "iPad11,4": return "iPad Air (3rd Gen, WiFi+Cellular)" 284 | case "iPad11,6": return "iPad (8th Gen, WiFi)" 285 | case "iPad11,7": return "iPad (8th Gen, WiFi+Cellular)" 286 | case "iPad12,1": return "iPad (9th Gen, WiFi)" 287 | case "iPad12,2": return "iPad (9th Gen, WiFi+Cellular)" 288 | case "iPad13,1": return "iPad Air (4th Gen, WiFi)" 289 | case "iPad13,2": return "iPad Air (4th Gen, WiFi+Cellular)" 290 | case "iPad13,4": return "iPad Pro 11 inch (5th Gen)" 291 | case "iPad13,5": return "iPad Pro 11 inch (5th Gen)" 292 | case "iPad13,6": return "iPad Pro 11 inch (5th Gen)" 293 | case "iPad13,7": return "iPad Pro 11 inch (5th Gen)" 294 | case "iPad13,8": return "iPad Pro 12.9 inch (5th Gen)" 295 | case "iPad13,9": return "iPad Pro 12.9 inch (5th Gen)" 296 | case "iPad13,10": return "iPad Pro 12.9 inch (5th Gen)" 297 | case "iPad13,11": return "iPad Pro 12.9 inch (5th Gen)" 298 | case "iPad13,16": return "iPad Air (5th Gen, WiFi)" 299 | case "iPad13,17": return "iPad Air (5th Gen, WiFi+Cellular)" 300 | case "iPad13,18": return "iPad (10th Gen)" 301 | case "iPad13,19": return "iPad (10th Gen)" 302 | case "iPad14,1": return "iPad Mini (6th Gen, WiFi)" 303 | case "iPad14,2": return "iPad Mini (6th Gen, WiFi+Cellular)" 304 | case "iPad14,3": return "iPad Pro 11 inch (4th Gen)" 305 | case "iPad14,4": return "iPad Pro 11 inch (4th Gen)" 306 | case "iPad14,5": return "iPad Pro 12.9 inch (6th Gen)" 307 | case "iPad14,6": return "iPad Pro 12.9 inch (6th Gen)" 308 | case "iPad14,8": return "iPad Air (6th Gen)" 309 | case "iPad14,9": return "iPad Air (6th Gen)" 310 | case "iPad14,10": return "iPad Air (7th Gen)" 311 | case "iPad14,11": return "iPad Air (7th Gen)" 312 | case "iPad16,1": return "iPad Mini (7th Gen, WiFi)" 313 | case "iPad16,2": return "iPad Mini (7th Gen, WiFi+Cellular)" 314 | case "iPad16,3": return "iPad Pro 11 inch (5th Gen)" 315 | case "iPad16,4": return "iPad Pro 11 inch (5th Gen)" 316 | case "iPad16,5": return "iPad Pro 12.9 inch (7th Gen)" 317 | case "iPad16,6": return "iPad Pro 12.9 inch (7th Gen)" 318 | 319 | case "arm64": return "Simulator (arm64)" 320 | 321 | // 未知设备 322 | default: return getDeviceModel() 323 | } 324 | 325 | } 326 | 327 | /// 解析电池序列号,返回供应商名称 328 | /// - Parameter serialNumber: 电池的序列号 329 | /// - Returns: 供应商名称, 如果未知则返回 "Unknown" 330 | static func getBatteryManufacturer(from serialNumber: String) -> String { 331 | // 定义序列号前缀与供应商的映射表 332 | let manufacturerMapping: [String: String] = [ 333 | "F8Y": NSLocalizedString("Sunwoda", tableName: "BatteryManufacturer", comment: "欣旺达"), 334 | "SWD": NSLocalizedString("Sunwoda", tableName: "BatteryManufacturer", comment: "欣旺达"), 335 | "F5D": NSLocalizedString("Desay", tableName: "BatteryManufacturer", comment: "德赛"), 336 | "DTP": NSLocalizedString("Desay", tableName: "BatteryManufacturer", comment: "德赛"), 337 | "DSY": NSLocalizedString("Desay", tableName: "BatteryManufacturer", comment: "德赛"), 338 | "FG9": NSLocalizedString("Simplo", tableName: "BatteryManufacturer", comment: "新普"), 339 | "SMP": NSLocalizedString("Simplo", tableName: "BatteryManufacturer", comment: "新普"), 340 | "ATL": NSLocalizedString("ATL", tableName: "BatteryManufacturer", comment: "ATL"), 341 | "LGC": NSLocalizedString("LG", tableName: "BatteryManufacturer", comment: "LG"), 342 | "SON": NSLocalizedString("Sony", tableName: "BatteryManufacturer", comment: "索尼"), 343 | ] 344 | 345 | // 获取序列号前三个字符作为前缀 346 | let prefix = String(serialNumber.prefix(3)) 347 | 348 | // 返回供应商名称, 如果找不到匹配项,则返回未知 349 | return manufacturerMapping[prefix] ?? "Unknown" 350 | } 351 | } 352 | 353 | 354 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/AllBatteryDataViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class AllBatteryDataViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 5 | 6 | private var tableView = UITableView() 7 | 8 | private var batteryInfoGroups: [InfoItemGroup] = [] 9 | 10 | private var refreshTimer: Timer? 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | if #available(iOS 13.0, *) { 16 | view.backgroundColor = .systemBackground 17 | } else { 18 | // Fallback on earlier versions 19 | view.backgroundColor = .white 20 | } 21 | 22 | title = NSLocalizedString("AllData", comment: "") 23 | 24 | // iOS 15 之后的版本使用新的UITableView样式 25 | if #available(iOS 15.0, *) { 26 | tableView = UITableView(frame: .zero, style: .insetGrouped) 27 | } else { 28 | tableView = UITableView(frame: .zero, style: .grouped) 29 | } 30 | 31 | // 设置表格视图的代理和数据源 32 | tableView.delegate = self 33 | tableView.dataSource = self 34 | 35 | // 注册表格单元格 36 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 37 | 38 | // 将表格视图添加到主视图 39 | view.addSubview(tableView) 40 | 41 | // 设置表格视图的布局 42 | tableView.translatesAutoresizingMaskIntoConstraints = false 43 | NSLayoutConstraint.activate([ 44 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 45 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor), 46 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor), 47 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 48 | ]) 49 | 50 | } 51 | 52 | override func viewWillAppear(_ animated: Bool) { 53 | super.viewWillAppear(animated) 54 | startAutoRefresh() // 页面回来时重新启动定时器 55 | loadBatteryData() 56 | } 57 | 58 | override func viewWillDisappear(_ animated: Bool) { 59 | super.viewWillDisappear(animated) 60 | stopAutoRefresh() // 页面离开时停止定时器 61 | } 62 | 63 | private func startAutoRefresh() { 64 | // 确保旧的定时器被清除,避免重复创建 65 | stopAutoRefresh() 66 | 67 | if SettingsUtils.instance.getAutoRefreshDataView() { 68 | // 创建新的定时器,每 3 秒刷新一次 69 | refreshTimer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(loadBatteryData), userInfo: nil, repeats: true) 70 | } 71 | } 72 | 73 | private func stopAutoRefresh() { 74 | refreshTimer?.invalidate() 75 | refreshTimer = nil 76 | } 77 | 78 | @objc private func loadBatteryData() { 79 | 80 | batteryInfoGroups = BatteryDataController.getInstance.getAllBatteryInfoGroups() 81 | 82 | // 防止 ViewController 释放后仍然执行 UI 更新 83 | DispatchQueue.main.async { 84 | if self.isViewLoaded && self.view.window != nil { 85 | // 刷新列表 86 | self.tableView.reloadData() 87 | } 88 | } 89 | } 90 | 91 | // MARK: - 设置总分组数量 92 | func numberOfSections(in tableView: UITableView) -> Int { 93 | return batteryInfoGroups.count + 1 94 | } 95 | 96 | // MARK: - 列表总长度 97 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 98 | if section < batteryInfoGroups.count { 99 | return batteryInfoGroups[section].items.count 100 | } else if section == batteryInfoGroups.count { 101 | return 1 102 | } else { 103 | return 0 104 | } 105 | } 106 | 107 | // MARK: - 设置每个分组的顶部标题 108 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 109 | if section < batteryInfoGroups.count { 110 | return batteryInfoGroups[section].titleText 111 | } 112 | return nil 113 | } 114 | 115 | // MARK: - 设置每个分组的底部标题 可以为分组设置尾部文本,如果没有尾部可以返回 nil 116 | func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 117 | 118 | if section < batteryInfoGroups.count { 119 | return batteryInfoGroups[section].footerText 120 | } else if section == batteryInfoGroups.count { 121 | return String.localizedStringWithFormat(NSLocalizedString("BatteryDataSourceMessage", comment: ""), BatteryDataController.getInstance.getProviderName(), BatteryDataController.getInstance.getFormatUpdateTime()) 122 | } 123 | return nil 124 | } 125 | 126 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 127 | let cell = UITableViewCell(style: .default, reuseIdentifier: "Cell") 128 | cell.accessoryType = .none 129 | cell.textLabel?.numberOfLines = 0 // 允许换行 130 | 131 | if indexPath.section < batteryInfoGroups.count { 132 | cell.textLabel?.text = batteryInfoGroups[indexPath.section].items[indexPath.row].text 133 | cell.tag = batteryInfoGroups[indexPath.section].items[indexPath.row].id 134 | } else if indexPath.section == batteryInfoGroups.count { 135 | cell.textLabel?.text = NSLocalizedString("RawData", comment: "") 136 | cell.accessoryType = .disclosureIndicator 137 | } 138 | return cell 139 | } 140 | 141 | // MARK: - Cell的点击事件 142 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 143 | 144 | tableView.deselectRow(at: indexPath, animated: true) 145 | 146 | if indexPath.section < batteryInfoGroups.count { // 隐藏序列号的操作 147 | let cell = tableView.cellForRow(at: indexPath) 148 | if let id = cell?.tag { 149 | switch id { 150 | case BatteryInfoItemID.batterySerialNumber, BatteryInfoItemID.chargerSerialNumber: 151 | BatteryDataController.getInstance.toggleMaskSerialNumber() 152 | loadBatteryData() 153 | default: 154 | break 155 | } 156 | } 157 | } else if indexPath.section == batteryInfoGroups.count { 158 | let rawDataViewController = RawDataViewController() 159 | rawDataViewController.hidesBottomBarWhenPushed = true // 隐藏底部导航栏 160 | self.navigationController?.pushViewController(rawDataViewController, animated: true) 161 | } 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/DataRecordSettingsViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class DataRecordSettingsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 5 | 6 | private var tableView = UITableView() 7 | 8 | private let settingsUtils = SettingsUtils.instance 9 | 10 | private let tableTitleList = [nil, NSLocalizedString("RecordFrequencySettings", comment: "记录频率设置"), nil, nil] 11 | 12 | private let tableCellList = [[NSLocalizedString("Enable", comment: "启用"), NSLocalizedString("HistoryRecordViewInHomeView", comment: "在主界面显示历史记录界面"), NSLocalizedString("RecordShowDesignCapacity", comment: ""), NSLocalizedString("EnableHistoryStatistics", comment: "")], [NSLocalizedString("Automatic", comment: ""), NSLocalizedString("DataChanged", comment: ""), NSLocalizedString("EveryDay", comment: ""), NSLocalizedString("Manual", comment: "")], [NSLocalizedString("ExportAllRecordsToCSV", comment: "")], [NSLocalizedString("DeleteAllRecords", comment: "")]] 13 | 14 | private var reloadMainTabBar = false 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | title = NSLocalizedString("DataRecordSettings", comment: "") 20 | 21 | // iOS 15 之后的版本使用新的UITableView样式 22 | if #available(iOS 15.0, *) { 23 | tableView = UITableView(frame: .zero, style: .insetGrouped) 24 | } else { 25 | tableView = UITableView(frame: .zero, style: .grouped) 26 | } 27 | 28 | // 设置表格视图的代理和数据源 29 | tableView.delegate = self 30 | tableView.dataSource = self 31 | 32 | // 注册表格单元格 33 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 34 | 35 | // 将表格视图添加到主视图 36 | view.addSubview(tableView) 37 | 38 | // 设置表格视图的布局 39 | tableView.translatesAutoresizingMaskIntoConstraints = false 40 | NSLayoutConstraint.activate([ 41 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 42 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor), 43 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor), 44 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 45 | ]) 46 | 47 | 48 | } 49 | 50 | override func viewDidDisappear(_ animated: Bool) { 51 | if reloadMainTabBar { 52 | NotificationCenter.default.post(name: Notification.Name("ShowHistoryViewChanged"), object: nil) // 通知主界面更新视图 53 | } 54 | } 55 | 56 | // MARK: - 设置总分组数量 57 | func numberOfSections(in tableView: UITableView) -> Int { 58 | return tableTitleList.count 59 | } 60 | 61 | // MARK: - 设置每个分组的Cell数量 62 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 63 | return tableCellList[section].count 64 | } 65 | 66 | // MARK: - 设置每个分组的顶部标题 67 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 68 | return tableTitleList[section] 69 | } 70 | 71 | // MARK: - 构造每个Cell 72 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 73 | let cell = UITableViewCell(style: .default, reuseIdentifier: "Cell") 74 | 75 | cell.textLabel?.text = tableCellList[indexPath.section][indexPath.row] 76 | cell.textLabel?.numberOfLines = 0 // 允许换行 77 | 78 | if indexPath.section == 0 { 79 | let switchView = UISwitch(frame: .zero) 80 | switchView.tag = indexPath.row // 设置识别id 81 | switchView.addTarget(self, action: #selector(self.onSwitchChanged(_:)), for: .valueChanged) 82 | cell.accessoryView = switchView 83 | cell.selectionStyle = .none 84 | if indexPath.row == 0 { 85 | switchView.isOn = SettingsUtils.instance.getEnableRecordBatteryData() 86 | } else if indexPath.row == 1 { 87 | switchView.isOn = SettingsUtils.instance.getShowHistoryRecordViewInHomeView() 88 | } else if indexPath.row == 2 { 89 | switchView.isOn = SettingsUtils.instance.getRecordShowDesignCapacity() 90 | } else if indexPath.row == 3 { 91 | switchView.isOn = SettingsUtils.instance.getEnableHistoryStatistics() 92 | } 93 | } else if indexPath.section == 1 { 94 | cell.selectionStyle = .default 95 | if indexPath.row == (settingsUtils.getRecordFrequency().rawValue - 1) { 96 | cell.accessoryType = .checkmark 97 | } else { 98 | cell.accessoryType = .none 99 | } 100 | } else if indexPath.section == 2 { 101 | cell.textLabel?.textAlignment = .center 102 | cell.textLabel?.textColor = .systemBlue 103 | } else if indexPath.section == 3 { 104 | cell.textLabel?.textAlignment = .center 105 | cell.textLabel?.textColor = .systemRed 106 | } 107 | 108 | return cell 109 | } 110 | 111 | // MARK: - Cell的点击事件 112 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 113 | tableView.deselectRow(at: indexPath, animated: true) 114 | 115 | if indexPath.section == 1 { 116 | // 取消之前的选择 117 | tableView.cellForRow(at: IndexPath(row: settingsUtils.getRecordFrequency().rawValue - 1, section: indexPath.section))?.accessoryType = .none 118 | // 保存选项 119 | settingsUtils.setRecordFrequency(value: indexPath.row + 1) 120 | // 设置当前的cell选中状态 121 | tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark 122 | } else if indexPath.section == 2 { 123 | // 导出记录为CSV 124 | if let csvFileURL = BatteryRecordDatabaseManager.shared.exportToCSV() { 125 | let activityVC = UIActivityViewController(activityItems: [csvFileURL], applicationActivities: nil) 126 | present(activityVC, animated: true, completion: nil) 127 | } 128 | } else if indexPath.section == 3 { 129 | // 删除全部数据的按钮 130 | let alert = UIAlertController( 131 | title: NSLocalizedString("DeleteAllRecordsTitle", comment: "确定要删除所有数据吗?"), 132 | message: NSLocalizedString("DeleteAllRecordsMessage", comment: "此操作会删除所有历史记录"), 133 | preferredStyle: .alert 134 | ) 135 | 136 | // "确定" 按钮(红色,左边) 137 | let deleteAction = UIAlertAction(title: NSLocalizedString("Confirm", comment: ""), style: .destructive) { _ in 138 | BatteryRecordDatabaseManager.shared.deleteAllRecords() 139 | } 140 | 141 | // "取消" 按钮(蓝色,右边) 142 | let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil) 143 | 144 | // 添加按钮,iOS 会自动按照规范排列 145 | alert.addAction(deleteAction) // 红色 146 | alert.addAction(cancelAction) // 蓝色 147 | 148 | // 显示弹窗 149 | present(alert, animated: true, completion: nil) 150 | } 151 | } 152 | 153 | @objc func onSwitchChanged(_ sender: UISwitch) { 154 | 155 | if sender.tag == 0 { // 启用的开关 156 | settingsUtils.setRecordFrequency(value: .Toggle) // 切换启用的开关 157 | } else if sender.tag == 1 { 158 | settingsUtils.setShowHistoryRecordViewInHomeView(value: sender.isOn) // 切换显示在主界面的开关 159 | reloadMainTabBar = true // 更改刷新标记 160 | } else if sender.tag == 2 { 161 | settingsUtils.setRecordShowDesignCapacity(value: sender.isOn) // 切换显示设计容量开关 162 | } else if sender.tag == 3 { 163 | settingsUtils.setEnableHistoryStatistics(value: sender.isOn) // 切换启用历史数据统计 164 | } 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/DisplaySettingsViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class DisplaySettingsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 5 | 6 | private var tableView = UITableView() 7 | 8 | private let settingsUtils = SettingsUtils.instance 9 | 10 | private let tableTitleList = [nil, NSLocalizedString("DisplayedHomeGroups", comment: ""), NSLocalizedString("AvailableGroups", comment: ""), nil] 11 | 12 | private let tableCellList = [[NSLocalizedString("AutoRefreshDataViewSetting", comment: ""), NSLocalizedString("ForceShowChargingData", comment: ""), NSLocalizedString("ShowSettingsBatteryInfo", comment: ""), NSLocalizedString("UseHistoryRecordToCalculateSettingsBatteryInfoRefreshDate", comment: ""), NSLocalizedString("DoubleClickTabBarButtonToScrollToTop", comment: "")], [], [], [NSLocalizedString("ResetDisplayedHomeGroups", comment: "")]] 13 | 14 | private var homeGroupIDs: [Int] = [] 15 | 16 | private var allGroupIDs: [Int] { 17 | BatteryInfoGroupID.allCases.map { $0.rawValue } 18 | } 19 | 20 | private var availableGroupIDs: [Int] = [] 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | title = NSLocalizedString("DisplaySettings", comment: "") 26 | 27 | // iOS 15 之后的版本使用新的UITableView样式 28 | if #available(iOS 15.0, *) { 29 | tableView = UITableView(frame: .zero, style: .insetGrouped) 30 | } else { 31 | tableView = UITableView(frame: .zero, style: .grouped) 32 | } 33 | 34 | // 允许UITableView可以编辑 35 | tableView.isEditing = true 36 | tableView.allowsSelectionDuringEditing = true 37 | 38 | // 设置表格视图的代理和数据源 39 | tableView.delegate = self 40 | tableView.dataSource = self 41 | 42 | // 注册表格单元格 43 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 44 | 45 | // 将表格视图添加到主视图 46 | view.addSubview(tableView) 47 | 48 | // 设置表格视图的布局 49 | tableView.translatesAutoresizingMaskIntoConstraints = false 50 | NSLayoutConstraint.activate([ 51 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 52 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor), 53 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor), 54 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 55 | ]) 56 | 57 | reloadGroupIDs() 58 | } 59 | 60 | private func reloadGroupIDs() { 61 | homeGroupIDs = settingsUtils.getHomeItemGroupSequence() 62 | availableGroupIDs = allGroupIDs.filter { !homeGroupIDs.contains($0) } 63 | } 64 | 65 | // MARK: - 设置总分组数量 66 | func numberOfSections(in tableView: UITableView) -> Int { 67 | return tableTitleList.count 68 | } 69 | 70 | // MARK: - 设置每个分组的Cell数量 71 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 72 | if section == 0 || section == 3 { 73 | return tableCellList[section].count 74 | } else if section == 1 { 75 | return homeGroupIDs.count 76 | } else if section == 2 { 77 | return availableGroupIDs.count 78 | } 79 | return 0 80 | } 81 | 82 | // MARK: - 设置每个分组的顶部标题 83 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 84 | return tableTitleList[section] 85 | } 86 | 87 | // MARK: - 设置每个分组的底部标题 可以为分组设置尾部文本,如果没有尾部可以返回 nil 88 | func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 89 | 90 | if section == 0 { 91 | return NSLocalizedString("DisplayGeneralSettingsFooterMessage", comment: "") 92 | } else if section == 1 { 93 | return NSLocalizedString("DisplayedHomeGroupsFooterMessage", comment: "") 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // MARK: - 构造每个Cell 100 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 101 | let cell = UITableViewCell(style: .default, reuseIdentifier: "Cell") 102 | cell.accessoryView = .none 103 | cell.selectionStyle = .none 104 | cell.accessoryView = nil 105 | 106 | cell.textLabel?.numberOfLines = 0 // 允许换行 107 | 108 | if indexPath.section == 0 { 109 | cell.textLabel?.text = tableCellList[indexPath.section][indexPath.row] 110 | 111 | let switchView = UISwitch(frame: .zero) 112 | switchView.tag = indexPath.row // 设置识别id 113 | if indexPath.row == 0 { 114 | switchView.isOn = SettingsUtils.instance.getAutoRefreshDataView() 115 | } else if indexPath.row == 1 { 116 | switchView.isOn = SettingsUtils.instance.getForceShowChargingData() 117 | } else if indexPath.row == 2 { 118 | switchView.isOn = SettingsUtils.instance.getShowSettingsBatteryInfo() 119 | } else if indexPath.row == 3 { 120 | switchView.isOn = SettingsUtils.instance.getUseHistoryRecordToCalculateSettingsBatteryInfoRefreshDate() 121 | switchView.isEnabled = SettingsUtils.instance.getEnableRecordBatteryData() 122 | if !SettingsUtils.instance.getEnableRecordBatteryData() { 123 | cell.textLabel?.textColor = .lightGray // 文本设置成灰色 124 | } 125 | cell.isUserInteractionEnabled = SettingsUtils.instance.getEnableRecordBatteryData() 126 | }else if indexPath.row == 4 { 127 | switchView.isOn = SettingsUtils.instance.getDoubleClickTabBarButtonToScrollToTop() 128 | } 129 | switchView.addTarget(self, action: #selector(self.onSwitchChanged(_:)), for: .valueChanged) 130 | cell.accessoryView = switchView 131 | cell.selectionStyle = .none 132 | } else if indexPath.section == 1 { 133 | let groupID = homeGroupIDs[indexPath.row] 134 | cell.textLabel?.text = BatteryInfoGroupName.getName(for: groupID) 135 | cell.selectionStyle = .none 136 | cell.accessoryType = .none // explicitly no accessory 137 | } else if indexPath.section == 2 { 138 | let groupID = availableGroupIDs[indexPath.row] 139 | cell.textLabel?.text = BatteryInfoGroupName.getName(for: groupID) 140 | cell.selectionStyle = .default 141 | cell.imageView?.tintColor = .systemGreen 142 | if #available(iOS 13.0, *) { 143 | cell.imageView?.image = UIImage(systemName: "plus.circle") 144 | } else { 145 | cell.imageView?.image = UIImage(named: "plus.circle") 146 | } 147 | cell.imageView?.isUserInteractionEnabled = true 148 | cell.imageView?.tag = groupID 149 | cell.accessoryView = nil 150 | } else if indexPath.section == 3 { 151 | cell.textLabel?.text = tableCellList[indexPath.section][indexPath.row] 152 | cell.textLabel?.textAlignment = .center 153 | cell.textLabel?.textColor = .systemRed 154 | cell.selectionStyle = .default 155 | } 156 | return cell 157 | } 158 | 159 | // MARK: - Cell的点击事件 160 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 161 | tableView.deselectRow(at: indexPath, animated: true) 162 | 163 | if indexPath.section == 2 { // 将第三组的item添加到第二组 164 | let selectedID = availableGroupIDs[indexPath.row] 165 | var updated = homeGroupIDs 166 | updated.append(selectedID) 167 | SettingsUtils.instance.setHomeItemGroupSequence(updated) // 保存数据 168 | reloadGroupIDs() // 刷新数据 169 | // 来个动画 170 | let newItemRow = homeGroupIDs.firstIndex(of: selectedID) ?? homeGroupIDs.count - 1 171 | tableView.beginUpdates() 172 | tableView.insertRows(at: [IndexPath(row: newItemRow, section: 1)], with: .automatic) 173 | tableView.deleteRows(at: [indexPath], with: .automatic) 174 | tableView.endUpdates() 175 | 176 | } else if indexPath.section == 3 { // 恢复首页信息组显示的默认设置 177 | // 恢复默认首页排序 178 | let alert = UIAlertController( 179 | title: NSLocalizedString("Alert", comment: "确定重置首页显示的信息组吗?"), 180 | message: NSLocalizedString("ResetDisplayedHomeGroupsMessage", comment: "此操作会重置为默认的首页显示排序"), 181 | preferredStyle: .alert 182 | ) 183 | 184 | // "确定" 按钮(红色,左边) 185 | let deleteAction = UIAlertAction(title: NSLocalizedString("Confirm", comment: ""), style: .destructive) { _ in 186 | self.settingsUtils.resetHomeItemGroupSequence() 187 | self.reloadGroupIDs() 188 | self.tableView.reloadData() 189 | } 190 | 191 | // "取消" 按钮(蓝色,右边) 192 | let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil) 193 | 194 | // 添加按钮,iOS 会自动按照规范排列 195 | alert.addAction(deleteAction) // 红色 196 | alert.addAction(cancelAction) // 蓝色 197 | 198 | // 显示弹窗 199 | present(alert, animated: true, completion: nil) 200 | } 201 | } 202 | 203 | @objc func onSwitchChanged(_ sender: UISwitch) { 204 | if sender.tag == 0 { 205 | SettingsUtils.instance.setAutoRefreshDataView(value: sender.isOn) 206 | } else if sender.tag == 1 { 207 | SettingsUtils.instance.setForceShowChargingData(value: sender.isOn) 208 | } else if sender.tag == 2 { 209 | SettingsUtils.instance.setShowSettingsBatteryInfo(value: sender.isOn) 210 | } else if sender.tag == 3 { 211 | SettingsUtils.instance.setUseHistoryRecordToCalculateSettingsBatteryInfoRefreshDate(value: sender.isOn) 212 | } else if sender.tag == 4 { 213 | SettingsUtils.instance.setDoubleClickTabBarButtonToScrollToTop(value: sender.isOn) 214 | } 215 | } 216 | 217 | // MARK: - 只允许第二组编辑 218 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 219 | if indexPath.section == 1 { 220 | // 如果只剩下一个,就不允许编辑 221 | return homeGroupIDs.count > 1 222 | } 223 | return false 224 | } 225 | 226 | // MARK: - 只允许第2组拖动 227 | func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { 228 | return indexPath.section == 1 229 | } 230 | 231 | // MARK: - 第二组拖动的范围 232 | func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, 233 | toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { 234 | if (sourceIndexPath.section == 1 || sourceIndexPath.section == 2) && proposedDestinationIndexPath.section == 1 { 235 | return proposedDestinationIndexPath 236 | } 237 | // 强制拖动只允许落在 section 1 238 | return sourceIndexPath 239 | } 240 | 241 | // MARK: - 第一组的排序 242 | func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 243 | guard sourceIndexPath.section == 1, destinationIndexPath.section == 1 else { return } 244 | var updated = homeGroupIDs 245 | let moved = updated.remove(at: sourceIndexPath.row) 246 | updated.insert(moved, at: destinationIndexPath.row) 247 | SettingsUtils.instance.setHomeItemGroupSequence(updated) 248 | reloadGroupIDs() 249 | tableView.reloadData() 250 | } 251 | 252 | // MARK: - 允许删除 section 1 的行 253 | func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { 254 | if indexPath.section == 1 { 255 | return .delete 256 | } 257 | return .none 258 | } 259 | 260 | // MARK: - 删除行为处理 261 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 262 | if editingStyle == .delete && indexPath.section == 1 { 263 | var updated = homeGroupIDs 264 | let removedID = updated.remove(at: indexPath.row) 265 | SettingsUtils.instance.setHomeItemGroupSequence(updated) 266 | 267 | // Pre-update data source before animation 268 | homeGroupIDs = updated 269 | availableGroupIDs = allGroupIDs.filter { !homeGroupIDs.contains($0) } 270 | 271 | tableView.beginUpdates() 272 | tableView.deleteRows(at: [indexPath], with: .automatic) 273 | 274 | // Find where to insert the row back into section 2 275 | if let insertRow = availableGroupIDs.firstIndex(of: removedID) { 276 | tableView.insertRows(at: [IndexPath(row: insertRow, section: 2)], with: .automatic) 277 | } 278 | tableView.endUpdates() 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/HistoryRecordViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class HistoryRecordViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, ScrollableToTop { 5 | 6 | private var tableView = UITableView() 7 | 8 | private var historyDataRecords: [BatteryDataRecord] = [] 9 | 10 | private var recordShowDesignCapacity = SettingsUtils.instance.getRecordShowDesignCapacity() 11 | 12 | private var capacityAccuracy: SettingsUtils.MaximumCapacityAccuracy? 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | if #available(iOS 13.0, *) { 18 | view.backgroundColor = .systemBackground 19 | } else { 20 | // Fallback on earlier versions 21 | view.backgroundColor = .white 22 | } 23 | 24 | title = NSLocalizedString("History", comment: "") 25 | 26 | // iOS 15 之后的版本使用新的UITableView样式 27 | if #available(iOS 15.0, *) { 28 | tableView = UITableView(frame: .zero, style: .insetGrouped) 29 | } else { 30 | tableView = UITableView(frame: .zero, style: .grouped) 31 | } 32 | 33 | // 设置表格视图的代理和数据源 34 | tableView.delegate = self 35 | tableView.dataSource = self 36 | 37 | // 注册表格单元格 38 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 39 | 40 | // 将表格视图添加到主视图 41 | view.addSubview(tableView) 42 | 43 | // 设置表格视图的布局 44 | tableView.translatesAutoresizingMaskIntoConstraints = false 45 | NSLayoutConstraint.activate([ 46 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 47 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor), 48 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor), 49 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 50 | ]) 51 | 52 | } 53 | 54 | override func viewWillAppear(_ animated: Bool) { 55 | super.viewWillAppear(animated) 56 | // 右上角增加统计按钮 57 | if SettingsUtils.instance.getEnableHistoryStatistics() { 58 | if #available(iOS 13.0, *) { 59 | navigationItem.rightBarButtonItem = UIBarButtonItem( 60 | image: UIImage(systemName: "chart.pie"), 61 | style: .plain, 62 | target: self, 63 | action: #selector(onClickStatisticsButton) 64 | ) 65 | } else { 66 | navigationItem.rightBarButtonItem = UIBarButtonItem( 67 | title: NSLocalizedString("HistoryStatistics", comment: "统计"), 68 | style: .plain, 69 | target: self, 70 | action: #selector(onClickStatisticsButton) 71 | ) 72 | } 73 | } else { 74 | navigationItem.rightBarButtonItem = nil 75 | } 76 | 77 | // 获取容量准确度参数 78 | self.capacityAccuracy = SettingsUtils.instance.getMaximumCapacityAccuracy() 79 | // 重新加载历史数据 80 | loadHistoryDataRecords() 81 | 82 | // 防止 ViewController 释放后仍然执行 UI 更新 83 | DispatchQueue.main.async { 84 | if self.isViewLoaded && self.view.window != nil { 85 | self.recordShowDesignCapacity = SettingsUtils.instance.getRecordShowDesignCapacity() 86 | // 刷新列表 87 | self.tableView.reloadData() 88 | } 89 | } 90 | 91 | } 92 | 93 | // MARK: - 加载历史数据 94 | private func loadHistoryDataRecords() { 95 | historyDataRecords = BatteryRecordDatabaseManager.shared.fetchAllRecords() 96 | } 97 | 98 | // MARK: - 设置总分组数量 99 | func numberOfSections(in tableView: UITableView) -> Int { 100 | return 2 101 | } 102 | 103 | // MARK: - 列表总长度 104 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 105 | if section == 1 { 106 | return historyDataRecords.count 107 | } 108 | return 1 109 | } 110 | 111 | // MARK: - 设置每个分组的顶部标题 112 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 113 | if section == 1 && historyDataRecords.count == 0 { 114 | return NSLocalizedString("NoRecord", comment: "") 115 | } 116 | return nil 117 | } 118 | 119 | // MARK: - 创建cell 120 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 121 | let cell = UITableViewCell(style: .default, reuseIdentifier: "Cell") 122 | 123 | // 防止出现复用bug 124 | cell.textLabel?.textAlignment = .natural 125 | 126 | if indexPath.section == 0 { 127 | cell.textLabel?.text = NSLocalizedString("ManualAddRecord", comment: "") 128 | cell.textLabel?.textAlignment = .center 129 | } else if indexPath.section == 1 { 130 | let recordData = self.historyDataRecords[indexPath.row] 131 | 132 | if let maximumCapacity = recordData.maximumCapacity { 133 | // 这种情况下应该是用户自己添加的 134 | cell.textLabel?.text = String.localizedStringWithFormat(NSLocalizedString("MaximumCapacity", comment: ""), String(maximumCapacity)) + "\n" + 135 | String.localizedStringWithFormat(NSLocalizedString("CycleCount", comment: ""), String(recordData.cycleCount)) + "\n" + 136 | String.localizedStringWithFormat(NSLocalizedString("RecordCreateDate", comment: ""), BatteryFormatUtils.formatTimestamp(recordData.createDate)) 137 | } else { 138 | // 自动记录的 139 | if let nominal = recordData.nominalChargeCapacity, let design = recordData.designCapacity { 140 | cell.textLabel?.text = String.localizedStringWithFormat(NSLocalizedString("MaximumCapacity", comment: ""), BatteryFormatUtils.getFormatMaximumCapacity(nominalChargeCapacity: nominal, designCapacity: design, accuracy: capacityAccuracy!)) + "\n" + 141 | String.localizedStringWithFormat(NSLocalizedString("CycleCount", comment: ""), String(recordData.cycleCount)) + "\n" + 142 | String.localizedStringWithFormat(NSLocalizedString("RemainingCapacity", comment: ""), String(nominal)) + "\n" 143 | 144 | if self.recordShowDesignCapacity { // 是否显示设计容量 145 | cell.textLabel?.text = cell.textLabel?.text?.appending( 146 | String.localizedStringWithFormat(NSLocalizedString("DesignCapacity", comment: ""), String(design)) + "\n" + 147 | String.localizedStringWithFormat(NSLocalizedString("RecordCreateDate", comment: ""), BatteryFormatUtils.formatTimestamp(recordData.createDate))) 148 | } else { 149 | cell.textLabel?.text = cell.textLabel?.text?.appending( 150 | String.localizedStringWithFormat(NSLocalizedString("RecordCreateDate", comment: ""), BatteryFormatUtils.formatTimestamp(recordData.createDate))) 151 | } 152 | } 153 | 154 | } 155 | } 156 | 157 | cell.textLabel?.numberOfLines = 0 // 允许换行 158 | 159 | return cell 160 | } 161 | 162 | // MARK: - Cell的点击事件 163 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 164 | 165 | tableView.deselectRow(at: indexPath, animated: true) 166 | 167 | if indexPath.section == 0 { 168 | 169 | guard let batteryInfoDict = getBatteryInfo() as? [String: Any] else { 170 | return 171 | } 172 | 173 | let batteryInfo = BatteryRAWInfo(dict: batteryInfoDict) 174 | 175 | // 记录历史数据 176 | if let cycleCount = batteryInfo.cycleCount, let nominalChargeCapacity = batteryInfo.nominalChargeCapacity, let designCapacity = batteryInfo.designCapacity { 177 | 178 | if BatteryDataController.recordBatteryData(manualRecord: true, cycleCount: cycleCount, nominalChargeCapacity: nominalChargeCapacity, designCapacity: designCapacity) { 179 | loadHistoryDataRecords() 180 | self.tableView.insertRows(at: [IndexPath(row: 0, section: 1)], with: .automatic) 181 | } 182 | } 183 | } 184 | 185 | } 186 | 187 | // MARK: - iOS 13+ 长按菜单 (UIContextMenuConfiguration) 188 | @available(iOS 13.0, *) 189 | func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { 190 | if indexPath.section == 1 { 191 | return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in 192 | let editAction = UIAction(title: NSLocalizedString("Copy", comment: ""), image: UIImage(systemName: "doc.on.doc")) { _ in 193 | self.copyRecord(forRowAt: indexPath) 194 | } 195 | 196 | let deleteAction = UIAction(title: NSLocalizedString("Delete", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive) { _ in 197 | self.deleteRecord(forRowAt: indexPath) 198 | } 199 | 200 | return UIMenu(title: "", children: [editAction, deleteAction]) 201 | } 202 | } 203 | return nil 204 | } 205 | 206 | // MARK: - 左侧添加“复制”按钮 207 | func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 208 | 209 | if indexPath.section == 1 { 210 | let copyAction = UIContextualAction(style: .normal, title: NSLocalizedString("Copy", comment: "")) { (action, view, completionHandler) in 211 | self.copyRecord(forRowAt: indexPath) 212 | completionHandler(true) 213 | } 214 | copyAction.backgroundColor = .systemBlue // 复制按钮颜色 215 | 216 | return UISwipeActionsConfiguration(actions: [copyAction]) 217 | } 218 | return nil 219 | } 220 | 221 | // MARK: - 让 section = 1 才能删除 222 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 223 | return indexPath.section == 1 // 仅允许 section 1 可以删除 224 | } 225 | 226 | // MARK: - 滑动删除功能 227 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 228 | if indexPath.section == 1 { 229 | if editingStyle == .delete { 230 | deleteRecord(forRowAt: indexPath) 231 | } 232 | } 233 | } 234 | 235 | private func copyRecord(forRowAt indexPath: IndexPath) { 236 | let cell = tableView.cellForRow(at: indexPath) 237 | UIPasteboard.general.string = cell?.textLabel?.text 238 | } 239 | 240 | private func deleteRecord(forRowAt indexPath: IndexPath) { 241 | let alert = UIAlertController(title: NSLocalizedString("DeleteRecordMessage", comment: ""), message: "", preferredStyle: .alert) 242 | alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel)) 243 | alert.addAction(UIAlertAction(title: NSLocalizedString("Confirm", comment: ""), style: .default, handler: { _ in 244 | // 删除记录 245 | if BatteryRecordDatabaseManager.shared.deleteRecord(byID: self.historyDataRecords[indexPath.row].id) { 246 | self.loadHistoryDataRecords() 247 | self.tableView.deleteRows(at: [indexPath], with: .automatic) 248 | } 249 | })) 250 | present(alert, animated: true) 251 | } 252 | 253 | // 滚动UITableView到顶部 254 | func scrollToTop() { 255 | let offset = CGPoint(x: 0, y: -tableView.adjustedContentInset.top) 256 | tableView.setContentOffset(offset, animated: true) 257 | } 258 | 259 | // 点击历史数据统计按钮 260 | @objc private func onClickStatisticsButton() { 261 | let historyStatisticsViewController = HistoryStatisticsViewController() 262 | historyStatisticsViewController.hidesBottomBarWhenPushed = true // 隐藏底部导航栏 263 | self.navigationController?.pushViewController(historyStatisticsViewController, animated: true) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/HistoryStatisticsViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class HistoryStatisticsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 5 | 6 | private var tableView = UITableView() 7 | 8 | override func viewDidLoad() { 9 | super.viewDidLoad() 10 | 11 | title = NSLocalizedString("HistoryStatistics", comment: "") 12 | 13 | // iOS 15 之后的版本使用新的UITableView样式 14 | if #available(iOS 15.0, *) { 15 | tableView = UITableView(frame: .zero, style: .insetGrouped) 16 | } else { 17 | tableView = UITableView(frame: .zero, style: .grouped) 18 | } 19 | 20 | // 设置表格视图的代理和数据源 21 | tableView.delegate = self 22 | tableView.dataSource = self 23 | 24 | // 注册表格单元格 25 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 26 | 27 | // 将表格视图添加到主视图 28 | view.addSubview(tableView) 29 | 30 | // 设置表格视图的布局 31 | tableView.translatesAutoresizingMaskIntoConstraints = false 32 | NSLayoutConstraint.activate([ 33 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 34 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor), 35 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor), 36 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 37 | ]) 38 | 39 | } 40 | 41 | // MARK: - 设置总分组数量 42 | func numberOfSections(in tableView: UITableView) -> Int { 43 | return 1 44 | } 45 | 46 | // MARK: - 列表总长度 47 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 48 | return 1 49 | } 50 | 51 | // MARK: - 创建cell 52 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 53 | let cell = UITableViewCell(style: .default, reuseIdentifier: "Cell") 54 | cell.textLabel?.numberOfLines = 0 55 | 56 | do { 57 | let records = BatteryRecordDatabaseManager.shared.fetchAllRecords() 58 | guard records.count >= 2 else { 59 | throw NSError(domain: "BatteryStats", code: 1, userInfo: nil) 60 | } 61 | 62 | guard let first = records.last, let last = records.first else { 63 | throw NSError(domain: "BatteryStats", code: 3, userInfo: nil) 64 | } 65 | 66 | let totalRecords = records.count 67 | let timeInterval = max(1, last.createDate - first.createDate) 68 | let days = Double(timeInterval) / 86400.0 69 | let totalDays = String(Int(days)) 70 | 71 | let healthValues = records.map { $0.nominalChargeCapacity ?? 0 } 72 | guard let minHealth = healthValues.min(), let maxHealth = healthValues.max(), let designCapacity = last.designCapacity else { 73 | throw NSError(domain: "BatteryStats", code: 2, userInfo: nil) 74 | } 75 | 76 | guard designCapacity != 0 else { 77 | throw NSError(domain: "BatteryStats", code: 4, userInfo: nil) 78 | } 79 | 80 | let deltaHealth = Double(minHealth - maxHealth) / Double(designCapacity) * 100 81 | let deltaCapacity = minHealth - maxHealth 82 | let deltaCycles = last.cycleCount - first.cycleCount 83 | let avgCyclePerDay = Double(deltaCycles) / days 84 | 85 | cell.textLabel?.text = String(format: NSLocalizedString("BatteryHistorySummaryContent", comment: ""), 86 | String(totalRecords), 87 | String(totalDays), 88 | String(format: "%.2f", deltaHealth), 89 | String(deltaCapacity), 90 | String(maxHealth), 91 | String(minHealth), 92 | String(deltaCycles), 93 | String(format: "%.2f", avgCyclePerDay) 94 | ) 95 | } catch { 96 | cell.textLabel?.text = NSLocalizedString("NotEnoughData", comment: "") 97 | } 98 | 99 | return cell 100 | } 101 | 102 | // MARK: - Cell的点击事件 103 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 104 | tableView.deselectRow(at: indexPath, animated: true) 105 | } 106 | 107 | @available(iOS 13.0, *) 108 | func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { 109 | let cell = tableView.cellForRow(at: indexPath) 110 | let text = cell?.textLabel?.text 111 | 112 | return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in 113 | let copyAction = UIAction(title: NSLocalizedString("Copy", comment: ""), image: UIImage(systemName: "doc.on.doc")) { _ in 114 | UIPasteboard.general.string = text 115 | } 116 | return UIMenu(title: "", children: [copyAction]) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/HomeViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, ScrollableToTop { 5 | 6 | private var tableView = UITableView() 7 | 8 | private var settingsUtils = SettingsUtils.instance 9 | 10 | private var batteryInfoGroups: [InfoItemGroup] = [] 11 | 12 | private var refreshTimer: Timer? 13 | private var showOSBuildVersion = false 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | if #available(iOS 13.0, *) { 19 | view.backgroundColor = .systemBackground 20 | } else { 21 | // Fallback on earlier versions 22 | view.backgroundColor = .white 23 | } 24 | 25 | title = NSLocalizedString("CFBundleDisplayName", comment: "") 26 | 27 | // iOS 15 之后的版本使用新的UITableView样式 28 | if #available(iOS 15.0, *) { 29 | tableView = UITableView(frame: .zero, style: .insetGrouped) 30 | } else { 31 | tableView = UITableView(frame: .zero, style: .grouped) 32 | } 33 | 34 | // 设置表格视图的代理和数据源 35 | tableView.delegate = self 36 | tableView.dataSource = self 37 | 38 | // 注册表格单元格 39 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 40 | 41 | // 将表格视图添加到主视图 42 | view.addSubview(tableView) 43 | 44 | // 设置表格视图的布局 45 | tableView.translatesAutoresizingMaskIntoConstraints = false 46 | NSLayoutConstraint.activate([ 47 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 48 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor), 49 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor), 50 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 51 | ]) 52 | 53 | if !BatteryDataController.checkRunTimePermission() { 54 | 55 | if !BatteryDataController.checkInstallPermission() { 56 | let alert = UIAlertController(title: NSLocalizedString("Alert", comment: ""), message: NSLocalizedString("NeedRunTimePermissionMessage", comment: ""), preferredStyle: .alert) 57 | alert.addAction(UIAlertAction(title: NSLocalizedString("Dismiss", comment: ""), style: .cancel)) 58 | present(alert, animated: true) 59 | 60 | return 61 | } else { 62 | let alert = UIAlertController(title: NSLocalizedString("Alert", comment: ""), message: NSLocalizedString("TemporaryNotSupportMessage", comment: ""), preferredStyle: .alert) 63 | alert.addAction(UIAlertAction(title: NSLocalizedString("Dismiss", comment: ""), style: .cancel)) 64 | present(alert, animated: true) 65 | 66 | return 67 | } 68 | 69 | } 70 | } 71 | 72 | override func viewWillAppear(_ animated: Bool) { 73 | super.viewWillAppear(animated) 74 | startAutoRefresh() // 页面回来时重新启动定时器 75 | loadBatteryData() 76 | } 77 | 78 | override func viewWillDisappear(_ animated: Bool) { 79 | super.viewWillDisappear(animated) 80 | stopAutoRefresh() // 页面离开时停止定时器 81 | } 82 | 83 | @objc private func loadBatteryData() { 84 | 85 | batteryInfoGroups = BatteryDataController.getInstance.getHomeInfoGroups() 86 | 87 | // 防止 ViewController 释放后仍然执行 UI 更新 88 | DispatchQueue.main.async { 89 | if self.isViewLoaded && self.view.window != nil { 90 | // 刷新列表 91 | self.tableView.reloadData() 92 | } 93 | } 94 | } 95 | 96 | private func startAutoRefresh() { 97 | // 确保旧的定时器被清除,避免重复创建 98 | stopAutoRefresh() 99 | 100 | if settingsUtils.getAutoRefreshDataView() { 101 | // 创建新的定时器,每 3 秒刷新一次 102 | refreshTimer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(loadBatteryData), userInfo: nil, repeats: true) 103 | } 104 | } 105 | 106 | private func stopAutoRefresh() { 107 | refreshTimer?.invalidate() 108 | refreshTimer = nil 109 | } 110 | 111 | // MARK: - 设置总分组数量 112 | func numberOfSections(in tableView: UITableView) -> Int { 113 | return batteryInfoGroups.count + 2 114 | } 115 | 116 | // MARK: - 列表总长度 117 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 118 | if section == 0 { // 系统信息 119 | return 2 120 | } else if section >= 1 && section <= batteryInfoGroups.count { // batteryInfoGroups 动态数据 121 | return batteryInfoGroups[section - 1].items.count 122 | } else if section == batteryInfoGroups.count + 1 { // 最后一组(显示全部数据 / 原始数据) 123 | return 2 124 | } else { 125 | return 0 126 | } 127 | } 128 | 129 | // MARK: - 设置每个分组的顶部标题 130 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 131 | if section >= 1 && section <= batteryInfoGroups.count { 132 | return batteryInfoGroups[section - 1].titleText 133 | } 134 | return nil 135 | } 136 | 137 | // MARK: - 设置每个分组的底部标题 可以为分组设置尾部文本,如果没有尾部可以返回 nil 138 | func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 139 | 140 | if section >= 1 && section <= batteryInfoGroups.count { 141 | return batteryInfoGroups[section - 1].footerText 142 | } else if section == batteryInfoGroups.count + 1 { 143 | return String.localizedStringWithFormat(NSLocalizedString("BatteryDataSourceMessage", comment: ""), BatteryDataController.getInstance.getProviderName(), BatteryDataController.getInstance.getFormatUpdateTime()) 144 | } 145 | return nil 146 | } 147 | 148 | // MARK: - 创建cell 149 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 150 | let cell = UITableViewCell(style: .default, reuseIdentifier: "Cell") 151 | cell.textLabel?.numberOfLines = 0 // 允许换行 152 | 153 | if indexPath.section == 0 { 154 | if indexPath.row == 0 { // 系统版本号 155 | if !SystemInfoUtils.isRunningOniPadOS() { 156 | cell.textLabel?.text = SystemInfoUtils.getDeviceName() + " " + SystemInfoUtils.getDiskTotalSpace() + " (" + String.localizedStringWithFormat(NSLocalizedString("iOSVersion", comment: ""), UIDevice.current.systemVersion) + ")" 157 | } else { 158 | cell.textLabel?.text = SystemInfoUtils.getDeviceName() + " " + SystemInfoUtils.getDiskTotalSpace() + " (" + String.localizedStringWithFormat(NSLocalizedString("iPadOSVersion", comment: ""), UIDevice.current.systemVersion) + ")" 159 | } 160 | 161 | if self.showOSBuildVersion { 162 | let buildVersion: String = " [" + (SystemInfoUtils.getSystemBuildVersion() ?? "") + "]" 163 | cell.textLabel?.text = (cell.textLabel?.text)! + buildVersion 164 | } 165 | 166 | if let regionCode = SystemInfoUtils.getDeviceRegionCode() { 167 | cell.textLabel?.text = (cell.textLabel?.text)! + " " + regionCode 168 | } 169 | 170 | } else if indexPath.row == 1 { // 设备启动时间 171 | cell.textLabel?.text = SystemInfoUtils.getDeviceUptimeUsingSysctl() 172 | } 173 | } else if indexPath.section >= 1 && indexPath.section <= batteryInfoGroups.count { 174 | cell.textLabel?.text = batteryInfoGroups[indexPath.section - 1].items[indexPath.row].text 175 | cell.tag = batteryInfoGroups[indexPath.section - 1].items[indexPath.row].id 176 | } else if indexPath.section == batteryInfoGroups.count + 1 { 177 | cell.accessoryType = .disclosureIndicator 178 | if indexPath.row == 0 { // 显示全部数据 179 | cell.textLabel?.text = NSLocalizedString("AllData", comment: "") 180 | } else if indexPath.row == 1 { // 显示原始数据 181 | cell.textLabel?.text = NSLocalizedString("RawData", comment: "") 182 | } 183 | } 184 | return cell 185 | } 186 | 187 | // MARK: - Cell的点击事件 188 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 189 | 190 | tableView.deselectRow(at: indexPath, animated: true) 191 | if indexPath.section == 0 && indexPath.row == 0 { 192 | self.showOSBuildVersion = !showOSBuildVersion 193 | tableView.reloadRows(at: [indexPath], with: .none) 194 | } else if indexPath.section >= 1 && indexPath.section <= batteryInfoGroups.count { 195 | let cell = tableView.cellForRow(at: indexPath) 196 | if let id = cell?.tag { 197 | switch id { 198 | case BatteryInfoItemID.batterySerialNumber, BatteryInfoItemID.chargerSerialNumber: 199 | BatteryDataController.getInstance.toggleMaskSerialNumber() 200 | loadBatteryData() 201 | default: 202 | break 203 | } 204 | } 205 | } else if indexPath.section == batteryInfoGroups.count + 1 { 206 | if indexPath.row == 0 { // 显示全部数据 207 | let allBatteryDataViewController = AllBatteryDataViewController() 208 | allBatteryDataViewController.hidesBottomBarWhenPushed = true // 隐藏底部导航栏 209 | self.navigationController?.pushViewController(allBatteryDataViewController, animated: true) 210 | } else { // 显示原始数据 211 | let rawDataViewController = RawDataViewController() 212 | rawDataViewController.hidesBottomBarWhenPushed = true // 隐藏底部导航栏 213 | self.navigationController?.pushViewController(rawDataViewController, animated: true) 214 | } 215 | 216 | } 217 | } 218 | 219 | // 滚动UITableView到顶部 220 | func scrollToTop() { 221 | let offset = CGPoint(x: 0, y: -tableView.adjustedContentInset.top) 222 | tableView.setContentOffset(offset, animated: true) 223 | } 224 | 225 | } 226 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/LanguageSettingsViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class LanguageSettingsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 5 | 6 | private var tableView = UITableView() 7 | 8 | private let settingsUtils = SettingsUtils.instance 9 | 10 | private let tableCellList = [NSLocalizedString("UseSystemLanguage", comment: ""), "English", "简体中文"] 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | title = NSLocalizedString("LanguageSettings", comment: "") 16 | 17 | // iOS 15 之后的版本使用新的UITableView样式 18 | if #available(iOS 15.0, *) { 19 | tableView = UITableView(frame: .zero, style: .insetGrouped) 20 | } else { 21 | tableView = UITableView(frame: .zero, style: .grouped) 22 | } 23 | 24 | // 设置表格视图的代理和数据源 25 | tableView.delegate = self 26 | tableView.dataSource = self 27 | 28 | // 注册表格单元格 29 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 30 | 31 | // 将表格视图添加到主视图 32 | view.addSubview(tableView) 33 | 34 | // 设置表格视图的布局 35 | tableView.translatesAutoresizingMaskIntoConstraints = false 36 | NSLayoutConstraint.activate([ 37 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 38 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor), 39 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor), 40 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 41 | ]) 42 | 43 | } 44 | 45 | // MARK: - 设置总分组数量 46 | func numberOfSections(in tableView: UITableView) -> Int { 47 | return 1 48 | } 49 | 50 | // MARK: - 设置每个分组的Cell数量 51 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 52 | return tableCellList.count 53 | } 54 | 55 | // MARK: - 构造每个Cell 56 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 57 | let cell = UITableViewCell(style: .default, reuseIdentifier: "Cell") 58 | 59 | cell.textLabel?.text = tableCellList[indexPath.row] 60 | cell.textLabel?.numberOfLines = 0 // 允许换行 61 | 62 | cell.selectionStyle = .default 63 | if indexPath.row == settingsUtils.getApplicationLanguage().rawValue { 64 | cell.accessoryType = .checkmark 65 | } else { 66 | cell.accessoryType = .none 67 | } 68 | 69 | return cell 70 | } 71 | 72 | // MARK: - Cell的点击事件 73 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 74 | tableView.deselectRow(at: indexPath, animated: true) 75 | 76 | // 取消之前的选择 77 | tableView.cellForRow(at: IndexPath(row: settingsUtils.getApplicationLanguage().rawValue, section: indexPath.section))?.accessoryType = .none 78 | // 保存选项 79 | settingsUtils.setApplicationLanguage(value: indexPath.row) 80 | // 设置当前的cell选中状态 81 | tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark 82 | 83 | // 刷新界面显示 84 | ApplicationLanguageController.loadLanguageFromSettings() 85 | reloadAppRootView() 86 | 87 | // 重新初始化数据提供者,解决语言切换的小bug 88 | BatteryDataController.configureInstance(provider: IOKitBatteryDataProvider()) 89 | } 90 | 91 | func reloadAppRootView() { 92 | guard let window = UIApplication.shared.windows.first else { return } 93 | 94 | let tabBarController = MainUITabBarController() 95 | window.rootViewController = tabBarController 96 | window.makeKeyAndVisible() 97 | 98 | // 切换到设置 tab 99 | tabBarController.selectedIndex = settingsUtils.getShowHistoryRecordViewInHomeView() ? 2 : 1 100 | 101 | // 在设置导航控制器中重新 push LanguageSettingsViewController 102 | if let settingsNav = tabBarController.viewControllers?[2] as? UINavigationController { 103 | 104 | // 创建一个 SettingsViewController,并设置标题和 tabBarItem 105 | let settingsVC = SettingsViewController() 106 | settingsVC.title = NSLocalizedString("Settings", comment: "") 107 | settingsVC.tabBarItem = MainUITabBarController.createTabBarItem(title: "Settings", image: "gear", selectedImage: "gear.fill", fallbackImage: "") 108 | 109 | // 重建栈:Settings → LanguageSettings 110 | settingsNav.setViewControllers([settingsVC], animated: false) 111 | 112 | let languageVC = LanguageSettingsViewController() 113 | languageVC.hidesBottomBarWhenPushed = true // 隐藏底部导航栏 114 | settingsNav.pushViewController(languageVC, animated: false) 115 | } 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/MainUITabBarController.swift: -------------------------------------------------------------------------------- 1 | // Protocol for view controllers that can scroll to top 2 | protocol ScrollableToTop { 3 | func scrollToTop() 4 | } 5 | import Foundation 6 | import UIKit 7 | 8 | class MainUITabBarController: UITabBarController, UITabBarControllerDelegate { 9 | 10 | private let homeViewController = HomeViewController() 11 | private let historyRecordViewController = HistoryRecordViewController() 12 | private let settingsViewController = SettingsViewController() 13 | 14 | private var lastSelectedIndex: Int = 0 15 | private var lastTapTimestamp: TimeInterval = 0 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | if #available(iOS 13.0, *) { 21 | view.backgroundColor = .systemBackground 22 | } else { 23 | // Fallback on earlier versions 24 | view.backgroundColor = .white 25 | } 26 | 27 | // 隐藏iPad OS 18开始的顶部TabBar 28 | if #available(iOS 18.0, *), UIDevice.current.userInterfaceIdiom == .pad { 29 | setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: .compact), forChild: self) 30 | } 31 | 32 | homeViewController.tabBarItem = MainUITabBarController.createTabBarItem(title: "Home", image: "house", selectedImage: "house.fill", fallbackImage: "") 33 | historyRecordViewController.tabBarItem = MainUITabBarController.createTabBarItem(title: "History", image: "list.dash", selectedImage: "list.bullet", fallbackImage: "") 34 | settingsViewController.tabBarItem = MainUITabBarController.createTabBarItem(title: "Settings", image: "gear", selectedImage: "gear.fill", fallbackImage: "") 35 | 36 | updateTabBarControllers(selfLoad: true) 37 | 38 | // 监听设置变化,动态更新 tab 39 | NotificationCenter.default.addObserver(self, selector: #selector(updateTabBarControllers), name: Notification.Name("ShowHistoryViewChanged"), object: nil) 40 | 41 | delegate = self 42 | } 43 | 44 | static func createTabBarItem(title: String, image: String, selectedImage: String,fallbackImage: String) -> UITabBarItem { 45 | let localizedTitle = NSLocalizedString(title, comment: "") 46 | if #available(iOS 13.0, *) { 47 | return UITabBarItem(title: localizedTitle, image: UIImage(systemName: image), selectedImage: UIImage(systemName: selectedImage)) 48 | } else { 49 | return UITabBarItem(title: localizedTitle, image: UIImage(named: fallbackImage), selectedImage: UIImage(named: fallbackImage)) 50 | } 51 | } 52 | 53 | @objc private func updateTabBarControllers(selfLoad: Bool) { 54 | let homeNav = UINavigationController(rootViewController: homeViewController) 55 | let settingsNav = UINavigationController(rootViewController: settingsViewController) 56 | 57 | var newViewControllers: [UIViewController] = [homeNav] 58 | 59 | if SettingsUtils.instance.getShowHistoryRecordViewInHomeView() { 60 | let historyNav = UINavigationController(rootViewController: historyRecordViewController) 61 | newViewControllers.append(historyNav) 62 | } 63 | 64 | newViewControllers.append(settingsNav) 65 | 66 | // 更新 `viewControllers` 67 | self.viewControllers = newViewControllers 68 | 69 | // 判断是否是其他ViewController通过通知的调用 70 | if !selfLoad { 71 | selectedIndex = newViewControllers.count == 2 ? 1 : 2 72 | } 73 | } 74 | 75 | deinit { 76 | NotificationCenter.default.removeObserver(self) 77 | } 78 | 79 | func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { 80 | 81 | if !SettingsUtils.instance.getDoubleClickTabBarButtonToScrollToTop() { // 判断是否启用此功能 82 | return 83 | } 84 | 85 | let now = Date().timeIntervalSince1970 86 | 87 | if selectedIndex == lastSelectedIndex { 88 | if now - lastTapTimestamp < 0.5 { 89 | if let nav = viewController as? UINavigationController, 90 | let topViewController = nav.topViewController as? ScrollableToTop { 91 | topViewController.scrollToTop() 92 | } 93 | } 94 | } 95 | 96 | lastTapTimestamp = now 97 | lastSelectedIndex = selectedIndex 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/RawDataViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class RawDataViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 4 | 5 | private var batteryInfo: [(key: String, value: Any)] = [] 6 | private var tableView = UITableView() 7 | 8 | override func viewDidLoad() { 9 | super.viewDidLoad() 10 | self.title = NSLocalizedString("RawData", comment: "") 11 | 12 | if #available(iOS 13.0, *) { 13 | view.backgroundColor = .systemBackground 14 | } else { 15 | // Fallback on earlier versions 16 | view.backgroundColor = .white 17 | } 18 | 19 | // iOS 15 之后的版本使用新的UITableView样式 20 | if #available(iOS 15.0, *) { 21 | tableView = UITableView(frame: .zero, style: .insetGrouped) 22 | } else { 23 | tableView = UITableView(frame: .zero, style: .grouped) 24 | } 25 | 26 | // 初始化 UITableView 27 | tableView.dataSource = self 28 | tableView.delegate = self 29 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 30 | view.addSubview(tableView) 31 | 32 | // 添加下拉刷新 33 | let refreshControl = UIRefreshControl() 34 | refreshControl.addTarget(self, action: #selector(reloadBatteryInfo), for: .valueChanged) 35 | tableView.refreshControl = refreshControl 36 | 37 | // 添加右上角刷新按钮 38 | navigationItem.rightBarButtonItem = UIBarButtonItem( 39 | barButtonSystemItem: .refresh, 40 | target: self, 41 | action: #selector(reloadBatteryInfo) 42 | ) 43 | 44 | let copyButton = UIButton(type: .system) 45 | copyButton.setTitle(NSLocalizedString("CopyAllData", comment: ""), for: .normal) 46 | copyButton.addTarget(self, action: #selector(copyBatteryInfo), for: .touchUpInside) 47 | copyButton.backgroundColor = .systemBlue 48 | copyButton.setTitleColor(.white, for: .normal) 49 | copyButton.layer.cornerRadius = 8 50 | view.addSubview(copyButton) 51 | 52 | tableView.translatesAutoresizingMaskIntoConstraints = false 53 | copyButton.translatesAutoresizingMaskIntoConstraints = false 54 | 55 | NSLayoutConstraint.activate([ 56 | // TableView 约束 57 | tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 58 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 59 | tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 60 | tableView.bottomAnchor.constraint(equalTo: copyButton.topAnchor, constant: -10), 61 | 62 | // CopyButton 约束 63 | copyButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), 64 | copyButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), 65 | copyButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), 66 | copyButton.heightAnchor.constraint(equalToConstant: 40) 67 | ]) 68 | 69 | reloadBatteryInfo() 70 | } 71 | 72 | @objc func reloadBatteryInfo() { 73 | // 获取电池信息 74 | if let info = fetchBatteryInfo() { 75 | batteryInfo = info.sorted { $0.key < $1.key } // key按照A->Z进行排序,不然每次进入的数据都是乱的 76 | } 77 | 78 | // 刷新 TableView 79 | tableView.reloadData() 80 | 81 | // 结束下拉刷新动画 82 | self.tableView.refreshControl?.endRefreshing() 83 | } 84 | 85 | // 获取电池信息 86 | func fetchBatteryInfo() -> [String: Any]? { 87 | return BatteryDataController.getInstance.getBatteryRAWInfo() 88 | } 89 | 90 | // MARK: - UITableViewDataSource 91 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 92 | return batteryInfo.count 93 | } 94 | 95 | // MARK: - 设置每个分组的底部标题 可以为分组设置尾部文本,如果没有尾部可以返回 nil 96 | func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 97 | return String.localizedStringWithFormat(NSLocalizedString("BatteryDataSourceMessage", comment: ""), BatteryDataController.getInstance.getProviderName(), BatteryDataController.getInstance.getFormatUpdateTime()) 98 | } 99 | 100 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 101 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 102 | 103 | // 直接从有序数组中取值,获取属性名和值 104 | let item = batteryInfo[indexPath.row] 105 | let key = item.key 106 | let value = item.value 107 | 108 | // 配置 Cell 109 | cell.textLabel?.text = "\"\(key)\": \"\(value)\"," 110 | cell.textLabel?.numberOfLines = 0 111 | cell.textLabel?.font = UIFont.systemFont(ofSize: 16) 112 | cell.selectionStyle = .none 113 | 114 | return cell 115 | } 116 | 117 | // MARK: - Cell的点击事件 118 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 119 | tableView.deselectRow(at: indexPath, animated: true) 120 | } 121 | 122 | @available(iOS 13.0, *) 123 | func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { 124 | let item = batteryInfo[indexPath.row] 125 | let text = "\"\(item.key)\": \"\(item.value)\"," 126 | 127 | return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in 128 | let copyAction = UIAction(title: NSLocalizedString("Copy", comment: ""), image: UIImage(systemName: "doc.on.doc")) { _ in 129 | UIPasteboard.general.string = text 130 | } 131 | return UIMenu(title: "", children: [copyAction]) 132 | } 133 | } 134 | 135 | @objc func copyBatteryInfo() { 136 | // 将电池信息格式化为字符串 137 | let formattedInfo = batteryInfo.map { "\($0.key): \($0.value)" }.joined(separator: "\n") 138 | 139 | // 复制到剪贴板 140 | UIPasteboard.general.string = formattedInfo 141 | 142 | // 显示提示 143 | let alert = UIAlertController(title: NSLocalizedString("CopySuccessful", comment: ""), message: NSLocalizedString("RawDataCopySuccessfulMessage", comment: ""), preferredStyle: .alert) 144 | alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), style: .default, handler: nil)) 145 | present(alert, animated: true, completion: nil) 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /BatteryInfo/ViewController/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class SettingsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 5 | 6 | let versionCode = "1.1.9" 7 | 8 | private var tableView = UITableView() 9 | 10 | private let settingsUtils = SettingsUtils.instance 11 | 12 | private let tableTitleList = [nil, NSLocalizedString("MaximumCapacityAccuracy", comment: ""), NSLocalizedString("About", comment: "")] 13 | 14 | private let tableCellList = [[NSLocalizedString("LanguageSettings", comment: ""), NSLocalizedString("DisplaySettings", comment: ""), NSLocalizedString("DataRecordSettings", comment: "")], [NSLocalizedString("KeepOriginal", comment: ""), NSLocalizedString("Ceiling", comment: ""), NSLocalizedString("Round", comment: ""), NSLocalizedString("Floor", comment: "")], [NSLocalizedString("Version", comment: ""), "GitHub", "Havoc", NSLocalizedString("ThanksForXiaoboVlog", comment: "")]] 15 | // NSLocalizedString("ShowCPUFrequency", comment: "") 16 | 17 | // 标记一下每个分组的编号,防止新增一组还需要修改好几处的代码 18 | private let maximumCapacityAccuracyAtSection = 1 19 | private let aboutAtSection = 2 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | title = NSLocalizedString("Settings", comment: "") 25 | 26 | // iOS 15 之后的版本使用新的UITableView样式 27 | if #available(iOS 15.0, *) { 28 | tableView = UITableView(frame: .zero, style: .insetGrouped) 29 | } else { 30 | tableView = UITableView(frame: .zero, style: .grouped) 31 | } 32 | 33 | // 设置表格视图的代理和数据源 34 | tableView.delegate = self 35 | tableView.dataSource = self 36 | 37 | // 注册表格单元格 38 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 39 | 40 | // 将表格视图添加到主视图 41 | view.addSubview(tableView) 42 | 43 | // 设置表格视图的布局 44 | tableView.translatesAutoresizingMaskIntoConstraints = false 45 | NSLayoutConstraint.activate([ 46 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 47 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor), 48 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor), 49 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 50 | ]) 51 | 52 | } 53 | 54 | // MARK: - 设置总分组数量 55 | func numberOfSections(in tableView: UITableView) -> Int { 56 | return tableTitleList.count 57 | } 58 | 59 | // MARK: - 设置每个分组的Cell数量 60 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 61 | return tableCellList[section].count 62 | } 63 | 64 | // MARK: - 设置每个分组的顶部标题 65 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 66 | return tableTitleList[section] 67 | } 68 | 69 | // MARK: - 设置每个分组的底部标题 可以为分组设置尾部文本,如果没有尾部可以返回 nil 70 | func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 71 | 72 | if section == 0 { 73 | return NSLocalizedString("AutoRefreshDataFooterMessage", comment: "") 74 | } 75 | return nil 76 | } 77 | 78 | // MARK: - 构造每个Cell 79 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 80 | var cell = UITableViewCell(style: .default, reuseIdentifier: "Cell") 81 | cell.accessoryView = .none 82 | cell.selectionStyle = .none 83 | 84 | cell.textLabel?.text = tableCellList[indexPath.section][indexPath.row] 85 | cell.textLabel?.numberOfLines = 0 // 允许换行 86 | 87 | if indexPath.section == 0 { 88 | cell.accessoryType = .disclosureIndicator 89 | cell.selectionStyle = .default // 启用选中效果 90 | } else if indexPath.section == maximumCapacityAccuracyAtSection { 91 | cell.selectionStyle = .default 92 | if indexPath.row == settingsUtils.getMaximumCapacityAccuracy().rawValue { 93 | cell.accessoryType = .checkmark 94 | } else { 95 | cell.accessoryType = .none 96 | } 97 | } else if indexPath.section == aboutAtSection { // 关于 98 | if indexPath.row == 0 { 99 | cell = UITableViewCell(style: .value1, reuseIdentifier: "cell") 100 | cell.textLabel?.text = tableCellList[indexPath.section][indexPath.row] 101 | let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? NSLocalizedString("Unknown", comment: "") 102 | if version != versionCode { // 判断版本号是不是有人篡改 103 | cell.detailTextLabel?.text = versionCode 104 | } else { 105 | cell.detailTextLabel?.text = version 106 | } 107 | cell.selectionStyle = .none 108 | cell.accessoryType = .none 109 | } else { 110 | cell.accessoryType = .disclosureIndicator 111 | cell.selectionStyle = .default // 启用选中效果 112 | } 113 | } 114 | 115 | return cell 116 | } 117 | 118 | // MARK: - Cell的点击事件 119 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 120 | 121 | tableView.deselectRow(at: indexPath, animated: true) 122 | if indexPath.section == 0 { 123 | if indexPath.row == 0 { 124 | let languageSettingsViewController = LanguageSettingsViewController() 125 | languageSettingsViewController.hidesBottomBarWhenPushed = true // 隐藏底部导航栏 126 | self.navigationController?.pushViewController(languageSettingsViewController, animated: true) 127 | } else if indexPath.row == 1 { 128 | let displaySettingsViewController = DisplaySettingsViewController() 129 | displaySettingsViewController.hidesBottomBarWhenPushed = true // 隐藏底部导航栏 130 | self.navigationController?.pushViewController(displaySettingsViewController, animated: true) 131 | } else if indexPath.row == 2 { 132 | let dataRecordSettingsViewController = DataRecordSettingsViewController() 133 | dataRecordSettingsViewController.hidesBottomBarWhenPushed = true // 隐藏底部导航栏 134 | self.navigationController?.pushViewController(dataRecordSettingsViewController, animated: true) 135 | } 136 | } else if indexPath.section == maximumCapacityAccuracyAtSection { // 切换设置 137 | // 取消之前的选择 138 | tableView.cellForRow(at: IndexPath(row: settingsUtils.getMaximumCapacityAccuracy().rawValue, section: indexPath.section))?.accessoryType = .none 139 | // 保存选项 140 | settingsUtils.setMaximumCapacityAccuracy(value: indexPath.row) 141 | // 设置当前的cell选中状态 142 | tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark 143 | } else if indexPath.section == aboutAtSection { 144 | if indexPath.row == 1 { 145 | if let url = URL(string: "https://github.com/DevelopCubeLab/BatteryInfo") { 146 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 147 | } 148 | } else if indexPath.row == 2 { 149 | if let url = URL(string: "https://havoc.app/package/batteryinfo") { 150 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 151 | } 152 | } else if indexPath.row == 3 { 153 | if let url = URL(string: "https://m.xiaobovlog.cn/") { 154 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 155 | } 156 | } 157 | } 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARCHS := arm64 2 | TARGET := iphone:clang:latest:12.2 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | # 使用 Xcode 项目构建 7 | XCODEPROJ_NAME = BatteryInfo 8 | BUILD_VERSION = "1.1.9" 9 | 10 | # 指定 Theos 使用 xcodeproj 规则 11 | include $(THEOS_MAKE_PATH)/xcodeproj.mk 12 | 13 | # 在打包阶段用ldid签名赋予权力,顺便删除_CodeSignature 14 | before-package:: 15 | @if [ -f $(THEOS_STAGING_DIR)/Applications/$(XCODEPROJ_NAME).app/Info.plist ]; then \ 16 | echo -e "\033[32mSigning with ldid...\033[0m"; \ 17 | ldid -Sentitlements.plist $(THEOS_STAGING_DIR)/Applications/$(XCODEPROJ_NAME).app; \ 18 | else \ 19 | @echo -e "\033[31mNo Info.plist found. Skipping ldid signing.\033[0m"; \ 20 | fi 21 | @echo -e "\033[32mRemoving _CodeSignature folder..." 22 | @rm -rf $(THEOS_STAGING_DIR)/Applications/$(XCODEPROJ_NAME).app/_CodeSignature 23 | @echo -e "\033[32mCopy RootHelper to package..." 24 | # 这里必须要手动复制RootHelper到包内,不要放到Xcode工程目录下,不然就无法运行二进制文件 25 | @cp -f SettingsBatteryHelper/SettingsBatteryHelper $(THEOS_STAGING_DIR)/Applications/$(XCODEPROJ_NAME).app/ 26 | 27 | # 打包后重命名为 .tipa 只有打ipa包的时候才需要,打deb包不需要 28 | after-package:: 29 | @if [ "$(PACKAGE_FORMAT)" = "ipa" ]; then \ 30 | echo -e "\033[32mRenaming .ipa to .tipa...\033[0m"; \ 31 | mv ./packages/com.developlab.batteryinfo_$(BUILD_VERSION).ipa ./packages/com.developlab.batteryinfo_$(BUILD_VERSION).tipa || echo -e "\033[31mNo .ipa file found.\033[0m"; \ 32 | echo -e "\033[1;32m\n** Build Succeeded **\n\033[0m"; \ 33 | fi 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BatteryInfo 2 | iOS Utils TrollStore App check and record battery information 3 | TrollStore App battery information, requires TrollStore or jailbreak installation, supports iOS 12.2 at minimum, below iOS 14.0 can use `deb` to install. 4 | Now available 5 | Havoc 6 | [Havoc](https://havoc.app/package/batteryinfo) 7 | 8 | **Note: This project will not release/store files in parts other than the App sandbox, and will NOT include ANY URL Scheme. You can use it with confidence and will not be detected by third-party app on App Stores. For specific issues, please [view](https://bsky.app/profile/opa334.bsky.social/post/3ll7zkia24c2s).** 9 | If you encounter a bug or a desired function or feature, you can [submit an issue](https://github.com/DevelopCubeLab/BatteryInfo/issues/new) 10 | 11 | TrollStore App 电池信息,需要TrollStore/越狱安装,最低支持iOS12.2,低于14.0系统的用户可以使用`deb`安装。 12 | 现已上架 13 | Havoc 14 | [Havoc](https://havoc.app/package/batteryinfo) 15 | 16 | **注意:本项目不会在除本App沙盒以外的部分释放/存储文件,并且不会包含任何URL Scheme,您可以放心使用,而不会被第三方App Store的App检测到,具体问题请[查看](https://bsky.app/profile/opa334.bsky.social/post/3ll7zkia24c2s)** 17 | 持续改进,如遇到Bug或者想要的功能可以[提交issue](https://github.com/DevelopCubeLab/BatteryInfo/issues/new) 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /SettingsBatteryHelper/Makefile: -------------------------------------------------------------------------------- 1 | TARGET := iphone:clang:latest:14.0 2 | ARCHS := arm64e 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | TOOL_NAME = SettingsBatteryHelper 7 | 8 | SettingsBatteryHelper_FILES = main.m 9 | SettingsBatteryHelper_CFLAGS = -fobjc-arc 10 | SettingsBatteryHelper_CODESIGN_FLAGS = -Sentitlements.plist 11 | SettingsBatteryHelper_INSTALL_PATH = /usr/local/bin 12 | 13 | include $(THEOS_MAKE_PATH)/tool.mk 14 | 15 | after-stage:: 16 | @echo "Copying file to parent directory..." 17 | cp $(THEOS_STAGING_DIR)/usr/local/bin/$(TOOL_NAME) ../$(TOOL_NAME)/ 18 | @echo "Copy completed." 19 | -------------------------------------------------------------------------------- /SettingsBatteryHelper/SettingsBatteryHelper: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevelopCubeLab/BatteryInfo/ba48f11000d8bf514a5ffbe421cc920f9a7811c1/SettingsBatteryHelper/SettingsBatteryHelper -------------------------------------------------------------------------------- /SettingsBatteryHelper/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | application-identifier 6 | com.developlab.batteryinfohelper 7 | platform-application 8 | 9 | com.apple.private.persona-mgmt 10 | 11 | com.apple.private.security.no-sandbox 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SettingsBatteryHelper/main.m: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define BATTERY_HEALTH_PATH "/var/MobileSoftwareUpdate/Hardware/Battery/Library/Preferences/com.apple.batteryhealthdata.plist" 11 | 12 | void print_battery_health() { 13 | CFURLRef fileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, CFSTR(BATTERY_HEALTH_PATH), kCFURLPOSIXPathStyle, false); 14 | CFReadStreamRef stream = CFReadStreamCreateWithFile(kCFAllocatorDefault, fileURL); 15 | 16 | if (!stream || !CFReadStreamOpen(stream)) { 17 | printf("ERROR: Cannot open battery health file\n"); 18 | return; 19 | } 20 | 21 | CFPropertyListRef plist = CFPropertyListCreateWithStream(kCFAllocatorDefault, stream, 0, kCFPropertyListImmutable, NULL, NULL); 22 | CFReadStreamClose(stream); 23 | CFRelease(stream); 24 | CFRelease(fileURL); 25 | 26 | if (!plist || CFGetTypeID(plist) != CFDictionaryGetTypeID()) { 27 | printf("ERROR: Invalid battery health data\n"); 28 | return; 29 | } 30 | 31 | CFDictionaryRef dict = (CFDictionaryRef)plist; 32 | 33 | CFNumberRef cycleCountNum = CFDictionaryGetValue(dict, CFSTR("CycleCount")); 34 | CFNumberRef maxCapacityNum = CFDictionaryGetValue(dict, CFSTR("Maximum Capacity Percent")); 35 | 36 | int cycleCount = -1; 37 | int maxCapacity = -1; 38 | 39 | if (cycleCountNum) { 40 | CFNumberGetValue(cycleCountNum, kCFNumberIntType, &cycleCount); 41 | } 42 | 43 | if (maxCapacityNum) { 44 | CFNumberGetValue(maxCapacityNum, kCFNumberIntType, &maxCapacity); 45 | } 46 | 47 | printf("{\"CycleCount\": %d, \"Maximum Capacity Percent\": %d}\n", cycleCount, maxCapacity); 48 | CFRelease(plist); 49 | } 50 | 51 | int main(int argc, char *argv[]) { 52 | // 确保进程运行在 root 权限 53 | setuid(0); 54 | setgid(0); 55 | 56 | if (getuid() != 0) { 57 | printf("ERROR: BatteryHealthHelper must be run as root.\n"); 58 | return -1; 59 | } 60 | 61 | print_battery_health(); 62 | 63 | // 确保缓冲区刷新 64 | fflush(stdout); 65 | fflush(stderr); 66 | 67 | return 0; 68 | } 69 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.developlab.batteryinfo 2 | Name: Battery Info 3 | Version: 1.1.9 4 | Architecture: iphoneos-arm 5 | Description: Battery information 6 | Maintainer: developlab 7 | Author: developlab 8 | Section: Utilities 9 | Depends: firmware (>= 12.2) | ${LIBSWIFT} 10 | -------------------------------------------------------------------------------- /entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | get-task-allow 6 | 7 | platform-application 8 | 9 | com.apple.private.security.no-sandbox 10 | 11 | com.apple.private.persona-mgmt 12 | 13 | com.apple.private.security.storage.AppDataContainers 14 | 15 | com.apple.security.iokit-user-client-class 16 | 17 | IOPMPowerSourceClient 18 | 19 | com.apple.security.exception.iokit-user-client-class 20 | 21 | IOAccelerator 22 | IOGPUDeviceUserClient 23 | IOSurfaceRootUserClient 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------