├── .gitignore ├── ScrollingPerformance.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata └── ScrollingPerformance ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ ├── Contents.json │ ├── Icon-20.png │ ├── Icon-20@2x-1.png │ ├── Icon-20@2x.png │ ├── Icon-20@3x.png │ ├── Icon-29.png │ ├── Icon-29@2x-1.png │ ├── Icon-29@2x.png │ ├── Icon-29@3x.png │ ├── Icon-30.png │ ├── Icon-40.png │ ├── Icon-40@2x-1.png │ ├── Icon-40@2x.png │ ├── Icon-40@3x.png │ ├── Icon-50.png │ ├── Icon-50@2x.png │ ├── Icon-57.png │ ├── Icon-57@2x.png │ ├── Icon-60@2x.png │ ├── Icon-60@3x.png │ ├── Icon-72.png │ ├── Icon-72@2x.png │ ├── Icon-76.png │ ├── Icon-76@2x.png │ └── Icon-83.5@2x.png ├── Contents.json ├── IncomingUser.imageset │ ├── Contents.json │ └── incomingUser.jpg ├── MessageBubble.imageset │ ├── Contents.json │ ├── MessageBubble.png │ ├── MessageBubble@2x.png │ └── MessageBubble@3x.png ├── MessageBubbleNoTail.imageset │ ├── Contents.json │ ├── MessageBubbleNoTail.png │ ├── MessageBubbleNoTail@2x.png │ └── MessageBubbleNoTail@3x.png ├── MoonBackground.imageset │ ├── Contents.json │ └── moonBackground.jpg ├── OkCupid-Logo.imageset │ ├── Contents.json │ └── OkCupid-Logo.png ├── OutgoingUser.imageset │ ├── Contents.json │ └── outgoingUser.jpg └── UFO.imageset │ ├── Contents.json │ └── UFO.pdf ├── Base.lproj └── LaunchScreen.storyboard ├── Info.plist ├── NSAttributedString+Util.swift ├── NSMutableDictionary+Util.swift ├── OKConversationAssetCache.swift ├── OKConversationAssetFactory.swift ├── OKConversationMessageClient.swift ├── OKConversationSizingCache.swift ├── OKConversationSizingFactory.swift ├── OKConversationViewController.swift ├── OKLoading.swift ├── OKLoadingCell.swift ├── OKMessage.swift ├── OKMessageCell.swift ├── OKTimestampCell.swift └── UIImage+Util.swift /.gitignore: -------------------------------------------------------------------------------- 1 | /Podfile.lock 2 | /Pods 3 | xcuserdata 4 | .DS_Store -------------------------------------------------------------------------------- /ScrollingPerformance.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1700ADC21EEDC6120001D54A /* OKConversationSizingCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1700ADC11EEDC6120001D54A /* OKConversationSizingCache.swift */; }; 11 | 1700ADC51EEDC69B0001D54A /* OKConversationSizingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1700ADC41EEDC69B0001D54A /* OKConversationSizingFactory.swift */; }; 12 | 17325B621ED12D7D002A557B /* OKTimestampCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17325B611ED12D7D002A557B /* OKTimestampCell.swift */; }; 13 | 17325B641ED13BD0002A557B /* OKConversationAssetCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17325B631ED13BD0002A557B /* OKConversationAssetCache.swift */; }; 14 | 17325B661ED147CC002A557B /* UIImage+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17325B651ED147CC002A557B /* UIImage+Util.swift */; }; 15 | 17325B681ED16945002A557B /* NSAttributedString+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17325B671ED16945002A557B /* NSAttributedString+Util.swift */; }; 16 | 17325B6C1ED52AC1002A557B /* OKLoadingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17325B6B1ED52AC1002A557B /* OKLoadingCell.swift */; }; 17 | 177FAC8A1EE4DE1800E816F5 /* OKConversationAssetFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177FAC891EE4DE1800E816F5 /* OKConversationAssetFactory.swift */; }; 18 | 178541EA1EE8698600C5097A /* OKLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178541E91EE8698600C5097A /* OKLoading.swift */; }; 19 | 1786C62D1EF24FE200A5E0A0 /* NSMutableDictionary+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1786C62C1EF24FE200A5E0A0 /* NSMutableDictionary+Util.swift */; }; 20 | 17FA96A41ECBCBB700AA8799 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FA96A31ECBCBB700AA8799 /* AppDelegate.swift */; }; 21 | 17FA96AB1ECBCBB700AA8799 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17FA96AA1ECBCBB700AA8799 /* Assets.xcassets */; }; 22 | 17FA96AE1ECBCBB700AA8799 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17FA96AC1ECBCBB700AA8799 /* LaunchScreen.storyboard */; }; 23 | 17FA96B61ECBCC5C00AA8799 /* OKConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FA96B51ECBCC5C00AA8799 /* OKConversationViewController.swift */; }; 24 | 17FA96B81ECBD22A00AA8799 /* OKMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FA96B71ECBD22A00AA8799 /* OKMessageCell.swift */; }; 25 | 17FA96BA1ECBD56600AA8799 /* OKMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FA96B91ECBD56600AA8799 /* OKMessage.swift */; }; 26 | 17FA96C81ECBF10C00AA8799 /* OKConversationMessageClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FA96C71ECBF10C00AA8799 /* OKConversationMessageClient.swift */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 1700ADC11EEDC6120001D54A /* OKConversationSizingCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKConversationSizingCache.swift; sourceTree = ""; }; 31 | 1700ADC41EEDC69B0001D54A /* OKConversationSizingFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKConversationSizingFactory.swift; sourceTree = ""; }; 32 | 17325B611ED12D7D002A557B /* OKTimestampCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKTimestampCell.swift; sourceTree = ""; }; 33 | 17325B631ED13BD0002A557B /* OKConversationAssetCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKConversationAssetCache.swift; sourceTree = ""; }; 34 | 17325B651ED147CC002A557B /* UIImage+Util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Util.swift"; sourceTree = ""; }; 35 | 17325B671ED16945002A557B /* NSAttributedString+Util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Util.swift"; sourceTree = ""; }; 36 | 17325B6B1ED52AC1002A557B /* OKLoadingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKLoadingCell.swift; sourceTree = ""; }; 37 | 177FAC891EE4DE1800E816F5 /* OKConversationAssetFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKConversationAssetFactory.swift; sourceTree = ""; }; 38 | 178541E91EE8698600C5097A /* OKLoading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKLoading.swift; sourceTree = ""; }; 39 | 1786C62C1EF24FE200A5E0A0 /* NSMutableDictionary+Util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableDictionary+Util.swift"; sourceTree = ""; }; 40 | 17FA96A01ECBCBB700AA8799 /* ScrollingPerformance.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScrollingPerformance.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 17FA96A31ECBCBB700AA8799 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 42 | 17FA96AA1ECBCBB700AA8799 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 17FA96AD1ECBCBB700AA8799 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 17FA96AF1ECBCBB700AA8799 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | 17FA96B51ECBCC5C00AA8799 /* OKConversationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKConversationViewController.swift; sourceTree = ""; }; 46 | 17FA96B71ECBD22A00AA8799 /* OKMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKMessageCell.swift; sourceTree = ""; }; 47 | 17FA96B91ECBD56600AA8799 /* OKMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKMessage.swift; sourceTree = ""; }; 48 | 17FA96C71ECBF10C00AA8799 /* OKConversationMessageClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OKConversationMessageClient.swift; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | 17FA969D1ECBCBB700AA8799 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXFrameworksBuildPhase section */ 60 | 61 | /* Begin PBXGroup section */ 62 | 178541EC1EE8753E00C5097A /* Caches */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 17325B631ED13BD0002A557B /* OKConversationAssetCache.swift */, 66 | 1700ADC11EEDC6120001D54A /* OKConversationSizingCache.swift */, 67 | ); 68 | name = Caches; 69 | sourceTree = ""; 70 | }; 71 | 1792575B1EEE06C40075AFEE /* Factories */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 177FAC891EE4DE1800E816F5 /* OKConversationAssetFactory.swift */, 75 | 1700ADC41EEDC69B0001D54A /* OKConversationSizingFactory.swift */, 76 | ); 77 | name = Factories; 78 | sourceTree = ""; 79 | }; 80 | 17ED41611ECD435000B0EEC5 /* Extensions */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 17325B671ED16945002A557B /* NSAttributedString+Util.swift */, 84 | 1786C62C1EF24FE200A5E0A0 /* NSMutableDictionary+Util.swift */, 85 | 17325B651ED147CC002A557B /* UIImage+Util.swift */, 86 | ); 87 | name = Extensions; 88 | sourceTree = ""; 89 | }; 90 | 17FA96971ECBCBB700AA8799 = { 91 | isa = PBXGroup; 92 | children = ( 93 | 17FA96A21ECBCBB700AA8799 /* ScrollingPerformance */, 94 | 17FA96A11ECBCBB700AA8799 /* Products */, 95 | ); 96 | sourceTree = ""; 97 | }; 98 | 17FA96A11ECBCBB700AA8799 /* Products */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 17FA96A01ECBCBB700AA8799 /* ScrollingPerformance.app */, 102 | ); 103 | name = Products; 104 | path = ScrollingPerformance; 105 | sourceTree = ""; 106 | }; 107 | 17FA96A21ECBCBB700AA8799 /* ScrollingPerformance */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 17FA96C11ECBD8D900AA8799 /* App Files */, 111 | 17FA96C61ECBF0D700AA8799 /* Clients */, 112 | 178541EC1EE8753E00C5097A /* Caches */, 113 | 17ED41611ECD435000B0EEC5 /* Extensions */, 114 | 1792575B1EEE06C40075AFEE /* Factories */, 115 | 17FA96C01ECBD8BE00AA8799 /* Models */, 116 | 17FA96BD1ECBD89700AA8799 /* View Controllers */, 117 | 17FA96BE1ECBD8A000AA8799 /* Views */, 118 | ); 119 | path = ScrollingPerformance; 120 | sourceTree = ""; 121 | }; 122 | 17FA96BD1ECBD89700AA8799 /* View Controllers */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 17FA96B51ECBCC5C00AA8799 /* OKConversationViewController.swift */, 126 | ); 127 | name = "View Controllers"; 128 | sourceTree = ""; 129 | }; 130 | 17FA96BE1ECBD8A000AA8799 /* Views */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 17325B6B1ED52AC1002A557B /* OKLoadingCell.swift */, 134 | 17FA96B71ECBD22A00AA8799 /* OKMessageCell.swift */, 135 | 17325B611ED12D7D002A557B /* OKTimestampCell.swift */, 136 | ); 137 | name = Views; 138 | sourceTree = ""; 139 | }; 140 | 17FA96C01ECBD8BE00AA8799 /* Models */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 178541E91EE8698600C5097A /* OKLoading.swift */, 144 | 17FA96B91ECBD56600AA8799 /* OKMessage.swift */, 145 | ); 146 | name = Models; 147 | sourceTree = ""; 148 | }; 149 | 17FA96C11ECBD8D900AA8799 /* App Files */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 17FA96A31ECBCBB700AA8799 /* AppDelegate.swift */, 153 | 17FA96AA1ECBCBB700AA8799 /* Assets.xcassets */, 154 | 17FA96AC1ECBCBB700AA8799 /* LaunchScreen.storyboard */, 155 | 17FA96AF1ECBCBB700AA8799 /* Info.plist */, 156 | ); 157 | name = "App Files"; 158 | sourceTree = ""; 159 | }; 160 | 17FA96C61ECBF0D700AA8799 /* Clients */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 17FA96C71ECBF10C00AA8799 /* OKConversationMessageClient.swift */, 164 | ); 165 | name = Clients; 166 | sourceTree = ""; 167 | }; 168 | /* End PBXGroup section */ 169 | 170 | /* Begin PBXNativeTarget section */ 171 | 17FA969F1ECBCBB700AA8799 /* ScrollingPerformance */ = { 172 | isa = PBXNativeTarget; 173 | buildConfigurationList = 17FA96B21ECBCBB700AA8799 /* Build configuration list for PBXNativeTarget "ScrollingPerformance" */; 174 | buildPhases = ( 175 | 17FA969C1ECBCBB700AA8799 /* Sources */, 176 | 17FA969D1ECBCBB700AA8799 /* Frameworks */, 177 | 17FA969E1ECBCBB700AA8799 /* Resources */, 178 | ); 179 | buildRules = ( 180 | ); 181 | dependencies = ( 182 | ); 183 | name = ScrollingPerformance; 184 | productName = MessagingPerformance; 185 | productReference = 17FA96A01ECBCBB700AA8799 /* ScrollingPerformance.app */; 186 | productType = "com.apple.product-type.application"; 187 | }; 188 | /* End PBXNativeTarget section */ 189 | 190 | /* Begin PBXProject section */ 191 | 17FA96981ECBCBB700AA8799 /* Project object */ = { 192 | isa = PBXProject; 193 | attributes = { 194 | LastSwiftUpdateCheck = 0830; 195 | LastUpgradeCheck = 0830; 196 | ORGANIZATIONNAME = OkCupid; 197 | TargetAttributes = { 198 | 17FA969F1ECBCBB700AA8799 = { 199 | CreatedOnToolsVersion = 8.3.2; 200 | ProvisioningStyle = Automatic; 201 | }; 202 | }; 203 | }; 204 | buildConfigurationList = 17FA969B1ECBCBB700AA8799 /* Build configuration list for PBXProject "ScrollingPerformance" */; 205 | compatibilityVersion = "Xcode 3.2"; 206 | developmentRegion = English; 207 | hasScannedForEncodings = 0; 208 | knownRegions = ( 209 | en, 210 | Base, 211 | ); 212 | mainGroup = 17FA96971ECBCBB700AA8799; 213 | productRefGroup = 17FA96A11ECBCBB700AA8799 /* Products */; 214 | projectDirPath = ""; 215 | projectRoot = ""; 216 | targets = ( 217 | 17FA969F1ECBCBB700AA8799 /* ScrollingPerformance */, 218 | ); 219 | }; 220 | /* End PBXProject section */ 221 | 222 | /* Begin PBXResourcesBuildPhase section */ 223 | 17FA969E1ECBCBB700AA8799 /* Resources */ = { 224 | isa = PBXResourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | 17FA96AE1ECBCBB700AA8799 /* LaunchScreen.storyboard in Resources */, 228 | 17FA96AB1ECBCBB700AA8799 /* Assets.xcassets in Resources */, 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | /* End PBXResourcesBuildPhase section */ 233 | 234 | /* Begin PBXSourcesBuildPhase section */ 235 | 17FA969C1ECBCBB700AA8799 /* Sources */ = { 236 | isa = PBXSourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | 17325B6C1ED52AC1002A557B /* OKLoadingCell.swift in Sources */, 240 | 17FA96A41ECBCBB700AA8799 /* AppDelegate.swift in Sources */, 241 | 17325B681ED16945002A557B /* NSAttributedString+Util.swift in Sources */, 242 | 177FAC8A1EE4DE1800E816F5 /* OKConversationAssetFactory.swift in Sources */, 243 | 17FA96BA1ECBD56600AA8799 /* OKMessage.swift in Sources */, 244 | 1700ADC21EEDC6120001D54A /* OKConversationSizingCache.swift in Sources */, 245 | 17325B661ED147CC002A557B /* UIImage+Util.swift in Sources */, 246 | 1786C62D1EF24FE200A5E0A0 /* NSMutableDictionary+Util.swift in Sources */, 247 | 17325B621ED12D7D002A557B /* OKTimestampCell.swift in Sources */, 248 | 178541EA1EE8698600C5097A /* OKLoading.swift in Sources */, 249 | 17325B641ED13BD0002A557B /* OKConversationAssetCache.swift in Sources */, 250 | 17FA96B81ECBD22A00AA8799 /* OKMessageCell.swift in Sources */, 251 | 17FA96C81ECBF10C00AA8799 /* OKConversationMessageClient.swift in Sources */, 252 | 17FA96B61ECBCC5C00AA8799 /* OKConversationViewController.swift in Sources */, 253 | 1700ADC51EEDC69B0001D54A /* OKConversationSizingFactory.swift in Sources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | /* End PBXSourcesBuildPhase section */ 258 | 259 | /* Begin PBXVariantGroup section */ 260 | 17FA96AC1ECBCBB700AA8799 /* LaunchScreen.storyboard */ = { 261 | isa = PBXVariantGroup; 262 | children = ( 263 | 17FA96AD1ECBCBB700AA8799 /* Base */, 264 | ); 265 | name = LaunchScreen.storyboard; 266 | sourceTree = ""; 267 | }; 268 | /* End PBXVariantGroup section */ 269 | 270 | /* Begin XCBuildConfiguration section */ 271 | 17FA96B01ECBCBB700AA8799 /* Debug */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | ALWAYS_SEARCH_USER_PATHS = NO; 275 | CLANG_ANALYZER_NONNULL = YES; 276 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 277 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 278 | CLANG_CXX_LIBRARY = "libc++"; 279 | CLANG_ENABLE_MODULES = YES; 280 | CLANG_ENABLE_OBJC_ARC = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_CONSTANT_CONVERSION = YES; 283 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 284 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 285 | CLANG_WARN_EMPTY_BODY = YES; 286 | CLANG_WARN_ENUM_CONVERSION = YES; 287 | CLANG_WARN_INFINITE_RECURSION = YES; 288 | CLANG_WARN_INT_CONVERSION = YES; 289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 290 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 291 | CLANG_WARN_UNREACHABLE_CODE = YES; 292 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 293 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 294 | COPY_PHASE_STRIP = NO; 295 | DEBUG_INFORMATION_FORMAT = dwarf; 296 | ENABLE_STRICT_OBJC_MSGSEND = YES; 297 | ENABLE_TESTABILITY = YES; 298 | GCC_C_LANGUAGE_STANDARD = gnu99; 299 | GCC_DYNAMIC_NO_PIC = NO; 300 | GCC_NO_COMMON_BLOCKS = YES; 301 | GCC_OPTIMIZATION_LEVEL = 0; 302 | GCC_PREPROCESSOR_DEFINITIONS = ( 303 | "DEBUG=1", 304 | "$(inherited)", 305 | ); 306 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 307 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 308 | GCC_WARN_UNDECLARED_SELECTOR = YES; 309 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 310 | GCC_WARN_UNUSED_FUNCTION = YES; 311 | GCC_WARN_UNUSED_VARIABLE = YES; 312 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 313 | MTL_ENABLE_DEBUG_INFO = YES; 314 | ONLY_ACTIVE_ARCH = YES; 315 | SDKROOT = iphoneos; 316 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 317 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 318 | }; 319 | name = Debug; 320 | }; 321 | 17FA96B11ECBCBB700AA8799 /* Release */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ALWAYS_SEARCH_USER_PATHS = NO; 325 | CLANG_ANALYZER_NONNULL = YES; 326 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 327 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 328 | CLANG_CXX_LIBRARY = "libc++"; 329 | CLANG_ENABLE_MODULES = YES; 330 | CLANG_ENABLE_OBJC_ARC = YES; 331 | CLANG_WARN_BOOL_CONVERSION = YES; 332 | CLANG_WARN_CONSTANT_CONVERSION = YES; 333 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 334 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 335 | CLANG_WARN_EMPTY_BODY = YES; 336 | CLANG_WARN_ENUM_CONVERSION = YES; 337 | CLANG_WARN_INFINITE_RECURSION = YES; 338 | CLANG_WARN_INT_CONVERSION = YES; 339 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 340 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 341 | CLANG_WARN_UNREACHABLE_CODE = YES; 342 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 343 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 344 | COPY_PHASE_STRIP = NO; 345 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 346 | ENABLE_NS_ASSERTIONS = NO; 347 | ENABLE_STRICT_OBJC_MSGSEND = YES; 348 | GCC_C_LANGUAGE_STANDARD = gnu99; 349 | GCC_NO_COMMON_BLOCKS = YES; 350 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 351 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 352 | GCC_WARN_UNDECLARED_SELECTOR = YES; 353 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 354 | GCC_WARN_UNUSED_FUNCTION = YES; 355 | GCC_WARN_UNUSED_VARIABLE = YES; 356 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 357 | MTL_ENABLE_DEBUG_INFO = NO; 358 | SDKROOT = iphoneos; 359 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 360 | VALIDATE_PRODUCT = YES; 361 | }; 362 | name = Release; 363 | }; 364 | 17FA96B31ECBCBB700AA8799 /* Debug */ = { 365 | isa = XCBuildConfiguration; 366 | buildSettings = { 367 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 368 | DEVELOPMENT_TEAM = ""; 369 | INFOPLIST_FILE = ScrollingPerformance/Info.plist; 370 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 371 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 372 | PRODUCT_BUNDLE_IDENTIFIER = com.okcupid.www.ScrollingPerformance; 373 | PRODUCT_NAME = "$(TARGET_NAME)"; 374 | SWIFT_VERSION = 3.0; 375 | TARGETED_DEVICE_FAMILY = "1,2"; 376 | }; 377 | name = Debug; 378 | }; 379 | 17FA96B41ECBCBB700AA8799 /* Release */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 | DEVELOPMENT_TEAM = ""; 384 | INFOPLIST_FILE = ScrollingPerformance/Info.plist; 385 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 386 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 387 | PRODUCT_BUNDLE_IDENTIFIER = com.okcupid.www.ScrollingPerformance; 388 | PRODUCT_NAME = "$(TARGET_NAME)"; 389 | SWIFT_VERSION = 3.0; 390 | TARGETED_DEVICE_FAMILY = "1,2"; 391 | }; 392 | name = Release; 393 | }; 394 | /* End XCBuildConfiguration section */ 395 | 396 | /* Begin XCConfigurationList section */ 397 | 17FA969B1ECBCBB700AA8799 /* Build configuration list for PBXProject "ScrollingPerformance" */ = { 398 | isa = XCConfigurationList; 399 | buildConfigurations = ( 400 | 17FA96B01ECBCBB700AA8799 /* Debug */, 401 | 17FA96B11ECBCBB700AA8799 /* Release */, 402 | ); 403 | defaultConfigurationIsVisible = 0; 404 | defaultConfigurationName = Release; 405 | }; 406 | 17FA96B21ECBCBB700AA8799 /* Build configuration list for PBXNativeTarget "ScrollingPerformance" */ = { 407 | isa = XCConfigurationList; 408 | buildConfigurations = ( 409 | 17FA96B31ECBCBB700AA8799 /* Debug */, 410 | 17FA96B41ECBCBB700AA8799 /* Release */, 411 | ); 412 | defaultConfigurationIsVisible = 0; 413 | defaultConfigurationName = Release; 414 | }; 415 | /* End XCConfigurationList section */ 416 | }; 417 | rootObject = 17FA96981ECBCBB700AA8799 /* Project object */; 418 | } 419 | -------------------------------------------------------------------------------- /ScrollingPerformance.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ScrollingPerformance/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ScrollingPerformance 4 | // 5 | // Created by Jordan Guggenheim on 6/9/17. 6 | // Copyright © 2017 OkCupid. All rights reserved. 7 | // 8 | 9 | let kIsOptimized = true 10 | 11 | import UIKit 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 19 | 20 | window = UIWindow(frame: UIScreen.main.bounds) 21 | 22 | let rootViewController = OKConversationViewController(assetFactory: OKConversationAssetFactory(), 23 | sizingFactory: OKConversationSizingFactory(), 24 | messageClient: OKConversationMessageClient()) 25 | 26 | window?.rootViewController = UINavigationController(rootViewController: rootViewController) 27 | window?.makeKeyAndVisible() 28 | 29 | return true 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-29.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "57x57", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-57.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "57x57", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-57@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "60x60", 59 | "idiom" : "iphone", 60 | "filename" : "Icon-60@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "60x60", 65 | "idiom" : "iphone", 66 | "filename" : "Icon-60@3x.png", 67 | "scale" : "3x" 68 | }, 69 | { 70 | "size" : "20x20", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-20.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "20x20", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-20@2x-1.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "29x29", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-30.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "29x29", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-29@2x-1.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "40x40", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-40.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "40x40", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-40@2x-1.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "50x50", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-50.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "50x50", 113 | "idiom" : "ipad", 114 | "filename" : "Icon-50@2x.png", 115 | "scale" : "2x" 116 | }, 117 | { 118 | "size" : "72x72", 119 | "idiom" : "ipad", 120 | "filename" : "Icon-72.png", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "size" : "72x72", 125 | "idiom" : "ipad", 126 | "filename" : "Icon-72@2x.png", 127 | "scale" : "2x" 128 | }, 129 | { 130 | "size" : "76x76", 131 | "idiom" : "ipad", 132 | "filename" : "Icon-76.png", 133 | "scale" : "1x" 134 | }, 135 | { 136 | "size" : "76x76", 137 | "idiom" : "ipad", 138 | "filename" : "Icon-76@2x.png", 139 | "scale" : "2x" 140 | }, 141 | { 142 | "size" : "83.5x83.5", 143 | "idiom" : "ipad", 144 | "filename" : "Icon-83.5@2x.png", 145 | "scale" : "2x" 146 | } 147 | ], 148 | "info" : { 149 | "version" : 1, 150 | "author" : "xcode" 151 | } 152 | } -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-20.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-29.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-30.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-50.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-50@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-57.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-57@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-72.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/IncomingUser.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "incomingUser.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/IncomingUser.imageset/incomingUser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/IncomingUser.imageset/incomingUser.jpg -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MessageBubble.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "MessageBubble.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "MessageBubble@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "MessageBubble@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MessageBubble.imageset/MessageBubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/MessageBubble.imageset/MessageBubble.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MessageBubble.imageset/MessageBubble@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/MessageBubble.imageset/MessageBubble@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MessageBubble.imageset/MessageBubble@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/MessageBubble.imageset/MessageBubble@3x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MessageBubbleNoTail.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "MessageBubbleNoTail.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "MessageBubbleNoTail@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "MessageBubbleNoTail@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MessageBubbleNoTail.imageset/MessageBubbleNoTail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/MessageBubbleNoTail.imageset/MessageBubbleNoTail.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MessageBubbleNoTail.imageset/MessageBubbleNoTail@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/MessageBubbleNoTail.imageset/MessageBubbleNoTail@2x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MessageBubbleNoTail.imageset/MessageBubbleNoTail@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/MessageBubbleNoTail.imageset/MessageBubbleNoTail@3x.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MoonBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "moonBackground.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/MoonBackground.imageset/moonBackground.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/MoonBackground.imageset/moonBackground.jpg -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/OkCupid-Logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "OkCupid-Logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/OkCupid-Logo.imageset/OkCupid-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/OkCupid-Logo.imageset/OkCupid-Logo.png -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/OutgoingUser.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "outgoingUser.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/OutgoingUser.imageset/outgoingUser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/OutgoingUser.imageset/outgoingUser.jpg -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/UFO.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "UFO.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /ScrollingPerformance/Assets.xcassets/UFO.imageset/UFO.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkCupid/iOS-Scrolling-Performance/73236027124a5b95d5f96efd5054923b32c6e7ea/ScrollingPerformance/Assets.xcassets/UFO.imageset/UFO.pdf -------------------------------------------------------------------------------- /ScrollingPerformance/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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ScrollingPerformance/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Messages 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIRequiresFullScreen 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /ScrollingPerformance/NSAttributedString+Util.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+Util.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension NSAttributedString { 11 | 12 | func image(with size: CGSize) -> UIImage { 13 | 14 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 15 | 16 | UIGraphicsGetCurrentContext() 17 | 18 | draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) 19 | 20 | guard let image = UIGraphicsGetImageFromCurrentImageContext() else { 21 | UIGraphicsEndImageContext() 22 | return UIImage() 23 | } 24 | 25 | UIGraphicsEndImageContext() 26 | 27 | return image 28 | } 29 | 30 | } 31 | 32 | extension NSAttributedString: NSDiscardableContent { 33 | 34 | public func beginContentAccess() -> Bool { 35 | return true 36 | } 37 | public func endContentAccess() {} 38 | public func discardContentIfPossible() {} 39 | public func isContentDiscarded() -> Bool { 40 | return false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ScrollingPerformance/NSMutableDictionary+Util.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSMutableDictionary+Util.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSMutableDictionary: NSDiscardableContent { 11 | 12 | public func beginContentAccess() -> Bool { 13 | return true 14 | } 15 | public func endContentAccess() {} 16 | public func discardContentIfPossible() {} 17 | public func isContentDiscarded() -> Bool { 18 | return false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKConversationAssetCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKConversationAssetCache.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OKConversationAssetCache { 11 | 12 | fileprivate var messageAttributedStringCache = NSCache() 13 | fileprivate var messageBubbleImageCache = NSCache() 14 | fileprivate var messageLabelImageCache = NSCache() 15 | fileprivate var timestampAttributedStringCache = NSCache() 16 | 17 | //MARK: - Lifecycle 18 | 19 | init() { 20 | NotificationCenter.default.addObserver(self, selector: #selector(didReceiveMemoryWarning), name: NSNotification.Name.UIApplicationDidReceiveMemoryWarning, object: nil) 21 | } 22 | 23 | deinit { 24 | NotificationCenter.default.removeObserver(self) 25 | } 26 | 27 | //MARK: - Message Attributed String 28 | 29 | func cachedMessageAttributedString(message: OKMessage) -> NSAttributedString? { 30 | return messageAttributedStringCache.object(forKey: message) 31 | } 32 | 33 | func setCachedMessageAttributedString(_ messageAttributedString: NSAttributedString, message: OKMessage) { 34 | messageAttributedStringCache.setObject(messageAttributedString, forKey: message) 35 | } 36 | 37 | //MARK: - Message Bubble Image 38 | 39 | func cachedMessageBubbleImage(for message: OKMessage) -> UIImage? { 40 | return messageBubbleImageCache.object(forKey: message) 41 | } 42 | 43 | func setCachedMessageBubbleImage(_ messageBubbleImage: UIImage, message: OKMessage) { 44 | messageBubbleImageCache.setObject(messageBubbleImage, forKey: message) 45 | } 46 | 47 | //MARK: - Message Label Image 48 | 49 | func cachedMessageLabelImage(for size: CGSize, message: OKMessage) -> UIImage? { 50 | return messageLabelImageCache.object(forKey: message)?[NSValue(cgSize: size)] as? UIImage 51 | } 52 | 53 | func setCachedMessageLabelImage(_ messageLabelImage: UIImage, forSize: CGSize, message: OKMessage) { 54 | if let currentCache = messageLabelImageCache.object(forKey: message) { 55 | currentCache[NSValue(cgSize: forSize)] = messageLabelImage 56 | messageLabelImageCache.setObject(currentCache, forKey: message) 57 | 58 | } else { 59 | let currentCache = NSMutableDictionary() 60 | currentCache[NSValue(cgSize: forSize)] = messageLabelImage 61 | messageLabelImageCache.setObject(currentCache, forKey: message) 62 | } 63 | } 64 | 65 | //MARK: - Timestamp Attributed String 66 | 67 | func cachedTimestampAttributedString(message: OKMessage) -> NSAttributedString? { 68 | return timestampAttributedStringCache.object(forKey: message) 69 | } 70 | 71 | func setCachedTimestampAttributedString(_ timestampAttributedString: NSAttributedString, message: OKMessage) { 72 | timestampAttributedStringCache.setObject(timestampAttributedString, forKey: message) 73 | } 74 | 75 | //MARK: - Notifications 76 | 77 | @objc fileprivate func didReceiveMemoryWarning() { 78 | clearCache() 79 | } 80 | 81 | //MARK: Cache Maintenance 82 | 83 | func clearCache() { 84 | messageAttributedStringCache.removeAllObjects() 85 | messageBubbleImageCache.removeAllObjects() 86 | messageLabelImageCache.removeAllObjects() 87 | timestampAttributedStringCache.removeAllObjects() 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKConversationAssetFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKConversationAssetFactory.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OKConversationAssetFactory { 11 | 12 | fileprivate let assetCache: OKConversationAssetCache? 13 | fileprivate let dateFormatter = DateFormatter() 14 | 15 | //MARK: - Lifecycle 16 | 17 | init(assetCache: OKConversationAssetCache? = OKConversationAssetCache()) { 18 | self.assetCache = kIsOptimized ? assetCache : nil 19 | } 20 | 21 | //MARK: - Message Assets 22 | 23 | func bubbleImage(for message: OKMessage) -> UIImage { 24 | if let bubbleImage = assetCache?.cachedMessageBubbleImage(for: message) { 25 | return bubbleImage 26 | 27 | } else { 28 | let bubbleColor = message.isIncoming ? UIColor.lightGray : UIColor.black 29 | var bubbleImage = message.isTailEnabled ? #imageLiteral(resourceName: "MessageBubble") : #imageLiteral(resourceName: "MessageBubbleNoTail") 30 | 31 | if kIsOptimized { 32 | bubbleImage = bubbleImage.image(maskColor: bubbleColor.cgColor) 33 | 34 | } else { 35 | bubbleImage = bubbleImage.withRenderingMode(.alwaysTemplate) 36 | } 37 | 38 | if message.isIncoming && kIsOptimized, let cgImage = bubbleImage.cgImage { 39 | bubbleImage = UIImage(cgImage: cgImage, scale: bubbleImage.scale, orientation: .upMirrored) 40 | } 41 | 42 | let center = CGPoint(x: bubbleImage.size.width / 2, y: bubbleImage.size.height / 2) 43 | let capInsets = UIEdgeInsetsMake(center.y, center.x, center.y, center.x) 44 | 45 | bubbleImage = bubbleImage.resizableImage(withCapInsets: capInsets, resizingMode: .stretch) 46 | assetCache?.setCachedMessageBubbleImage(bubbleImage, message: message) 47 | 48 | return bubbleImage 49 | } 50 | } 51 | 52 | func messageAttributedString(with message: OKMessage) -> NSAttributedString { 53 | if let attributedString = assetCache?.cachedMessageAttributedString(message: message) { 54 | return attributedString 55 | 56 | } else { 57 | let paragraphStyle = NSMutableParagraphStyle() 58 | paragraphStyle.lineSpacing = 4 59 | 60 | let attributes = [NSForegroundColorAttributeName : message.isIncoming ? UIColor.black : UIColor.white, 61 | NSFontAttributeName : UIFont.systemFont(ofSize: 16), 62 | NSParagraphStyleAttributeName : paragraphStyle] 63 | 64 | let attributedString = NSAttributedString(string: message.text, attributes: attributes) 65 | assetCache?.setCachedMessageAttributedString(attributedString, message: message) 66 | 67 | return attributedString 68 | } 69 | } 70 | 71 | func messageLabelImage(for labelSize: CGSize, message: OKMessage) -> UIImage { 72 | if let messageLabelImage = assetCache?.cachedMessageLabelImage(for: labelSize, message: message) { 73 | return messageLabelImage 74 | 75 | } else { 76 | let attributedString = messageAttributedString(with: message) 77 | let messageLabelImage = attributedString.image(with: labelSize) 78 | assetCache?.setCachedMessageLabelImage(messageLabelImage, forSize: labelSize, message: message) 79 | 80 | return messageLabelImage 81 | } 82 | } 83 | 84 | //MARK: - Timestamp Assets 85 | 86 | func timestampAttributedString(with message: OKMessage) -> NSAttributedString { 87 | if let attributedString = assetCache?.cachedTimestampAttributedString(message: message) { 88 | return attributedString 89 | 90 | } else { 91 | let paragraphStyle = NSMutableParagraphStyle() 92 | paragraphStyle.alignment = .center 93 | 94 | let attributes = [NSForegroundColorAttributeName : UIColor.white, 95 | NSFontAttributeName : UIFont.systemFont(ofSize: 12), 96 | NSParagraphStyleAttributeName : paragraphStyle] 97 | 98 | dateFormatter.dateStyle = .short 99 | dateFormatter.timeStyle = .short 100 | 101 | let attributedString = NSAttributedString(string: dateFormatter.string(from: message.timestamp), attributes: attributes) 102 | assetCache?.setCachedTimestampAttributedString(attributedString, message: message) 103 | 104 | return attributedString 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKConversationMessageClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKConversationMessageClient.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | let kAvatarImageKey = "kAvatarImageKey" 11 | let kIsAvatarEnabledKey = "kIsAvatarEnabledKey" 12 | let kIsIncomingKey = "kIsIncomingKey" 13 | let kIsTailEnabledKey = "kIsTailEnabledKey" 14 | let kIsTimestampEnabledKey = "kIsTimestampEnabledKey" 15 | let kMessageTextKey = "kMessageTextKey" 16 | let kMessageTimestampKey = "kMessageTimestampKey" 17 | 18 | final class OKConversationMessageClient { 19 | 20 | fileprivate let alphabet = "abcdefghijklmnopqrstuvwxyz" 21 | fileprivate let mixedCharacters = "abc🚀defghi✨jklmnopqr🌎💥stuvwxyz🌙" 22 | fileprivate let emojis = "💥✨🌎🌍🌏🔭🌙💫🚀🛰🤖🐵👨‍🚀🌕🇺🇸👽👾🖖" 23 | 24 | //MARK: - Create Conversation 25 | 26 | func createConversation(sideChangeCount: Int) -> [AnyObject] { 27 | 28 | var objects = [AnyObject]() 29 | 30 | let string = emojis 31 | 32 | var characterArray = string.characters.map({ String($0) }) 33 | 34 | let characterArrayMaxIndex = characterArray.count - 1 35 | 36 | var isIncomingMessage = true 37 | var isTimestampMessage = true 38 | 39 | var currentIndex = 0 40 | 41 | for sideChangeIndex in 0..() 13 | fileprivate var timestampLabelSizeCache = NSCache() 14 | 15 | //MARK: - Lifecycle 16 | 17 | init() { 18 | NotificationCenter.default.addObserver(self, selector: #selector(didReceiveMemoryWarning), name: NSNotification.Name.UIApplicationDidReceiveMemoryWarning, object: nil) 19 | } 20 | 21 | deinit { 22 | NotificationCenter.default.removeObserver(self) 23 | } 24 | 25 | //MARK: - Message Label Size 26 | 27 | func cachedMessageLabelSize(availableWidth: CGFloat, message: OKMessage) -> CGSize? { 28 | return messageLabelSizeCache.object(forKey: message)?[availableWidth] as? CGSize 29 | } 30 | 31 | func setCachedMessageLabelSize(_ messageLabelSize: CGSize, availableWidth: CGFloat, message: OKMessage) { 32 | if let currentCache = messageLabelSizeCache.object(forKey: message) { 33 | currentCache[availableWidth] = messageLabelSize 34 | messageLabelSizeCache.setObject(currentCache, forKey: message) 35 | 36 | } else { 37 | let currentCache = NSMutableDictionary() 38 | currentCache[availableWidth] = messageLabelSize 39 | messageLabelSizeCache.setObject(currentCache, forKey: message) 40 | } 41 | } 42 | 43 | //MARK: - Timestamp Label Size 44 | 45 | func cachedTimestampLabelSize(for size: CGSize, message: OKMessage) -> CGSize? { 46 | return timestampLabelSizeCache.object(forKey: message)?[size.width] as? CGSize 47 | } 48 | 49 | func setCachedTimestampLabelSize(_ timestampLabelSize: CGSize, for size: CGSize, message: OKMessage) { 50 | if let currentCache = timestampLabelSizeCache.object(forKey: message) { 51 | currentCache[size.width] = timestampLabelSize 52 | timestampLabelSizeCache.setObject(currentCache, forKey: message) 53 | 54 | } else { 55 | let currentCache = NSMutableDictionary() 56 | currentCache[size.width] = timestampLabelSize 57 | timestampLabelSizeCache.setObject(currentCache, forKey: message) 58 | } 59 | } 60 | 61 | //MARK: - Notifications 62 | 63 | @objc fileprivate func didReceiveMemoryWarning() { 64 | clearCache() 65 | } 66 | 67 | //MARK: Cache Maintenance 68 | 69 | func clearCache() { 70 | messageLabelSizeCache.removeAllObjects() 71 | timestampLabelSizeCache.removeAllObjects() 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKConversationSizingFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKConversationSizingFactory.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OKConversationSizingFactory { 11 | 12 | fileprivate let sizingCache: OKConversationSizingCache? 13 | fileprivate let measuringLabel = UILabel() 14 | 15 | // Message Cell 16 | let avatarSize = CGSize(width: 45, height: 45) 17 | let avatarToBubblePadding: CGFloat = 8 18 | let bubbleTailWidth: CGFloat = 6 19 | let bubbleLabelHorizontalPadding: CGFloat = 16 20 | let bubbleTrailingPadding: CGFloat = 90 21 | let bubbleLabelVerticalPadding: CGFloat = 12 22 | let messageOuterPadding: CGFloat = 8 23 | let messageSpacing = 1 / UIScreen.main.scale 24 | 25 | // Timestamp Cell 26 | let timestampTopVerticalPadding: CGFloat = 12 27 | let timestampBottomVerticalPadding: CGFloat = 6 28 | 29 | // Loading Cell 30 | let loadingCellHeight: CGFloat = 70 31 | let loadingCellImageLength: CGFloat = 35 32 | 33 | //MARK: - Lifecycle 34 | 35 | init(sizingCache: OKConversationSizingCache? = OKConversationSizingCache()) { 36 | self.sizingCache = kIsOptimized ? sizingCache : nil 37 | } 38 | 39 | //MARK: - Message Sizes 40 | 41 | func bubbleImageSize(for labelSize: CGSize) -> CGSize { 42 | var bubbleSize = labelSize 43 | bubbleSize.width += bubbleTailWidth + bubbleLabelHorizontalPadding * 2 44 | bubbleSize.height += bubbleLabelVerticalPadding * 2 45 | 46 | return bubbleSize 47 | } 48 | 49 | func messageLabelSize(for size: CGSize, message: OKMessage, attributedString: NSAttributedString) -> CGSize { 50 | let availableWidth = availableMessageLabelWidth(for: size) 51 | 52 | if let messageLabelSize = sizingCache?.cachedMessageLabelSize(availableWidth: availableWidth, message: message) { 53 | return messageLabelSize 54 | 55 | } else { 56 | measuringLabel.attributedText = attributedString 57 | measuringLabel.preferredMaxLayoutWidth = availableWidth 58 | measuringLabel.numberOfLines = 0 59 | 60 | let messageLabelSize = measuringLabel.sizeThatFits(CGSize(width: availableWidth, height: .infinity)) 61 | sizingCache?.setCachedMessageLabelSize(messageLabelSize, availableWidth: availableWidth, message: message) 62 | 63 | return messageLabelSize 64 | } 65 | } 66 | 67 | //MARK: - Message Sizes Helpers 68 | 69 | fileprivate func availableMessageLabelWidth(for size: CGSize) -> CGFloat { 70 | return size.width - messageOuterPadding - avatarSize.width - avatarToBubblePadding - bubbleTailWidth - bubbleLabelHorizontalPadding * 2 - bubbleTrailingPadding - messageOuterPadding 71 | } 72 | 73 | //MARK: - Timestamp Sizes 74 | 75 | func timestampLabelSize(for size: CGSize, message: OKMessage, attributedString: NSAttributedString) -> CGSize { 76 | if let timestampLabelSize = sizingCache?.cachedTimestampLabelSize(for: size, message: message) { 77 | return timestampLabelSize 78 | 79 | } else { 80 | measuringLabel.attributedText = attributedString 81 | measuringLabel.preferredMaxLayoutWidth = size.width 82 | measuringLabel.numberOfLines = 1 83 | 84 | let timestampLabelSize = measuringLabel.sizeThatFits(size) 85 | sizingCache?.setCachedTimestampLabelSize(timestampLabelSize, for: size, message: message) 86 | 87 | return timestampLabelSize 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKConversationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKConversationViewController.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OKConversationViewController: UIViewController { 11 | 12 | override var prefersStatusBarHidden: Bool { 13 | return false 14 | } 15 | 16 | fileprivate let collectionView: UICollectionView = { 17 | let flowLayout = UICollectionViewFlowLayout() 18 | flowLayout.minimumInteritemSpacing = 0 19 | flowLayout.minimumLineSpacing = 0 20 | return UICollectionView(frame: .zero, collectionViewLayout: flowLayout) 21 | }() 22 | 23 | fileprivate var dataSource = [AnyObject]() 24 | 25 | fileprivate let messageMeasuringCell = OKMessageCell() 26 | fileprivate let timestampMeasuringCell = OKTimestampCell() 27 | 28 | fileprivate let assetFactory: OKConversationAssetFactory 29 | fileprivate let sizingFactory: OKConversationSizingFactory 30 | fileprivate let messageClient: OKConversationMessageClient 31 | 32 | //MARK: - Lifecycle 33 | 34 | init(assetFactory: OKConversationAssetFactory, sizingFactory: OKConversationSizingFactory, messageClient: OKConversationMessageClient) { 35 | self.assetFactory = assetFactory 36 | self.sizingFactory = sizingFactory 37 | self.messageClient = messageClient 38 | 39 | super.init(nibName: nil, bundle: nil) 40 | } 41 | 42 | required init?(coder aDecoder: NSCoder) { 43 | fatalError("init(coder:) has not been implemented") 44 | } 45 | 46 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 47 | fatalError("init(nibName:bundle:) has not been implemented") 48 | } 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | 53 | setupNavigationBar() 54 | setupCollectionView() 55 | } 56 | 57 | override func viewDidAppear(_ animated: Bool) { 58 | super.viewDidAppear(animated) 59 | 60 | setupMessages() 61 | } 62 | 63 | //MARK: - Layout 64 | 65 | override func viewWillLayoutSubviews() { 66 | super.viewWillLayoutSubviews() 67 | 68 | collectionView.collectionViewLayout.invalidateLayout() 69 | collectionView.frame = view.bounds 70 | collectionView.performBatchUpdates(nil, completion: nil) 71 | } 72 | 73 | //MARK: - Animations 74 | 75 | // This animation probably belongs in a flow layout 76 | fileprivate func animateReloadData() { 77 | collectionView.reloadData() 78 | collectionView.performBatchUpdates(nil, completion: nil) 79 | 80 | enableMessageCellLabels() 81 | 82 | let visibleIndexPaths = collectionView.indexPathsForVisibleItems.sorted() 83 | 84 | for (index, indexPath) in visibleIndexPaths.enumerated() { 85 | let cell = collectionView.cellForItem(at: indexPath) 86 | let height = cell?.contentView.frame.height ?? 0 87 | cell?.transform = CGAffineTransform(translationX: 0, y: -height).scaledBy(x: 1.75, y: 1.75) 88 | cell?.alpha = 0 89 | 90 | UIView.animate(withDuration: 0.2, delay: 0.02 * TimeInterval(index), options: .curveEaseOut, animations: { 91 | cell?.transform = .identity 92 | cell?.alpha = 1 93 | }, completion: nil) 94 | } 95 | } 96 | 97 | //MARK: - Helpers 98 | 99 | fileprivate func enableMessageCellLabels() { 100 | for cell in collectionView.visibleCells { 101 | if let cell = cell as? OKMessageCell, cell.isMessageImageView { 102 | cell.isMessageImageView = false 103 | } 104 | } 105 | } 106 | 107 | func dequeueMessageCellComponent(with message: OKMessage, collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { 108 | let component = message.components()[indexPath.item] 109 | 110 | switch component { 111 | case .timestamp: 112 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OKTimestampCell.reuseID, for: indexPath) as! OKTimestampCell 113 | cell.configure(with: message, assetFactory: assetFactory, sizingFactory: sizingFactory) 114 | return cell 115 | 116 | case .message: 117 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OKMessageCell.reuseID, for: indexPath) as! OKMessageCell 118 | cell.configure(with: message, assetFactory: assetFactory, sizingFactory: sizingFactory) 119 | return cell 120 | } 121 | } 122 | 123 | fileprivate func sizeForMessageCellComponent(with message: OKMessage, at indexPath: IndexPath) -> CGSize { 124 | let component = message.components()[indexPath.item] 125 | 126 | switch component { 127 | case .timestamp: 128 | return timestampMeasuringCell.sizeThatFits(CGSize(width: collectionView.bounds.width, height: .infinity), 129 | message: message, 130 | assetFactory: assetFactory, 131 | sizingFactory: sizingFactory) 132 | 133 | case .message: 134 | if !kIsOptimized { 135 | messageMeasuringCell.configure(with: message, assetFactory: assetFactory, sizingFactory: sizingFactory) 136 | } 137 | 138 | return messageMeasuringCell.sizeThatFits(CGSize(width: collectionView.bounds.width, height: .infinity), 139 | message: message, 140 | assetFactory: assetFactory, 141 | sizingFactory: sizingFactory) 142 | } 143 | } 144 | 145 | //MARK: - Setup 146 | 147 | fileprivate func setupNavigationBar() { 148 | navigationController?.navigationBar.barStyle = .black 149 | } 150 | 151 | fileprivate func setupCollectionView() { 152 | view.addSubview(collectionView) 153 | collectionView.alwaysBounceVertical = true 154 | collectionView.backgroundView = UIImageView(image: #imageLiteral(resourceName: "MoonBackground")) 155 | collectionView.indicatorStyle = .white 156 | collectionView.register(OKLoadingCell.self, forCellWithReuseIdentifier: OKLoadingCell.reuseID) 157 | collectionView.register(OKMessageCell.self, forCellWithReuseIdentifier: OKMessageCell.reuseID) 158 | collectionView.register(OKTimestampCell.self, forCellWithReuseIdentifier: OKTimestampCell.reuseID) 159 | collectionView.dataSource = self 160 | collectionView.delegate = self 161 | } 162 | 163 | fileprivate func setupMessages() { 164 | DispatchQueue.main.async { 165 | self.dataSource.append(OKLoading()) 166 | self.collectionView.reloadData() 167 | } 168 | 169 | DispatchQueue.global().async { 170 | let conversation = self.messageClient.createConversation(sideChangeCount: 100) 171 | 172 | if kIsOptimized { 173 | let portraitSize = CGSize(width: self.collectionView.bounds.width, height: .infinity) 174 | let landscapeSize = CGSize(width: self.collectionView.bounds.height, height: .infinity) 175 | 176 | let messages = conversation.filter({ $0 is OKMessage }) as! [OKMessage] 177 | 178 | // Cache all sizing and image assets on background thread 179 | for message in messages { 180 | self.messageMeasuringCell.cache(for: portraitSize, 181 | message: message, 182 | assetFactory: self.assetFactory, 183 | sizingFactory: self.sizingFactory) 184 | 185 | self.messageMeasuringCell.cache(for: landscapeSize, 186 | message: message, 187 | assetFactory: self.assetFactory, 188 | sizingFactory: self.sizingFactory) 189 | 190 | if !message.isTimestampEnabled { 191 | continue 192 | } 193 | 194 | self.timestampMeasuringCell.cache(for: portraitSize, 195 | message: message, 196 | assetFactory: self.assetFactory, 197 | sizingFactory: self.sizingFactory) 198 | 199 | self.timestampMeasuringCell.cache(for: landscapeSize, 200 | message: message, 201 | assetFactory: self.assetFactory, 202 | sizingFactory: self.sizingFactory) 203 | } 204 | } 205 | 206 | DispatchQueue.main.async { 207 | self.dataSource.insert(contentsOf: conversation, at: 0) 208 | self.animateReloadData() 209 | } 210 | } 211 | } 212 | 213 | } 214 | 215 | //MARK: - UICollectionViewDataSource 216 | 217 | extension OKConversationViewController: UICollectionViewDataSource { 218 | 219 | func numberOfSections(in collectionView: UICollectionView) -> Int { 220 | return dataSource.count 221 | } 222 | 223 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 224 | let object = dataSource[section] 225 | 226 | if let object = object as? OKMessage { 227 | return object.components().count 228 | 229 | } else { 230 | return 1 231 | } 232 | } 233 | 234 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 235 | let object = dataSource[indexPath.section] 236 | 237 | if let message = object as? OKMessage { 238 | return dequeueMessageCellComponent(with: message, collectionView: collectionView, at: indexPath) 239 | 240 | } else if object is OKLoading { 241 | let loadingCell = collectionView.dequeueReusableCell(withReuseIdentifier: OKLoadingCell.reuseID, for: indexPath) as! OKLoadingCell 242 | loadingCell.configure(with: sizingFactory) 243 | return loadingCell 244 | } 245 | 246 | fatalError("Did not properly dequeue collectionView(_:cellForItemAt:)") 247 | } 248 | 249 | } 250 | 251 | //MARK: - UICollectionViewDelegateFlowLayout 252 | 253 | extension OKConversationViewController: UICollectionViewDelegateFlowLayout { 254 | 255 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 256 | let object = dataSource[section] 257 | 258 | if object is OKMessage { 259 | return UIEdgeInsets(top: sizingFactory.messageSpacing, left: 0, bottom: sizingFactory.messageSpacing, right: 0) 260 | 261 | } else { 262 | return .zero 263 | } 264 | } 265 | 266 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 267 | let object = dataSource[indexPath.section] 268 | 269 | if object is OKLoading { 270 | return CGSize(width: collectionView.bounds.width, height: sizingFactory.loadingCellHeight) 271 | 272 | } else if let message = object as? OKMessage { 273 | return sizeForMessageCellComponent(with: message, at: indexPath) 274 | } 275 | 276 | fatalError("Did not properly return size collectionView(_:layout:sizeForItemAt:)") 277 | } 278 | 279 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 280 | if let cell = cell as? OKLoadingCell { 281 | cell.startAnimating() 282 | } 283 | } 284 | 285 | func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 286 | if let cell = cell as? OKLoadingCell { 287 | cell.stopAnimating() 288 | } 289 | } 290 | 291 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 292 | if !decelerate { 293 | enableMessageCellLabels() 294 | } 295 | } 296 | 297 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 298 | enableMessageCellLabels() 299 | } 300 | 301 | } 302 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKLoading.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKLoading.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | class OKLoading { 11 | // This class is a stub for collectionView(_:cellForItemAt:) logic in OKConversationViewController 12 | // It can be customized to pass configurations to the OKLoadingCell 13 | } 14 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKLoadingCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKLoadingCell.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OKLoadingCell: UICollectionViewCell { 11 | 12 | static let reuseID = "kLoadingCellReuseIdentifier" 13 | 14 | fileprivate let imageView: UIImageView = { 15 | let imageView = UIImageView(image: #imageLiteral(resourceName: "UFO")) 16 | imageView.contentMode = .scaleAspectFit 17 | return imageView 18 | }() 19 | 20 | fileprivate var rotationAnimation: CABasicAnimation? 21 | 22 | fileprivate(set) weak var sizingFactory: OKConversationSizingFactory? 23 | 24 | //MARK: - Lifecycle 25 | 26 | override init(frame: CGRect) { 27 | super.init(frame: frame) 28 | 29 | contentView.addSubview(imageView) 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | //MARK: - Configure 37 | 38 | func configure(with sizingFactory: OKConversationSizingFactory) { 39 | self.sizingFactory = sizingFactory 40 | 41 | setNeedsLayout() 42 | } 43 | 44 | //MARK: - Layout 45 | 46 | override func layoutSubviews() { 47 | super.layoutSubviews() 48 | 49 | layoutCell() 50 | } 51 | 52 | //MARK: - Layout Helpers 53 | 54 | fileprivate func layoutCell() { 55 | guard let sizingFactory = sizingFactory else { 56 | fatalError("OKLoadingCell did not call configure(with:) before layout") 57 | } 58 | 59 | imageView.frame = CGRect(x: 0, 60 | y: 0, 61 | width: sizingFactory.loadingCellImageLength, 62 | height: sizingFactory.loadingCellImageLength) 63 | 64 | imageView.center = contentView.center 65 | } 66 | 67 | //MARK: - Animation 68 | 69 | func startAnimating() { 70 | rotationAnimation = CABasicAnimation(keyPath: "transform.rotation") 71 | 72 | guard let rotationAnimation = rotationAnimation else { 73 | return 74 | } 75 | 76 | rotationAnimation.fromValue = 0.0 77 | rotationAnimation.toValue = .pi * 2.0 78 | rotationAnimation.duration = 1.25 79 | rotationAnimation.repeatCount = .infinity 80 | rotationAnimation.isRemovedOnCompletion = false 81 | 82 | layer.add(rotationAnimation, forKey: "rotationAnimation") 83 | } 84 | 85 | func stopAnimating() { 86 | layer.removeAllAnimations() 87 | rotationAnimation = nil 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKMessage.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | enum OKMessageComponents { 11 | case timestamp, message 12 | } 13 | 14 | final class OKMessage: NSObject { 15 | 16 | let avatarImage: UIImage 17 | let isAvatarEnabled: Bool 18 | let isIncoming: Bool 19 | let isTailEnabled: Bool 20 | let isTimestampEnabled: Bool 21 | let text: String 22 | let timestamp: Date 23 | 24 | init?(json: [String : Any]) { 25 | guard 26 | let avatarImage = json[kAvatarImageKey] as? UIImage, 27 | let isAvatarEnabled = json[kIsAvatarEnabledKey] as? Bool, 28 | let isIncoming = json[kIsIncomingKey] as? Bool, 29 | let isTailEnabled = json[kIsTailEnabledKey] as? Bool, 30 | let isTimestampEnabled = json[kIsTimestampEnabledKey] as? Bool, 31 | let text = json[kMessageTextKey] as? String, 32 | let timestamp = json[kMessageTimestampKey] as? Date 33 | else { return nil } 34 | 35 | self.avatarImage = avatarImage 36 | self.isAvatarEnabled = isAvatarEnabled 37 | self.isIncoming = isIncoming 38 | self.isTailEnabled = isTailEnabled 39 | self.isTimestampEnabled = isTimestampEnabled 40 | self.text = text 41 | self.timestamp = timestamp 42 | } 43 | 44 | //MARK: - Helpers 45 | 46 | func components() -> [OKMessageComponents] { 47 | return isTimestampEnabled ? [.timestamp, .message] : [.message] 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKMessageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKMessageCell.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OKMessageCell: UICollectionViewCell { 11 | 12 | static let reuseID = "kMessageCellReuseIdentifier" 13 | 14 | fileprivate let avatarButton = UIButton() 15 | fileprivate let bubbleImageView = UIImageView() 16 | fileprivate let messageLabel = UILabel() 17 | fileprivate let messageLabelImageView = UIImageView() 18 | 19 | fileprivate(set) var message: OKMessage? 20 | fileprivate(set) weak var assetFactory: OKConversationAssetFactory? 21 | fileprivate(set) weak var sizingFactory: OKConversationSizingFactory? 22 | 23 | var isMessageImageView = false { 24 | didSet { 25 | if isMessageImageView != oldValue { 26 | toggleVisibleMessageView() 27 | } 28 | } 29 | } 30 | 31 | //MARK: - Lifecycle 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: frame) 35 | 36 | setupAvatarView() 37 | setupBubbleImageView() 38 | setupMessageViews() 39 | } 40 | 41 | required init?(coder aDecoder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | //MARK: - Configure 46 | 47 | func configure(with message: OKMessage, assetFactory: OKConversationAssetFactory, sizingFactory: OKConversationSizingFactory) { 48 | self.message = message 49 | self.assetFactory = assetFactory 50 | self.sizingFactory = sizingFactory 51 | 52 | isMessageImageView = kIsOptimized 53 | 54 | configureAvatarImageView(with: message) 55 | configureBubbleImageView(with: message) 56 | configureMessageLabel(with: message, assetFactory: assetFactory) 57 | 58 | setNeedsLayout() 59 | } 60 | 61 | //MARK: - Configure Helpers 62 | 63 | fileprivate func configureAvatarImageView(with message: OKMessage) { 64 | avatarButton.isHidden = !message.isAvatarEnabled 65 | 66 | if !avatarButton.isHidden { 67 | avatarButton.setImage(message.avatarImage, for: .normal) 68 | } 69 | } 70 | 71 | fileprivate func configureBubbleImageView(with message: OKMessage) { 72 | if !kIsOptimized { 73 | bubbleImageView.transform = message.isIncoming ? CGAffineTransform(scaleX: -1, y: 1) : .identity 74 | bubbleImageView.tintColor = message.isIncoming ? .lightGray : .black 75 | } 76 | } 77 | 78 | fileprivate func configureMessageLabel(with message: OKMessage, assetFactory: OKConversationAssetFactory) { 79 | messageLabel.attributedText = assetFactory.messageAttributedString(with: message) 80 | } 81 | 82 | //MARK: - Layout 83 | 84 | override func layoutSubviews() { 85 | super.layoutSubviews() 86 | 87 | layoutCell() 88 | } 89 | 90 | //MARK: - Layout Helpers 91 | 92 | fileprivate func layoutCell() { 93 | guard let message = message, let assetFactory = assetFactory, let sizingFactory = sizingFactory else { 94 | fatalError("OKMessageCell did not call configure(with:assetFactory:sizingFactory:) before layout") 95 | } 96 | 97 | let bounds = contentView.bounds 98 | 99 | let attributedString = assetFactory.messageAttributedString(with: message) 100 | 101 | let avatarSize = sizingFactory.avatarSize 102 | let labelSize = sizingFactory.messageLabelSize(for: bounds.size, message: message, attributedString: attributedString) 103 | let bubbleSize = sizingFactory.bubbleImageSize(for: labelSize) 104 | let bubbleLabelVerticalPadding = sizingFactory.bubbleLabelVerticalPadding 105 | let bubbleLabelHorizontalPadding = sizingFactory.bubbleLabelHorizontalPadding 106 | let bubbleTailWidth = sizingFactory.bubbleTailWidth 107 | let outerPadding = sizingFactory.messageOuterPadding 108 | 109 | if message.isIncoming { 110 | avatarButton.frame = CGRect(x: outerPadding, 111 | y: bounds.maxY - avatarSize.height, 112 | width: avatarSize.width, 113 | height: avatarSize.height) 114 | 115 | bubbleImageView.frame = CGRect(x: avatarButton.frame.maxX + outerPadding, 116 | y: 0, 117 | width: bubbleSize.width, 118 | height: bubbleSize.height) 119 | 120 | messageView().frame = CGRect(x: bubbleImageView.frame.origin.x + bubbleTailWidth + bubbleLabelHorizontalPadding, 121 | y: bubbleLabelVerticalPadding, 122 | width: labelSize.width, 123 | height: labelSize.height) 124 | 125 | } else { 126 | avatarButton.frame = CGRect(x: bounds.maxX - outerPadding - avatarSize.width, 127 | y: bounds.maxY - avatarSize.height, 128 | width: avatarSize.width, 129 | height: avatarSize.height) 130 | 131 | bubbleImageView.frame = CGRect(x: avatarButton.frame.origin.x - outerPadding - bubbleSize.width, 132 | y: 0, 133 | width: bubbleSize.width, 134 | height: bubbleSize.height) 135 | 136 | messageView().frame = CGRect(x: bubbleImageView.frame.origin.x + bubbleLabelHorizontalPadding, 137 | y: bubbleLabelVerticalPadding, 138 | width: labelSize.width, 139 | height: labelSize.height) 140 | } 141 | 142 | avatarButton.layer.cornerRadius = avatarButton.frame.height / 2 143 | bubbleImageView.image = assetFactory.bubbleImage(for: message) 144 | messageLabelImageView.image = kIsOptimized ? assetFactory.messageLabelImage(for: labelSize, message: message) : nil 145 | } 146 | 147 | fileprivate func messageView() -> UIView { 148 | return isMessageImageView ? messageLabelImageView : messageLabel 149 | } 150 | 151 | fileprivate func toggleVisibleMessageView() { 152 | messageLabel.frame = .zero 153 | messageLabelImageView.frame = .zero 154 | 155 | messageLabel.alpha = isMessageImageView ? 0 : 1 156 | messageLabelImageView.alpha = isMessageImageView ? 1 : 0 157 | 158 | setNeedsLayout() 159 | } 160 | 161 | //MARK: - Caching 162 | 163 | func cache(for size: CGSize, message: OKMessage, assetFactory: OKConversationAssetFactory, sizingFactory: OKConversationSizingFactory) { 164 | let attributedString = assetFactory.messageAttributedString(with: message) 165 | let labelSize = sizingFactory.messageLabelSize(for: size, message: message, attributedString: attributedString) 166 | 167 | _ = assetFactory.messageLabelImage(for: labelSize, message: message) 168 | _ = assetFactory.bubbleImage(for: message) 169 | } 170 | 171 | //MARK: - Sizing 172 | 173 | func sizeThatFits(_ size: CGSize, message: OKMessage, assetFactory: OKConversationAssetFactory, sizingFactory: OKConversationSizingFactory) -> CGSize { 174 | let attributedString = assetFactory.messageAttributedString(with: message) 175 | let labelSize = sizingFactory.messageLabelSize(for: size, message: message, attributedString: attributedString) 176 | let bubbleSize = sizingFactory.bubbleImageSize(for: labelSize) 177 | 178 | let avatarHeight = message.isAvatarEnabled ? sizingFactory.avatarSize.height : 0 179 | 180 | return CGSize(width: size.width, height: max(bubbleSize.height, avatarHeight)) 181 | } 182 | 183 | //MARK: - Setup 184 | 185 | fileprivate func setupAvatarView() { 186 | avatarButton.imageView?.contentMode = .scaleAspectFill 187 | avatarButton.layer.masksToBounds = true 188 | contentView.addSubview(avatarButton) 189 | } 190 | 191 | fileprivate func setupBubbleImageView() { 192 | bubbleImageView.alpha = 0.9 193 | contentView.addSubview(bubbleImageView) 194 | } 195 | 196 | fileprivate func setupMessageViews() { 197 | messageLabel.numberOfLines = 0 198 | contentView.addSubview(messageLabel) 199 | contentView.addSubview(messageLabelImageView) 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /ScrollingPerformance/OKTimestampCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKTimestampCell.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OKTimestampCell: UICollectionViewCell { 11 | 12 | static let reuseID = "kTimestampCellReuseIdentifier" 13 | 14 | fileprivate let timestampLabel = UILabel() 15 | 16 | fileprivate(set) var message: OKMessage? 17 | fileprivate(set) weak var assetFactory: OKConversationAssetFactory? 18 | fileprivate(set) weak var sizingFactory: OKConversationSizingFactory? 19 | 20 | //MARK: - Lifecycle 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | 25 | setupTimestampLabel() 26 | } 27 | 28 | required init?(coder aDecoder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | //MARK: - Configure 33 | 34 | func configure(with message: OKMessage, assetFactory: OKConversationAssetFactory, sizingFactory: OKConversationSizingFactory) { 35 | self.message = message 36 | self.assetFactory = assetFactory 37 | self.sizingFactory = sizingFactory 38 | 39 | timestampLabel.attributedText = assetFactory.timestampAttributedString(with: message) 40 | 41 | setNeedsLayout() 42 | } 43 | 44 | //MARK: - Layout 45 | 46 | override func layoutSubviews() { 47 | super.layoutSubviews() 48 | 49 | layoutCell() 50 | } 51 | 52 | //MARK: - Layout Helpers 53 | 54 | fileprivate func layoutCell() { 55 | guard let sizingFactory = sizingFactory else { 56 | fatalError("OKTimestampCell did not call configure(with:assetFactory:sizingFactory:) before layout") 57 | } 58 | 59 | let topVerticalPadding = sizingFactory.timestampTopVerticalPadding 60 | let bottomVerticalPadding = sizingFactory.timestampBottomVerticalPadding 61 | 62 | timestampLabel.frame = CGRect(x: 0, 63 | y: topVerticalPadding, 64 | width: contentView.bounds.width, 65 | height: contentView.bounds.height - topVerticalPadding - bottomVerticalPadding) 66 | } 67 | 68 | //MARK: - Caching 69 | 70 | func cache(for size: CGSize, message: OKMessage, assetFactory: OKConversationAssetFactory, sizingFactory: OKConversationSizingFactory) { 71 | let attributedString = assetFactory.timestampAttributedString(with: message) 72 | _ = sizingFactory.timestampLabelSize(for: size, message: message, attributedString: attributedString) 73 | } 74 | 75 | //MARK: - Sizing 76 | 77 | func sizeThatFits(_ size: CGSize, message: OKMessage, assetFactory: OKConversationAssetFactory, sizingFactory: OKConversationSizingFactory) -> CGSize { 78 | let attributedString = assetFactory.timestampAttributedString(with: message) 79 | let labelSize = sizingFactory.timestampLabelSize(for: size, message: message, attributedString: attributedString) 80 | let verticalPadding = sizingFactory.timestampTopVerticalPadding + sizingFactory.timestampBottomVerticalPadding 81 | 82 | return CGSize(width: size.width, height: labelSize.height + verticalPadding) 83 | } 84 | 85 | //MARK: - Setup 86 | 87 | fileprivate func setupTimestampLabel() { 88 | contentView.addSubview(timestampLabel) 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /ScrollingPerformance/UIImage+Util.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Util.swift 3 | // ScrollingPerformance 4 | // 5 | // Copyright © 2017 OkCupid. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImage { 11 | 12 | func image(maskColor: CGColor) -> UIImage { 13 | 14 | let imageRect = CGRect(x: 0, y: 0, width: size.width, height: size.height) 15 | 16 | UIGraphicsBeginImageContextWithOptions(imageRect.size, false, scale) 17 | 18 | guard let context = UIGraphicsGetCurrentContext(), let cgImage = cgImage else { 19 | UIGraphicsEndImageContext() 20 | return UIImage() 21 | } 22 | 23 | context.scaleBy(x: 1, y: -1) 24 | context.translateBy(x: 0, y: -(imageRect.size.height)) 25 | context.clip(to: imageRect, mask: cgImage) 26 | context.setFillColor(maskColor) 27 | context.fill(imageRect) 28 | 29 | guard let image = UIGraphicsGetImageFromCurrentImageContext() else { 30 | UIGraphicsEndImageContext() 31 | return UIImage() 32 | } 33 | 34 | UIGraphicsEndImageContext() 35 | 36 | return image; 37 | } 38 | 39 | } 40 | 41 | extension UIImage: NSDiscardableContent { 42 | 43 | public func beginContentAccess() -> Bool { 44 | return true 45 | } 46 | public func endContentAccess() {} 47 | public func discardContentIfPossible() {} 48 | public func isContentDiscarded() -> Bool { 49 | return false 50 | } 51 | } 52 | --------------------------------------------------------------------------------