├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Demo ├── Ticker Label 2.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved ├── Ticker Label.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── Ticker Label │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info 2.plist │ ├── Info.plist │ ├── SceneDelegate.swift │ └── ViewController.swift ├── Ticker LabelTests │ ├── Info.plist │ └── Ticker_LabelTests.swift └── Ticker LabelUITests │ ├── Info.plist │ └── Ticker_LabelUITests.swift ├── Package.swift ├── README.md ├── Sources └── MOTickerLabel │ ├── Dollar.swift │ ├── MOSingleCounterLabel.swift │ ├── MOTickerLabel.swift │ ├── UIImage+Extensions.swift │ ├── UILabel+Extensions.swift │ └── UIView+Extensions.swift └── Tests ├── LinuxMain.swift └── MOTickerLabelTests ├── MOTickerLabelTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Ticker Label 2.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Ticker Label 2.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "MOTickerLabel", 6 | "repositoryURL": "https://github.com/mosaic-io/MOTickerLabel", 7 | "state": { 8 | "branch": null, 9 | "revision": "5bf460582362529fa24a125f00aa10b8d752f98b", 10 | "version": "1.0.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Ticker Label.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CAC1E015240B8D8100ABFD34 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC1E014240B8D8100ABFD34 /* AppDelegate.swift */; }; 11 | CAC1E017240B8D8100ABFD34 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC1E016240B8D8100ABFD34 /* SceneDelegate.swift */; }; 12 | CAC1E019240B8D8100ABFD34 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC1E018240B8D8100ABFD34 /* ViewController.swift */; }; 13 | CAC1E01C240B8D8100ABFD34 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CAC1E01A240B8D8100ABFD34 /* Main.storyboard */; }; 14 | CAC1E01E240B8D8F00ABFD34 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAC1E01D240B8D8F00ABFD34 /* Assets.xcassets */; }; 15 | CAC1E021240B8D8F00ABFD34 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CAC1E01F240B8D8F00ABFD34 /* LaunchScreen.storyboard */; }; 16 | CAC1E02C240B8D8F00ABFD34 /* Ticker_LabelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC1E02B240B8D8F00ABFD34 /* Ticker_LabelTests.swift */; }; 17 | CAC1E037240B8D9000ABFD34 /* Ticker_LabelUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC1E036240B8D9000ABFD34 /* Ticker_LabelUITests.swift */; }; 18 | CAE30914240C45F200C30E61 /* MOTickerLabel in Frameworks */ = {isa = PBXBuildFile; productRef = CAE30913240C45F200C30E61 /* MOTickerLabel */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | CAC1E028240B8D8F00ABFD34 /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = CAC1E009240B8D8100ABFD34 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = CAC1E010240B8D8100ABFD34; 27 | remoteInfo = "Ticker Label"; 28 | }; 29 | CAC1E033240B8D9000ABFD34 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = CAC1E009240B8D8100ABFD34 /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = CAC1E010240B8D8100ABFD34; 34 | remoteInfo = "Ticker Label"; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | CAC1E011240B8D8100ABFD34 /* Ticker Label.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ticker Label.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | CAC1E014240B8D8100ABFD34 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 41 | CAC1E016240B8D8100ABFD34 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 42 | CAC1E018240B8D8100ABFD34 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 43 | CAC1E01B240B8D8100ABFD34 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 44 | CAC1E01D240B8D8F00ABFD34 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 45 | CAC1E020240B8D8F00ABFD34 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 46 | CAC1E022240B8D8F00ABFD34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | CAC1E027240B8D8F00ABFD34 /* Ticker LabelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Ticker LabelTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | CAC1E02B240B8D8F00ABFD34 /* Ticker_LabelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ticker_LabelTests.swift; sourceTree = ""; }; 49 | CAC1E02D240B8D8F00ABFD34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | CAC1E032240B8D9000ABFD34 /* Ticker LabelUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Ticker LabelUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | CAC1E036240B8D9000ABFD34 /* Ticker_LabelUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ticker_LabelUITests.swift; sourceTree = ""; }; 52 | CAC1E038240B8D9000ABFD34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | CAC1E050240B939C00ABFD34 /* MOTickerLabel */ = {isa = PBXFileReference; lastKnownFileType = folder; name = MOTickerLabel; path = ../MOTickerLabel; sourceTree = ""; }; 54 | /* End PBXFileReference section */ 55 | 56 | /* Begin PBXFrameworksBuildPhase section */ 57 | CAC1E00E240B8D8100ABFD34 /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | CAE30914240C45F200C30E61 /* MOTickerLabel in Frameworks */, 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | CAC1E024240B8D8F00ABFD34 /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | CAC1E02F240B8D8F00ABFD34 /* Frameworks */ = { 73 | isa = PBXFrameworksBuildPhase; 74 | buildActionMask = 2147483647; 75 | files = ( 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXFrameworksBuildPhase section */ 80 | 81 | /* Begin PBXGroup section */ 82 | CAC1E008240B8D8100ABFD34 = { 83 | isa = PBXGroup; 84 | children = ( 85 | CAC1E050240B939C00ABFD34 /* MOTickerLabel */, 86 | CAC1E013240B8D8100ABFD34 /* Ticker Label */, 87 | CAC1E02A240B8D8F00ABFD34 /* Ticker LabelTests */, 88 | CAC1E035240B8D9000ABFD34 /* Ticker LabelUITests */, 89 | CAC1E012240B8D8100ABFD34 /* Products */, 90 | ); 91 | sourceTree = ""; 92 | }; 93 | CAC1E012240B8D8100ABFD34 /* Products */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | CAC1E011240B8D8100ABFD34 /* Ticker Label.app */, 97 | CAC1E027240B8D8F00ABFD34 /* Ticker LabelTests.xctest */, 98 | CAC1E032240B8D9000ABFD34 /* Ticker LabelUITests.xctest */, 99 | ); 100 | name = Products; 101 | sourceTree = ""; 102 | }; 103 | CAC1E013240B8D8100ABFD34 /* Ticker Label */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | CAC1E014240B8D8100ABFD34 /* AppDelegate.swift */, 107 | CAC1E016240B8D8100ABFD34 /* SceneDelegate.swift */, 108 | CAC1E018240B8D8100ABFD34 /* ViewController.swift */, 109 | CAC1E01A240B8D8100ABFD34 /* Main.storyboard */, 110 | CAC1E01D240B8D8F00ABFD34 /* Assets.xcassets */, 111 | CAC1E01F240B8D8F00ABFD34 /* LaunchScreen.storyboard */, 112 | CAC1E022240B8D8F00ABFD34 /* Info.plist */, 113 | ); 114 | path = "Ticker Label"; 115 | sourceTree = ""; 116 | }; 117 | CAC1E02A240B8D8F00ABFD34 /* Ticker LabelTests */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | CAC1E02B240B8D8F00ABFD34 /* Ticker_LabelTests.swift */, 121 | CAC1E02D240B8D8F00ABFD34 /* Info.plist */, 122 | ); 123 | path = "Ticker LabelTests"; 124 | sourceTree = ""; 125 | }; 126 | CAC1E035240B8D9000ABFD34 /* Ticker LabelUITests */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | CAC1E036240B8D9000ABFD34 /* Ticker_LabelUITests.swift */, 130 | CAC1E038240B8D9000ABFD34 /* Info.plist */, 131 | ); 132 | path = "Ticker LabelUITests"; 133 | sourceTree = ""; 134 | }; 135 | /* End PBXGroup section */ 136 | 137 | /* Begin PBXNativeTarget section */ 138 | CAC1E010240B8D8100ABFD34 /* Ticker Label */ = { 139 | isa = PBXNativeTarget; 140 | buildConfigurationList = CAC1E03B240B8D9000ABFD34 /* Build configuration list for PBXNativeTarget "Ticker Label" */; 141 | buildPhases = ( 142 | CAC1E00D240B8D8100ABFD34 /* Sources */, 143 | CAC1E00E240B8D8100ABFD34 /* Frameworks */, 144 | CAC1E00F240B8D8100ABFD34 /* Resources */, 145 | ); 146 | buildRules = ( 147 | ); 148 | dependencies = ( 149 | ); 150 | name = "Ticker Label"; 151 | packageProductDependencies = ( 152 | CAE30913240C45F200C30E61 /* MOTickerLabel */, 153 | ); 154 | productName = "Ticker Label"; 155 | productReference = CAC1E011240B8D8100ABFD34 /* Ticker Label.app */; 156 | productType = "com.apple.product-type.application"; 157 | }; 158 | CAC1E026240B8D8F00ABFD34 /* Ticker LabelTests */ = { 159 | isa = PBXNativeTarget; 160 | buildConfigurationList = CAC1E03E240B8D9000ABFD34 /* Build configuration list for PBXNativeTarget "Ticker LabelTests" */; 161 | buildPhases = ( 162 | CAC1E023240B8D8F00ABFD34 /* Sources */, 163 | CAC1E024240B8D8F00ABFD34 /* Frameworks */, 164 | CAC1E025240B8D8F00ABFD34 /* Resources */, 165 | ); 166 | buildRules = ( 167 | ); 168 | dependencies = ( 169 | CAC1E029240B8D8F00ABFD34 /* PBXTargetDependency */, 170 | ); 171 | name = "Ticker LabelTests"; 172 | productName = "Ticker LabelTests"; 173 | productReference = CAC1E027240B8D8F00ABFD34 /* Ticker LabelTests.xctest */; 174 | productType = "com.apple.product-type.bundle.unit-test"; 175 | }; 176 | CAC1E031240B8D8F00ABFD34 /* Ticker LabelUITests */ = { 177 | isa = PBXNativeTarget; 178 | buildConfigurationList = CAC1E041240B8D9000ABFD34 /* Build configuration list for PBXNativeTarget "Ticker LabelUITests" */; 179 | buildPhases = ( 180 | CAC1E02E240B8D8F00ABFD34 /* Sources */, 181 | CAC1E02F240B8D8F00ABFD34 /* Frameworks */, 182 | CAC1E030240B8D8F00ABFD34 /* Resources */, 183 | ); 184 | buildRules = ( 185 | ); 186 | dependencies = ( 187 | CAC1E034240B8D9000ABFD34 /* PBXTargetDependency */, 188 | ); 189 | name = "Ticker LabelUITests"; 190 | productName = "Ticker LabelUITests"; 191 | productReference = CAC1E032240B8D9000ABFD34 /* Ticker LabelUITests.xctest */; 192 | productType = "com.apple.product-type.bundle.ui-testing"; 193 | }; 194 | /* End PBXNativeTarget section */ 195 | 196 | /* Begin PBXProject section */ 197 | CAC1E009240B8D8100ABFD34 /* Project object */ = { 198 | isa = PBXProject; 199 | attributes = { 200 | LastSwiftUpdateCheck = 1130; 201 | LastUpgradeCheck = 1130; 202 | ORGANIZATIONNAME = Mosaic; 203 | TargetAttributes = { 204 | CAC1E010240B8D8100ABFD34 = { 205 | CreatedOnToolsVersion = 11.3.1; 206 | }; 207 | CAC1E026240B8D8F00ABFD34 = { 208 | CreatedOnToolsVersion = 11.3.1; 209 | TestTargetID = CAC1E010240B8D8100ABFD34; 210 | }; 211 | CAC1E031240B8D8F00ABFD34 = { 212 | CreatedOnToolsVersion = 11.3.1; 213 | TestTargetID = CAC1E010240B8D8100ABFD34; 214 | }; 215 | }; 216 | }; 217 | buildConfigurationList = CAC1E00C240B8D8100ABFD34 /* Build configuration list for PBXProject "Ticker Label" */; 218 | compatibilityVersion = "Xcode 9.3"; 219 | developmentRegion = en; 220 | hasScannedForEncodings = 0; 221 | knownRegions = ( 222 | en, 223 | Base, 224 | ); 225 | mainGroup = CAC1E008240B8D8100ABFD34; 226 | packageReferences = ( 227 | CAE30912240C45F200C30E61 /* XCRemoteSwiftPackageReference "MOTickerLabel" */, 228 | ); 229 | productRefGroup = CAC1E012240B8D8100ABFD34 /* Products */; 230 | projectDirPath = ""; 231 | projectRoot = ""; 232 | targets = ( 233 | CAC1E010240B8D8100ABFD34 /* Ticker Label */, 234 | CAC1E026240B8D8F00ABFD34 /* Ticker LabelTests */, 235 | CAC1E031240B8D8F00ABFD34 /* Ticker LabelUITests */, 236 | ); 237 | }; 238 | /* End PBXProject section */ 239 | 240 | /* Begin PBXResourcesBuildPhase section */ 241 | CAC1E00F240B8D8100ABFD34 /* Resources */ = { 242 | isa = PBXResourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | CAC1E021240B8D8F00ABFD34 /* LaunchScreen.storyboard in Resources */, 246 | CAC1E01E240B8D8F00ABFD34 /* Assets.xcassets in Resources */, 247 | CAC1E01C240B8D8100ABFD34 /* Main.storyboard in Resources */, 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | CAC1E025240B8D8F00ABFD34 /* Resources */ = { 252 | isa = PBXResourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | CAC1E030240B8D8F00ABFD34 /* Resources */ = { 259 | isa = PBXResourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | }; 265 | /* End PBXResourcesBuildPhase section */ 266 | 267 | /* Begin PBXSourcesBuildPhase section */ 268 | CAC1E00D240B8D8100ABFD34 /* Sources */ = { 269 | isa = PBXSourcesBuildPhase; 270 | buildActionMask = 2147483647; 271 | files = ( 272 | CAC1E019240B8D8100ABFD34 /* ViewController.swift in Sources */, 273 | CAC1E015240B8D8100ABFD34 /* AppDelegate.swift in Sources */, 274 | CAC1E017240B8D8100ABFD34 /* SceneDelegate.swift in Sources */, 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | CAC1E023240B8D8F00ABFD34 /* Sources */ = { 279 | isa = PBXSourcesBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | CAC1E02C240B8D8F00ABFD34 /* Ticker_LabelTests.swift in Sources */, 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | }; 286 | CAC1E02E240B8D8F00ABFD34 /* Sources */ = { 287 | isa = PBXSourcesBuildPhase; 288 | buildActionMask = 2147483647; 289 | files = ( 290 | CAC1E037240B8D9000ABFD34 /* Ticker_LabelUITests.swift in Sources */, 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | /* End PBXSourcesBuildPhase section */ 295 | 296 | /* Begin PBXTargetDependency section */ 297 | CAC1E029240B8D8F00ABFD34 /* PBXTargetDependency */ = { 298 | isa = PBXTargetDependency; 299 | target = CAC1E010240B8D8100ABFD34 /* Ticker Label */; 300 | targetProxy = CAC1E028240B8D8F00ABFD34 /* PBXContainerItemProxy */; 301 | }; 302 | CAC1E034240B8D9000ABFD34 /* PBXTargetDependency */ = { 303 | isa = PBXTargetDependency; 304 | target = CAC1E010240B8D8100ABFD34 /* Ticker Label */; 305 | targetProxy = CAC1E033240B8D9000ABFD34 /* PBXContainerItemProxy */; 306 | }; 307 | /* End PBXTargetDependency section */ 308 | 309 | /* Begin PBXVariantGroup section */ 310 | CAC1E01A240B8D8100ABFD34 /* Main.storyboard */ = { 311 | isa = PBXVariantGroup; 312 | children = ( 313 | CAC1E01B240B8D8100ABFD34 /* Base */, 314 | ); 315 | name = Main.storyboard; 316 | sourceTree = ""; 317 | }; 318 | CAC1E01F240B8D8F00ABFD34 /* LaunchScreen.storyboard */ = { 319 | isa = PBXVariantGroup; 320 | children = ( 321 | CAC1E020240B8D8F00ABFD34 /* Base */, 322 | ); 323 | name = LaunchScreen.storyboard; 324 | sourceTree = ""; 325 | }; 326 | /* End PBXVariantGroup section */ 327 | 328 | /* Begin XCBuildConfiguration section */ 329 | CAC1E039240B8D9000ABFD34 /* Debug */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ALWAYS_SEARCH_USER_PATHS = NO; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_ENABLE_OBJC_WEAK = YES; 340 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 341 | CLANG_WARN_BOOL_CONVERSION = YES; 342 | CLANG_WARN_COMMA = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 345 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 346 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 356 | CLANG_WARN_STRICT_PROTOTYPES = YES; 357 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 358 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | COPY_PHASE_STRIP = NO; 362 | DEBUG_INFORMATION_FORMAT = dwarf; 363 | ENABLE_STRICT_OBJC_MSGSEND = YES; 364 | ENABLE_TESTABILITY = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu11; 366 | GCC_DYNAMIC_NO_PIC = NO; 367 | GCC_NO_COMMON_BLOCKS = YES; 368 | GCC_OPTIMIZATION_LEVEL = 0; 369 | GCC_PREPROCESSOR_DEFINITIONS = ( 370 | "DEBUG=1", 371 | "$(inherited)", 372 | ); 373 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 374 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 375 | GCC_WARN_UNDECLARED_SELECTOR = YES; 376 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 377 | GCC_WARN_UNUSED_FUNCTION = YES; 378 | GCC_WARN_UNUSED_VARIABLE = YES; 379 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 380 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 381 | MTL_FAST_MATH = YES; 382 | ONLY_ACTIVE_ARCH = YES; 383 | SDKROOT = iphoneos; 384 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 385 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 386 | }; 387 | name = Debug; 388 | }; 389 | CAC1E03A240B8D9000ABFD34 /* Release */ = { 390 | isa = XCBuildConfiguration; 391 | buildSettings = { 392 | ALWAYS_SEARCH_USER_PATHS = NO; 393 | CLANG_ANALYZER_NONNULL = YES; 394 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 395 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 396 | CLANG_CXX_LIBRARY = "libc++"; 397 | CLANG_ENABLE_MODULES = YES; 398 | CLANG_ENABLE_OBJC_ARC = YES; 399 | CLANG_ENABLE_OBJC_WEAK = YES; 400 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 401 | CLANG_WARN_BOOL_CONVERSION = YES; 402 | CLANG_WARN_COMMA = YES; 403 | CLANG_WARN_CONSTANT_CONVERSION = YES; 404 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 405 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 406 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 407 | CLANG_WARN_EMPTY_BODY = YES; 408 | CLANG_WARN_ENUM_CONVERSION = YES; 409 | CLANG_WARN_INFINITE_RECURSION = YES; 410 | CLANG_WARN_INT_CONVERSION = YES; 411 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 412 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 413 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 415 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 416 | CLANG_WARN_STRICT_PROTOTYPES = YES; 417 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 418 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 419 | CLANG_WARN_UNREACHABLE_CODE = YES; 420 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 421 | COPY_PHASE_STRIP = NO; 422 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 423 | ENABLE_NS_ASSERTIONS = NO; 424 | ENABLE_STRICT_OBJC_MSGSEND = YES; 425 | GCC_C_LANGUAGE_STANDARD = gnu11; 426 | GCC_NO_COMMON_BLOCKS = YES; 427 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 428 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 429 | GCC_WARN_UNDECLARED_SELECTOR = YES; 430 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 431 | GCC_WARN_UNUSED_FUNCTION = YES; 432 | GCC_WARN_UNUSED_VARIABLE = YES; 433 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 434 | MTL_ENABLE_DEBUG_INFO = NO; 435 | MTL_FAST_MATH = YES; 436 | SDKROOT = iphoneos; 437 | SWIFT_COMPILATION_MODE = wholemodule; 438 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 439 | VALIDATE_PRODUCT = YES; 440 | }; 441 | name = Release; 442 | }; 443 | CAC1E03C240B8D9000ABFD34 /* Debug */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 447 | CODE_SIGN_STYLE = Automatic; 448 | INFOPLIST_FILE = "Ticker Label/Info.plist"; 449 | LD_RUNPATH_SEARCH_PATHS = ( 450 | "$(inherited)", 451 | "@executable_path/Frameworks", 452 | ); 453 | PRODUCT_BUNDLE_IDENTIFIER = "com.mosaic.Ticker-Label"; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | SWIFT_VERSION = 5.0; 456 | TARGETED_DEVICE_FAMILY = "1,2"; 457 | }; 458 | name = Debug; 459 | }; 460 | CAC1E03D240B8D9000ABFD34 /* Release */ = { 461 | isa = XCBuildConfiguration; 462 | buildSettings = { 463 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 464 | CODE_SIGN_STYLE = Automatic; 465 | INFOPLIST_FILE = "Ticker Label/Info.plist"; 466 | LD_RUNPATH_SEARCH_PATHS = ( 467 | "$(inherited)", 468 | "@executable_path/Frameworks", 469 | ); 470 | PRODUCT_BUNDLE_IDENTIFIER = "com.mosaic.Ticker-Label"; 471 | PRODUCT_NAME = "$(TARGET_NAME)"; 472 | SWIFT_VERSION = 5.0; 473 | TARGETED_DEVICE_FAMILY = "1,2"; 474 | }; 475 | name = Release; 476 | }; 477 | CAC1E03F240B8D9000ABFD34 /* Debug */ = { 478 | isa = XCBuildConfiguration; 479 | buildSettings = { 480 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 481 | BUNDLE_LOADER = "$(TEST_HOST)"; 482 | CODE_SIGN_STYLE = Automatic; 483 | INFOPLIST_FILE = "Ticker LabelTests/Info.plist"; 484 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 485 | LD_RUNPATH_SEARCH_PATHS = ( 486 | "$(inherited)", 487 | "@executable_path/Frameworks", 488 | "@loader_path/Frameworks", 489 | ); 490 | PRODUCT_BUNDLE_IDENTIFIER = "com.mosaic.Ticker-LabelTests"; 491 | PRODUCT_NAME = "$(TARGET_NAME)"; 492 | SWIFT_VERSION = 5.0; 493 | TARGETED_DEVICE_FAMILY = "1,2"; 494 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ticker Label.app/Ticker Label"; 495 | }; 496 | name = Debug; 497 | }; 498 | CAC1E040240B8D9000ABFD34 /* Release */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 502 | BUNDLE_LOADER = "$(TEST_HOST)"; 503 | CODE_SIGN_STYLE = Automatic; 504 | INFOPLIST_FILE = "Ticker LabelTests/Info.plist"; 505 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 506 | LD_RUNPATH_SEARCH_PATHS = ( 507 | "$(inherited)", 508 | "@executable_path/Frameworks", 509 | "@loader_path/Frameworks", 510 | ); 511 | PRODUCT_BUNDLE_IDENTIFIER = "com.mosaic.Ticker-LabelTests"; 512 | PRODUCT_NAME = "$(TARGET_NAME)"; 513 | SWIFT_VERSION = 5.0; 514 | TARGETED_DEVICE_FAMILY = "1,2"; 515 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ticker Label.app/Ticker Label"; 516 | }; 517 | name = Release; 518 | }; 519 | CAC1E042240B8D9000ABFD34 /* Debug */ = { 520 | isa = XCBuildConfiguration; 521 | buildSettings = { 522 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 523 | CODE_SIGN_STYLE = Automatic; 524 | INFOPLIST_FILE = "Ticker LabelUITests/Info.plist"; 525 | LD_RUNPATH_SEARCH_PATHS = ( 526 | "$(inherited)", 527 | "@executable_path/Frameworks", 528 | "@loader_path/Frameworks", 529 | ); 530 | PRODUCT_BUNDLE_IDENTIFIER = "com.mosaic.Ticker-LabelUITests"; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | SWIFT_VERSION = 5.0; 533 | TARGETED_DEVICE_FAMILY = "1,2"; 534 | TEST_TARGET_NAME = "Ticker Label"; 535 | }; 536 | name = Debug; 537 | }; 538 | CAC1E043240B8D9000ABFD34 /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 542 | CODE_SIGN_STYLE = Automatic; 543 | INFOPLIST_FILE = "Ticker LabelUITests/Info.plist"; 544 | LD_RUNPATH_SEARCH_PATHS = ( 545 | "$(inherited)", 546 | "@executable_path/Frameworks", 547 | "@loader_path/Frameworks", 548 | ); 549 | PRODUCT_BUNDLE_IDENTIFIER = "com.mosaic.Ticker-LabelUITests"; 550 | PRODUCT_NAME = "$(TARGET_NAME)"; 551 | SWIFT_VERSION = 5.0; 552 | TARGETED_DEVICE_FAMILY = "1,2"; 553 | TEST_TARGET_NAME = "Ticker Label"; 554 | }; 555 | name = Release; 556 | }; 557 | /* End XCBuildConfiguration section */ 558 | 559 | /* Begin XCConfigurationList section */ 560 | CAC1E00C240B8D8100ABFD34 /* Build configuration list for PBXProject "Ticker Label" */ = { 561 | isa = XCConfigurationList; 562 | buildConfigurations = ( 563 | CAC1E039240B8D9000ABFD34 /* Debug */, 564 | CAC1E03A240B8D9000ABFD34 /* Release */, 565 | ); 566 | defaultConfigurationIsVisible = 0; 567 | defaultConfigurationName = Release; 568 | }; 569 | CAC1E03B240B8D9000ABFD34 /* Build configuration list for PBXNativeTarget "Ticker Label" */ = { 570 | isa = XCConfigurationList; 571 | buildConfigurations = ( 572 | CAC1E03C240B8D9000ABFD34 /* Debug */, 573 | CAC1E03D240B8D9000ABFD34 /* Release */, 574 | ); 575 | defaultConfigurationIsVisible = 0; 576 | defaultConfigurationName = Release; 577 | }; 578 | CAC1E03E240B8D9000ABFD34 /* Build configuration list for PBXNativeTarget "Ticker LabelTests" */ = { 579 | isa = XCConfigurationList; 580 | buildConfigurations = ( 581 | CAC1E03F240B8D9000ABFD34 /* Debug */, 582 | CAC1E040240B8D9000ABFD34 /* Release */, 583 | ); 584 | defaultConfigurationIsVisible = 0; 585 | defaultConfigurationName = Release; 586 | }; 587 | CAC1E041240B8D9000ABFD34 /* Build configuration list for PBXNativeTarget "Ticker LabelUITests" */ = { 588 | isa = XCConfigurationList; 589 | buildConfigurations = ( 590 | CAC1E042240B8D9000ABFD34 /* Debug */, 591 | CAC1E043240B8D9000ABFD34 /* Release */, 592 | ); 593 | defaultConfigurationIsVisible = 0; 594 | defaultConfigurationName = Release; 595 | }; 596 | /* End XCConfigurationList section */ 597 | 598 | /* Begin XCRemoteSwiftPackageReference section */ 599 | CAE30912240C45F200C30E61 /* XCRemoteSwiftPackageReference "MOTickerLabel" */ = { 600 | isa = XCRemoteSwiftPackageReference; 601 | repositoryURL = "https://github.com/mosaic-io/MOTickerLabel"; 602 | requirement = { 603 | kind = upToNextMajorVersion; 604 | minimumVersion = 1.0.0; 605 | }; 606 | }; 607 | /* End XCRemoteSwiftPackageReference section */ 608 | 609 | /* Begin XCSwiftPackageProductDependency section */ 610 | CAE30913240C45F200C30E61 /* MOTickerLabel */ = { 611 | isa = XCSwiftPackageProductDependency; 612 | package = CAE30912240C45F200C30E61 /* XCRemoteSwiftPackageReference "MOTickerLabel" */; 613 | productName = MOTickerLabel; 614 | }; 615 | /* End XCSwiftPackageProductDependency section */ 616 | }; 617 | rootObject = CAC1E009240B8D8100ABFD34 /* Project object */; 618 | } 619 | -------------------------------------------------------------------------------- /Demo/Ticker Label.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Ticker Label.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Ticker Label.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "MOTickerLabel", 6 | "repositoryURL": "https://github.com/mosaic-io/MOTickerLabel", 7 | "state": { 8 | "branch": null, 9 | "revision": "5bf460582362529fa24a125f00aa10b8d752f98b", 10 | "version": "1.0.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Ticker Label/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Ticker Label 4 | // 5 | // Created by Mike Choi on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Demo/Ticker Label/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Demo/Ticker Label/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Ticker Label/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 | -------------------------------------------------------------------------------- /Demo/Ticker Label/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 | -------------------------------------------------------------------------------- /Demo/Ticker Label/Info 2.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Demo/Ticker Label/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Demo/Ticker Label/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Ticker Label 4 | // 5 | // Created by Mike Choi on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | } 22 | 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | func sceneDidBecomeActive(_ scene: UIScene) { 31 | // Called when the scene has moved from an inactive state to an active state. 32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 33 | } 34 | 35 | func sceneWillResignActive(_ scene: UIScene) { 36 | // Called when the scene will move from an active state to an inactive state. 37 | // This may occur due to temporary interruptions (ex. an incoming phone call). 38 | } 39 | 40 | func sceneWillEnterForeground(_ scene: UIScene) { 41 | // Called as the scene transitions from the background to the foreground. 42 | // Use this method to undo the changes made on entering the background. 43 | } 44 | 45 | func sceneDidEnterBackground(_ scene: UIScene) { 46 | // Called as the scene transitions from the foreground to the background. 47 | // Use this method to save data, release shared resources, and store enough scene-specific state information 48 | // to restore the scene back to its current state. 49 | } 50 | 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /Demo/Ticker Label/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Ticker Label 4 | // 5 | // Created by Mike Choi on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Combine 11 | import MOTickerLabel 12 | 13 | final class ViewController: UIViewController { 14 | 15 | let slider = UISlider() 16 | let label = MOTickerLabel(frame: CGRect(x: 0, y: 0, width: 300, height: 50), value: Dollar(float: 12.03)) 17 | 18 | @Published var value: Float? 19 | var sliderStream: AnyCancellable? 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | view.backgroundColor = .white 25 | 26 | slider.minimumValue = 9 27 | slider.maximumValue = 999 28 | slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged) 29 | slider.translatesAutoresizingMaskIntoConstraints = false 30 | label.translatesAutoresizingMaskIntoConstraints = false 31 | 32 | view.addSubview(slider) 33 | view.addSubview(label) 34 | 35 | NSLayoutConstraint.activate([ 36 | slider.topAnchor.constraint(equalTo: view.topAnchor, constant: 100), 37 | slider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), 38 | slider.widthAnchor.constraint(equalToConstant: 350), 39 | label.centerXAnchor.constraint(equalTo: view.centerXAnchor), 40 | label.centerYAnchor.constraint(equalTo: view.centerYAnchor) 41 | ]) 42 | 43 | 44 | sliderStream = $value 45 | .compactMap { $0 } 46 | .map { Dollar(float: $0) } 47 | .removeDuplicates() 48 | .debounce(for: 0.005, scheduler: RunLoop.main) 49 | .assign(to: \.value, on: self.label) 50 | } 51 | 52 | @objc func sliderChanged(_ sender: UISlider) { 53 | value = sender.value 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Demo/Ticker LabelTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/Ticker LabelTests/Ticker_LabelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ticker_LabelTests.swift 3 | // Ticker LabelTests 4 | // 5 | // Created by Mike Choi on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Ticker_Label 11 | 12 | class Ticker_LabelTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Demo/Ticker LabelUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/Ticker LabelUITests/Ticker_LabelUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ticker_LabelUITests.swift 3 | // Ticker LabelUITests 4 | // 5 | // Created by Mike Choi on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class Ticker_LabelUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 20 | } 21 | 22 | override func tearDown() { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func testExample() { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use recording to get started writing UI tests. 32 | // Use XCTAssert and related functions to verify your tests produce the correct results. 33 | } 34 | 35 | func testLaunchPerformance() { 36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 37 | // This measures how long it takes to launch your application. 38 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { 39 | XCUIApplication().launch() 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MOTickerLabel", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "MOTickerLabel", 15 | targets: ["MOTickerLabel"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "MOTickerLabel", 26 | dependencies: []), 27 | .testTarget( 28 | name: "MOTickerLabelTests", 29 | dependencies: ["MOTickerLabel"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MOTickerLabel 2 | 3 | Ticker Label that animates changes in monetary value. To learn more about how `MOTickerLabel` works, checkout this [Mosaic Enginnering blog post](https://blog.getmosaic.io/The-Ticker). 4 | 5 | > The label currently only supports US dollar. 6 | 7 | ## Sample Code 8 | 9 | ```swift 10 | let label = TickerLabel(frame: CGRect(x: 0, y: 0, width: 300, height: 50), value: Dollar(float: 12.03)) 11 | view.addSubview(label) 12 | label.value = Dollar(float: 10.19) 13 | 14 | // Animate change after two seconds! 15 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 16 | self.label.value = Dollar(float: 940.39) 17 | } 18 | ``` 19 | 20 | ## Demo 21 | 22 | ![](https://blog.getmosaic.io/aa65c8e28bce049e831b31d185ff3dd8/final.gif) 23 | 24 | # LICENSE 25 | 26 | MIT License 27 | 28 | Copyright (c) [2020] [MOTickerLabel by Mosaic Engineering] 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 46 | SOFTWARE. 47 | -------------------------------------------------------------------------------- /Sources/MOTickerLabel/Dollar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dollar.swift 3 | // Ticker Label 4 | // 5 | // Created by Mike Choi on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Dollar: Comparable, CustomStringConvertible { 12 | static var commaFormatter: NumberFormatter = { 13 | let formatter = NumberFormatter() 14 | formatter.numberStyle = .currency 15 | formatter.maximumFractionDigits = 2 16 | formatter.locale = Locale(identifier: "en_US") 17 | return formatter 18 | }() 19 | 20 | var amount: Float 21 | 22 | var numberOfDigits: Int { 23 | if amount < 1 { return 3 } 24 | else { return Int(floor(log10(amount))) + 1 + 2 } 25 | } 26 | 27 | public var description: String { 28 | Dollar.commaFormatter.string(from: NSNumber(value: Double(amount))) ?? "$---" 29 | } 30 | 31 | public init(float: Float) { 32 | amount = Float(round(100 * float) / 100) 33 | } 34 | 35 | public static func < (lhs: Dollar, rhs: Dollar) -> Bool { 36 | lhs.amount < rhs.amount 37 | } 38 | 39 | /** 40 | Algorithm is `(1234 // (10 ** 2)) % 10` 41 | 42 | - Returns: nth element of `amount`, where `0`th element is the least significant digit 43 | - Note: Returns `nil` if provided index is out of bounds 44 | */ 45 | func digit(at i: Int) -> Int? { 46 | if amount == 0.00 { return 0 } 47 | if i >= numberOfDigits { return nil } 48 | 49 | let decimalsRemoved = Int(amount * 100) 50 | let divisionFactor = Int(pow(Double(10), Double(numberOfDigits - i - 1))) 51 | let movedToOne = decimalsRemoved / divisionFactor 52 | return movedToOne % 10 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/MOTickerLabel/MOSingleCounterLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MOSingleCounterLabel.swift 3 | // MOTickerLabel 4 | // 5 | // Created by Mosaic Engineering on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | 12 | public class MOSingleCounterLabel: UIView { 13 | lazy var runningAnimator: UIViewPropertyAnimator = { 14 | let animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1.0, animations: nil) 15 | animator.scrubsLinearly = false 16 | return animator 17 | }() 18 | 19 | public var textColor: UIColor = .label { 20 | didSet { 21 | numberWheel.arrangedSubviews.forEach { 22 | ($0 as? UILabel)?.textColor = textColor 23 | } 24 | } 25 | } 26 | 27 | public var font: UIFont = .boldSystemFont(ofSize: 14) { 28 | didSet { 29 | numberWheel.arrangedSubviews.compactMap { $0 as? UILabel }.forEach { 30 | $0.font = font 31 | $0.setFontSizeToFill() 32 | $0.sizeToFit() 33 | } 34 | 35 | scrollView.setNeedsLayout() 36 | scrollView.layoutIfNeeded() 37 | setNeedsLayout() 38 | layoutIfNeeded() 39 | 40 | if let val = value { 41 | scrollView.setContentOffset(offset(of: val), animated: true) 42 | } 43 | } 44 | } 45 | 46 | public var value: Int? { 47 | didSet { 48 | guard let newValue = value else { 49 | return 50 | } 51 | 52 | if let oldValue = oldValue, oldValue != newValue { 53 | animateChange(old: oldValue, new: newValue) 54 | } 55 | } 56 | } 57 | 58 | lazy var numberLabels: [UILabel] = { 59 | return (0...9).map { 60 | let label = UILabel(frame: self.frame) 61 | label.translatesAutoresizingMaskIntoConstraints = false 62 | label.textAlignment = .center 63 | label.text = "\($0)" 64 | label.font = font 65 | label.textColor = textColor 66 | label.setFontSizeToFill() 67 | label.sizeToFit() 68 | return label 69 | } 70 | }() 71 | 72 | lazy var numberWheel: UIStackView = { 73 | let stackView = UIStackView(frame: self.frame) 74 | stackView.axis = .vertical 75 | stackView.spacing = 0 76 | stackView.alignment = .center 77 | stackView.distribution = .fillEqually 78 | stackView.translatesAutoresizingMaskIntoConstraints = false 79 | numberLabels.forEach { 80 | stackView.addArrangedSubview($0) 81 | } 82 | stackView.addArrangedSubview(UIView()) 83 | return stackView 84 | }() 85 | 86 | lazy var scrollView: UIScrollView = { 87 | let scrollView = UIScrollView(frame: self.frame) 88 | scrollView.translatesAutoresizingMaskIntoConstraints = false 89 | scrollView.addSubview(numberWheel) 90 | scrollView.showsHorizontalScrollIndicator = false 91 | scrollView.showsVerticalScrollIndicator = false 92 | scrollView.isUserInteractionEnabled = false 93 | scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 8, right: 0.0) 94 | numberWheel.pin(to: scrollView) 95 | numberWheel.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true 96 | return scrollView 97 | }() 98 | 99 | init(frame: CGRect, value: Int, font: UIFont? = nil) { 100 | super.init(frame: frame) 101 | self.value = value 102 | 103 | if let customFont = font { 104 | self.font = customFont 105 | } 106 | 107 | addSubview(scrollView) 108 | scrollView.pin(to: self) 109 | scrollView.setNeedsLayout() 110 | scrollView.layoutIfNeeded() 111 | scrollView.setContentOffset(offset(of: value), animated: false) 112 | } 113 | 114 | required init?(coder: NSCoder) { 115 | fatalError("init(coder:) has not been implemented") 116 | } 117 | 118 | func offset(of number: Int) -> CGPoint { 119 | let label = numberLabels[number] 120 | return CGPoint(x: 0, y: label.frame.minY) 121 | } 122 | 123 | func animateChange(old: Int, new: Int) { 124 | let destinationOffset = offset(of: new) 125 | 126 | if runningAnimator.isRunning { 127 | runningAnimator.stopAnimation(true) 128 | scrollView.contentOffset = self.scrollView.contentOffset 129 | } 130 | 131 | runningAnimator.addAnimations { 132 | self.scrollView.setContentOffset(destinationOffset, animated: true) 133 | } 134 | runningAnimator.startAnimation() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/MOTickerLabel/MOTickerLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MOTickerLabel.swift 3 | // MOTickerLabel 4 | // 5 | // Created by Mosaic Engineering on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class MOTickerLabel: UIView { 12 | 13 | lazy var runningAnimator: UIViewPropertyAnimator = { 14 | let animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1.0, animations: nil) 15 | animator.scrubsLinearly = false 16 | return animator 17 | }() 18 | 19 | lazy var arrayModificationAnimator: UIViewPropertyAnimator = { 20 | let animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1.0, animations: nil) 21 | animator.scrubsLinearly = false 22 | return animator 23 | }() 24 | 25 | lazy var stackView: UIStackView = { 26 | let stackView = UIStackView(frame: self.frame) 27 | stackView.translatesAutoresizingMaskIntoConstraints = false 28 | stackView.axis = .horizontal 29 | stackView.spacing = -2 30 | stackView.alignment = .fill 31 | return stackView 32 | }() 33 | 34 | lazy var approximateFrame: CGRect = { 35 | CGRect(x: 0, y: 0, width: frame.height, height: frame.height) 36 | }() 37 | 38 | public var textColor: UIColor = .label { 39 | didSet { 40 | stackView.arrangedSubviews.forEach { 41 | if let label = $0 as? MOSingleCounterLabel { 42 | label.textColor = textColor 43 | } else { 44 | ($0 as? UILabel)?.textColor = textColor 45 | } 46 | } 47 | } 48 | } 49 | 50 | public var font: UIFont = .boldSystemFont(ofSize: 14) { 51 | didSet { 52 | stackView.arrangedSubviews.forEach { 53 | if let label = $0 as? MOSingleCounterLabel { 54 | label.font = font 55 | } else if let staticLabel = $0 as? UILabel { 56 | staticLabel.font = font 57 | } 58 | } 59 | } 60 | } 61 | 62 | public var value: Dollar { 63 | didSet { 64 | animateDigitInsertion(from: oldValue, to: value) 65 | animate(from: oldValue, to: value) 66 | } 67 | } 68 | 69 | public init(frame: CGRect, value: Dollar) { 70 | self.value = value 71 | super.init(frame: frame) 72 | commonInit() 73 | } 74 | 75 | required init?(coder: NSCoder) { 76 | self.value = Dollar(float: 0.0) 77 | super.init(coder: coder) 78 | commonInit() 79 | } 80 | 81 | func commonInit() { 82 | backgroundColor = .clear 83 | 84 | addSubview(stackView) 85 | stackView.pin(to: self) 86 | setupInitialLabels() 87 | } 88 | 89 | func animate(from old: Dollar, to new: Dollar) { 90 | if runningAnimator.isRunning { 91 | runningAnimator.stopAnimation(false) 92 | runningAnimator.finishAnimation(at: .end) 93 | } 94 | 95 | runningAnimator.addAnimations { 96 | (0.. 0: 118 | (0.. MOSingleCounterLabel in 121 | let label = MOSingleCounterLabel(frame: self.approximateFrame, value: value, font: self.font) 122 | label.textColor = textColor 123 | return label 124 | } 125 | .forEach { label in 126 | self.stackView.insertArrangedSubview(label, at: 1) 127 | } 128 | default: 129 | break 130 | } 131 | 132 | stride(from: stackView.arrangedSubviews.count - 4, through: 0, by: -4).dropFirst().forEach { i in 133 | let view = stackView.arrangedSubviews[i] 134 | if view as? UILabel == nil { 135 | stackView.insertArrangedSubview(staticLabel(with: ","), at: i + 1) 136 | } 137 | } 138 | 139 | stackView.layoutIfNeeded() 140 | } 141 | 142 | func label(at idx: Int) -> MOSingleCounterLabel { 143 | stackView.arrangedSubviews.compactMap { $0 as? MOSingleCounterLabel }[idx] 144 | } 145 | 146 | func setupInitialLabels() { 147 | let numberLabels = (0.. (MOSingleCounterLabel) in 150 | let label = MOSingleCounterLabel(frame: self.approximateFrame, value: value, font: self.font) 151 | label.font = self.font 152 | label.textColor = textColor 153 | return label 154 | } 155 | 156 | numberLabels.enumerated().forEach { (offset, label) in 157 | if offset % 3 == 1 && offset > 3 { 158 | stackView.addArrangedSubview(staticLabel(with: ",")) 159 | } 160 | stackView.addArrangedSubview(label) 161 | } 162 | 163 | insertNotations() 164 | stackView.addArrangedSubview(UIView()) 165 | } 166 | 167 | func insertNotations() { 168 | let dollarSign = staticLabel(with: "$") 169 | let period = staticLabel(with: ".") 170 | stackView.insertArrangedSubview(dollarSign, at: 0) 171 | stackView.insertArrangedSubview(period, at: stackView.arrangedSubviews.count - 2) 172 | } 173 | 174 | func staticLabel(with string: String) -> UILabel { 175 | let label = UILabel(frame: approximateFrame) 176 | label.translatesAutoresizingMaskIntoConstraints = false 177 | label.text = string 178 | label.font = font 179 | label.textAlignment = .center 180 | label.setFontSizeToFill() 181 | label.sizeToFit() 182 | label.textColor = textColor 183 | return label 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Sources/MOTickerLabel/UIImage+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Extensions.swift 3 | // MOTickerLabel 4 | // 5 | // Created by Mosaic Engineering on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | static func imageWithLabel(label: UILabel) -> UIImage { 13 | UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0.0) 14 | label.layer.render(in: UIGraphicsGetCurrentContext()!) 15 | let img = UIGraphicsGetImageFromCurrentImageContext() 16 | UIGraphicsEndImageContext() 17 | return img!.withRenderingMode(.alwaysTemplate) 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Sources/MOTickerLabel/UILabel+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+Extensions.swift 3 | // MOTickerLabel 4 | // 5 | // Created by Mike Choi on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UILabel { 12 | func setFontSizeToFill() { 13 | let frameSize = self.bounds.size 14 | guard frameSize.height>0 && frameSize.width>0 && self.text != nil else {return} 15 | 16 | var fontPoints = self.font.pointSize 17 | var fontSize = self.text!.size(withAttributes: [NSAttributedString.Key.font: self.font.withSize(fontPoints)]) 18 | var increment = CGFloat(0) 19 | 20 | if fontSize.width > frameSize.width || fontSize.height > frameSize.height { 21 | increment = -1 22 | } else { 23 | increment = 1 24 | } 25 | 26 | while true { 27 | fontSize = self.text!.size(withAttributes: [NSAttributedString.Key.font: self.font.withSize(fontPoints+increment)]) 28 | if increment < 0 { 29 | if fontSize.width < frameSize.width && fontSize.height < frameSize.height { 30 | fontPoints += increment 31 | break 32 | } 33 | } else { 34 | if fontSize.width > frameSize.width || fontSize.height > frameSize.height { 35 | break 36 | } 37 | } 38 | fontPoints += increment 39 | } 40 | 41 | self.font = self.font.withSize(fontPoints) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/MOTickerLabel/UIView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extensions.swift 3 | // MOTickerLabel 4 | // 5 | // Created by Mosaic Engineering on 2/29/20. 6 | // Copyright © 2020 Mosaic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | func pin(to view: UIView) { 13 | NSLayoutConstraint.activate([ 14 | leadingAnchor.constraint(equalTo: view.leadingAnchor), 15 | trailingAnchor.constraint(equalTo: view.trailingAnchor), 16 | topAnchor.constraint(equalTo: view.topAnchor), 17 | bottomAnchor.constraint(equalTo: view.bottomAnchor) 18 | ]) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import MOTickerLabelTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += MOTickerLabelTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/MOTickerLabelTests/MOTickerLabelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MOTickerLabel 3 | 4 | final class MOTickerLabelTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | 11 | static var allTests = [ 12 | ("testExample", testExample), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Tests/MOTickerLabelTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(MOTickerLabelTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------