├── .gitignore ├── Infinite-NSScrollView-Diagram.monopic ├── Infinite-NSScrollView.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── Infinite-NSScrollView ├── AppDelegate.h ├── AppDelegate.m ├── Base.lproj │ └── MainMenu.xib ├── HTContentTileView.h ├── HTContentTileView.m ├── HTInfiniteClipView.h ├── HTInfiniteClipView.m ├── HTInfiniteDocumentView.h ├── HTInfiniteDocumentView.m ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist └── main.m ├── Infinite-NSScrollViewTests ├── Infinite_NSScrollViewTests.m └── Info.plist ├── LICENSE └── README /.gitignore: -------------------------------------------------------------------------------- 1 | *.xccheckout 2 | xcuserdata 3 | *.mode1v3 4 | *.mode2v3 5 | .DS_Store -------------------------------------------------------------------------------- /Infinite-NSScrollView-Diagram.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helftone/infinite-nsscrollview/233ec5db52aa8a05e227023f435e09673250b2e3/Infinite-NSScrollView-Diagram.monopic -------------------------------------------------------------------------------- /Infinite-NSScrollView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F32274041ACB37AE00B569EC /* HTInfiniteClipView.m in Sources */ = {isa = PBXBuildFile; fileRef = F32274031ACB37AE00B569EC /* HTInfiniteClipView.m */; }; 11 | F360A8821ACABE99002C0333 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F360A8811ACABE99002C0333 /* AppDelegate.m */; }; 12 | F360A8841ACABE99002C0333 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F360A8831ACABE99002C0333 /* main.m */; }; 13 | F360A8861ACABE99002C0333 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F360A8851ACABE99002C0333 /* Images.xcassets */; }; 14 | F360A8891ACABE99002C0333 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = F360A8871ACABE99002C0333 /* MainMenu.xib */; }; 15 | F360A8951ACABE99002C0333 /* Infinite_NSScrollViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F360A8941ACABE99002C0333 /* Infinite_NSScrollViewTests.m */; }; 16 | F360A8A01ACABEDC002C0333 /* HTInfiniteDocumentView.m in Sources */ = {isa = PBXBuildFile; fileRef = F360A89F1ACABEDC002C0333 /* HTInfiniteDocumentView.m */; }; 17 | F360A8A31ACB16BF002C0333 /* HTContentTileView.m in Sources */ = {isa = PBXBuildFile; fileRef = F360A8A21ACB16BF002C0333 /* HTContentTileView.m */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | F360A88F1ACABE99002C0333 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = F360A8731ACABE99002C0333 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = F360A87A1ACABE99002C0333; 26 | remoteInfo = "Infinite-NSScrollView"; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | F32274021ACB37AE00B569EC /* HTInfiniteClipView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTInfiniteClipView.h; sourceTree = ""; }; 32 | F32274031ACB37AE00B569EC /* HTInfiniteClipView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTInfiniteClipView.m; sourceTree = ""; }; 33 | F360A87B1ACABE99002C0333 /* Infinite-NSScrollView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Infinite-NSScrollView.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | F360A87F1ACABE99002C0333 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | F360A8801ACABE99002C0333 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 36 | F360A8811ACABE99002C0333 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 37 | F360A8831ACABE99002C0333 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 38 | F360A8851ACABE99002C0333 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 39 | F360A8881ACABE99002C0333 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 40 | F360A88E1ACABE99002C0333 /* Infinite-NSScrollViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Infinite-NSScrollViewTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | F360A8931ACABE99002C0333 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | F360A8941ACABE99002C0333 /* Infinite_NSScrollViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Infinite_NSScrollViewTests.m; sourceTree = ""; }; 43 | F360A89E1ACABEDC002C0333 /* HTInfiniteDocumentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTInfiniteDocumentView.h; sourceTree = ""; }; 44 | F360A89F1ACABEDC002C0333 /* HTInfiniteDocumentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTInfiniteDocumentView.m; sourceTree = ""; }; 45 | F360A8A11ACB16BF002C0333 /* HTContentTileView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTContentTileView.h; sourceTree = ""; }; 46 | F360A8A21ACB16BF002C0333 /* HTContentTileView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTContentTileView.m; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | F360A8781ACABE99002C0333 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | F360A88B1ACABE99002C0333 /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | F360A8721ACABE99002C0333 = { 68 | isa = PBXGroup; 69 | children = ( 70 | F360A87D1ACABE99002C0333 /* Infinite-NSScrollView */, 71 | F360A8911ACABE99002C0333 /* Infinite-NSScrollViewTests */, 72 | F360A87C1ACABE99002C0333 /* Products */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | F360A87C1ACABE99002C0333 /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | F360A87B1ACABE99002C0333 /* Infinite-NSScrollView.app */, 80 | F360A88E1ACABE99002C0333 /* Infinite-NSScrollViewTests.xctest */, 81 | ); 82 | name = Products; 83 | sourceTree = ""; 84 | }; 85 | F360A87D1ACABE99002C0333 /* Infinite-NSScrollView */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | F360A8801ACABE99002C0333 /* AppDelegate.h */, 89 | F360A8811ACABE99002C0333 /* AppDelegate.m */, 90 | F360A8851ACABE99002C0333 /* Images.xcassets */, 91 | F360A8871ACABE99002C0333 /* MainMenu.xib */, 92 | F360A87E1ACABE99002C0333 /* Supporting Files */, 93 | F360A89E1ACABEDC002C0333 /* HTInfiniteDocumentView.h */, 94 | F360A89F1ACABEDC002C0333 /* HTInfiniteDocumentView.m */, 95 | F360A8A11ACB16BF002C0333 /* HTContentTileView.h */, 96 | F360A8A21ACB16BF002C0333 /* HTContentTileView.m */, 97 | F32274021ACB37AE00B569EC /* HTInfiniteClipView.h */, 98 | F32274031ACB37AE00B569EC /* HTInfiniteClipView.m */, 99 | ); 100 | path = "Infinite-NSScrollView"; 101 | sourceTree = ""; 102 | }; 103 | F360A87E1ACABE99002C0333 /* Supporting Files */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | F360A87F1ACABE99002C0333 /* Info.plist */, 107 | F360A8831ACABE99002C0333 /* main.m */, 108 | ); 109 | name = "Supporting Files"; 110 | sourceTree = ""; 111 | }; 112 | F360A8911ACABE99002C0333 /* Infinite-NSScrollViewTests */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | F360A8941ACABE99002C0333 /* Infinite_NSScrollViewTests.m */, 116 | F360A8921ACABE99002C0333 /* Supporting Files */, 117 | ); 118 | path = "Infinite-NSScrollViewTests"; 119 | sourceTree = ""; 120 | }; 121 | F360A8921ACABE99002C0333 /* Supporting Files */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | F360A8931ACABE99002C0333 /* Info.plist */, 125 | ); 126 | name = "Supporting Files"; 127 | sourceTree = ""; 128 | }; 129 | /* End PBXGroup section */ 130 | 131 | /* Begin PBXNativeTarget section */ 132 | F360A87A1ACABE99002C0333 /* Infinite-NSScrollView */ = { 133 | isa = PBXNativeTarget; 134 | buildConfigurationList = F360A8981ACABE99002C0333 /* Build configuration list for PBXNativeTarget "Infinite-NSScrollView" */; 135 | buildPhases = ( 136 | F360A8771ACABE99002C0333 /* Sources */, 137 | F360A8781ACABE99002C0333 /* Frameworks */, 138 | F360A8791ACABE99002C0333 /* Resources */, 139 | ); 140 | buildRules = ( 141 | ); 142 | dependencies = ( 143 | ); 144 | name = "Infinite-NSScrollView"; 145 | productName = "Infinite-NSScrollView"; 146 | productReference = F360A87B1ACABE99002C0333 /* Infinite-NSScrollView.app */; 147 | productType = "com.apple.product-type.application"; 148 | }; 149 | F360A88D1ACABE99002C0333 /* Infinite-NSScrollViewTests */ = { 150 | isa = PBXNativeTarget; 151 | buildConfigurationList = F360A89B1ACABE99002C0333 /* Build configuration list for PBXNativeTarget "Infinite-NSScrollViewTests" */; 152 | buildPhases = ( 153 | F360A88A1ACABE99002C0333 /* Sources */, 154 | F360A88B1ACABE99002C0333 /* Frameworks */, 155 | F360A88C1ACABE99002C0333 /* Resources */, 156 | ); 157 | buildRules = ( 158 | ); 159 | dependencies = ( 160 | F360A8901ACABE99002C0333 /* PBXTargetDependency */, 161 | ); 162 | name = "Infinite-NSScrollViewTests"; 163 | productName = "Infinite-NSScrollViewTests"; 164 | productReference = F360A88E1ACABE99002C0333 /* Infinite-NSScrollViewTests.xctest */; 165 | productType = "com.apple.product-type.bundle.unit-test"; 166 | }; 167 | /* End PBXNativeTarget section */ 168 | 169 | /* Begin PBXProject section */ 170 | F360A8731ACABE99002C0333 /* Project object */ = { 171 | isa = PBXProject; 172 | attributes = { 173 | LastUpgradeCheck = 0620; 174 | ORGANIZATIONNAME = Helftone; 175 | TargetAttributes = { 176 | F360A87A1ACABE99002C0333 = { 177 | CreatedOnToolsVersion = 6.2; 178 | }; 179 | F360A88D1ACABE99002C0333 = { 180 | CreatedOnToolsVersion = 6.2; 181 | TestTargetID = F360A87A1ACABE99002C0333; 182 | }; 183 | }; 184 | }; 185 | buildConfigurationList = F360A8761ACABE99002C0333 /* Build configuration list for PBXProject "Infinite-NSScrollView" */; 186 | compatibilityVersion = "Xcode 3.2"; 187 | developmentRegion = English; 188 | hasScannedForEncodings = 0; 189 | knownRegions = ( 190 | en, 191 | Base, 192 | ); 193 | mainGroup = F360A8721ACABE99002C0333; 194 | productRefGroup = F360A87C1ACABE99002C0333 /* Products */; 195 | projectDirPath = ""; 196 | projectRoot = ""; 197 | targets = ( 198 | F360A87A1ACABE99002C0333 /* Infinite-NSScrollView */, 199 | F360A88D1ACABE99002C0333 /* Infinite-NSScrollViewTests */, 200 | ); 201 | }; 202 | /* End PBXProject section */ 203 | 204 | /* Begin PBXResourcesBuildPhase section */ 205 | F360A8791ACABE99002C0333 /* Resources */ = { 206 | isa = PBXResourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | F360A8861ACABE99002C0333 /* Images.xcassets in Resources */, 210 | F360A8891ACABE99002C0333 /* MainMenu.xib in Resources */, 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | }; 214 | F360A88C1ACABE99002C0333 /* Resources */ = { 215 | isa = PBXResourcesBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | ); 219 | runOnlyForDeploymentPostprocessing = 0; 220 | }; 221 | /* End PBXResourcesBuildPhase section */ 222 | 223 | /* Begin PBXSourcesBuildPhase section */ 224 | F360A8771ACABE99002C0333 /* Sources */ = { 225 | isa = PBXSourcesBuildPhase; 226 | buildActionMask = 2147483647; 227 | files = ( 228 | F360A8A31ACB16BF002C0333 /* HTContentTileView.m in Sources */, 229 | F360A8841ACABE99002C0333 /* main.m in Sources */, 230 | F360A8821ACABE99002C0333 /* AppDelegate.m in Sources */, 231 | F360A8A01ACABEDC002C0333 /* HTInfiniteDocumentView.m in Sources */, 232 | F32274041ACB37AE00B569EC /* HTInfiniteClipView.m in Sources */, 233 | ); 234 | runOnlyForDeploymentPostprocessing = 0; 235 | }; 236 | F360A88A1ACABE99002C0333 /* Sources */ = { 237 | isa = PBXSourcesBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | F360A8951ACABE99002C0333 /* Infinite_NSScrollViewTests.m in Sources */, 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | }; 244 | /* End PBXSourcesBuildPhase section */ 245 | 246 | /* Begin PBXTargetDependency section */ 247 | F360A8901ACABE99002C0333 /* PBXTargetDependency */ = { 248 | isa = PBXTargetDependency; 249 | target = F360A87A1ACABE99002C0333 /* Infinite-NSScrollView */; 250 | targetProxy = F360A88F1ACABE99002C0333 /* PBXContainerItemProxy */; 251 | }; 252 | /* End PBXTargetDependency section */ 253 | 254 | /* Begin PBXVariantGroup section */ 255 | F360A8871ACABE99002C0333 /* MainMenu.xib */ = { 256 | isa = PBXVariantGroup; 257 | children = ( 258 | F360A8881ACABE99002C0333 /* Base */, 259 | ); 260 | name = MainMenu.xib; 261 | sourceTree = ""; 262 | }; 263 | /* End PBXVariantGroup section */ 264 | 265 | /* Begin XCBuildConfiguration section */ 266 | F360A8961ACABE99002C0333 /* Debug */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ALWAYS_SEARCH_USER_PATHS = NO; 270 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 271 | CLANG_CXX_LIBRARY = "libc++"; 272 | CLANG_ENABLE_MODULES = YES; 273 | CLANG_ENABLE_OBJC_ARC = YES; 274 | CLANG_WARN_BOOL_CONVERSION = YES; 275 | CLANG_WARN_CONSTANT_CONVERSION = YES; 276 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 277 | CLANG_WARN_EMPTY_BODY = YES; 278 | CLANG_WARN_ENUM_CONVERSION = YES; 279 | CLANG_WARN_INT_CONVERSION = YES; 280 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 281 | CLANG_WARN_UNREACHABLE_CODE = YES; 282 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 283 | CODE_SIGN_IDENTITY = "-"; 284 | COPY_PHASE_STRIP = NO; 285 | ENABLE_STRICT_OBJC_MSGSEND = YES; 286 | GCC_C_LANGUAGE_STANDARD = gnu99; 287 | GCC_DYNAMIC_NO_PIC = NO; 288 | GCC_OPTIMIZATION_LEVEL = 0; 289 | GCC_PREPROCESSOR_DEFINITIONS = ( 290 | "DEBUG=1", 291 | "$(inherited)", 292 | ); 293 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 294 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 295 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 296 | GCC_WARN_UNDECLARED_SELECTOR = YES; 297 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 298 | GCC_WARN_UNUSED_FUNCTION = YES; 299 | GCC_WARN_UNUSED_VARIABLE = YES; 300 | MACOSX_DEPLOYMENT_TARGET = 10.9; 301 | MTL_ENABLE_DEBUG_INFO = YES; 302 | ONLY_ACTIVE_ARCH = YES; 303 | SDKROOT = macosx; 304 | }; 305 | name = Debug; 306 | }; 307 | F360A8971ACABE99002C0333 /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 312 | CLANG_CXX_LIBRARY = "libc++"; 313 | CLANG_ENABLE_MODULES = YES; 314 | CLANG_ENABLE_OBJC_ARC = YES; 315 | CLANG_WARN_BOOL_CONVERSION = YES; 316 | CLANG_WARN_CONSTANT_CONVERSION = YES; 317 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 318 | CLANG_WARN_EMPTY_BODY = YES; 319 | CLANG_WARN_ENUM_CONVERSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 322 | CLANG_WARN_UNREACHABLE_CODE = YES; 323 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 324 | CODE_SIGN_IDENTITY = "-"; 325 | COPY_PHASE_STRIP = NO; 326 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 327 | ENABLE_NS_ASSERTIONS = NO; 328 | ENABLE_STRICT_OBJC_MSGSEND = YES; 329 | GCC_C_LANGUAGE_STANDARD = gnu99; 330 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 331 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 332 | GCC_WARN_UNDECLARED_SELECTOR = YES; 333 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 334 | GCC_WARN_UNUSED_FUNCTION = YES; 335 | GCC_WARN_UNUSED_VARIABLE = YES; 336 | MACOSX_DEPLOYMENT_TARGET = 10.9; 337 | MTL_ENABLE_DEBUG_INFO = NO; 338 | SDKROOT = macosx; 339 | }; 340 | name = Release; 341 | }; 342 | F360A8991ACABE99002C0333 /* Debug */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 346 | COMBINE_HIDPI_IMAGES = YES; 347 | INFOPLIST_FILE = "Infinite-NSScrollView/Info.plist"; 348 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 349 | PRODUCT_NAME = "$(TARGET_NAME)"; 350 | }; 351 | name = Debug; 352 | }; 353 | F360A89A1ACABE99002C0333 /* Release */ = { 354 | isa = XCBuildConfiguration; 355 | buildSettings = { 356 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 357 | COMBINE_HIDPI_IMAGES = YES; 358 | INFOPLIST_FILE = "Infinite-NSScrollView/Info.plist"; 359 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 360 | PRODUCT_NAME = "$(TARGET_NAME)"; 361 | }; 362 | name = Release; 363 | }; 364 | F360A89C1ACABE99002C0333 /* Debug */ = { 365 | isa = XCBuildConfiguration; 366 | buildSettings = { 367 | BUNDLE_LOADER = "$(TEST_HOST)"; 368 | COMBINE_HIDPI_IMAGES = YES; 369 | FRAMEWORK_SEARCH_PATHS = ( 370 | "$(DEVELOPER_FRAMEWORKS_DIR)", 371 | "$(inherited)", 372 | ); 373 | GCC_PREPROCESSOR_DEFINITIONS = ( 374 | "DEBUG=1", 375 | "$(inherited)", 376 | ); 377 | INFOPLIST_FILE = "Infinite-NSScrollViewTests/Info.plist"; 378 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 379 | PRODUCT_NAME = "$(TARGET_NAME)"; 380 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Infinite-NSScrollView.app/Contents/MacOS/Infinite-NSScrollView"; 381 | }; 382 | name = Debug; 383 | }; 384 | F360A89D1ACABE99002C0333 /* Release */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | BUNDLE_LOADER = "$(TEST_HOST)"; 388 | COMBINE_HIDPI_IMAGES = YES; 389 | FRAMEWORK_SEARCH_PATHS = ( 390 | "$(DEVELOPER_FRAMEWORKS_DIR)", 391 | "$(inherited)", 392 | ); 393 | INFOPLIST_FILE = "Infinite-NSScrollViewTests/Info.plist"; 394 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 395 | PRODUCT_NAME = "$(TARGET_NAME)"; 396 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Infinite-NSScrollView.app/Contents/MacOS/Infinite-NSScrollView"; 397 | }; 398 | name = Release; 399 | }; 400 | /* End XCBuildConfiguration section */ 401 | 402 | /* Begin XCConfigurationList section */ 403 | F360A8761ACABE99002C0333 /* Build configuration list for PBXProject "Infinite-NSScrollView" */ = { 404 | isa = XCConfigurationList; 405 | buildConfigurations = ( 406 | F360A8961ACABE99002C0333 /* Debug */, 407 | F360A8971ACABE99002C0333 /* Release */, 408 | ); 409 | defaultConfigurationIsVisible = 0; 410 | defaultConfigurationName = Release; 411 | }; 412 | F360A8981ACABE99002C0333 /* Build configuration list for PBXNativeTarget "Infinite-NSScrollView" */ = { 413 | isa = XCConfigurationList; 414 | buildConfigurations = ( 415 | F360A8991ACABE99002C0333 /* Debug */, 416 | F360A89A1ACABE99002C0333 /* Release */, 417 | ); 418 | defaultConfigurationIsVisible = 0; 419 | defaultConfigurationName = Release; 420 | }; 421 | F360A89B1ACABE99002C0333 /* Build configuration list for PBXNativeTarget "Infinite-NSScrollViewTests" */ = { 422 | isa = XCConfigurationList; 423 | buildConfigurations = ( 424 | F360A89C1ACABE99002C0333 /* Debug */, 425 | F360A89D1ACABE99002C0333 /* Release */, 426 | ); 427 | defaultConfigurationIsVisible = 0; 428 | defaultConfigurationName = Release; 429 | }; 430 | /* End XCConfigurationList section */ 431 | }; 432 | rootObject = F360A8731ACABE99002C0333 /* Project object */; 433 | } 434 | -------------------------------------------------------------------------------- /Infinite-NSScrollView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // Infinite-NSScrollView 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : NSObject 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // Infinite-NSScrollView 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "HTInfiniteClipView.h" 11 | 12 | @interface AppDelegate () 13 | @property(readwrite, strong, nonatomic) IBOutlet NSWindow* window; 14 | @property(readwrite, strong, nonatomic) IBOutlet NSScrollView* scrollView; 15 | @end 16 | 17 | @implementation AppDelegate 18 | 19 | -(void)applicationDidFinishLaunching:(NSNotification*)note_in { 20 | // Enable layer-backing for the view hierarchy, required by the tile views 21 | NSView* contentView = [self.window contentView]; 22 | [contentView setWantsLayer:YES]; 23 | 24 | // The frame size needs to be larger than the sum of largest attached screen + recenter threshold. 25 | NSView* docView = [self.scrollView documentView]; 26 | [docView setFrameSize:CGSizeMake(4000, 4000)]; 27 | 28 | // Provide the illusion of an infinite scroll view by hiding all the scrollers. 29 | [self.scrollView setHasVerticalScroller:NO]; 30 | [self.scrollView setHasHorizontalScroller:NO]; 31 | 32 | // There's no need for elasticity. 33 | [self.scrollView setHorizontalScrollElasticity:NSScrollElasticityNone]; 34 | [self.scrollView setVerticalScrollElasticity:NSScrollElasticityNone]; 35 | 36 | // Force an initial recenter so user can scroll in all directions. 37 | HTInfiniteClipView* infiniteClipView = (HTInfiniteClipView*)[self.scrollView contentView]; 38 | [infiniteClipView recenterClipView]; 39 | } 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 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 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | Default 541 | 542 | 543 | 544 | 545 | 546 | 547 | Left to Right 548 | 549 | 550 | 551 | 552 | 553 | 554 | Right to Left 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | Default 566 | 567 | 568 | 569 | 570 | 571 | 572 | Left to Right 573 | 574 | 575 | 576 | 577 | 578 | 579 | Right to Left 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/HTContentTileView.h: -------------------------------------------------------------------------------- 1 | // 2 | // HTContentView.h 3 | // Infinite-NSScrollView 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface HTContentTileView : NSView 12 | 13 | @property(readwrite, strong, nonatomic) NSColor* backgroundColor; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/HTContentTileView.m: -------------------------------------------------------------------------------- 1 | // 2 | // HTContentView.m 3 | // Infinite-NSScrollView 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import "HTContentTileView.h" 10 | 11 | @implementation HTContentTileView 12 | 13 | -(BOOL)wantsUpdateLayer { 14 | return YES; 15 | } 16 | 17 | -(void)updateLayer { 18 | CALayer* layer = [self layer]; 19 | [layer setBackgroundColor:[self.backgroundColor CGColor]]; 20 | } 21 | 22 | -(void)setBackgroundColor:(NSColor*)backgroundColor_in { 23 | if(_backgroundColor != backgroundColor_in) { 24 | _backgroundColor = backgroundColor_in; 25 | [self setNeedsDisplay:YES]; 26 | } 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/HTInfiniteClipView.h: -------------------------------------------------------------------------------- 1 | // 2 | // HTInfiniteClipView.h 3 | // Infinite-NSScrollView 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface HTInfiniteClipView : NSClipView 12 | 13 | -(void)recenterClipView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/HTInfiniteClipView.m: -------------------------------------------------------------------------------- 1 | // 2 | // HTInfiniteClipView.m 3 | // Infinite-NSScrollView 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import "HTInfiniteClipView.h" 10 | #import "HTInfiniteDocumentView.h" 11 | 12 | @interface HTInfiniteClipView () 13 | @property(readwrite, assign, nonatomic) BOOL inRecenter; 14 | @property(readwrite, assign, nonatomic) BOOL recenterScheduled; 15 | @end 16 | 17 | @implementation HTInfiniteClipView 18 | 19 | -(void)internal_initInfiniteClipView { 20 | [self setPostsBoundsChangedNotifications:YES]; 21 | NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 22 | [center addObserver:self selector:@selector(internal_viewGeometryChanged:) name:NSViewBoundsDidChangeNotification object:self]; 23 | [center addObserver:self selector:@selector(internal_viewGeometryChanged:) name:NSViewFrameDidChangeNotification object:self]; 24 | } 25 | 26 | -(instancetype)initWithCoder:(NSCoder*)decoder_in { 27 | if((self = [super initWithCoder:decoder_in])) { 28 | [self internal_initInfiniteClipView]; 29 | } 30 | 31 | return self; 32 | } 33 | 34 | -(instancetype)initWithFrame:(NSRect)frameRect_in { 35 | if((self = [super initWithFrame:frameRect_in])) { 36 | [self internal_initInfiniteClipView]; 37 | } 38 | 39 | return self; 40 | } 41 | 42 | -(void)dealloc { 43 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 44 | } 45 | 46 | 47 | #pragma mark - Notifications 48 | 49 | -(void)internal_viewGeometryChanged:(NSNotification*)note_in { 50 | CGPoint recenterOffset = [self internal_clipRecenterOffset]; 51 | if(!CGPointEqualToPoint(recenterOffset, CGPointZero)) { 52 | // We cannot perform recentering from within the notification (it's synchrounous) due to a bug 53 | // in NSScrollView ... *sigh* 54 | [self internal_scheduleRecenter]; 55 | } 56 | 57 | HTInfiniteDocumentView* docView = [self documentView]; 58 | [docView layoutDocumentView]; 59 | } 60 | 61 | #pragma mark - Recenter 62 | 63 | // ┌────────────────────────────────────────────────┐ 64 | // │Document View │ 65 | // │ │ 66 | // │ │ 67 | // │ │ 68 | // │ │ 69 | // │ │ 70 | // │ │ 71 | // │ │ 72 | // │ │ 73 | // │ │ 74 | // │ ┌────────────────┐ │ 75 | // │ │ │ │ 76 | // │◁──▷│ Clip View │ │ 77 | // │ │ │ │ 78 | // │ └────────────────┘ │ 79 | // │ △ │ 80 | // │ │ │ 81 | // │ ▽ │ 82 | // └────────────────────────────────────────────────┘ 83 | // (0,0) 84 | // 85 | // Recenter Offset: (200,100) 86 | // 87 | // │ 88 | // │ 89 | // │ 90 | // │ 91 | // ▼ 92 | // 93 | // 94 | // ┌────────────────────────────────────────────────┐ 95 | // │Document View │ 96 | // │ │ 97 | // │ │ 98 | // │ │ 99 | // │ │ 100 | // │ │ 101 | // │ ┌────────────────┐ │ 102 | // │ │ │ │ 103 | // │◁─────────────▷│ Clip View │ │ 104 | // │ │ │ │ 105 | // │ └────────────────┘ │ 106 | // │ △ │ 107 | // │ │ │ 108 | // │ │ │ 109 | // │ ● │ │ 110 | // │ (0,0) │ │ 111 | // │ │ │ 112 | // │ ▽ │ 113 | // └────────────────────────────────────────────────┘ 114 | // (-200,-100) 115 | // 116 | // Created with Monodraw 117 | // 118 | 119 | // A recenter is performed whenever the clip view gets close to the edge, so that we avoid bouncing 120 | // and breaking the illusion of an infinite scroll view. 121 | -(void)recenterClipView { 122 | CGPoint clipRecenterOffset = [self internal_clipRecenterOffset]; 123 | if(!CGPointEqualToPoint(clipRecenterOffset, CGPointZero)) { 124 | _inRecenter = YES; 125 | 126 | // We need to add the negative clip offset to the doc view so that the content move in the right direction. 127 | CGRect recenterDocBounds = [self.documentView bounds]; 128 | recenterDocBounds.origin.x -= clipRecenterOffset.x, recenterDocBounds.origin.y -= clipRecenterOffset.y; 129 | [self.documentView setBoundsOrigin:recenterDocBounds.origin]; 130 | 131 | const CGRect clipBounds = [self bounds]; 132 | CGPoint recenterClipOrigin = clipBounds.origin; 133 | recenterClipOrigin.x += clipRecenterOffset.x, recenterClipOrigin.y += clipRecenterOffset.y; 134 | [self setBoundsOrigin:recenterClipOrigin]; 135 | 136 | _inRecenter = NO; 137 | 138 | #ifdef DEBUG 139 | NSLog(@"Did recenter infinite clip view: %@", self); 140 | #endif 141 | } 142 | } 143 | 144 | // ┌──────────────────────────────────────────────────────────────────────────────────┐ 145 | // │Document View △ │ 146 | // │ │ 147 | // │ │ │ 148 | // │ │ 149 | // │ │ │ 150 | // │ maxVerticalDistance │ 151 | // │ │ │ 152 | // │ │ 153 | // │ │ │ 154 | // ▲ │ ▽ │ 155 | // │ │ ┌──────────────────────┐ │ 156 | // │ │ │ │ │ 157 | // │ │ minHorizontalDistance │ │ maxHorizontalDistance │ 158 | // │ │◁ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▷│ Clip View │◁ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▷│ 159 | // │ │ │ │ │ 160 | // │ │ │ │ │ 161 | // │ │ └──────────────────────┘ │ 162 | // │ │ △ │ 163 | // │ │ │ 164 | // │ │ │ │ 165 | // │ │ │ 166 | // │ │ │ minVerticalDistance │ 167 | // │ │ │ 168 | // │ │ │ │ 169 | // │ │ │ 170 | // │ │ │ │ 171 | // │ │ ▽ │ 172 | // │ └──────────────────────────────────────────────────────────────────────────────────┘ 173 | // │ 174 | // │ 175 | // X───┼─────────────────────────────────────▶ 176 | // │ 177 | // Y 178 | // 179 | // Created with Monodraw 180 | // 181 | 182 | -(CGPoint)internal_clipRecenterOffset { 183 | // The threshold needs to be larger than the maximum single scroll distance (otherwise the scroll 184 | // edge will be hit). Through experimentation, all values stayed below 500.0. 185 | static const CGFloat HTRecenterThreshold = 500.0; 186 | 187 | NSView* docView = [self documentView]; 188 | const CGRect docFrame = [docView frame], clipBounds = [self bounds]; 189 | 190 | // Compute the distances to the edges, if any of these values gets less than or equal to zero, 191 | // then the scroll view edge has been hit and the recenter threshold has to be increased. 192 | const CGFloat minHorizontalDistance = CGRectGetMinX(clipBounds) - CGRectGetMinX(docFrame); 193 | const CGFloat maxHorizontalDistance = CGRectGetMaxX(docFrame) - CGRectGetMaxX(clipBounds); 194 | const CGFloat minVerticalDistance = CGRectGetMinY(clipBounds) - CGRectGetMinY(docFrame); 195 | const CGFloat maxVerticalDistance = CGRectGetMaxY(docFrame) - CGRectGetMaxY(clipBounds); 196 | if(minHorizontalDistance < HTRecenterThreshold || 197 | maxHorizontalDistance < HTRecenterThreshold || 198 | minVerticalDistance < HTRecenterThreshold || 199 | maxVerticalDistance < HTRecenterThreshold) 200 | { 201 | // Compute desired clip origin and then just return the offset from current origin. 202 | CGPoint recenterClipOrigin = CGPointZero; 203 | recenterClipOrigin.x = CGRectGetMinX(docFrame) + round((CGRectGetWidth(docFrame) - CGRectGetWidth(clipBounds)) / 2.0); 204 | recenterClipOrigin.y = CGRectGetMinY(docFrame) + round((CGRectGetHeight(docFrame) - CGRectGetHeight(clipBounds)) / 2.0); 205 | return CGPointMake(recenterClipOrigin.x - clipBounds.origin.x, recenterClipOrigin.y - clipBounds.origin.y); 206 | } 207 | 208 | return CGPointZero; 209 | } 210 | 211 | 212 | -(void)internal_scheduleRecenter { 213 | if(self.recenterScheduled || self.inRecenter) { 214 | return; 215 | } 216 | 217 | self.recenterScheduled = YES; 218 | __typeof__(self) __weak weakSelf = self; 219 | dispatch_async(dispatch_get_main_queue(), ^{ 220 | // __typeof__(self) is evaluated at compile time and will _not_ result in retaining self 221 | __typeof__(self) strongSelf = weakSelf; 222 | if(strongSelf != nil) { 223 | strongSelf.recenterScheduled = NO; 224 | [strongSelf recenterClipView]; 225 | } 226 | }); 227 | } 228 | 229 | -(void)scrollToPoint:(NSPoint)newOrigin_in { 230 | // NSScrollView implements smooth scrolling _only_ for mouse wheel event. This happens inside -scrollToPoint: 231 | // which will cache the call and subsequently update the bounds. Unfortunately, if we recenter while 232 | // in a smooth scroll, the scroll view will keep scrolling but will not take into account the recenter. 233 | // Smooth scrolling can be disabled for an app using NSScrollAnimationEnabled. 234 | // 235 | // In order to workaround the issue, we just bypass smooth scrolling and directly scroll. 236 | // 237 | // NB: we cannot recenter from here, nsscrollview screws it up if we use a trackpad 238 | 239 | [self setBoundsOrigin:newOrigin_in]; 240 | } 241 | 242 | @end 243 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/HTInfiniteDocumentView.h: -------------------------------------------------------------------------------- 1 | // 2 | // HTInfiniteDocumentView.h 3 | // Infinite-NSScrollView 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface HTInfiniteDocumentView : NSView 12 | 13 | -(void)layoutDocumentView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/HTInfiniteDocumentView.m: -------------------------------------------------------------------------------- 1 | // 2 | // HTInfiniteDocumentView.m 3 | // Infinite-NSScrollView 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import "HTInfiniteDocumentView.h" 10 | #import "HTContentTileView.h" 11 | 12 | typedef struct { 13 | NSInteger x, y; 14 | } HTTileKey; 15 | 16 | @interface HTInfiniteDocumentView () 17 | @property(readwrite, strong, nonatomic) NSMutableDictionary* tileViewMap; 18 | @end 19 | 20 | @implementation HTInfiniteDocumentView 21 | 22 | -(void)internal_initInfiniteDocumentView { 23 | self.tileViewMap = [NSMutableDictionary dictionary]; 24 | [self layoutDocumentView]; 25 | } 26 | 27 | -(instancetype)initWithFrame:(NSRect)frameRect_in { 28 | if((self = [super initWithFrame:frameRect_in])) { 29 | [self internal_initInfiniteDocumentView]; 30 | } 31 | 32 | return self; 33 | } 34 | 35 | -(instancetype)initWithCoder:(NSCoder*)coder_in { 36 | if((self = [super initWithCoder:coder_in])) { 37 | [self internal_initInfiniteDocumentView]; 38 | } 39 | 40 | return self; 41 | } 42 | 43 | // This method should be called whenever the visible rect changes. This can be done synchronously 44 | // (e.g., on bounds change) or asynchronously (e.g., async dispatch) or any other way. 45 | -(void)layoutDocumentView { 46 | static const CGSize tileSize = {.width = 250.0, .height = 250.0}; 47 | 48 | const CGRect visibleRect = [self visibleRect]; 49 | 50 | // min = inclusive; max = exclusive; i.e., [min, max) 51 | const NSInteger xMinIndex = floor(CGRectGetMinX(visibleRect) / tileSize.width); 52 | const NSInteger xMaxIndex = ceil(CGRectGetMaxX(visibleRect) / tileSize.width); 53 | 54 | const NSInteger yMinIndex = floor(CGRectGetMinY(visibleRect) / tileSize.height); 55 | const NSInteger yMaxIndex = ceil(CGRectGetMaxY(visibleRect) / tileSize.height); 56 | 57 | for(NSInteger x = xMinIndex; x < xMaxIndex; ++x) { 58 | for(NSInteger y = yMinIndex; y < yMaxIndex; ++y) { 59 | if(ABS(x + y) % 2 != 0) { 60 | // skip every other tile and alternate between rows 61 | continue; 62 | } 63 | 64 | const HTTileKey tileKey = (HTTileKey){.x = x, .y = y}; 65 | NSValue* tileKeyBoxed = [NSValue valueWithBytes:&tileKey objCType:@encode(HTTileKey)]; 66 | if(self.tileViewMap[tileKeyBoxed] == nil) { 67 | const CGRect tileFrame = (CGRect){.origin = CGPointMake(x * tileSize.width, y * tileSize.height), .size = tileSize}; 68 | const CGRect viewFrame = CGRectInset(tileFrame, 50, 50); 69 | HTContentTileView* tileView = [[HTContentTileView alloc] initWithFrame:viewFrame]; 70 | tileView.backgroundColor = x % 2 == 0 ? [NSColor blackColor] : [NSColor blueColor]; 71 | [self addSubview:tileView]; 72 | 73 | self.tileViewMap[tileKeyBoxed] = tileView; 74 | } 75 | } 76 | } 77 | 78 | // remove offscreen views 79 | for(NSValue* tileKeyBoxed in [self.tileViewMap allKeys]) { 80 | HTTileKey tileKey = {}; 81 | [tileKeyBoxed getValue:&tileKey]; 82 | 83 | if(!(xMinIndex <= tileKey.x && tileKey.x < xMaxIndex && yMinIndex <= tileKey.y && tileKey.y < yMaxIndex)) { 84 | NSView* tileView = self.tileViewMap[tileKeyBoxed]; 85 | [self.tileViewMap removeObjectForKey:tileKeyBoxed]; 86 | [tileView removeFromSuperview]; 87 | } 88 | } 89 | } 90 | 91 | -(void)resizeSubviewsWithOldSize:(NSSize)oldSize_in { 92 | [self layoutDocumentView]; 93 | } 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Infinite-NSScrollView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.helftone.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2015 Helftone. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Infinite-NSScrollView/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Infinite-NSScrollView 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | return NSApplicationMain(argc, argv); 13 | } 14 | -------------------------------------------------------------------------------- /Infinite-NSScrollViewTests/Infinite_NSScrollViewTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // Infinite_NSScrollViewTests.m 3 | // Infinite-NSScrollViewTests 4 | // 5 | // Created by Milen Dzhumerov on 31/03/2015. 6 | // Copyright (c) 2015 Helftone. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface Infinite_NSScrollViewTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation Infinite_NSScrollViewTests 17 | 18 | - (void)setUp { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | [super tearDown]; 26 | } 27 | 28 | - (void)testExample { 29 | // This is an example of a functional test case. 30 | XCTAssert(YES, @"Pass"); 31 | } 32 | 33 | - (void)testPerformanceExample { 34 | // This is an example of a performance test case. 35 | [self measureBlock:^{ 36 | // Put the code you want to measure the time of here. 37 | }]; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /Infinite-NSScrollViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.helftone.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Helftone Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | infinite-nsscrollview 2 | ===================== 3 | 4 | Demonstrates how to create an infinite NSScrollView. 5 | 6 | For more information, http://blog.helftone.com/infinite-nsscrollview/ 7 | 8 | --------------------------------------------------------------------------------