├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── BatteryView.podspec ├── CHANGELOG.md ├── Example ├── BatteryViewDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── BatteryViewDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── BatteryShapeDemo.swift │ └── Info.plist ├── LICENSE.txt ├── Package.swift ├── README.md ├── Screenshots └── Battery.png └── Sources ├── .swiftformat ├── .swiftlint.yml ├── BatteryShape.swift ├── BatteryView.swift └── PrivacyInfo.xcprivacy /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [yonat] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: yonat 7 | 8 | --- 9 | 10 | **Description of the problem:** 11 | [description] 12 | 13 | **Minimal project that reproduces the problem (so I'll be able to figure out how to fix it):** 14 | [link to a Minimal Reproducible Example as described at https://ootips.org/yonat/repex ] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: yonat 7 | 8 | --- 9 | 10 | **Description:** 11 | [description] 12 | 13 | **Problems I encountered when trying to implement this myself:** 14 | [if none, please submit a pull request.] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Example/Pods 2 | 3 | -------------------------------------------------------------------------------- /BatteryView.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | 4 | s.name = "BatteryView" 5 | s.version = "1.4.4" 6 | s.summary = "Simple battery shaped UIView." 7 | 8 | s.description = <<-DESC 9 | Usage: 10 | 11 | ```swift 12 | let batteryView = BatteryView(frame: smallRect) 13 | batteryView.level = 42 // anywhere in 0...100 14 | ``` 15 | DESC 16 | 17 | s.homepage = "https://github.com/yonat/BatteryView" 18 | s.screenshots = "https://raw.githubusercontent.com/yonat/BatteryView/master/Screenshots/Battery.png" 19 | s.license = { :type => "MIT", :file => "LICENSE.txt" } 20 | 21 | s.author = { "Yonat Sharon" => "yonat@ootips.org" } 22 | 23 | s.swift_version = '4.2' 24 | s.swift_versions = ['4.2', '5.0'] 25 | s.platform = :ios, "11.0" 26 | s.requires_arc = true 27 | s.weak_framework = 'SwiftUI' 28 | 29 | s.source = { :git => "https://github.com/yonat/BatteryView.git", :tag => s.version } 30 | s.source_files = "Sources/*" 31 | s.resource_bundles = {s.name => ['Sources/PrivacyInfo.xcprivacy']} 32 | 33 | s.pod_target_xcconfig = { 'LD_RUNPATH_SEARCH_PATHS' => '$(FRAMEWORK_SEARCH_PATHS)' } # fix Interface Builder render error 34 | 35 | end 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.4.4] - 2024-11-01 8 | 9 | ### Fixed 10 | - fix path of privacy manifest PrivacyInfo.xcprivacy. 11 | 12 | ## [1.4.0] - 2023-08-19 13 | 14 | ### Added 15 | - support iOS 13 dark mode. 16 | - add privacy manifest PrivacyInfo.xcprivacy. 17 | - add SwiftUI wrapper `BatteryShape`. 18 | 19 | ## [1.3.9] - 2019-09-12 20 | 21 | ### Fixed 22 | - show `noLevelText` when embedded in a SwiftUI view. 23 | - fill terminal when battery level is 100. 24 | 25 | ## [1.3.8] - 2019-08-22 26 | 27 | ### Added 28 | - support Swift Package Manager. 29 | 30 | ### Fixed 31 | - fix Interface Builder render error. 32 | 33 | ## [1.3.7] - 2019-07-13 34 | 35 | ### Added 36 | - add accessibility support. 37 | 38 | ## [1.3.6] - 2019-07-10 39 | 40 | ### Added 41 | - when the level is undefined or out of bounds, show question mark on battery (configurable via `noLevelText`). 42 | 43 | ## [1.3.5] - 2019-06-21 44 | 45 | ### Changed 46 | - Swift 5, CocoaPods 1.7. 47 | 48 | ## [1.3.4] - 2019-06-01 49 | 50 | ### Added 51 | - add `currentFillColor`. 52 | 53 | ## [1.3.3] - 2019-05-29 54 | 55 | ### Fixed 56 | - fix compile error in Xcode 10.2 57 | 58 | ## [1.3.2] 59 | 60 | ### Added 61 | - add `gradientThreshold` to allow gradually changing from `highLevelColor` to `lowLevelColor`. 62 | 63 | ## [1.3.1] 64 | 65 | ### Added 66 | - support UIAppearance. 67 | 68 | ## [1.3.0] 69 | 70 | ### Changed 71 | - Swift 4.2 72 | 73 | ### Fixed 74 | - make the battery full at level 100%. (Thanks lampmanyao) 75 | 76 | ## [1.2.2] - 2018-07-06 77 | 78 | ### Fixed 79 | - ensure the corners of the fill aren't sticking out when the border width is 1. (Thanks rondavis007) 80 | 81 | ## [1.2.1] - 2018-05-20 82 | 83 | ### Changed 84 | - use SwiftLint and SwiftFormat 85 | 86 | ## [1.2] - 2017-11-10 87 | 88 | ### Added 89 | - added borderColor. 90 | 91 | ## [1.1] - 2017-07-15 92 | 93 | ### Changed 94 | - Swift 4 95 | -------------------------------------------------------------------------------- /Example/BatteryViewDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DC64D11823311F7A00506A50 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC64D11723311F7A00506A50 /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 11 | DC64D11A23311F9900506A50 /* BatteryShapeDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC64D11923311F9900506A50 /* BatteryShapeDemo.swift */; }; 12 | DC64D11C2331205200506A50 /* BatteryShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC64D11B2331205200506A50 /* BatteryShape.swift */; }; 13 | DCBCBEF21DEC700D00844FCB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBCBEF11DEC700D00844FCB /* AppDelegate.swift */; }; 14 | DCBCBEF71DEC700D00844FCB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCBCBEF51DEC700D00844FCB /* Main.storyboard */; }; 15 | DCBCBEF91DEC700D00844FCB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCBCBEF81DEC700D00844FCB /* Assets.xcassets */; }; 16 | DCBCBEFC1DEC700D00844FCB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCBCBEFA1DEC700D00844FCB /* LaunchScreen.storyboard */; }; 17 | DCBCBF041DEC70DD00844FCB /* BatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBCBF031DEC70DD00844FCB /* BatteryView.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | DC26956C229E73B200821568 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; 22 | DC26956D229E73B200821568 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 23 | DC64D11723311F7A00506A50 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 24 | DC64D11923311F9900506A50 /* BatteryShapeDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryShapeDemo.swift; sourceTree = ""; }; 25 | DC64D11B2331205200506A50 /* BatteryShape.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BatteryShape.swift; path = ../Sources/BatteryShape.swift; sourceTree = ""; }; 26 | DCBCBEEE1DEC700D00844FCB /* BatteryViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BatteryViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | DCBCBEF11DEC700D00844FCB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 28 | DCBCBEF61DEC700D00844FCB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | DCBCBEF81DEC700D00844FCB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | DCBCBEFB1DEC700D00844FCB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | DCBCBEFD1DEC700D00844FCB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | DCBCBF031DEC70DD00844FCB /* BatteryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BatteryView.swift; path = ../Sources/BatteryView.swift; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | DCBCBEEB1DEC700D00844FCB /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | DC64D11823311F7A00506A50 /* SwiftUI.framework in Frameworks */, 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | DC64D11623311F7A00506A50 /* Frameworks */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | DC64D11723311F7A00506A50 /* SwiftUI.framework */, 51 | ); 52 | name = Frameworks; 53 | sourceTree = ""; 54 | }; 55 | DCBCBEE51DEC700D00844FCB = { 56 | isa = PBXGroup; 57 | children = ( 58 | DC26956C229E73B200821568 /* CHANGELOG.md */, 59 | DC26956D229E73B200821568 /* README.md */, 60 | DCBCBF031DEC70DD00844FCB /* BatteryView.swift */, 61 | DC64D11B2331205200506A50 /* BatteryShape.swift */, 62 | DCBCBEF01DEC700D00844FCB /* BatteryViewDemo */, 63 | DCBCBEEF1DEC700D00844FCB /* Products */, 64 | DC64D11623311F7A00506A50 /* Frameworks */, 65 | ); 66 | sourceTree = ""; 67 | }; 68 | DCBCBEEF1DEC700D00844FCB /* Products */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | DCBCBEEE1DEC700D00844FCB /* BatteryViewDemo.app */, 72 | ); 73 | name = Products; 74 | sourceTree = ""; 75 | }; 76 | DCBCBEF01DEC700D00844FCB /* BatteryViewDemo */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | DCBCBEF11DEC700D00844FCB /* AppDelegate.swift */, 80 | DC64D11923311F9900506A50 /* BatteryShapeDemo.swift */, 81 | DCBCBEF51DEC700D00844FCB /* Main.storyboard */, 82 | DCBCBEF81DEC700D00844FCB /* Assets.xcassets */, 83 | DCBCBEFA1DEC700D00844FCB /* LaunchScreen.storyboard */, 84 | DCBCBEFD1DEC700D00844FCB /* Info.plist */, 85 | ); 86 | path = BatteryViewDemo; 87 | sourceTree = ""; 88 | }; 89 | /* End PBXGroup section */ 90 | 91 | /* Begin PBXNativeTarget section */ 92 | DCBCBEED1DEC700D00844FCB /* BatteryViewDemo */ = { 93 | isa = PBXNativeTarget; 94 | buildConfigurationList = DCBCBF001DEC700D00844FCB /* Build configuration list for PBXNativeTarget "BatteryViewDemo" */; 95 | buildPhases = ( 96 | DCD5945920B17EC4001B6E44 /* SwiftFormat */, 97 | DCBCBEEA1DEC700D00844FCB /* Sources */, 98 | DCBCBEEB1DEC700D00844FCB /* Frameworks */, 99 | DCBCBEEC1DEC700D00844FCB /* Resources */, 100 | DCD5945A20B17ED4001B6E44 /* SwiftLint */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = BatteryViewDemo; 107 | productName = BatteryViewDemo; 108 | productReference = DCBCBEEE1DEC700D00844FCB /* BatteryViewDemo.app */; 109 | productType = "com.apple.product-type.application"; 110 | }; 111 | /* End PBXNativeTarget section */ 112 | 113 | /* Begin PBXProject section */ 114 | DCBCBEE61DEC700D00844FCB /* Project object */ = { 115 | isa = PBXProject; 116 | attributes = { 117 | LastSwiftUpdateCheck = 0810; 118 | LastUpgradeCheck = 1020; 119 | ORGANIZATIONNAME = "Yonat Sharon"; 120 | TargetAttributes = { 121 | DCBCBEED1DEC700D00844FCB = { 122 | CreatedOnToolsVersion = 8.1; 123 | LastSwiftMigration = 1020; 124 | ProvisioningStyle = Automatic; 125 | }; 126 | }; 127 | }; 128 | buildConfigurationList = DCBCBEE91DEC700D00844FCB /* Build configuration list for PBXProject "BatteryViewDemo" */; 129 | compatibilityVersion = "Xcode 3.2"; 130 | developmentRegion = en; 131 | hasScannedForEncodings = 0; 132 | knownRegions = ( 133 | en, 134 | Base, 135 | ); 136 | mainGroup = DCBCBEE51DEC700D00844FCB; 137 | productRefGroup = DCBCBEEF1DEC700D00844FCB /* Products */; 138 | projectDirPath = ""; 139 | projectRoot = ""; 140 | targets = ( 141 | DCBCBEED1DEC700D00844FCB /* BatteryViewDemo */, 142 | ); 143 | }; 144 | /* End PBXProject section */ 145 | 146 | /* Begin PBXResourcesBuildPhase section */ 147 | DCBCBEEC1DEC700D00844FCB /* Resources */ = { 148 | isa = PBXResourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | DCBCBEFC1DEC700D00844FCB /* LaunchScreen.storyboard in Resources */, 152 | DCBCBEF91DEC700D00844FCB /* Assets.xcassets in Resources */, 153 | DCBCBEF71DEC700D00844FCB /* Main.storyboard in Resources */, 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXResourcesBuildPhase section */ 158 | 159 | /* Begin PBXShellScriptBuildPhase section */ 160 | DCD5945920B17EC4001B6E44 /* SwiftFormat */ = { 161 | isa = PBXShellScriptBuildPhase; 162 | alwaysOutOfDate = 1; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | ); 166 | inputPaths = ( 167 | ); 168 | name = SwiftFormat; 169 | outputPaths = ( 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | shellPath = /bin/sh; 173 | shellScript = "if which swiftformat >/dev/null; then\nswiftformat --swiftversion ${SWIFT_VERSION} \"${SRCROOT}/../Sources\"\nswiftformat --swiftversion ${SWIFT_VERSION} --config \"${SRCROOT}/../Sources/.swiftformat\" \"${SRCROOT}\"\nfi\n"; 174 | }; 175 | DCD5945A20B17ED4001B6E44 /* SwiftLint */ = { 176 | isa = PBXShellScriptBuildPhase; 177 | alwaysOutOfDate = 1; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | ); 181 | inputPaths = ( 182 | ); 183 | name = SwiftLint; 184 | outputPaths = ( 185 | ); 186 | runOnlyForDeploymentPostprocessing = 0; 187 | shellPath = /bin/sh; 188 | shellScript = "if which swiftlint >/dev/null; then\nswiftlint lint --config \"${SRCROOT}/../Sources/.swiftlint.yml\"\ncd \"${SRCROOT}/../Sources\"\nswiftlint\nfi\n"; 189 | }; 190 | /* End PBXShellScriptBuildPhase section */ 191 | 192 | /* Begin PBXSourcesBuildPhase section */ 193 | DCBCBEEA1DEC700D00844FCB /* Sources */ = { 194 | isa = PBXSourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | DC64D11A23311F9900506A50 /* BatteryShapeDemo.swift in Sources */, 198 | DC64D11C2331205200506A50 /* BatteryShape.swift in Sources */, 199 | DCBCBF041DEC70DD00844FCB /* BatteryView.swift in Sources */, 200 | DCBCBEF21DEC700D00844FCB /* AppDelegate.swift in Sources */, 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | }; 204 | /* End PBXSourcesBuildPhase section */ 205 | 206 | /* Begin PBXVariantGroup section */ 207 | DCBCBEF51DEC700D00844FCB /* Main.storyboard */ = { 208 | isa = PBXVariantGroup; 209 | children = ( 210 | DCBCBEF61DEC700D00844FCB /* Base */, 211 | ); 212 | name = Main.storyboard; 213 | sourceTree = ""; 214 | }; 215 | DCBCBEFA1DEC700D00844FCB /* LaunchScreen.storyboard */ = { 216 | isa = PBXVariantGroup; 217 | children = ( 218 | DCBCBEFB1DEC700D00844FCB /* Base */, 219 | ); 220 | name = LaunchScreen.storyboard; 221 | sourceTree = ""; 222 | }; 223 | /* End PBXVariantGroup section */ 224 | 225 | /* Begin XCBuildConfiguration section */ 226 | DCBCBEFE1DEC700D00844FCB /* Debug */ = { 227 | isa = XCBuildConfiguration; 228 | buildSettings = { 229 | ALWAYS_SEARCH_USER_PATHS = NO; 230 | CLANG_ANALYZER_NONNULL = YES; 231 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 232 | CLANG_CXX_LIBRARY = "libc++"; 233 | CLANG_ENABLE_MODULES = YES; 234 | CLANG_ENABLE_OBJC_ARC = YES; 235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_COMMA = YES; 238 | CLANG_WARN_CONSTANT_CONVERSION = YES; 239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 242 | CLANG_WARN_EMPTY_BODY = YES; 243 | CLANG_WARN_ENUM_CONVERSION = YES; 244 | CLANG_WARN_INFINITE_RECURSION = YES; 245 | CLANG_WARN_INT_CONVERSION = YES; 246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 251 | CLANG_WARN_STRICT_PROTOTYPES = YES; 252 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 253 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 254 | CLANG_WARN_UNREACHABLE_CODE = YES; 255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 256 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 257 | COPY_PHASE_STRIP = NO; 258 | DEBUG_INFORMATION_FORMAT = dwarf; 259 | ENABLE_STRICT_OBJC_MSGSEND = YES; 260 | ENABLE_TESTABILITY = YES; 261 | GCC_C_LANGUAGE_STANDARD = gnu99; 262 | GCC_DYNAMIC_NO_PIC = NO; 263 | GCC_NO_COMMON_BLOCKS = YES; 264 | GCC_OPTIMIZATION_LEVEL = 0; 265 | GCC_PREPROCESSOR_DEFINITIONS = ( 266 | "DEBUG=1", 267 | "$(inherited)", 268 | ); 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 276 | MTL_ENABLE_DEBUG_INFO = YES; 277 | ONLY_ACTIVE_ARCH = YES; 278 | SDKROOT = iphoneos; 279 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 280 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 281 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 282 | SWIFT_VERSION = 5.0; 283 | TARGETED_DEVICE_FAMILY = "1,2"; 284 | }; 285 | name = Debug; 286 | }; 287 | DCBCBEFF1DEC700D00844FCB /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ALWAYS_SEARCH_USER_PATHS = NO; 291 | CLANG_ANALYZER_NONNULL = YES; 292 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 293 | CLANG_CXX_LIBRARY = "libc++"; 294 | CLANG_ENABLE_MODULES = YES; 295 | CLANG_ENABLE_OBJC_ARC = YES; 296 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 297 | CLANG_WARN_BOOL_CONVERSION = YES; 298 | CLANG_WARN_COMMA = YES; 299 | CLANG_WARN_CONSTANT_CONVERSION = YES; 300 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 301 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 302 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 303 | CLANG_WARN_EMPTY_BODY = YES; 304 | CLANG_WARN_ENUM_CONVERSION = YES; 305 | CLANG_WARN_INFINITE_RECURSION = YES; 306 | CLANG_WARN_INT_CONVERSION = YES; 307 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 308 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 309 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 310 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 311 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 312 | CLANG_WARN_STRICT_PROTOTYPES = YES; 313 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 314 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 315 | CLANG_WARN_UNREACHABLE_CODE = YES; 316 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 317 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 318 | COPY_PHASE_STRIP = NO; 319 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 320 | ENABLE_NS_ASSERTIONS = NO; 321 | ENABLE_STRICT_OBJC_MSGSEND = YES; 322 | GCC_C_LANGUAGE_STANDARD = gnu99; 323 | GCC_NO_COMMON_BLOCKS = YES; 324 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 325 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 326 | GCC_WARN_UNDECLARED_SELECTOR = YES; 327 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 328 | GCC_WARN_UNUSED_FUNCTION = YES; 329 | GCC_WARN_UNUSED_VARIABLE = YES; 330 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 331 | MTL_ENABLE_DEBUG_INFO = NO; 332 | SDKROOT = iphoneos; 333 | SWIFT_COMPILATION_MODE = wholemodule; 334 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 335 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 336 | SWIFT_VERSION = 5.0; 337 | TARGETED_DEVICE_FAMILY = "1,2"; 338 | VALIDATE_PRODUCT = YES; 339 | }; 340 | name = Release; 341 | }; 342 | DCBCBF011DEC700D00844FCB /* Debug */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 346 | INFOPLIST_FILE = BatteryViewDemo/Info.plist; 347 | LD_RUNPATH_SEARCH_PATHS = ( 348 | "$(inherited)", 349 | "@executable_path/Frameworks", 350 | ); 351 | PRODUCT_BUNDLE_IDENTIFIER = com.roysharon.BatteryViewDemo; 352 | PRODUCT_NAME = "$(TARGET_NAME)"; 353 | }; 354 | name = Debug; 355 | }; 356 | DCBCBF021DEC700D00844FCB /* Release */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 360 | INFOPLIST_FILE = BatteryViewDemo/Info.plist; 361 | LD_RUNPATH_SEARCH_PATHS = ( 362 | "$(inherited)", 363 | "@executable_path/Frameworks", 364 | ); 365 | PRODUCT_BUNDLE_IDENTIFIER = com.roysharon.BatteryViewDemo; 366 | PRODUCT_NAME = "$(TARGET_NAME)"; 367 | }; 368 | name = Release; 369 | }; 370 | /* End XCBuildConfiguration section */ 371 | 372 | /* Begin XCConfigurationList section */ 373 | DCBCBEE91DEC700D00844FCB /* Build configuration list for PBXProject "BatteryViewDemo" */ = { 374 | isa = XCConfigurationList; 375 | buildConfigurations = ( 376 | DCBCBEFE1DEC700D00844FCB /* Debug */, 377 | DCBCBEFF1DEC700D00844FCB /* Release */, 378 | ); 379 | defaultConfigurationIsVisible = 0; 380 | defaultConfigurationName = Release; 381 | }; 382 | DCBCBF001DEC700D00844FCB /* Build configuration list for PBXNativeTarget "BatteryViewDemo" */ = { 383 | isa = XCConfigurationList; 384 | buildConfigurations = ( 385 | DCBCBF011DEC700D00844FCB /* Debug */, 386 | DCBCBF021DEC700D00844FCB /* Release */, 387 | ); 388 | defaultConfigurationIsVisible = 0; 389 | defaultConfigurationName = Release; 390 | }; 391 | /* End XCConfigurationList section */ 392 | }; 393 | rootObject = DCBCBEE61DEC700D00844FCB /* Project object */; 394 | } 395 | -------------------------------------------------------------------------------- /Example/BatteryViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/BatteryViewDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/BatteryViewDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // swiftlint:disable numbers_smell 4 | // 5 | // Created by Yonat Sharon on 28.11.2016. 6 | // Copyright © 2016 Yonat Sharon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | #if canImport(SwiftUI) 12 | import SwiftUI 13 | #endif 14 | 15 | class BatteryViewController: UIViewController { 16 | @IBOutlet var battery: BatteryView! 17 | @IBOutlet var showSwiftUIButton: UIButton! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | let bigBattery = BatteryView(frame: view.bounds.insetBy(dx: 80, dy: 150).offsetBy(dx: 0, dy: -20)) 22 | bigBattery.gradientThreshold = 50 23 | view.addSubview(bigBattery) 24 | 25 | let littleBattery = BatteryView(frame: CGRect(x: 50, y: 30, width: 25, height: 45)) 26 | littleBattery.lowThreshold = 20 27 | littleBattery.gradientThreshold = 80 28 | littleBattery.borderColor = .orange 29 | view.addSubview(littleBattery) 30 | 31 | let horizontalBettery = BatteryView(frame: CGRect(x: 140, y: 30, width: 140, height: 60)) 32 | horizontalBettery.direction = .minXEdge 33 | horizontalBettery.lowThreshold = 30 34 | horizontalBettery.gradientThreshold = 50 35 | horizontalBettery.borderWidth = 3 36 | horizontalBettery.highLevelColor = .purple 37 | horizontalBettery.lowLevelColor = .magenta 38 | horizontalBettery.backgroundColor = .cyan 39 | horizontalBettery.cornerRadius = 10 40 | view.addSubview(horizontalBettery) 41 | 42 | for i in 0 ... 10 { 43 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(i)) { 44 | [bigBattery, littleBattery, horizontalBettery].forEach { $0.level = 100 - 10 * i } 45 | } 46 | } 47 | } 48 | 49 | override func viewDidAppear(_ animated: Bool) { 50 | super.viewDidAppear(animated) 51 | if #available(iOS 13.0, *) { 52 | showSwiftUIButton.isHidden = false 53 | showSwiftUIButton.layer.borderWidth = 1 54 | showSwiftUIButton.layer.cornerRadius = showSwiftUIButton.frame.height / 2 55 | showSwiftUIButton.layer.borderColor = view.tintColor.cgColor 56 | } 57 | } 58 | 59 | @IBAction func changeBatteryLevel(_ sender: UISlider) { 60 | battery.level = Int(sender.value.rounded()) 61 | } 62 | 63 | @IBAction func showSwiftUIDemo() { 64 | #if canImport(SwiftUI) 65 | if #available(iOS 13.0, *) { 66 | present(UIHostingController(rootView: BatteryShapeDemo()), animated: true) 67 | } 68 | #endif 69 | } 70 | } 71 | 72 | @UIApplicationMain 73 | class AppDelegate: UIResponder, UIApplicationDelegate { 74 | var window: UIWindow? 75 | } 76 | -------------------------------------------------------------------------------- /Example/BatteryViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Example/BatteryViewDemo/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 | 27 | 28 | -------------------------------------------------------------------------------- /Example/BatteryViewDemo/Base.lproj/Main.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Example/BatteryViewDemo/BatteryShapeDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BatteryShapeDemo.swift 3 | // 4 | // Copyright © 2019 Yonat Sharon. All rights reserved. 5 | // 6 | 7 | // swiftlint:disable numbers_smell 8 | 9 | #if canImport(SwiftUI) 10 | 11 | import SwiftUI 12 | 13 | @available(iOS 13.0, *) 14 | struct BatteryShapeDemo: View { 15 | @State private var level: Double = -1 16 | 17 | var body: some View { 18 | VStack(spacing: 32) { 19 | Spacer() 20 | HStack(spacing: 32) { 21 | BatteryShape(level: $level.int, noLevelText: "") 22 | .aspectRatio(0.5, contentMode: .fit) 23 | BatteryShape(level: $level.int, gradientThreshold: 100, borderColor: .brown) 24 | .aspectRatio(0.75, contentMode: .fit) 25 | } 26 | .frame(height: 150) 27 | BatteryShape(level: $level.int, isVertical: false, noLevelText: "🤷🏼‍♂️") 28 | .aspectRatio(2, contentMode: .fit) 29 | .frame(height: 80) 30 | BatteryShape( 31 | level: $level.int, 32 | gradientThreshold: 100, 33 | direction: .minXEdge, 34 | highLevelColor: .blue, 35 | lowLevelColor: .orange, 36 | noLevelColor: .systemBackground, 37 | noLevelText: "", 38 | borderColor: .cyan, 39 | borderWidth: 3, 40 | cornerRadius: 6 41 | ) 42 | .aspectRatio(3, contentMode: .fit) 43 | .frame(height: 80) 44 | Spacer() 45 | VStack { 46 | Text("Change Battery Level:").bold() 47 | Slider(value: $level, in: 0.0 ... 100.0, step: 1.0) 48 | } 49 | Spacer() 50 | Button(action: { self.level = -1 }, label: { Text("Clear Battery Level").bold() }) 51 | Spacer() 52 | } 53 | .padding() 54 | .onAppear { 55 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 56 | if self.level != -1 { return } 57 | withAnimation { self.level = 100 } 58 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 59 | withAnimation { self.level = 50 } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | private extension Double { 67 | var int: Int { 68 | get { Int(self) } 69 | set { self = Double(newValue) } 70 | } 71 | } 72 | 73 | #endif 74 | -------------------------------------------------------------------------------- /Example/BatteryViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.4.0 19 | CFBundleVersion 20 | 48 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yonat Sharon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "BatteryView", 7 | platforms: [ 8 | .iOS(.v11) 9 | ], 10 | products: [ 11 | .library( name: "BatteryView", targets: ["BatteryView"]) 12 | ], 13 | dependencies: [], 14 | targets: [ 15 | .target(name: "BatteryView", dependencies: [], path: "Sources", resources: [.process("PrivacyInfo.xcprivacy")]) 16 | ], 17 | swiftLanguageVersions: [.v5] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BatteryView 2 | Simple battery shaped UIView. 3 | 4 | [![Swift Version][swift-image]][swift-url] 5 | [![Build Status][travis-image]][travis-url] 6 | [![License][license-image]][license-url] 7 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/BatteryView.svg)](https://img.shields.io/cocoapods/v/BatteryView.svg) 8 | [![Platform](https://img.shields.io/cocoapods/p/BatteryView.svg?style=flat)](http://cocoapods.org/pods/BatteryView) 9 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 10 | 11 |

12 | 13 |

14 | 15 | ## Usage 16 | 17 | ```swift 18 | let batteryView = BatteryView(frame: smallRect) 19 | batteryView.level = 42 // anywhere in 0...100 20 | batteryView.lowThreshold = 25 // battery fill becomes red if level is below this threshold 21 | batteryView.gradientThreshold = 50 // battery fill gradually changes from green to red below this threshold 22 | ``` 23 | 24 | ## SwiftUI Usage 25 | 26 | ```swift 27 | BatteryShape(level: $level) 28 | ``` 29 | 30 | ## Changing Appearance 31 | 32 | The properties below can be set in Interface Builder, in code, or through a UIAppearance proxy (e.g., `BatteryView.appearance().borderColor = .gray`). 33 | When using SwiftUI you can set them in the `BatteryShape` initializer . 34 | 35 | **Colors:** 36 | 37 | ```swift 38 | batteryView.borderColor = .darkGray 39 | batteryView.highLevelColor = .green 40 | batteryView.lowLevelColor = .red 41 | batteryView.noLevelColor = .gray 42 | batteryView.noLevelText = "?" // shown over battery when the level is undefined or out of bounds 43 | ``` 44 | 45 | **Battery Shape:** 46 | 47 | ```swift 48 | batteryView.direction = .minXEdge // terminal facing left 49 | 50 | batteryView.terminalLengthRatio = 0.1 // relative to battery length 51 | batteryView.terminalWidthRatio = 0.4 // relative to battery width 52 | 53 | batteryView.borderWidth = 2.5 // default is batteryLength / 20 54 | batteryView.cornerRadius = 5 // default is batteryLength / 10 55 | ``` 56 | 57 | ## Installation 58 | 59 | ### CocoaPods: 60 | 61 | ```ruby 62 | pod 'BatteryView' 63 | ``` 64 | 65 | ### Swift Package Manager: 66 | 67 | ```swift 68 | dependencies: [ 69 | .package(url: "https://github.com/yonat/BatteryView", from: "1.4.4") 70 | ] 71 | ``` 72 | 73 | ## Meta 74 | 75 | [@yonatsharon](https://twitter.com/yonatsharon) 76 | 77 | [https://github.com/yonat/BatteryView](https://github.com/yonat/BatteryView) 78 | 79 | [swift-image]:https://img.shields.io/badge/swift-4.2-orange.svg 80 | [swift-url]: https://swift.org/ 81 | [license-image]: https://img.shields.io/badge/License-MIT-blue.svg 82 | [license-url]: LICENSE.txt 83 | [travis-image]: https://img.shields.io/travis/dbader/node-datadog-metrics/master.svg?style=flat-square 84 | [travis-url]: https://travis-ci.org/dbader/node-datadog-metrics 85 | -------------------------------------------------------------------------------- /Screenshots/Battery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonat/BatteryView/7ca59ef00bbc1f454470e54d8366aa6551f5be27/Screenshots/Battery.png -------------------------------------------------------------------------------- /Sources/.swiftformat: -------------------------------------------------------------------------------- 1 | --exclude Pods, Example/Pods 2 | --commas always 3 | --ifdef no-indent 4 | --disable unusedArguments, andOperator, yodaConditions, anyObjectProtocol 5 | -------------------------------------------------------------------------------- /Sources/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - Pods 3 | - Example/Pods 4 | 5 | opt_in_rules: 6 | - array_init 7 | - closure_end_indentation 8 | - contains_over_first_not_nil 9 | - convenience_type 10 | - empty_count 11 | - empty_string 12 | - explicit_init 13 | - fatal_error_message 14 | - first_where 15 | - force_unwrapping 16 | - identical_operands 17 | - implicit_return 18 | - implicitly_unwrapped_optional 19 | - joined_default_parameter 20 | - last_where 21 | - literal_expression_end_indentation 22 | - nimble_operator 23 | - operator_usage_whitespace 24 | - overridden_super_call 25 | - override_in_extension 26 | - pattern_matching_keywords 27 | - prohibited_super_call 28 | - redundant_nil_coalescing 29 | - redundant_type_annotation 30 | - single_test_class 31 | - sorted_first_last 32 | - toggle_bool 33 | - unneeded_parentheses_in_closure_argument 34 | - vertical_parameter_alignment_on_call 35 | - vertical_whitespace_closing_braces 36 | - vertical_whitespace_opening_braces 37 | 38 | disabled_rules: 39 | - unused_closure_parameter 40 | - large_tuple 41 | - multiple_closures_with_trailing_closure 42 | 43 | empty_count: warning 44 | force_cast: warning 45 | 46 | trailing_comma: 47 | mandatory_comma: true 48 | 49 | identifier_name: 50 | excluded: 51 | - i 52 | - n 53 | - id 54 | - on 55 | - x 56 | - y 57 | - dx 58 | - dy 59 | 60 | line_length: 61 | warning: 150 62 | ignores_function_declarations: true 63 | ignores_comments: true 64 | ignores_interpolated_strings: true 65 | ignores_urls: true 66 | 67 | nesting: 68 | type_level: 69 | warning: 2 70 | 71 | custom_rules: 72 | already_true: 73 | regex: "== true" 74 | message: "Don't compare to true, just use the bool value." 75 | already_bool: 76 | regex: "== false" 77 | message: "Don't compare to false, just use !value." 78 | numbers_smell: 79 | regex: '(return |case |\w\(|: |\?\? |\, |== |<=? |>=? |\+= |\-= |\/= |\*= |%= |\w\.\w+ = )\(*\d{2,}' 80 | message: "Numbers smell; define a constant instead." 81 | auto_generated_leftovers: 82 | regex: 'func [^\n]*\{\n(\s*super\.[^\n]*\n(\s*\/\/[^\n]*\n)*|(\s*\/\/[^\n]*\n)+)\s*\}' 83 | message: "Delete auto-generated functions that you don't use" 84 | # commented_code: 85 | # regex: '(?, 20 | lowThreshold: Int? = nil, 21 | gradientThreshold: Int? = nil, 22 | direction: CGRectEdge? = nil, 23 | isVertical: Bool? = nil, 24 | terminalLengthRatio: CGFloat? = nil, 25 | terminalWidthRatio: CGFloat? = nil, 26 | highLevelColor: UIColor? = nil, 27 | lowLevelColor: UIColor? = nil, 28 | noLevelColor: UIColor? = nil, 29 | noLevelText: String? = nil, 30 | borderColor: UIColor? = nil, 31 | borderWidth: CGFloat? = nil, 32 | cornerRadius: CGFloat? = nil 33 | ) { 34 | _level = level 35 | uiView.translatesAutoresizingMaskIntoConstraints = false 36 | uiView.lowThreshold =? lowThreshold 37 | uiView.gradientThreshold =? gradientThreshold 38 | uiView.direction =? direction 39 | uiView.isVertical =? isVertical 40 | uiView.terminalLengthRatio =? terminalLengthRatio 41 | uiView.terminalWidthRatio =? terminalWidthRatio 42 | uiView.highLevelColor =? highLevelColor 43 | uiView.lowLevelColor =? lowLevelColor 44 | uiView.noLevelColor =? noLevelColor 45 | uiView.noLevelText =? noLevelText 46 | uiView.borderColor =? borderColor 47 | uiView.borderWidth =? borderWidth 48 | uiView.cornerRadius =? cornerRadius 49 | } 50 | 51 | public func makeUIView(context: UIViewRepresentableContext) -> BatteryView { 52 | uiView 53 | } 54 | 55 | public func updateUIView(_ uiView: BatteryView, context: UIViewRepresentableContext) { 56 | uiView.level = level 57 | } 58 | } 59 | 60 | infix operator =?: AssignmentPrecedence 61 | 62 | /// Assign iff right side is not nil. 63 | func =? (lhs: inout T, rhs: T?) { 64 | if nil != rhs, let rhs = rhs { 65 | lhs = rhs 66 | } 67 | } 68 | 69 | #endif 70 | -------------------------------------------------------------------------------- /Sources/BatteryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BatteryView.swift 3 | // 4 | // Created by Yonat Sharon on 6/1/15. 5 | // Copyright (c) 2015-6 Yonat Sharon. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension Int { 11 | static let fullBattery = 100 12 | } 13 | 14 | /// Show a battery oriented toward `direction`, charged `level` percent. 15 | /// Turns red when level drops below `lowThreshold`, or gradually when below `gradientThreshold`. 16 | @IBDesignable open class BatteryView: UIView { 17 | // MARK: - Behavior Properties 18 | 19 | /// 0 to 100 percent full, unavailable = -1 20 | @IBInspectable open var level: Int = -1 { didSet { layoutLevel() } } 21 | 22 | /// change color when level crosses the threshold 23 | @IBInspectable open dynamic var lowThreshold: Int = 10 { didSet { layoutFillColor() } } 24 | 25 | /// gradually change color when level crosses the threshold 26 | @IBInspectable open dynamic var gradientThreshold: Int = 0 { didSet { layoutFillColor() } } 27 | 28 | // MARK: - Appearance Properties 29 | 30 | /// direction of battery terminal 31 | @objc open dynamic var direction: CGRectEdge = .minYEdge { didSet { setNeedsLayout() } } 32 | 33 | /// simplified direction of battery terminal (for Interface Builder) 34 | @IBInspectable open dynamic var isVertical: Bool { 35 | get { direction == .maxYEdge || direction == .minYEdge } 36 | set { direction = newValue ? .minYEdge : .maxXEdge } 37 | } 38 | 39 | // relative size of battery terminal 40 | @IBInspectable open dynamic var terminalLengthRatio: CGFloat = 0.1 { didSet { setNeedsLayout() } } 41 | @IBInspectable open dynamic var terminalWidthRatio: CGFloat = 0.4 { didSet { setNeedsLayout() } } 42 | 43 | @IBInspectable open dynamic var highLevelColor: UIColor = .init(red: 0.0, green: 0.9, blue: 0.0, alpha: 1) { didSet { layoutFillColor() } } 44 | @IBInspectable open dynamic var lowLevelColor: UIColor = .init(red: 0.9, green: 0.0, blue: 0.0, alpha: 1) { didSet { layoutFillColor() } } 45 | @IBInspectable open dynamic var noLevelColor: UIColor = .init(white: 0.8, alpha: 1) { didSet { layoutFillColor() } } 46 | // swiftlint:enable redundant_type_annotation 47 | 48 | /// label shown over battery when the level is undefined or out of range 49 | @IBInspectable open dynamic var noLevelText: String? = "?" 50 | 51 | @IBInspectable open dynamic var borderColor: UIColor = .foreground { 52 | didSet { 53 | bodyOutline.borderColor = borderColor.cgColor 54 | terminalOutline.borderColor = borderColor.cgColor 55 | } 56 | } 57 | 58 | /// set as 0 for default borderWidth = length / 20 59 | @IBInspectable open dynamic var borderWidth: CGFloat = 0 { didSet { layoutBattery(); layoutLevel() } } 60 | 61 | /// set as 0 for default cornerRadius = length / 10 62 | @IBInspectable open dynamic var cornerRadius: CGFloat = 0 { didSet { layoutCornerRadius() } } 63 | 64 | public var currentFillColor: UIColor { 65 | switch level { 66 | case 0 ... lowThreshold: 67 | return lowLevelColor 68 | case gradientThreshold ... .fullBattery: 69 | return highLevelColor 70 | case lowThreshold ... .fullBattery: 71 | let fraction = CGFloat(level - lowThreshold) / CGFloat(min(gradientThreshold, .fullBattery) - lowThreshold) 72 | return lowLevelColor.blend(with: highLevelColor, fraction: fraction) 73 | default: 74 | return noLevelColor 75 | } 76 | } 77 | 78 | // MARK: - Overrides 79 | 80 | override open var backgroundColor: UIColor? { didSet { layoutFillColor() } } 81 | 82 | override public init(frame: CGRect) { 83 | super.init(frame: frame) 84 | setUp() 85 | } 86 | 87 | public required init?(coder: NSCoder) { 88 | super.init(coder: coder) 89 | setUp() 90 | } 91 | 92 | override open func layoutSubviews() { 93 | super.layoutSubviews() 94 | layoutBattery() 95 | layoutLevel() 96 | } 97 | 98 | // MARK: - Subviews & Sublayers 99 | 100 | public let noLevelLabel = UILabel() 101 | private let bodyOutline = CALayer() 102 | private let terminalOutline = CALayer() 103 | private let terminalOpening = CALayer() 104 | private let levelFill = CALayer() 105 | 106 | private func setUp() { 107 | layer.addSublayer(bodyOutline) 108 | bodyOutline.masksToBounds = true 109 | bodyOutline.addSublayer(levelFill) 110 | layer.addSublayer(terminalOutline) 111 | layer.addSublayer(terminalOpening) 112 | setNeedsLayout() 113 | 114 | noLevelLabel.translatesAutoresizingMaskIntoConstraints = false 115 | addSubview(noLevelLabel) 116 | 117 | isAccessibilityElement = true 118 | accessibilityIdentifier = "battery" 119 | accessibilityLabel = "battery" 120 | } 121 | 122 | // MARK: - Layout 123 | 124 | private var length: CGFloat { isVertical ? bounds.height : bounds.width } 125 | 126 | private func layoutBattery() { 127 | // divide total length into body and terminal 128 | let terminalLength = terminalLengthRatio * length 129 | var (terminalFrame, bodyFrame) = bounds.divided(atDistance: terminalLength, from: direction) 130 | 131 | // layout body 132 | bodyOutline.frame = bodyFrame 133 | bodyOutline.borderWidth = borderWidth != 0 ? borderWidth : length / 20 134 | noLevelLabel.center = bodyOutline.center 135 | noLevelLabel.font = noLevelLabel.font.withSize(min(bodyFrame.width, 0.75 * bodyFrame.height)) 136 | 137 | // layout terminal 138 | let parallelInsetRatio = (1 - terminalWidthRatio) / 2 139 | let perpendicularInset = bodyOutline.borderWidth 140 | var (dx, dy) = isVertical 141 | ? (parallelInsetRatio * bounds.width, -perpendicularInset) 142 | : (-perpendicularInset, parallelInsetRatio * bounds.height) 143 | terminalFrame = terminalFrame.insetBy(dx: dx, dy: dy) 144 | (_, terminalFrame) = terminalFrame.divided(atDistance: perpendicularInset, from: direction) 145 | terminalOutline.frame = terminalFrame 146 | terminalOutline.borderWidth = bodyOutline.borderWidth 147 | 148 | // cover terminal opening 149 | var (_, coverFrame) = terminalFrame.divided(atDistance: perpendicularInset, from: direction) 150 | (dx, dy) = isVertical ? (perpendicularInset, -0.25) : (-0.25, perpendicularInset) 151 | coverFrame = coverFrame.insetBy(dx: dx, dy: dy) 152 | terminalOpening.frame = coverFrame 153 | terminalOpening.backgroundColor = noLevelColor.cgColor 154 | 155 | // layout empty levelFill 156 | levelFill.frame = bodyFrame.insetBy(dx: perpendicularInset, dy: perpendicularInset).integral 157 | levelFill.backgroundColor = noLevelColor.cgColor 158 | } 159 | 160 | private func layoutLevel() { 161 | var levelFrame = bodyOutline.bounds.insetBy(dx: bodyOutline.borderWidth, dy: bodyOutline.borderWidth) 162 | if level >= 0 && level <= .fullBattery { 163 | let levelInset = (isVertical ? levelFrame.height : levelFrame.width) * CGFloat(.fullBattery - level) / CGFloat(Int.fullBattery) 164 | (_, levelFrame) = levelFrame.divided(atDistance: levelInset, from: direction) 165 | noLevelLabel.text = nil 166 | accessibilityValue = level.description 167 | } else { 168 | noLevelLabel.text = noLevelText 169 | noLevelLabel.sizeToFit() 170 | accessibilityValue = noLevelText 171 | } 172 | levelFill.frame = levelFrame.integral 173 | layoutCornerRadius() 174 | layoutFillColor() 175 | } 176 | 177 | private func layoutFillColor() { 178 | levelFill.backgroundColor = currentFillColor.cgColor 179 | switch level { 180 | case .fullBattery: 181 | terminalOpening.backgroundColor = currentFillColor.cgColor 182 | case 0 ..< .fullBattery: 183 | terminalOpening.backgroundColor = (backgroundColor ?? .background).cgColor 184 | default: 185 | terminalOpening.backgroundColor = noLevelColor.cgColor 186 | } 187 | } 188 | 189 | private func layoutCornerRadius() { 190 | bodyOutline.cornerRadius = cornerRadius != 0 ? cornerRadius : length / 10 191 | terminalOutline.cornerRadius = bodyOutline.cornerRadius / 2 192 | } 193 | } 194 | 195 | // swiftlint:disable identifier_name 196 | extension UIColor { 197 | func blend(with otherColor: UIColor, fraction: CGFloat) -> UIColor { 198 | let f = min(1, max(0, fraction)) 199 | var h1: CGFloat = 0, s1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0 200 | getHue(&h1, saturation: &s1, brightness: &b1, alpha: &a1) 201 | var h2: CGFloat = 0, s2: CGFloat = 0, b2: CGFloat = 0, a2: CGFloat = 0 202 | otherColor.getHue(&h2, saturation: &s2, brightness: &b2, alpha: &a2) 203 | let h = h1 + (h2 - h1) * f 204 | let s = s1 + (s2 - b1) * f 205 | let b = b1 + (b2 - b1) * f 206 | let a = a1 + (a2 - a1) * f 207 | return UIColor(hue: h, saturation: s, brightness: b, alpha: a) 208 | } 209 | } 210 | 211 | extension CALayer { 212 | var center: CGPoint { 213 | CGPoint(x: frame.midX, y: frame.midY) 214 | } 215 | } 216 | 217 | extension UIColor { 218 | static var background: UIColor { 219 | if #available(iOS 13, *) { 220 | return .systemBackground 221 | } else { 222 | return .white 223 | } 224 | } 225 | 226 | static var foreground: UIColor { 227 | if #available(iOS 13, *) { 228 | return .label 229 | } else { 230 | return .black 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /Sources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyCollectedDataTypes 8 | 9 | NSPrivacyAccessedAPITypes 10 | 11 | NSPrivacyTrackingDomains 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------