├── LazyScrollViewDemo ├── LazyScrollViewDemo │ ├── AsyncViewController.h │ ├── MoreViewController.h │ ├── ReuseViewController.h │ ├── MainViewController.h │ ├── OuterViewController.h │ ├── AppDelegate.h │ ├── main.m │ ├── AppDelegate.m │ ├── Info.plist │ ├── MainViewController.m │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── MoreViewController.m │ ├── AsyncViewController.m │ ├── OuterViewController.m │ └── ReuseViewController.m └── LazyScrollViewDemo.xcodeproj │ └── project.pbxproj ├── TMUtils ├── NSString+TMSafeUtils.h ├── TMUtils.h ├── NSString+TMSafeUtils.m ├── NSDictionary+TMSafeUtils.h ├── NSArray+TMSafeUtils.h ├── NSDictionary+TMSafeUtils.m └── NSArray+TMSafeUtils.m ├── LazyScrollView ├── TMLazyItemModel.m ├── LazyScroll.h ├── UIView+TMLazyScrollView.h ├── TMLazyReusePool.h ├── TMLazyItemModel.h ├── TMLazyModelBucket.h ├── UIView+TMLazyScrollView.m ├── TMLazyItemViewProtocol.h ├── TMLazyReusePool.m ├── TMLazyModelBucket.m ├── TMLazyScrollView.h └── TMLazyScrollView.m ├── .gitignore ├── Podfile ├── TMUtils.podspec ├── LazyScroll.podspec ├── LazyScrollViewTest ├── LazyScrollViewTest │ ├── Info.plist │ └── ModelBucketTest.m └── LazyScrollViewTest.xcodeproj │ └── project.pbxproj ├── LICENSE ├── update_header.py └── README.md /LazyScrollViewDemo/LazyScrollViewDemo/AsyncViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncViewController.h 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface AsyncViewController : UIViewController 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/MoreViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MoreViewController.h 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface MoreViewController : UIViewController 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/ReuseViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ReuseViewController.h 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface ReuseViewController : UIViewController 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/MainViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.h 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface MainViewController : UITableViewController 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/OuterViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // OuterViewController.h 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface OuterViewController : UITableViewController 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /TMUtils/NSString+TMSafeUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+TMSafeUtils.h 3 | // TMUtils 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface NSString (TMSafeUtils) 11 | 12 | - (long)longValue; 13 | - (NSNumber *)numberValue; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /TMUtils/TMUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMUtils.h 3 | // TMUtils 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #ifndef TMUtils_h 9 | #define TMUtils_h 10 | 11 | #import "NSString+TMSafeUtils.h" 12 | #import "NSArray+TMSafeUtils.h" 13 | #import "NSDictionary+TMSafeUtils.h" 14 | 15 | #endif /* TMUtils_h */ 16 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface AppDelegate : UIResponder 11 | 12 | @property (strong, nonatomic) UIWindow *window; 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "AppDelegate.h" 10 | 11 | int main(int argc, char * argv[]) { 12 | @autoreleasepool { 13 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LazyScrollView/TMLazyItemModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // TMLazyItemModel.m 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "TMLazyItemModel.h" 9 | 10 | @implementation TMLazyItemModel 11 | 12 | - (CGFloat)top 13 | { 14 | return CGRectGetMinY(_absRect); 15 | } 16 | 17 | - (CGFloat)bottom 18 | { 19 | return CGRectGetMaxY(_absRect); 20 | } 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /LazyScrollView/LazyScroll.h: -------------------------------------------------------------------------------- 1 | // 2 | // LazyScroll.h 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #ifndef LazyScroll_h 9 | #define LazyScroll_h 10 | 11 | #import "TMLazyItemViewProtocol.h" 12 | #import "TMLazyItemModel.h" 13 | #import "TMLazyReusePool.h" 14 | #import "TMLazyModelBucket.h" 15 | #import "UIView+TMLazyScrollView.h" 16 | #import "TMLazyScrollView.h" 17 | 18 | #endif /* LazyScroll_h */ 19 | -------------------------------------------------------------------------------- /TMUtils/NSString+TMSafeUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+TMSafeUtils.m 3 | // TMUtils 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "NSString+TMSafeUtils.h" 9 | 10 | @implementation NSString (TMSafeUtils) 11 | 12 | - (long)longValue 13 | { 14 | return (long)[self integerValue]; 15 | } 16 | 17 | - (NSNumber *)numberValue 18 | { 19 | NSNumberFormatter *formatter = [NSNumberFormatter new]; 20 | return [formatter numberFromString:self]; 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /LazyScrollView/UIView+TMLazyScrollView.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+TMLazyScrollView.h 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface UIView (TMLazyScrollView) 11 | 12 | @property (nonatomic, copy) NSString *muiID; 13 | @property (nonatomic, copy) NSString *reuseIdentifier; 14 | 15 | - (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier; 16 | - (instancetype)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | ## Build generated 4 | build/ 5 | DerivedData/ 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata/ 17 | 18 | ## Other 19 | *.moved-aside 20 | *.xccheckout 21 | *.xcscmblueprint 22 | 23 | ## Obj-C/Swift specific 24 | *.hmap 25 | *.ipa 26 | *.dSYM.zip 27 | *.dSYM 28 | 29 | # CocoaPods 30 | Pods/ 31 | Podfile.lock 32 | *.xcworkspace 33 | 34 | # Demo 35 | *Demo/Pods/ 36 | *Demo/Podfile.lock 37 | *Demo/*.xcworkspace 38 | -------------------------------------------------------------------------------- /LazyScrollView/TMLazyReusePool.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMLazyReusePool.h 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface TMLazyReusePool : NSObject 11 | 12 | - (void)addItemView:(UIView *)itemView forReuseIdentifier:(NSString *)reuseIdentifier; 13 | - (UIView *)dequeueItemViewForReuseIdentifier:(NSString *)reuseIdentifier; 14 | - (UIView *)dequeueItemViewForReuseIdentifier:(NSString *)reuseIdentifier andMuiID:(NSString *)muiID; 15 | - (void)clear; 16 | - (NSSet *)allItemViews; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source "https://github.com/CocoaPods/Specs.git" 2 | # source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' 3 | 4 | platform :ios, '7.0' 5 | 6 | target 'LazyScrollViewDemo' do 7 | project 'LazyScrollViewDemo/LazyScrollViewDemo.xcodeproj' 8 | pod 'LazyScroll', :path => './' 9 | pod 'TMUtils', :path => './' 10 | end 11 | 12 | target 'LazyScrollViewTest' do 13 | project 'LazyScrollViewTest/LazyScrollViewTest.xcodeproj' 14 | pod 'LazyScroll', :path => './' 15 | pod 'TMUtils', :path => './' 16 | pod 'OCHamcrest' 17 | end 18 | 19 | workspace 'LazyScrollView' 20 | -------------------------------------------------------------------------------- /TMUtils.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "TMUtils" 4 | s.version = "1.0.1" 5 | s.summary = "Common safe methods & utils for NSArray & NSDictionary." 6 | s.homepage = "https://github.com/alibaba/LazyScrollView" 7 | s.license = {:type => 'MIT'} 8 | s.author = { "HarrisonXi" => "gpra8764@gmail.com" } 9 | s.platform = :ios 10 | s.ios.deployment_target = "7.0" 11 | s.source = { :git => "https://github.com/alibaba/LazyScrollView.git", :tag => "1.0.1" } 12 | s.source_files = "TMUtils/*.{h,m}" 13 | s.requires_arc = true 14 | 15 | end 16 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "AppDelegate.h" 9 | #import "MainViewController.h" 10 | 11 | @implementation AppDelegate 12 | 13 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 14 | { 15 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 16 | self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[MainViewController new]]; 17 | self.window.backgroundColor = [UIColor whiteColor]; 18 | [self.window makeKeyAndVisible]; 19 | return YES; 20 | } 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /LazyScrollView/TMLazyItemModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMLazyItemModel.h 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | /** 12 | It is a model to store data of item view. 13 | */ 14 | @interface TMLazyItemModel : NSObject 15 | 16 | /** 17 | Item view's frame in LazyScrollView. 18 | */ 19 | @property (nonatomic, assign) CGRect absRect; 20 | @property (nonatomic, readonly) CGFloat top; 21 | @property (nonatomic, readonly) CGFloat bottom; 22 | 23 | /** 24 | Item view's unique ID in LazyScrollView. 25 | Will be set to string value of index if it's nil. 26 | The ID MUST BE unique. 27 | */ 28 | @property (nonatomic, copy) NSString *muiID; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /LazyScroll.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "LazyScroll" 4 | s.version = "1.0.1" 5 | s.summary = "A ScrollView to resolve the problem of reusability of views." 6 | s.homepage = "https://github.com/alibaba/LazyScrollView" 7 | s.license = { :type => 'MIT' } 8 | s.author = { "fydx" => "lbgg918@gmail.com", 9 | "HarrisonXi" => "gpra8764@gmail.com" } 10 | s.platform = :ios 11 | s.ios.deployment_target = "7.0" 12 | s.source = { :git => "https://github.com/alibaba/LazyScrollView.git", :tag => "1.0.1" } 13 | s.source_files = "LazyScrollView/*.{h,m}" 14 | s.requires_arc = true 15 | s.prefix_header_contents = '#import ' 16 | 17 | s.dependency 'TMUtils', '~> 1.0.1' 18 | 19 | end 20 | -------------------------------------------------------------------------------- /LazyScrollViewTest/LazyScrollViewTest/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015-2018 Alibaba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /TMUtils/NSDictionary+TMSafeUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+TMSafeUtils.h 3 | // TMUtils 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | @interface NSDictionary (TMSafeUtils) 12 | 13 | - (id)tm_safeObjectForKey:(id)key; 14 | - (id)tm_safeObjectForKey:(id)key class:(Class)aClass; 15 | 16 | - (bool)tm_boolForKey:(id)key; 17 | - (CGFloat)tm_floatForKey:(id)key; 18 | - (NSInteger)tm_integerForKey:(id)key; 19 | - (int)tm_intForKey:(id)key; 20 | - (long)tm_longForKey:(id)key; 21 | - (NSNumber *)tm_numberForKey:(id)key; 22 | - (NSString *)tm_stringForKey:(id)key; 23 | - (NSDictionary *)tm_dictionaryForKey:(id)key; 24 | - (NSArray *)tm_arrayForKey:(id)key; 25 | - (NSMutableDictionary *)tm_mutableDictionaryForKey:(id)key; 26 | - (NSMutableArray *)tm_mutableArrayForKey:(id)key; 27 | 28 | @end 29 | 30 | @interface NSMutableDictionary (TMSafeUtils) 31 | 32 | - (void)tm_safeSetObject:(id)anObject forKey:(id)key; 33 | - (void)tm_safeRemoveObjectForKey:(id)key; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /LazyScrollView/TMLazyModelBucket.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMLazyModelBucket.h 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "TMLazyItemModel.h" 10 | 11 | /** 12 | Every bucket store item models in an area. 13 | 1st bucket store item models which Y value is from 0 to bucketHeight. 14 | 2nd bucket store item models which Y value is from bucketHeight to bucketHeight * 2. 15 | */ 16 | @interface TMLazyModelBucket : NSObject 17 | 18 | @property (nonatomic, assign, readonly) CGFloat bucketHeight; 19 | 20 | - (instancetype)initWithBucketHeight:(CGFloat)bucketHeight; 21 | 22 | - (void)addModel:(TMLazyItemModel *)itemModel; 23 | - (void)addModels:(NSSet *)itemModels; 24 | - (void)removeModel:(TMLazyItemModel *)itemModel; 25 | - (void)removeModels:(NSSet *)itemModels; 26 | - (void)reloadModel:(TMLazyItemModel *)itemModel; 27 | - (void)reloadModels:(NSSet *)itemModels; 28 | - (void)clear; 29 | - (NSSet *)showingModelsFrom:(CGFloat)startY to:(CGFloat)endY; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /TMUtils/NSArray+TMSafeUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSArray+TMSafeUtils.h 3 | // TMUtils 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | @interface NSArray (TMSafeUtils) 12 | 13 | - (id)tm_safeObjectAtIndex:(NSUInteger)index; 14 | - (id)tm_safeObjectAtIndex:(NSUInteger)index class:(Class)aClass; 15 | 16 | - (bool)tm_boolAtIndex:(NSUInteger)index; 17 | - (CGFloat)tm_floatAtIndex:(NSUInteger)index; 18 | - (NSInteger)tm_integerAtIndex:(NSUInteger)index; 19 | - (int)tm_intAtIndex:(NSUInteger)index; 20 | - (long)tm_longAtIndex:(NSUInteger)index; 21 | - (NSNumber *)tm_numberAtIndex:(NSUInteger)index; 22 | - (NSString *)tm_stringAtIndex:(NSUInteger)index; 23 | - (NSDictionary *)tm_dictionaryAtIndex:(NSUInteger)index; 24 | - (NSArray *)tm_arrayAtIndex:(NSUInteger)index; 25 | - (NSMutableDictionary *)tm_mutableDictionaryAtIndex:(NSUInteger)index; 26 | - (NSMutableArray *)tm_mutableArrayAtIndex:(NSUInteger)index; 27 | 28 | @end 29 | 30 | @interface NSMutableArray (TMSafeUtils) 31 | 32 | - (void)tm_safeAddObject:(id)anObject; 33 | - (void)tm_safeInsertObject:(id)anObject atIndex:(NSUInteger)index; 34 | - (void)tm_safeReplaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject; 35 | - (void)tm_safeRemoveObjectAtIndex:(NSUInteger)index; 36 | - (void)tm_safeRemoveObject:(id)anObject; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /LazyScrollView/UIView+TMLazyScrollView.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+TMLazyScrollView.m 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "UIView+TMLazyScrollView.h" 9 | #import 10 | 11 | #define ReuseIdentifierKey @"ReuseIdentifierKey" 12 | #define MuiIdKey @"MuiIdKey" 13 | 14 | @implementation UIView (TMLazyScrollView) 15 | 16 | - (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier 17 | { 18 | if (self = [self init]) { 19 | self.reuseIdentifier = reuseIdentifier; 20 | } 21 | return self; 22 | } 23 | 24 | - (instancetype)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier 25 | { 26 | if (self = [self initWithFrame:frame]) { 27 | self.reuseIdentifier = reuseIdentifier; 28 | } 29 | return self; 30 | } 31 | 32 | - (NSString *)reuseIdentifier 33 | { 34 | return objc_getAssociatedObject(self, ReuseIdentifierKey); 35 | } 36 | 37 | - (void)setReuseIdentifier:(NSString *)reuseIdentifier 38 | { 39 | objc_setAssociatedObject(self, ReuseIdentifierKey, reuseIdentifier, OBJC_ASSOCIATION_COPY_NONATOMIC); 40 | } 41 | 42 | - (NSString *)muiID 43 | { 44 | return objc_getAssociatedObject(self, MuiIdKey); 45 | } 46 | 47 | - (void)setMuiID:(NSString *)muiID 48 | { 49 | objc_setAssociatedObject(self, MuiIdKey, muiID, OBJC_ASSOCIATION_COPY_NONATOMIC); 50 | } 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /LazyScrollView/TMLazyItemViewProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMLazyItemViewProtocol.h 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | /** 9 | If the item view in LazyScrollView implements this protocol, it 10 | can receive specified event callback in LazyScrollView's lifecycle. 11 | */ 12 | @protocol TMLazyItemViewProtocol 13 | 14 | @optional 15 | /** 16 | Will be called if the item view is dequeued in 17 | 'dequeueReusableItemWithIdentifier:' method. 18 | It is similar with 'prepareForReuse' method of UITableViewCell. 19 | */ 20 | - (void)mui_prepareForReuse; 21 | /** 22 | Will be called if the item view is loaded into buffer area. 23 | This callback always is used for setup item view. 24 | It is similar with 'viewDidLoad' method of UIViewController. 25 | */ 26 | - (void)mui_afterGetView; 27 | /** 28 | Will be called if the item view enters the visible area. 29 | The times starts from 0. 30 | If the item view is in the visible area and the LazyScrollView 31 | is reloaded, this callback will not be called. 32 | This callback always is used for user action tracking. Sometimes, 33 | it is also used for starting timer event. 34 | */ 35 | - (void)mui_didEnterWithTimes:(NSUInteger)times; 36 | /** 37 | Will be called if the item view leaves the visiable area. 38 | This callback always is used for stopping timer event. 39 | */ 40 | - (void)mui_didLeave; 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /update_header.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import re 3 | import sys,os 4 | 5 | def updateHeader(DIR, PROJ): 6 | for path in os.listdir(DIR): 7 | fullPath = os.path.join(DIR, path) 8 | if os.path.isdir(fullPath): 9 | if path != "Pods": 10 | updateHeader(fullPath, PROJ) 11 | elif os.path.isfile(fullPath): 12 | if path.lower().endswith('.m') or path.lower().endswith('.h'): 13 | print('Updating: %s' % (path)) 14 | codeFile = open(fullPath, 'r+') 15 | content = codeFile.read() 16 | content = re.sub('^(//[^\n]*\n)+//(?P[^\n]*)\n', 17 | '//\n' + 18 | '// ' + path + '\n' + 19 | '// ' + PROJ + '\n' + 20 | '//\n' + 21 | '// Copyright (c) 2015-2018 Alibaba. All rights reserved.\n' + 22 | '//' + '\g' + '\n', 23 | content) 24 | codeFile.seek(0) 25 | codeFile.write(content) 26 | codeFile.truncate() 27 | codeFile.close() 28 | 29 | updateHeader(os.path.join(sys.path[0], 'LazyScrollView'), 'LazyScrollView') 30 | updateHeader(os.path.join(sys.path[0], 'LazyScrollViewDemo'), 'LazyScrollViewDemo') 31 | updateHeader(os.path.join(sys.path[0], 'LazyScrollViewTest'), 'LazyScrollViewTest') 32 | updateHeader(os.path.join(sys.path[0], 'TMUtils'), 'TMUtils') 33 | print('Header updating is done.') 34 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/MainViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.m 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "MainViewController.h" 9 | #import "OuterViewController.h" 10 | #import "MoreViewController.h" 11 | 12 | @interface MainViewController () 13 | 14 | @property (nonatomic, strong) NSArray *demoArray; 15 | 16 | @end 17 | 18 | @implementation MainViewController 19 | 20 | - (instancetype)init 21 | { 22 | if (self = [super init]) { 23 | self.title = @"LazyScrollDemo"; 24 | self.demoArray = @[@"Reuse", @"OuterScrollView", @"LoadMore", @"Async"]; 25 | } 26 | return self; 27 | } 28 | 29 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 30 | { 31 | return self.demoArray.count; 32 | } 33 | 34 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 35 | { 36 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; 37 | if (!cell) { 38 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; 39 | } 40 | cell.textLabel.text = self.demoArray[indexPath.row]; 41 | return cell; 42 | } 43 | 44 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 45 | { 46 | NSString *demoName = self.demoArray[indexPath.row]; 47 | UIViewController *vc; 48 | if ([demoName isEqualToString:@"OuterScrollView"]) { 49 | vc = [OuterViewController new]; 50 | } else if ([demoName isEqualToString:@"LoadMore"]) { 51 | vc = [MoreViewController new]; 52 | } else { 53 | Class demoVcClass = NSClassFromString([demoName stringByAppendingString:@"ViewController"]); 54 | vc = [demoVcClass new]; 55 | } 56 | [self.navigationController pushViewController:vc animated:YES]; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/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 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /LazyScrollView/TMLazyReusePool.m: -------------------------------------------------------------------------------- 1 | // 2 | // TMLazyReusePool.m 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "TMLazyReusePool.h" 9 | #import "UIView+TMLazyScrollView.h" 10 | 11 | @interface TMLazyReusePool () { 12 | NSMutableDictionary *_reuseDict; 13 | } 14 | 15 | @end 16 | 17 | @implementation TMLazyReusePool 18 | 19 | - (instancetype)init 20 | { 21 | if (self = [super init]) { 22 | _reuseDict = [NSMutableDictionary dictionary]; 23 | } 24 | return self; 25 | } 26 | 27 | - (void)addItemView:(UIView *)itemView forReuseIdentifier:(NSString *)reuseIdentifier 28 | { 29 | if (reuseIdentifier == nil || reuseIdentifier.length == 0 || itemView == nil) { 30 | return; 31 | } 32 | NSMutableSet *reuseSet = [_reuseDict tm_safeObjectForKey:reuseIdentifier]; 33 | if (!reuseSet) { 34 | reuseSet = [NSMutableSet set]; 35 | [_reuseDict setObject:reuseSet forKey:reuseIdentifier]; 36 | } 37 | [reuseSet addObject:itemView]; 38 | } 39 | 40 | - (UIView *)dequeueItemViewForReuseIdentifier:(NSString *)reuseIdentifier 41 | { 42 | return [self dequeueItemViewForReuseIdentifier:reuseIdentifier andMuiID:nil]; 43 | } 44 | 45 | - (UIView *)dequeueItemViewForReuseIdentifier:(NSString *)reuseIdentifier andMuiID:(NSString *)muiID 46 | { 47 | if (reuseIdentifier == nil || reuseIdentifier.length == 0) { 48 | return nil; 49 | } 50 | UIView *result = nil; 51 | NSMutableSet *reuseSet = [_reuseDict tm_safeObjectForKey:reuseIdentifier]; 52 | if (reuseSet && reuseSet.count > 0) { 53 | if (!muiID) { 54 | result = [reuseSet anyObject]; 55 | } else { 56 | for (UIView *itemView in reuseSet) { 57 | if ([itemView.muiID isEqualToString:muiID]) { 58 | result = itemView; 59 | break; 60 | } 61 | } 62 | if (!result) { 63 | result = [reuseSet anyObject]; 64 | } 65 | } 66 | [reuseSet removeObject:result]; 67 | } 68 | return result; 69 | } 70 | 71 | - (void)clear 72 | { 73 | [_reuseDict removeAllObjects]; 74 | } 75 | 76 | - (NSSet *)allItemViews 77 | { 78 | NSMutableSet *result = [NSMutableSet set]; 79 | for (NSMutableSet *reuseSet in _reuseDict.allValues) { 80 | [result unionSet:reuseSet]; 81 | } 82 | return [result copy]; 83 | } 84 | 85 | @end 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CocoaPods](https://img.shields.io/cocoapods/v/LazyScroll.svg)]() [![CocoaPods](https://img.shields.io/cocoapods/p/LazyScroll.svg)]() [![CocoaPods](https://img.shields.io/cocoapods/l/LazyScroll.svg)]() 2 | 3 | # LazyScrollView 4 | 5 | [中文说明](http://pingguohe.net/2016/01/31/lazyscroll.html) 6 | 7 | > 基于 LazyScrollView,我们创造了一个动态创建模块化 UI 页面的解决方案,详情可见 [Tangram-iOS](https://github.com/alibaba/tangram-ios)。 8 | 9 | LazyScrollView is an iOS ScrollView, to resolve the problem of reusability of views. 10 | 11 | Comparing to UITableView, LazyScrollView can easily create different layout, instead of the single row flow layout. 12 | 13 | Comparing to UICollectionView, LazyScrollView can create views without Grid layout, and provides a easier way to create different kinds of layous in a ScrollView. 14 | 15 | > We create a modular UI solution for building UI page dynamically based on `LazyScrollView`, you can see more info from this repo: [Tangram-iOS](https://github.com/alibaba/tangram-ios) 16 | 17 | # Installation 18 | 19 | LazyScroll is available as `LazyScroll` in CocoaPods. 20 | 21 | pod 'LazyScroll' 22 | 23 | You can also download the source files from [release page](https://github.com/alibaba/LazyScrollView/releases) and add them into your project manually. 24 | 25 | # Usage 26 | 27 | #import "TMMuiLazyScrollView.h" 28 | 29 | Then, create LazyScrollView: 30 | 31 | ```objectivec 32 | TMMuiLazyScrollView *scrollview = [[TMMuiLazyScrollView alloc]init]; 33 | scrollview.frame = self.view.bounds; 34 | ``` 35 | 36 | Next, implement `TMMuiLazyScrollViewDataSource`: 37 | 38 | ```objectivec 39 | @protocol TMMuiLazyScrollViewDataSource 40 | 41 | @required 42 | 43 | // Number of items in scrollView. 44 | - (NSUInteger)numberOfItemInScrollView:(TMMuiLazyScrollView *)scrollView; 45 | 46 | // Return the view model (TMMuiRectModel) by index. 47 | - (TMMuiRectModel *)scrollView:(TMMuiLazyScrollView *)scrollView rectModelAtIndex:(NSUInteger)index; 48 | 49 | // Return view by the unique string that identify a model (muiID). 50 | // You should render the item view here. 51 | // You should ALWAYS try to reuse views by setting each view's reuseIdentifier. 52 | - (UIView *)scrollView:(TMMuiLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID; 53 | 54 | @end 55 | ``` 56 | 57 | Next, set datasource of LazyScrollView: 58 | 59 | ```objectivec 60 | scrollview.dataSource = self; 61 | ``` 62 | 63 | Finally, do reload: 64 | 65 | ```objectivec 66 | [scrollview reloadData]; 67 | ``` 68 | 69 | For more details, please clone the repo and open the demo project. 70 | -------------------------------------------------------------------------------- /LazyScrollView/TMLazyModelBucket.m: -------------------------------------------------------------------------------- 1 | // 2 | // TMLazyModelBucket.m 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "TMLazyModelBucket.h" 9 | 10 | @interface TMLazyModelBucket () { 11 | NSMutableArray *_buckets; 12 | } 13 | 14 | @end 15 | 16 | @implementation TMLazyModelBucket 17 | 18 | @synthesize bucketHeight = _bucketHeight; 19 | 20 | - (instancetype)initWithBucketHeight:(CGFloat)bucketHeight 21 | { 22 | if (self = [super init]) { 23 | _bucketHeight = bucketHeight; 24 | _buckets = [NSMutableArray array]; 25 | } 26 | return self; 27 | } 28 | 29 | - (void)addModel:(TMLazyItemModel *)itemModel 30 | { 31 | if (itemModel && itemModel.bottom > itemModel.top) { 32 | NSInteger startIndex = (NSInteger)floor(itemModel.top / _bucketHeight); 33 | NSInteger endIndex = (NSInteger)floor((itemModel.bottom - 0.01) / _bucketHeight); 34 | for (NSInteger index = 0; index <= endIndex; index++) { 35 | if (_buckets.count <= index) { 36 | [_buckets addObject:[NSMutableSet set]]; 37 | } 38 | if (index >= startIndex && index <= endIndex) { 39 | NSMutableSet *bucket = [_buckets objectAtIndex:index]; 40 | [bucket addObject:itemModel]; 41 | } 42 | } 43 | } 44 | } 45 | 46 | - (void)addModels:(NSSet *)itemModels 47 | { 48 | if (itemModels) { 49 | for (TMLazyItemModel *itemModel in itemModels) { 50 | [self addModel:itemModel]; 51 | } 52 | } 53 | } 54 | 55 | - (void)removeModel:(TMLazyItemModel *)itemModel 56 | { 57 | if (itemModel) { 58 | for (NSMutableSet *bucket in _buckets) { 59 | [bucket removeObject:itemModel]; 60 | } 61 | } 62 | } 63 | 64 | - (void)removeModels:(NSSet *)itemModels 65 | { 66 | if (itemModels) { 67 | for (NSMutableSet *bucket in _buckets) { 68 | [bucket minusSet:itemModels]; 69 | } 70 | } 71 | } 72 | 73 | - (void)reloadModel:(TMLazyItemModel *)itemModel 74 | { 75 | [self removeModel:itemModel]; 76 | [self addModel:itemModel]; 77 | } 78 | 79 | - (void)reloadModels:(NSSet *)itemModels 80 | { 81 | [self removeModels:itemModels]; 82 | [self addModels:itemModels]; 83 | } 84 | 85 | - (void)clear 86 | { 87 | [_buckets removeAllObjects]; 88 | } 89 | 90 | - (NSSet *)showingModelsFrom:(CGFloat)startY to:(CGFloat)endY 91 | { 92 | NSMutableSet *result = [NSMutableSet set]; 93 | NSInteger startIndex = (NSInteger)floor(startY / _bucketHeight); 94 | NSInteger endIndex = (NSInteger)floor((endY - 0.01) / _bucketHeight); 95 | for (NSInteger index = 0; index <= endIndex; index++) { 96 | if (_buckets.count > index && index >= startIndex && index <= endIndex) { 97 | NSSet *bucket = [_buckets objectAtIndex:index]; 98 | [result unionSet:bucket]; 99 | } 100 | } 101 | NSMutableSet *needToBeRemoved = [NSMutableSet set]; 102 | for (TMLazyItemModel *itemModel in result) { 103 | if (itemModel.top >= endY || itemModel.bottom <= startY) { 104 | [needToBeRemoved addObject:itemModel]; 105 | } 106 | } 107 | [result minusSet:needToBeRemoved]; 108 | return [result copy]; 109 | } 110 | 111 | @end 112 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/MoreViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MoreViewController.m 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "MoreViewController.h" 9 | #import 10 | #import 11 | 12 | @interface MoreViewController () { 13 | NSMutableArray * _rectArray; 14 | NSMutableArray * _colorArray; 15 | TMLazyScrollView * _scrollView; 16 | 17 | CGFloat maxY; 18 | } 19 | 20 | @end 21 | 22 | @implementation MoreViewController 23 | 24 | - (instancetype)init 25 | { 26 | if (self = [super init]) { 27 | self.title = @"More"; 28 | } 29 | return self; 30 | } 31 | 32 | - (void)viewDidLoad { 33 | [super viewDidLoad]; 34 | 35 | _scrollView = [[TMLazyScrollView alloc] initWithFrame:self.view.bounds]; 36 | _scrollView.dataSource = self; 37 | _scrollView.autoAddSubview = YES; 38 | [self.view addSubview:_scrollView]; 39 | 40 | _rectArray = [[NSMutableArray alloc] init]; 41 | maxY = 0; 42 | CGFloat currentY = 10; 43 | CGFloat viewWidth = CGRectGetWidth(self.view.bounds); 44 | for (int i = 0; i < 10; i++) { 45 | [self addRect:CGRectMake(10, i * 80 + currentY, viewWidth - 20, 80 - 3)]; 46 | } 47 | 48 | _colorArray = [NSMutableArray arrayWithCapacity:_rectArray.count]; 49 | CGFloat hue = 0; 50 | for (int i = 0; i < 20; i++) { 51 | [_colorArray addObject:[UIColor colorWithHue:hue saturation:1 brightness:1 alpha:1]]; 52 | hue += 0.05; 53 | } 54 | 55 | _scrollView.contentSize = CGSizeMake(viewWidth, maxY + 10); 56 | [_scrollView reloadData]; 57 | 58 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"LoadMore" style:UIBarButtonItemStylePlain target:self action:@selector(loadMoreAction)]; 59 | } 60 | 61 | - (void)loadMoreAction 62 | { 63 | CGFloat currentY = maxY + 3; 64 | CGFloat viewWidth = CGRectGetWidth(self.view.bounds); 65 | for (int i = 0; i < 10; i++) { 66 | [self addRect:CGRectMake(10, i * 80 + currentY, viewWidth - 20, 80 - 3)]; 67 | } 68 | 69 | _scrollView.contentSize = CGSizeMake(viewWidth, maxY + 10); 70 | [_scrollView loadMoreData]; 71 | } 72 | 73 | #pragma mark LazyScrollView 74 | 75 | - (NSUInteger)numberOfItemsInScrollView:(TMLazyScrollView *)scrollView 76 | { 77 | return _rectArray.count; 78 | } 79 | 80 | - (TMLazyItemModel *)scrollView:(TMLazyScrollView *)scrollView itemModelAtIndex:(NSUInteger)index 81 | { 82 | CGRect rect = [(NSValue *)[_rectArray objectAtIndex:index] CGRectValue]; 83 | TMLazyItemModel *rectModel = [[TMLazyItemModel alloc] init]; 84 | rectModel.absRect = rect; 85 | rectModel.muiID = [NSString stringWithFormat:@"%zd", index]; 86 | return rectModel; 87 | } 88 | 89 | - (UIView *)scrollView:(TMLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID 90 | { 91 | UIView *view = (UIView *)[scrollView dequeueReusableItemWithIdentifier:@"testView"]; 92 | NSInteger index = [muiID integerValue]; 93 | if (!view) { 94 | view = [UIView new]; 95 | view.reuseIdentifier = @"testView"; 96 | view.backgroundColor = [_colorArray tm_safeObjectAtIndex:index % 20]; 97 | } 98 | view.frame = [(NSValue *)[_rectArray objectAtIndex:index] CGRectValue]; 99 | return view; 100 | } 101 | 102 | #pragma mark Private 103 | 104 | - (void)addRect:(CGRect)newRect 105 | { 106 | if (CGRectGetMaxY(newRect) > maxY) { 107 | maxY = CGRectGetMaxY(newRect); 108 | } 109 | [_rectArray addObject:[NSValue valueWithCGRect:newRect]]; 110 | } 111 | 112 | @end 113 | -------------------------------------------------------------------------------- /TMUtils/NSDictionary+TMSafeUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+TMSafeUtils.m 3 | // TMUtils 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "NSDictionary+TMSafeUtils.h" 9 | #import "NSString+TMSafeUtils.h" 10 | 11 | @implementation NSDictionary (TMSafeUtils) 12 | 13 | - (id)tm_safeObjectForKey:(id)key 14 | { 15 | if (key == nil) { 16 | return nil; 17 | } 18 | id value = [self objectForKey:key]; 19 | if (value == [NSNull null]) { 20 | return nil; 21 | } 22 | return value; 23 | } 24 | 25 | - (id)tm_safeObjectForKey:(id)key class:(Class)aClass 26 | { 27 | id value = [self tm_safeObjectForKey:key]; 28 | if ([value isKindOfClass:aClass]) { 29 | return value; 30 | } 31 | return nil; 32 | } 33 | 34 | - (bool)tm_boolForKey:(id)key 35 | { 36 | id value = [self tm_safeObjectForKey:key]; 37 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 38 | return [value boolValue]; 39 | } 40 | return NO; 41 | } 42 | 43 | - (CGFloat)tm_floatForKey:(id)key 44 | { 45 | id value = [self tm_safeObjectForKey:key]; 46 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 47 | return [value floatValue]; 48 | } 49 | return 0; 50 | } 51 | 52 | - (NSInteger)tm_integerForKey:(id)key 53 | { 54 | id value = [self tm_safeObjectForKey:key]; 55 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 56 | return [value integerValue]; 57 | } 58 | return 0; 59 | } 60 | 61 | - (int)tm_intForKey:(id)key 62 | { 63 | id value = [self tm_safeObjectForKey:key]; 64 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 65 | return [value intValue]; 66 | } 67 | return 0; 68 | } 69 | 70 | - (long)tm_longForKey:(id)key 71 | { 72 | id value = [self tm_safeObjectForKey:key]; 73 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 74 | return [value longValue]; 75 | } 76 | return 0; 77 | } 78 | 79 | - (NSNumber *)tm_numberForKey:(id)key 80 | { 81 | id value = [self tm_safeObjectForKey:key]; 82 | if ([value isKindOfClass:[NSNumber class]]) { 83 | return value; 84 | } 85 | if ([value respondsToSelector:@selector(numberValue)]) { 86 | return [value numberValue]; 87 | } 88 | return nil; 89 | } 90 | 91 | - (NSString *)tm_stringForKey:(id)key 92 | { 93 | id value = [self tm_safeObjectForKey:key]; 94 | if ([value isKindOfClass:[NSString class]]) { 95 | return value; 96 | } 97 | if ([value respondsToSelector:@selector(stringValue)]) { 98 | return [value stringValue]; 99 | } 100 | return nil; 101 | } 102 | 103 | - (NSArray *)tm_arrayForKey:(id)key 104 | { 105 | return [self tm_safeObjectForKey:key class:[NSArray class]]; 106 | } 107 | 108 | - (NSDictionary *)tm_dictionaryForKey:(id)key 109 | { 110 | return [self tm_safeObjectForKey:key class:[NSDictionary class]]; 111 | } 112 | 113 | - (NSMutableArray *)tm_mutableArrayForKey:(id)key 114 | { 115 | return [self tm_safeObjectForKey:key class:[NSMutableArray class]]; 116 | } 117 | 118 | - (NSMutableDictionary *)tm_mutableDictionaryForKey:(id)key 119 | { 120 | return [self tm_safeObjectForKey:key class:[NSMutableDictionary class]]; 121 | } 122 | 123 | @end 124 | 125 | @implementation NSMutableDictionary (TMSafeUtils) 126 | 127 | - (void)tm_safeSetObject:(id)anObject forKey:(id)key 128 | { 129 | if (key && anObject) { 130 | [self setObject:anObject forKey:key]; 131 | } 132 | } 133 | 134 | -(void)tm_safeRemoveObjectForKey:(id)key 135 | { 136 | if (key) { 137 | [self removeObjectForKey:key]; 138 | } 139 | } 140 | 141 | @end 142 | 143 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/AsyncViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncViewController.m 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "AsyncViewController.h" 9 | #import 10 | #import 11 | 12 | @interface AsyncViewController () { 13 | NSMutableArray * _rectArray; 14 | NSMutableArray * _colorArray; 15 | TMLazyScrollView * _scrollView; 16 | 17 | BOOL enableDelay; 18 | } 19 | 20 | @end 21 | 22 | @implementation AsyncViewController 23 | 24 | - (instancetype)init 25 | { 26 | if (self = [super init]) { 27 | self.title = @"Sync"; 28 | } 29 | return self; 30 | } 31 | 32 | - (void)viewDidLoad { 33 | [super viewDidLoad]; 34 | 35 | _scrollView = [[TMLazyScrollView alloc] initWithFrame:self.view.bounds]; 36 | _scrollView.dataSource = self; 37 | _scrollView.autoAddSubview = YES; 38 | [self.view addSubview:_scrollView]; 39 | 40 | _rectArray = [[NSMutableArray alloc] init]; 41 | CGFloat currentY = 10, maxY = 0; 42 | CGFloat viewWidth = CGRectGetWidth(self.view.bounds); 43 | for (int i = 0; i < 50; i++) { 44 | [self addRect:CGRectMake(10, i * 80 + currentY, 98, 80 - 3) andUpdateMaxY:&maxY]; 45 | [self addRect:CGRectMake(111, i * 80 + currentY, 98, 80 - 3) andUpdateMaxY:&maxY]; 46 | [self addRect:CGRectMake(212, i * 80 + currentY, 98, 80 - 3) andUpdateMaxY:&maxY]; 47 | } 48 | 49 | _colorArray = [NSMutableArray arrayWithCapacity:_rectArray.count]; 50 | CGFloat hue = 0; 51 | for (int i = 0; i < 20; i++) { 52 | [_colorArray addObject:[UIColor colorWithHue:hue saturation:1 brightness:1 alpha:1]]; 53 | hue += 0.05; 54 | } 55 | 56 | _scrollView.contentSize = CGSizeMake(viewWidth, maxY + 10); 57 | [_scrollView reloadData]; 58 | enableDelay = YES; 59 | 60 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Switch" style:UIBarButtonItemStylePlain target:self action:@selector(switchAction)]; 61 | } 62 | 63 | - (void)switchAction 64 | { 65 | _scrollView.loadAllItemsImmediately = !_scrollView.loadAllItemsImmediately; 66 | if (_scrollView.loadAllItemsImmediately) { 67 | self.title = @"Sync"; 68 | } else { 69 | self.title = @"Async"; 70 | } 71 | } 72 | 73 | #pragma mark LazyScrollView 74 | 75 | - (NSUInteger)numberOfItemsInScrollView:(TMLazyScrollView *)scrollView 76 | { 77 | return _rectArray.count; 78 | } 79 | 80 | - (TMLazyItemModel *)scrollView:(TMLazyScrollView *)scrollView itemModelAtIndex:(NSUInteger)index 81 | { 82 | CGRect rect = [(NSValue *)[_rectArray objectAtIndex:index] CGRectValue]; 83 | TMLazyItemModel *rectModel = [[TMLazyItemModel alloc] init]; 84 | rectModel.absRect = rect; 85 | rectModel.muiID = [NSString stringWithFormat:@"%zd", index]; 86 | return rectModel; 87 | } 88 | 89 | - (UIView *)scrollView:(TMLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID 90 | { 91 | UIView *view = (UIView *)[scrollView dequeueReusableItemWithIdentifier:@"testView"]; 92 | NSInteger index = [muiID integerValue]; 93 | if (!view) { 94 | view = [UIView new]; 95 | view.reuseIdentifier = @"testView"; 96 | view.backgroundColor = [_colorArray tm_safeObjectAtIndex:index % 20]; 97 | } 98 | view.frame = [(NSValue *)[_rectArray objectAtIndex:index] CGRectValue]; 99 | if (enableDelay) { 100 | // Add a delay manually. 101 | [NSThread sleepForTimeInterval:0.015]; 102 | } 103 | return view; 104 | } 105 | 106 | #pragma mark Private 107 | 108 | - (void)addRect:(CGRect)newRect andUpdateMaxY:(CGFloat *)maxY 109 | { 110 | if (CGRectGetMaxY(newRect) > *maxY) { 111 | *maxY = CGRectGetMaxY(newRect); 112 | } 113 | [_rectArray addObject:[NSValue valueWithCGRect:newRect]]; 114 | } 115 | 116 | @end 117 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/OuterViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // OuterViewController.m 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "OuterViewController.h" 9 | #import 10 | #import 11 | 12 | @interface OuterViewController () { 13 | NSMutableArray * _rectArray; 14 | NSMutableArray * _colorArray; 15 | TMLazyScrollView * _scrollView; 16 | } 17 | 18 | @end 19 | 20 | @implementation OuterViewController 21 | 22 | - (instancetype)init 23 | { 24 | if (self = [super init]) { 25 | self.title = @"Outer"; 26 | } 27 | return self; 28 | } 29 | 30 | - (void)dealloc 31 | { 32 | // VERY IMPORTANT! 33 | _scrollView.outerScrollView = nil; 34 | } 35 | 36 | - (void)viewDidLoad 37 | { 38 | [super viewDidLoad]; 39 | 40 | _scrollView = [[TMLazyScrollView alloc] initWithFrame:self.view.bounds]; 41 | _scrollView.dataSource = self; 42 | _scrollView.autoAddSubview = YES; 43 | 44 | _rectArray = [[NSMutableArray alloc] init]; 45 | CGFloat maxY = 0; 46 | CGFloat viewWidth = CGRectGetWidth(self.view.bounds); 47 | for (int i = 0; i < 20; i++) { 48 | [self addRect:CGRectMake((i % 2) * (viewWidth - 20 + 3) / 2 + 10, i / 2 * 80 + 10, (viewWidth - 20 - 3) / 2, 80 - 3) andUpdateMaxY:&maxY]; 49 | } 50 | 51 | _colorArray = [NSMutableArray arrayWithCapacity:_rectArray.count]; 52 | CGFloat hue = 0; 53 | for (int i = 0; i < _rectArray.count; i++) { 54 | [_colorArray addObject:[UIColor colorWithHue:hue saturation:1 brightness:1 alpha:1]]; 55 | hue += 0.05; 56 | if (hue >= 1) { 57 | hue = 0; 58 | } 59 | } 60 | 61 | _scrollView.contentSize = CGSizeMake(viewWidth, maxY + 10); 62 | _scrollView.frame = CGRectMake(0, 0, viewWidth, maxY + 10); 63 | _scrollView.outerScrollView = self.tableView; 64 | self.tableView.tableFooterView = _scrollView; 65 | [_scrollView reloadData]; 66 | 67 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Reload" style:UIBarButtonItemStylePlain target:self action:@selector(reloadAction)]; 68 | } 69 | 70 | - (void)reloadAction 71 | { 72 | [_scrollView reloadData]; 73 | } 74 | 75 | #pragma mark TableView 76 | 77 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 78 | { 79 | return 10; 80 | } 81 | 82 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 83 | { 84 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; 85 | if (!cell) { 86 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; 87 | } 88 | if (indexPath.row == 0) { 89 | cell.textLabel.text = @"The table view's footer view is a LazyScrollView."; 90 | } else { 91 | cell.textLabel.text = [@(indexPath.row) stringValue]; 92 | } 93 | return cell; 94 | } 95 | 96 | #pragma mark LazyScrollView 97 | 98 | - (NSUInteger)numberOfItemsInScrollView:(TMLazyScrollView *)scrollView 99 | { 100 | return _rectArray.count; 101 | } 102 | 103 | - (TMLazyItemModel *)scrollView:(TMLazyScrollView *)scrollView itemModelAtIndex:(NSUInteger)index 104 | { 105 | CGRect rect = [(NSValue *)[_rectArray objectAtIndex:index] CGRectValue]; 106 | TMLazyItemModel *rectModel = [[TMLazyItemModel alloc] init]; 107 | rectModel.absRect = rect; 108 | rectModel.muiID = [NSString stringWithFormat:@"%zd", index]; 109 | return rectModel; 110 | } 111 | 112 | - (UIView *)scrollView:(TMLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID 113 | { 114 | UIView *view = (UIView *)[scrollView dequeueReusableItemWithIdentifier:@"testView"]; 115 | NSInteger index = [muiID integerValue]; 116 | if (!view) { 117 | view = [UIView new]; 118 | view.reuseIdentifier = @"testView"; 119 | view.backgroundColor = [_colorArray tm_safeObjectAtIndex:index]; 120 | } 121 | view.frame = [(NSValue *)[_rectArray objectAtIndex:index] CGRectValue]; 122 | return view; 123 | } 124 | 125 | #pragma mark Private 126 | 127 | - (void)addRect:(CGRect)newRect andUpdateMaxY:(CGFloat *)maxY 128 | { 129 | if (CGRectGetMaxY(newRect) > *maxY) { 130 | *maxY = CGRectGetMaxY(newRect); 131 | } 132 | [_rectArray addObject:[NSValue valueWithCGRect:newRect]]; 133 | } 134 | 135 | @end 136 | -------------------------------------------------------------------------------- /LazyScrollView/TMLazyScrollView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMLazyScrollView.h 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "TMLazyItemModel.h" 10 | #import "UIView+TMLazyScrollView.h" 11 | 12 | @class TMLazyReusePool; 13 | @class TMLazyScrollView; 14 | 15 | @protocol TMLazyScrollViewDataSource 16 | 17 | @required 18 | 19 | /** 20 | Similar with 'tableView:numberOfRowsInSection:' of UITableView. 21 | */ 22 | - (NSUInteger)numberOfItemsInScrollView:(nonnull TMLazyScrollView *)scrollView; 23 | 24 | /** 25 | Similar with 'tableView:heightForRowAtIndexPath:' of UITableView. 26 | Manager the correct muiID of item views will bring a higher performance. 27 | */ 28 | - (nonnull TMLazyItemModel *)scrollView:(nonnull TMLazyScrollView *)scrollView 29 | itemModelAtIndex:(NSUInteger)index; 30 | 31 | /** 32 | Similar with 'tableView:cellForRowAtIndexPath:' of UITableView. 33 | It will use muiID in item model instead of index. 34 | */ 35 | - (nonnull UIView *)scrollView:(nonnull TMLazyScrollView *)scrollView 36 | itemByMuiID:(nonnull NSString *)muiID; 37 | 38 | @end 39 | 40 | //**************************************************************** 41 | 42 | @interface TMLazyScrollView : UIScrollView 43 | 44 | @property (nonatomic, weak, nullable) id dataSource; 45 | 46 | /** 47 | Used for managing reuseable item views. 48 | */ 49 | @property (nonatomic, strong, nonnull) TMLazyReusePool *reusePool; 50 | 51 | /** 52 | LazyScrollView can be used as a subview of another ScrollView. 53 | For example: 54 | You can use LazyScrollView as footerView of TableView. 55 | Then the outerScrollView should be that TableView. 56 | You MUST set this property to nil before the outerScrollView's dealloc. 57 | */ 58 | @property (nonatomic, weak, nullable) UIScrollView *outerScrollView; 59 | 60 | /** 61 | If it is YES, LazyScrollView will add created item view into 62 | its subviews automatically. 63 | Default value is NO. 64 | Please only set this value before you reload data for the first time. 65 | */ 66 | @property (nonatomic, assign) BOOL autoAddSubview; 67 | 68 | /** 69 | If it is YES, LazyScrollView will clear all gestures for item view before 70 | reusing it. 71 | Default value is YES. 72 | */ 73 | @property (nonatomic, assign) BOOL autoClearGestures; 74 | 75 | /** 76 | If it is NO, LazyScrollView will try to load new item views in several frames. 77 | Default value is YES. 78 | */ 79 | @property (nonatomic, assign) BOOL loadAllItemsImmediately; 80 | 81 | /** 82 | Item views which is in the buffer area. 83 | They will be shown soon. 84 | */ 85 | @property (nonatomic, strong, readonly, nonnull) NSSet *visibleItems; 86 | 87 | /** 88 | Item views which is in the screen visible area. 89 | It is a sub set of "visibleItems". 90 | */ 91 | @property (nonatomic, strong, readonly, nonnull) NSSet *inScreenVisibleItems; 92 | 93 | - (void)reloadData; 94 | - (void)loadMoreData; 95 | 96 | /** 97 | Get reuseable item view by reuseIdentifier. 98 | */ 99 | - (nullable UIView *)dequeueReusableItemWithIdentifier:(nonnull NSString *)identifier; 100 | /** 101 | Get reuseable item view by reuseIdentifier and muiID. 102 | MuiID has higher priority. 103 | */ 104 | - (nullable UIView *)dequeueReusableItemWithIdentifier:(nonnull NSString *)identifier 105 | muiID:(nullable NSString *)muiID; 106 | 107 | /** 108 | Hide all visible items and recycle reusable item views. 109 | After call this method, every item view will receive 110 | 'afterGetView' & 'didEnterWithTimes' again. 111 | 112 | @param enableRecycle Recycle items or remove them. 113 | */ 114 | - (void)clearVisibleItems:(BOOL)enableRecycle; 115 | - (void)removeAllLayouts __deprecated_msg("use clearVisibleItems: or resetAll"); 116 | 117 | /** 118 | Remove reusable item views from reuse pool to release memory. 119 | */ 120 | - (void)clearReuseItems; 121 | - (void)cleanRecycledView __deprecated_msg("use clearReuseItems"); 122 | 123 | /** 124 | After call this method, the times of 'didEnterWithTimes' will start from 0. 125 | */ 126 | - (void)resetItemsEnterTimes; 127 | - (void)resetViewEnterTimes __deprecated_msg("use resetItemsEnterTimes"); 128 | 129 | /** 130 | Reset the state of LazyScrollView. 131 | */ 132 | - (void)resetAll; 133 | 134 | - (void)removeContentOffsetObserver __deprecated_msg("set outerScrollView to nil"); 135 | - (void)reLayout __deprecated_msg("use reloadData"); 136 | 137 | @end 138 | -------------------------------------------------------------------------------- /TMUtils/NSArray+TMSafeUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSArray+TMSafeUtils.m 3 | // TMUtils 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "NSArray+TMSafeUtils.h" 9 | #import "NSString+TMSafeUtils.h" 10 | 11 | @implementation NSArray (TMSafeUtils) 12 | 13 | - (id)tm_safeObjectAtIndex:(NSUInteger)index 14 | { 15 | if (index >= [self count]) { 16 | return nil; 17 | } 18 | id value = [self objectAtIndex:index]; 19 | if (value == [NSNull null]) { 20 | return nil; 21 | } 22 | return value; 23 | } 24 | 25 | - (id)tm_safeObjectAtIndex:(NSUInteger)index class:(Class)aClass 26 | { 27 | id value = [self tm_safeObjectAtIndex:index]; 28 | if ([value isKindOfClass:aClass]) { 29 | return value; 30 | } 31 | return nil; 32 | } 33 | 34 | - (bool)tm_boolAtIndex:(NSUInteger)index 35 | { 36 | id value = [self tm_safeObjectAtIndex:index]; 37 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 38 | return [value boolValue]; 39 | } 40 | return NO; 41 | } 42 | 43 | - (CGFloat)tm_floatAtIndex:(NSUInteger)index 44 | { 45 | id value = [self tm_safeObjectAtIndex:index]; 46 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 47 | return [value floatValue]; 48 | } 49 | return 0; 50 | } 51 | 52 | - (NSInteger)tm_integerAtIndex:(NSUInteger)index 53 | { 54 | id value = [self tm_safeObjectAtIndex:index]; 55 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 56 | return [value integerValue]; 57 | } 58 | return 0; 59 | } 60 | 61 | - (int)tm_intAtIndex:(NSUInteger)index 62 | { 63 | id value = [self tm_safeObjectAtIndex:index]; 64 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 65 | return [value intValue]; 66 | } 67 | return 0; 68 | } 69 | 70 | - (long)tm_longAtIndex:(NSUInteger)index 71 | { 72 | id value = [self tm_safeObjectAtIndex:index]; 73 | if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { 74 | return [value longValue]; 75 | } 76 | return 0; 77 | } 78 | 79 | - (NSNumber *)tm_numberAtIndex:(NSUInteger)index 80 | { 81 | id value = [self tm_safeObjectAtIndex:index]; 82 | if ([value isKindOfClass:[NSNumber class]]) { 83 | return value; 84 | } 85 | if ([value respondsToSelector:@selector(numberValue)]) { 86 | return [value numberValue]; 87 | } 88 | return nil; 89 | } 90 | 91 | - (NSString *)tm_stringAtIndex:(NSUInteger)index 92 | { 93 | id value = [self tm_safeObjectAtIndex:index]; 94 | if ([value isKindOfClass:[NSString class]]) { 95 | return value; 96 | } 97 | if ([value respondsToSelector:@selector(stringValue)]) { 98 | return [value stringValue]; 99 | } 100 | return nil; 101 | } 102 | 103 | - (NSArray *)tm_arrayAtIndex:(NSUInteger)index 104 | { 105 | return [self tm_safeObjectAtIndex:index class:[NSArray class]]; 106 | } 107 | 108 | - (NSDictionary *)tm_dictionaryAtIndex:(NSUInteger)index 109 | { 110 | return [self tm_safeObjectAtIndex:index class:[NSDictionary class]]; 111 | } 112 | 113 | - (NSMutableArray *)tm_mutableArrayAtIndex:(NSUInteger)index 114 | { 115 | return [self tm_safeObjectAtIndex:index class:[NSMutableArray class]]; 116 | } 117 | 118 | - (NSMutableDictionary *)tm_mutableDictionaryAtIndex:(NSUInteger)index 119 | { 120 | return [self tm_safeObjectAtIndex:index class:[NSMutableDictionary class]]; 121 | } 122 | 123 | @end 124 | 125 | @implementation NSMutableArray (TMSafeUtils) 126 | 127 | - (void)tm_safeAddObject:(id)anObject 128 | { 129 | if (anObject) { 130 | [self addObject:anObject]; 131 | } 132 | } 133 | 134 | - (void)tm_safeInsertObject:(id)anObject atIndex:(NSUInteger)index 135 | { 136 | if (anObject && index <= self.count) { 137 | [self insertObject:anObject atIndex:index]; 138 | } 139 | } 140 | 141 | - (void)tm_safeReplaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject 142 | { 143 | if (anObject && index < self.count) { 144 | [self replaceObjectAtIndex:index withObject:anObject]; 145 | } 146 | } 147 | 148 | - (void)tm_safeRemoveObjectAtIndex:(NSUInteger)index 149 | { 150 | if (index < self.count) { 151 | [self removeObjectAtIndex:index]; 152 | } 153 | } 154 | 155 | - (void)tm_safeRemoveObject:(id)anObject 156 | { 157 | if (anObject != nil) { 158 | [self removeObject:anObject]; 159 | } 160 | } 161 | 162 | @end 163 | 164 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo/ReuseViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ReuseViewController.m 3 | // LazyScrollViewDemo 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "ReuseViewController.h" 9 | #import 10 | #import 11 | 12 | @interface LazyScrollViewCustomView : UILabel 13 | 14 | @property (nonatomic, assign) NSUInteger reuseTimes; 15 | 16 | @end 17 | 18 | @implementation LazyScrollViewCustomView 19 | 20 | - (void)mui_prepareForReuse 21 | { 22 | self.reuseTimes++; 23 | } 24 | 25 | @end 26 | 27 | //**************************************************************** 28 | 29 | @interface ReuseViewController () { 30 | NSMutableArray * _rectArray; 31 | NSMutableArray * _colorArray; 32 | } 33 | 34 | @end 35 | 36 | @implementation ReuseViewController 37 | 38 | - (instancetype)init 39 | { 40 | if (self = [super init]) { 41 | self.title = @"Reuse"; 42 | } 43 | return self; 44 | } 45 | 46 | - (void)viewDidLoad { 47 | [super viewDidLoad]; 48 | 49 | // STEP 1: Create LazyScrollView 50 | TMLazyScrollView *scrollView = [[TMLazyScrollView alloc] initWithFrame:self.view.bounds]; 51 | scrollView.dataSource = self; 52 | scrollView.autoAddSubview = YES; 53 | [self.view addSubview:scrollView]; 54 | 55 | // Here is frame array for test. 56 | // LazyScrollView must know item view's frame before rending. 57 | _rectArray = [[NSMutableArray alloc] init]; 58 | CGFloat maxY = 0, currentY = 50; 59 | CGFloat viewWidth = CGRectGetWidth(self.view.bounds); 60 | // Create a double column layout with 10 elements. 61 | for (int i = 0; i < 10; i++) { 62 | [self addRect:CGRectMake((i % 2) * (viewWidth - 20 + 3) / 2 + 10, i / 2 * 80 + currentY, (viewWidth - 20 - 3) / 2, 80 - 3) andUpdateMaxY:&maxY]; 63 | } 64 | // Create a single column layout with 10 elements. 65 | currentY = maxY + 10; 66 | for (int i = 0; i < 10; i++) { 67 | [self addRect:CGRectMake(10, i * 80 + currentY, viewWidth - 20, 80 - 3) andUpdateMaxY:&maxY]; 68 | } 69 | // Create a double column layout with 10 elements. 70 | currentY = maxY + 10; 71 | for (int i = 0; i < 10; i++) { 72 | [self addRect:CGRectMake((i % 2) * (viewWidth - 20 + 3) / 2 + 10, i / 2 * 80 + currentY, (viewWidth - 20 - 3) / 2, 80 - 3) andUpdateMaxY:&maxY]; 73 | } 74 | 75 | // Create color array. 76 | // The color order is like rainbow. 77 | _colorArray = [NSMutableArray arrayWithCapacity:_rectArray.count]; 78 | CGFloat hue = 0; 79 | for (int i = 0; i < _rectArray.count; i++) { 80 | [_colorArray addObject:[UIColor colorWithHue:hue saturation:1 brightness:1 alpha:1]]; 81 | hue += 0.04; 82 | if (hue >= 1) { 83 | hue = 0; 84 | } 85 | } 86 | 87 | // STEP 3: reload LazyScrollView 88 | scrollView.contentSize = CGSizeMake(viewWidth, maxY + 10); 89 | [scrollView reloadData]; 90 | 91 | // A tip. 92 | UILabel *tipLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, viewWidth - 20, 30)]; 93 | tipLabel.font = [UIFont systemFontOfSize:12]; 94 | tipLabel.numberOfLines = 0; 95 | tipLabel.text = @"Item views's color should be from red to blue. They are reused. Magenta should not be appeared."; 96 | [scrollView addSubview:tipLabel]; 97 | } 98 | 99 | #pragma mark LazyScrollView 100 | 101 | // STEP 2: implement datasource. 102 | - (NSUInteger)numberOfItemsInScrollView:(TMLazyScrollView *)scrollView 103 | { 104 | return _rectArray.count; 105 | } 106 | 107 | - (TMLazyItemModel *)scrollView:(TMLazyScrollView *)scrollView itemModelAtIndex:(NSUInteger)index 108 | { 109 | CGRect rect = [(NSValue *)[_rectArray objectAtIndex:index] CGRectValue]; 110 | TMLazyItemModel *rectModel = [[TMLazyItemModel alloc] init]; 111 | rectModel.absRect = rect; 112 | rectModel.muiID = [NSString stringWithFormat:@"%zd", index]; 113 | return rectModel; 114 | } 115 | 116 | - (UIView *)scrollView:(TMLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID 117 | { 118 | // Find view that is reuseable first. 119 | LazyScrollViewCustomView *label = (LazyScrollViewCustomView *)[scrollView dequeueReusableItemWithIdentifier:@"testView"]; 120 | NSInteger index = [muiID integerValue]; 121 | if (!label) { 122 | NSLog(@"create a new label"); 123 | label = [LazyScrollViewCustomView new]; 124 | label.textAlignment = NSTextAlignmentCenter; 125 | label.numberOfLines = 0; 126 | label.reuseIdentifier = @"testView"; 127 | label.backgroundColor = [_colorArray tm_safeObjectAtIndex:index]; 128 | } 129 | label.frame = [(NSValue *)[_rectArray objectAtIndex:index] CGRectValue]; 130 | if (label.reuseTimes > 0) { 131 | label.text = [NSString stringWithFormat:@"%zd\nlast index: %@\nreuse times: %zd", index, label.muiID, label.reuseTimes]; 132 | } else { 133 | label.text = [NSString stringWithFormat:@"%zd", index]; 134 | } 135 | return label; 136 | } 137 | 138 | #pragma mark Private 139 | 140 | - (void)addRect:(CGRect)newRect andUpdateMaxY:(CGFloat *)maxY 141 | { 142 | if (CGRectGetMaxY(newRect) > *maxY) { 143 | *maxY = CGRectGetMaxY(newRect); 144 | } 145 | [_rectArray addObject:[NSValue valueWithCGRect:newRect]]; 146 | } 147 | 148 | @end 149 | -------------------------------------------------------------------------------- /LazyScrollViewTest/LazyScrollViewTest/ModelBucketTest.m: -------------------------------------------------------------------------------- 1 | // 2 | // ModelBucketTest.m 3 | // LazyScrollViewTest 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | #import 11 | 12 | @interface ModelBucketTest : XCTestCase 13 | 14 | @end 15 | 16 | @implementation ModelBucketTest 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)testModelBucket { 29 | TMLazyModelBucket *bucket= [[TMLazyModelBucket alloc] initWithBucketHeight:20]; 30 | TMLazyItemModel *firstModel = [ModelBucketTest createModelHelperWithY:0 height:10]; 31 | [bucket addModel:firstModel]; 32 | [bucket addModel:[ModelBucketTest createModelHelperWithY:10 height:10]]; 33 | [bucket addModel:[ModelBucketTest createModelHelperWithY:20 height:10]]; 34 | [bucket addModel:[ModelBucketTest createModelHelperWithY:30 height:10]]; 35 | [bucket addModel:[ModelBucketTest createModelHelperWithY:40 height:10]]; 36 | [bucket addModel:[ModelBucketTest createModelHelperWithY:50 height:10]]; 37 | [bucket addModels:[NSSet setWithArray:@[ 38 | [ModelBucketTest createModelHelperWithY:0 height:20], 39 | [ModelBucketTest createModelHelperWithY:10 height:20], 40 | [ModelBucketTest createModelHelperWithY:20 height:20], 41 | [ModelBucketTest createModelHelperWithY:30 height:20], 42 | [ModelBucketTest createModelHelperWithY:40 height:20], 43 | [ModelBucketTest createModelHelperWithY:0 height:30], 44 | [ModelBucketTest createModelHelperWithY:10 height:30], 45 | [ModelBucketTest createModelHelperWithY:20 height:30], 46 | [ModelBucketTest createModelHelperWithY:30 height:30], 47 | [ModelBucketTest createModelHelperWithY:0 height:40], 48 | [ModelBucketTest createModelHelperWithY:10 height:40], 49 | [ModelBucketTest createModelHelperWithY:20 height:40], 50 | [ModelBucketTest createModelHelperWithY:0 height:50], 51 | [ModelBucketTest createModelHelperWithY:10 height:50] 52 | ]]]; 53 | TMLazyItemModel *lastModel = [ModelBucketTest createModelHelperWithY:0 height:60]; 54 | [bucket addModel:lastModel]; 55 | 56 | assertThat([bucket showingModelsFrom:0 to:60], hasCountOf(21)); 57 | assertThat([bucket showingModelsFrom:0 to:50], hasCountOf(20)); 58 | assertThat([bucket showingModelsFrom:0 to:40], hasCountOf(18)); 59 | assertThat([bucket showingModelsFrom:0 to:30], hasCountOf(15)); 60 | NSSet *set = [bucket showingModelsFrom:0 to:20]; 61 | assertThat(set, hasCountOf(11)); 62 | set = [set valueForKey:@"muiID"]; 63 | assertThat(set, containsInAnyOrder(@"10", @"20", @"30", @"40", @"50", @"60", @"1010", @"1020", @"1030", @"1040", @"1050", nil)); 64 | set = [bucket showingModelsFrom:0 to:10]; 65 | assertThat(set, hasCountOf(6)); 66 | set = [set valueForKey:@"muiID"]; 67 | assertThat(set, containsInAnyOrder(@"10", @"20", @"30", @"40", @"50", @"60", nil)); 68 | 69 | [bucket removeModel:lastModel]; 70 | 71 | assertThat([bucket showingModelsFrom:0 to:60], hasCountOf(20)); 72 | assertThat([bucket showingModelsFrom:0 to:50], hasCountOf(19)); 73 | assertThat([bucket showingModelsFrom:0 to:40], hasCountOf(17)); 74 | assertThat([bucket showingModelsFrom:0 to:30], hasCountOf(14)); 75 | set = [bucket showingModelsFrom:0 to:20]; 76 | assertThat(set, hasCountOf(10)); 77 | set = [set valueForKey:@"muiID"]; 78 | assertThat(set, containsInAnyOrder(@"10", @"20", @"30", @"40", @"50", @"1010", @"1020", @"1030", @"1040", @"1050", nil)); 79 | set = [bucket showingModelsFrom:0 to:10]; 80 | assertThat(set, hasCountOf(5)); 81 | set = [set valueForKey:@"muiID"]; 82 | assertThat(set, containsInAnyOrder(@"10", @"20", @"30", @"40", @"50", nil)); 83 | 84 | firstModel.absRect = CGRectMake(0, 30, 10, 30); 85 | [bucket reloadModel:firstModel]; 86 | 87 | assertThat([bucket showingModelsFrom:0 to:60], hasCountOf(20)); 88 | assertThat([bucket showingModelsFrom:0 to:50], hasCountOf(19)); 89 | assertThat([bucket showingModelsFrom:0 to:40], hasCountOf(17)); 90 | assertThat([bucket showingModelsFrom:0 to:30], hasCountOf(13)); 91 | set = [bucket showingModelsFrom:0 to:20]; 92 | assertThat(set, hasCountOf(9)); 93 | set = [set valueForKey:@"muiID"]; 94 | assertThat(set, containsInAnyOrder(@"20", @"30", @"40", @"50", @"1010", @"1020", @"1030", @"1040", @"1050", nil)); 95 | set = [bucket showingModelsFrom:0 to:10]; 96 | assertThat(set, hasCountOf(4)); 97 | set = [set valueForKey:@"muiID"]; 98 | assertThat(set, containsInAnyOrder(@"20", @"30", @"40", @"50", nil)); 99 | set = [bucket showingModelsFrom:30 to:60]; 100 | assertThat(set, hasCountOf(15)); 101 | set = [set valueForKey:@"muiID"]; 102 | assertThat(set, containsInAnyOrder(@"10", @"40", @"50", @"1030", @"1040", @"1050", @"2020", @"2030", @"2040", @"3010", @"3020", @"3030", @"4010", @"4020", @"5010", nil)); 103 | set = [bucket showingModelsFrom:30 to:50]; 104 | assertThat(set, hasCountOf(14)); 105 | set = [set valueForKey:@"muiID"]; 106 | assertThat(set, containsInAnyOrder(@"10", @"40", @"50", @"1030", @"1040", @"1050", @"2020", @"2030", @"2040", @"3010", @"3020", @"3030", @"4010", @"4020", nil)); 107 | 108 | [bucket removeModels:[bucket showingModelsFrom:0 to:40]]; 109 | set = [bucket showingModelsFrom:0 to:60]; 110 | assertThat(set, hasCountOf(3)); 111 | set = [set valueForKey:@"muiID"]; 112 | assertThat(set, containsInAnyOrder(@"4010", @"4020", @"5010", nil)); 113 | 114 | [bucket clear]; 115 | assertThat([bucket showingModelsFrom:0 to:50], hasCountOf(0)); 116 | assertThat([bucket showingModelsFrom:0 to:40], hasCountOf(0)); 117 | assertThat([bucket showingModelsFrom:0 to:30], hasCountOf(0)); 118 | assertThat([bucket showingModelsFrom:0 to:20], hasCountOf(0)); 119 | assertThat([bucket showingModelsFrom:0 to:10], hasCountOf(0)); 120 | } 121 | 122 | + (TMLazyItemModel *)createModelHelperWithY:(CGFloat)y height:(CGFloat)height 123 | { 124 | TMLazyItemModel *model = [TMLazyItemModel new]; 125 | model.absRect = CGRectMake(0, y, 10, height); 126 | model.muiID = [NSString stringWithFormat:@"%.0f", y * 100 + height]; 127 | return model; 128 | } 129 | 130 | @end 131 | -------------------------------------------------------------------------------- /LazyScrollViewTest/LazyScrollViewTest.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 53E176C0B4093753A77EF855 /* libPods-LazyScrollViewTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8ED7BF79578DE1BA510C818B /* libPods-LazyScrollViewTest.a */; }; 11 | 92AED9EE2057C57C00E4D744 /* ModelBucketTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 92AED9ED2057C57C00E4D744 /* ModelBucketTest.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 8ED7BF79578DE1BA510C818B /* libPods-LazyScrollViewTest.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-LazyScrollViewTest.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | 92AED9EA2057C57C00E4D744 /* LazyScrollViewTest.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LazyScrollViewTest.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 17 | 92AED9ED2057C57C00E4D744 /* ModelBucketTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ModelBucketTest.m; sourceTree = ""; }; 18 | 92AED9EF2057C57C00E4D744 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 19 | B5607E29605A5026A43D96A6 /* Pods-LazyScrollViewTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LazyScrollViewTest.release.xcconfig"; path = "../Pods/Target Support Files/Pods-LazyScrollViewTest/Pods-LazyScrollViewTest.release.xcconfig"; sourceTree = ""; }; 20 | FCC6B3D1BF91291A7E8BDAF5 /* Pods-LazyScrollViewTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LazyScrollViewTest.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-LazyScrollViewTest/Pods-LazyScrollViewTest.debug.xcconfig"; sourceTree = ""; }; 21 | /* End PBXFileReference section */ 22 | 23 | /* Begin PBXFrameworksBuildPhase section */ 24 | 92AED9E72057C57C00E4D744 /* Frameworks */ = { 25 | isa = PBXFrameworksBuildPhase; 26 | buildActionMask = 2147483647; 27 | files = ( 28 | 53E176C0B4093753A77EF855 /* libPods-LazyScrollViewTest.a in Frameworks */, 29 | ); 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXFrameworksBuildPhase section */ 33 | 34 | /* Begin PBXGroup section */ 35 | 4B38F8836B3305993BD0FDB5 /* Frameworks */ = { 36 | isa = PBXGroup; 37 | children = ( 38 | 8ED7BF79578DE1BA510C818B /* libPods-LazyScrollViewTest.a */, 39 | ); 40 | name = Frameworks; 41 | sourceTree = ""; 42 | }; 43 | 92AED9DF2057C56F00E4D744 = { 44 | isa = PBXGroup; 45 | children = ( 46 | 92AED9EC2057C57C00E4D744 /* LazyScrollViewTest */, 47 | 92AED9EB2057C57C00E4D744 /* Products */, 48 | AB4262A336006D10EBD97FE5 /* Pods */, 49 | 4B38F8836B3305993BD0FDB5 /* Frameworks */, 50 | ); 51 | sourceTree = ""; 52 | }; 53 | 92AED9EB2057C57C00E4D744 /* Products */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | 92AED9EA2057C57C00E4D744 /* LazyScrollViewTest.xctest */, 57 | ); 58 | name = Products; 59 | sourceTree = ""; 60 | }; 61 | 92AED9EC2057C57C00E4D744 /* LazyScrollViewTest */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 92AED9ED2057C57C00E4D744 /* ModelBucketTest.m */, 65 | 92AED9EF2057C57C00E4D744 /* Info.plist */, 66 | ); 67 | path = LazyScrollViewTest; 68 | sourceTree = ""; 69 | }; 70 | AB4262A336006D10EBD97FE5 /* Pods */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | FCC6B3D1BF91291A7E8BDAF5 /* Pods-LazyScrollViewTest.debug.xcconfig */, 74 | B5607E29605A5026A43D96A6 /* Pods-LazyScrollViewTest.release.xcconfig */, 75 | ); 76 | name = Pods; 77 | sourceTree = ""; 78 | }; 79 | /* End PBXGroup section */ 80 | 81 | /* Begin PBXNativeTarget section */ 82 | 92AED9E92057C57C00E4D744 /* LazyScrollViewTest */ = { 83 | isa = PBXNativeTarget; 84 | buildConfigurationList = 92AED9F02057C57C00E4D744 /* Build configuration list for PBXNativeTarget "LazyScrollViewTest" */; 85 | buildPhases = ( 86 | B8A103919F72E186012F8A7C /* [CP] Check Pods Manifest.lock */, 87 | 92AED9E62057C57C00E4D744 /* Sources */, 88 | 92AED9E72057C57C00E4D744 /* Frameworks */, 89 | 92AED9E82057C57C00E4D744 /* Resources */, 90 | 7357D597561B2C39FF121950 /* [CP] Embed Pods Frameworks */, 91 | 212D0C87D1CE5785C8BFFA53 /* [CP] Copy Pods Resources */, 92 | ); 93 | buildRules = ( 94 | ); 95 | dependencies = ( 96 | ); 97 | name = LazyScrollViewTest; 98 | productName = LazyScrollViewTest; 99 | productReference = 92AED9EA2057C57C00E4D744 /* LazyScrollViewTest.xctest */; 100 | productType = "com.apple.product-type.bundle.unit-test"; 101 | }; 102 | /* End PBXNativeTarget section */ 103 | 104 | /* Begin PBXProject section */ 105 | 92AED9E02057C56F00E4D744 /* Project object */ = { 106 | isa = PBXProject; 107 | attributes = { 108 | LastUpgradeCheck = 0920; 109 | TargetAttributes = { 110 | 92AED9E92057C57C00E4D744 = { 111 | CreatedOnToolsVersion = 9.2; 112 | ProvisioningStyle = Automatic; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = 92AED9E32057C56F00E4D744 /* Build configuration list for PBXProject "LazyScrollViewTest" */; 117 | compatibilityVersion = "Xcode 8.0"; 118 | developmentRegion = en; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | ); 123 | mainGroup = 92AED9DF2057C56F00E4D744; 124 | productRefGroup = 92AED9EB2057C57C00E4D744 /* Products */; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | 92AED9E92057C57C00E4D744 /* LazyScrollViewTest */, 129 | ); 130 | }; 131 | /* End PBXProject section */ 132 | 133 | /* Begin PBXResourcesBuildPhase section */ 134 | 92AED9E82057C57C00E4D744 /* Resources */ = { 135 | isa = PBXResourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | /* End PBXResourcesBuildPhase section */ 142 | 143 | /* Begin PBXShellScriptBuildPhase section */ 144 | 212D0C87D1CE5785C8BFFA53 /* [CP] Copy Pods Resources */ = { 145 | isa = PBXShellScriptBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | ); 149 | inputPaths = ( 150 | ); 151 | name = "[CP] Copy Pods Resources"; 152 | outputPaths = ( 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | shellPath = /bin/sh; 156 | shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-LazyScrollViewTest/Pods-LazyScrollViewTest-resources.sh\"\n"; 157 | showEnvVarsInLog = 0; 158 | }; 159 | 7357D597561B2C39FF121950 /* [CP] Embed Pods Frameworks */ = { 160 | isa = PBXShellScriptBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | ); 164 | inputPaths = ( 165 | ); 166 | name = "[CP] Embed Pods Frameworks"; 167 | outputPaths = ( 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | shellPath = /bin/sh; 171 | shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-LazyScrollViewTest/Pods-LazyScrollViewTest-frameworks.sh\"\n"; 172 | showEnvVarsInLog = 0; 173 | }; 174 | B8A103919F72E186012F8A7C /* [CP] Check Pods Manifest.lock */ = { 175 | isa = PBXShellScriptBuildPhase; 176 | buildActionMask = 2147483647; 177 | files = ( 178 | ); 179 | inputPaths = ( 180 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 181 | "${PODS_ROOT}/Manifest.lock", 182 | ); 183 | name = "[CP] Check Pods Manifest.lock"; 184 | outputPaths = ( 185 | "$(DERIVED_FILE_DIR)/Pods-LazyScrollViewTest-checkManifestLockResult.txt", 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | shellPath = /bin/sh; 189 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 190 | showEnvVarsInLog = 0; 191 | }; 192 | /* End PBXShellScriptBuildPhase section */ 193 | 194 | /* Begin PBXSourcesBuildPhase section */ 195 | 92AED9E62057C57C00E4D744 /* Sources */ = { 196 | isa = PBXSourcesBuildPhase; 197 | buildActionMask = 2147483647; 198 | files = ( 199 | 92AED9EE2057C57C00E4D744 /* ModelBucketTest.m in Sources */, 200 | ); 201 | runOnlyForDeploymentPostprocessing = 0; 202 | }; 203 | /* End PBXSourcesBuildPhase section */ 204 | 205 | /* Begin XCBuildConfiguration section */ 206 | 92AED9E42057C56F00E4D744 /* Debug */ = { 207 | isa = XCBuildConfiguration; 208 | buildSettings = { 209 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 210 | }; 211 | name = Debug; 212 | }; 213 | 92AED9E52057C56F00E4D744 /* Release */ = { 214 | isa = XCBuildConfiguration; 215 | buildSettings = { 216 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 217 | }; 218 | name = Release; 219 | }; 220 | 92AED9F12057C57C00E4D744 /* Debug */ = { 221 | isa = XCBuildConfiguration; 222 | baseConfigurationReference = FCC6B3D1BF91291A7E8BDAF5 /* Pods-LazyScrollViewTest.debug.xcconfig */; 223 | buildSettings = { 224 | ALWAYS_SEARCH_USER_PATHS = NO; 225 | CLANG_ANALYZER_NONNULL = YES; 226 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 227 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 228 | CLANG_CXX_LIBRARY = "libc++"; 229 | CLANG_ENABLE_MODULES = YES; 230 | CLANG_ENABLE_OBJC_ARC = YES; 231 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 232 | CLANG_WARN_BOOL_CONVERSION = YES; 233 | CLANG_WARN_COMMA = YES; 234 | CLANG_WARN_CONSTANT_CONVERSION = YES; 235 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 236 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 237 | CLANG_WARN_EMPTY_BODY = YES; 238 | CLANG_WARN_ENUM_CONVERSION = YES; 239 | CLANG_WARN_INFINITE_RECURSION = YES; 240 | CLANG_WARN_INT_CONVERSION = YES; 241 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 242 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 244 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 245 | CLANG_WARN_STRICT_PROTOTYPES = YES; 246 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 247 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 248 | CLANG_WARN_UNREACHABLE_CODE = YES; 249 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 250 | CODE_SIGN_IDENTITY = "iPhone Developer"; 251 | CODE_SIGN_STYLE = Automatic; 252 | COPY_PHASE_STRIP = NO; 253 | DEBUG_INFORMATION_FORMAT = dwarf; 254 | ENABLE_STRICT_OBJC_MSGSEND = YES; 255 | ENABLE_TESTABILITY = YES; 256 | GCC_C_LANGUAGE_STANDARD = gnu11; 257 | GCC_DYNAMIC_NO_PIC = NO; 258 | GCC_NO_COMMON_BLOCKS = YES; 259 | GCC_OPTIMIZATION_LEVEL = 0; 260 | GCC_PREPROCESSOR_DEFINITIONS = ( 261 | "DEBUG=1", 262 | "$(inherited)", 263 | ); 264 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 265 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 266 | GCC_WARN_UNDECLARED_SELECTOR = YES; 267 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 268 | GCC_WARN_UNUSED_FUNCTION = YES; 269 | GCC_WARN_UNUSED_VARIABLE = YES; 270 | INFOPLIST_FILE = LazyScrollViewTest/Info.plist; 271 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 272 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 273 | MTL_ENABLE_DEBUG_INFO = YES; 274 | ONLY_ACTIVE_ARCH = YES; 275 | PRODUCT_BUNDLE_IDENTIFIER = tmall.LazyScrollViewTest; 276 | PRODUCT_NAME = "$(TARGET_NAME)"; 277 | SDKROOT = iphoneos; 278 | TARGETED_DEVICE_FAMILY = "1,2"; 279 | }; 280 | name = Debug; 281 | }; 282 | 92AED9F22057C57C00E4D744 /* Release */ = { 283 | isa = XCBuildConfiguration; 284 | baseConfigurationReference = B5607E29605A5026A43D96A6 /* Pods-LazyScrollViewTest.release.xcconfig */; 285 | buildSettings = { 286 | ALWAYS_SEARCH_USER_PATHS = NO; 287 | CLANG_ANALYZER_NONNULL = YES; 288 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 289 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 290 | CLANG_CXX_LIBRARY = "libc++"; 291 | CLANG_ENABLE_MODULES = YES; 292 | CLANG_ENABLE_OBJC_ARC = YES; 293 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 294 | CLANG_WARN_BOOL_CONVERSION = YES; 295 | CLANG_WARN_COMMA = YES; 296 | CLANG_WARN_CONSTANT_CONVERSION = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 299 | CLANG_WARN_EMPTY_BODY = YES; 300 | CLANG_WARN_ENUM_CONVERSION = YES; 301 | CLANG_WARN_INFINITE_RECURSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 304 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 305 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 306 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 307 | CLANG_WARN_STRICT_PROTOTYPES = YES; 308 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 309 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 310 | CLANG_WARN_UNREACHABLE_CODE = YES; 311 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 312 | CODE_SIGN_IDENTITY = "iPhone Developer"; 313 | CODE_SIGN_STYLE = Automatic; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 316 | ENABLE_NS_ASSERTIONS = NO; 317 | ENABLE_STRICT_OBJC_MSGSEND = YES; 318 | GCC_C_LANGUAGE_STANDARD = gnu11; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 321 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 322 | GCC_WARN_UNDECLARED_SELECTOR = YES; 323 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 324 | GCC_WARN_UNUSED_FUNCTION = YES; 325 | GCC_WARN_UNUSED_VARIABLE = YES; 326 | INFOPLIST_FILE = LazyScrollViewTest/Info.plist; 327 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 328 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 329 | MTL_ENABLE_DEBUG_INFO = NO; 330 | PRODUCT_BUNDLE_IDENTIFIER = tmall.LazyScrollViewTest; 331 | PRODUCT_NAME = "$(TARGET_NAME)"; 332 | SDKROOT = iphoneos; 333 | TARGETED_DEVICE_FAMILY = "1,2"; 334 | VALIDATE_PRODUCT = YES; 335 | }; 336 | name = Release; 337 | }; 338 | /* End XCBuildConfiguration section */ 339 | 340 | /* Begin XCConfigurationList section */ 341 | 92AED9E32057C56F00E4D744 /* Build configuration list for PBXProject "LazyScrollViewTest" */ = { 342 | isa = XCConfigurationList; 343 | buildConfigurations = ( 344 | 92AED9E42057C56F00E4D744 /* Debug */, 345 | 92AED9E52057C56F00E4D744 /* Release */, 346 | ); 347 | defaultConfigurationIsVisible = 0; 348 | defaultConfigurationName = Release; 349 | }; 350 | 92AED9F02057C57C00E4D744 /* Build configuration list for PBXNativeTarget "LazyScrollViewTest" */ = { 351 | isa = XCConfigurationList; 352 | buildConfigurations = ( 353 | 92AED9F12057C57C00E4D744 /* Debug */, 354 | 92AED9F22057C57C00E4D744 /* Release */, 355 | ); 356 | defaultConfigurationIsVisible = 0; 357 | defaultConfigurationName = Release; 358 | }; 359 | /* End XCConfigurationList section */ 360 | }; 361 | rootObject = 92AED9E02057C56F00E4D744 /* Project object */; 362 | } 363 | -------------------------------------------------------------------------------- /LazyScrollView/TMLazyScrollView.m: -------------------------------------------------------------------------------- 1 | // 2 | // TMLazyScrollView.m 3 | // LazyScrollView 4 | // 5 | // Copyright (c) 2015-2018 Alibaba. All rights reserved. 6 | // 7 | 8 | #import "TMLazyScrollView.h" 9 | #import 10 | #import "TMLazyItemViewProtocol.h" 11 | #import "TMLazyReusePool.h" 12 | #import "TMLazyModelBucket.h" 13 | 14 | #define LazyBufferHeight 20 15 | #define LazyBucketHeight 400 16 | void * const LazyObserverContext = "LazyObserverContext"; 17 | 18 | @interface TMLazyOuterScrollViewObserver: NSObject 19 | 20 | @property (nonatomic, weak) TMLazyScrollView *lazyScrollView; 21 | 22 | @end 23 | 24 | //**************************************************************** 25 | 26 | @interface TMLazyScrollView () { 27 | NSMutableSet *_visibleItems; 28 | NSMutableSet *_inScreenVisibleMuiIDs; 29 | 30 | // Store item models. 31 | TMLazyModelBucket *_modelBucket; 32 | NSInteger _itemCount; 33 | 34 | // Store muiID of items which need to be reloaded. 35 | NSMutableSet *_needReloadingMuiIDs; 36 | 37 | // Record current muiID of reloading item. 38 | // Will be used for dequeueReusableItem methods. 39 | NSString *_currentReloadingMuiID; 40 | 41 | // Store muiID of items which should be visible. 42 | NSMutableSet *_newVisibleMuiIDs; 43 | 44 | // Store muiID of items which are visible last time. 45 | NSSet *_lastInScreenVisibleMuiIDs; 46 | 47 | // Store the enter screen times of items. 48 | NSMutableDictionary *_enterTimesDict; 49 | 50 | // Record contentOffset of scrollView that used for calculating 51 | // views to show last time. 52 | CGPoint _lastContentOffset; 53 | } 54 | 55 | @property (nonatomic, strong) TMLazyOuterScrollViewObserver *outerScrollViewObserver; 56 | 57 | - (void)outerScrollViewDidScroll; 58 | 59 | @end 60 | 61 | @implementation TMLazyScrollView 62 | 63 | #pragma mark Getter & Setter 64 | 65 | - (NSSet *)inScreenVisibleItems 66 | { 67 | NSMutableSet * inScreenVisibleItems = [NSMutableSet set]; 68 | for (UIView *view in _visibleItems) { 69 | if ([_inScreenVisibleMuiIDs containsObject:view.muiID]) { 70 | [inScreenVisibleItems addObject:view]; 71 | } 72 | } 73 | return [inScreenVisibleItems copy]; 74 | } 75 | 76 | - (NSSet *)visibleItems 77 | { 78 | return [_visibleItems copy]; 79 | } 80 | 81 | - (void)setDataSource:(id)dataSource 82 | { 83 | if (_dataSource != dataSource) { 84 | if (dataSource == nil || [self isDataSourceValid:dataSource]) { 85 | _dataSource = dataSource; 86 | #ifdef DEBUG 87 | } else { 88 | NSAssert(NO, @"TMLazyScrollView - Invalid dataSource."); 89 | #endif 90 | } 91 | } 92 | } 93 | 94 | - (TMLazyOuterScrollViewObserver *)outerScrollViewObserver 95 | { 96 | if (!_outerScrollViewObserver) { 97 | _outerScrollViewObserver = [TMLazyOuterScrollViewObserver new]; 98 | _outerScrollViewObserver.lazyScrollView = self; 99 | } 100 | return _outerScrollViewObserver; 101 | } 102 | 103 | -(void)setOuterScrollView:(UIScrollView *)outerScrollView 104 | { 105 | if (_outerScrollView != outerScrollView) { 106 | if (_outerScrollView) { 107 | [_outerScrollView removeObserver:self.outerScrollViewObserver forKeyPath:@"contentOffset" context:LazyObserverContext]; 108 | } 109 | if (outerScrollView) { 110 | [outerScrollView addObserver:self.outerScrollViewObserver forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:LazyObserverContext]; 111 | } 112 | _outerScrollView = outerScrollView; 113 | } 114 | } 115 | 116 | #pragma mark Lifecycle 117 | 118 | - (id)initWithFrame:(CGRect)frame 119 | { 120 | if (self = [super initWithFrame:frame]) { 121 | self.clipsToBounds = YES; 122 | self.showsHorizontalScrollIndicator = NO; 123 | self.showsVerticalScrollIndicator = NO; 124 | _autoClearGestures = YES; 125 | _loadAllItemsImmediately = YES; 126 | 127 | _reusePool = [TMLazyReusePool new]; 128 | 129 | _visibleItems = [[NSMutableSet alloc] init]; 130 | 131 | _inScreenVisibleMuiIDs = [NSMutableSet set]; 132 | 133 | _modelBucket = [[TMLazyModelBucket alloc] initWithBucketHeight:LazyBucketHeight]; 134 | 135 | _needReloadingMuiIDs = [[NSMutableSet alloc] init]; 136 | 137 | _enterTimesDict = [[NSMutableDictionary alloc] init]; 138 | } 139 | return self; 140 | } 141 | 142 | - (void)dealloc 143 | { 144 | self.dataSource = nil; 145 | self.delegate = nil; 146 | self.outerScrollView = nil; 147 | } 148 | 149 | #pragma mark ScrollEvent 150 | 151 | - (void)setContentOffset:(CGPoint)contentOffset 152 | { 153 | [super setContentOffset:contentOffset]; 154 | if (LazyBufferHeight < ABS(contentOffset.y - _lastContentOffset.y)) { 155 | _lastContentOffset = self.contentOffset; 156 | [self assembleSubviews:NO]; 157 | } 158 | } 159 | 160 | - (void)outerScrollViewDidScroll 161 | { 162 | if (self.outerScrollView) { 163 | CGPoint contentOffset = [self.outerScrollView convertPoint:self.outerScrollView.contentOffset toView:self]; 164 | if (LazyBufferHeight < ABS(contentOffset.y - _lastContentOffset.y)) { 165 | _lastContentOffset = contentOffset; 166 | [self assembleSubviews:NO]; 167 | } 168 | } 169 | } 170 | 171 | - (void)layoutSubviews 172 | { 173 | [super layoutSubviews]; 174 | [self outerScrollViewDidScroll]; 175 | } 176 | 177 | #pragma mark CoreLogic 178 | 179 | - (void)assembleSubviews:(BOOL)isReload 180 | { 181 | if (self.outerScrollView) { 182 | CGRect frame = [self.superview convertRect:self.frame toView:self.outerScrollView]; 183 | CGRect visibleArea = CGRectIntersection(self.outerScrollView.bounds, frame); 184 | if (visibleArea.size.height > 0) { 185 | CGFloat offsetY = CGRectGetMinY(frame); 186 | CGFloat minY = CGRectGetMinY(visibleArea) - offsetY; 187 | CGFloat maxY = CGRectGetMaxY(visibleArea) - offsetY; 188 | [self assembleSubviews:isReload minY:minY maxY:maxY]; 189 | } else { 190 | [self assembleSubviews:isReload minY:0 maxY:-LazyBufferHeight * 2]; 191 | } 192 | } else { 193 | CGFloat minY = CGRectGetMinY(self.bounds); 194 | CGFloat maxY = CGRectGetMaxY(self.bounds); 195 | [self assembleSubviews:isReload minY:minY maxY:maxY]; 196 | } 197 | } 198 | 199 | - (void)recycleItems:(BOOL)isReload newVisibleMuiIDs:(NSSet *)newVisibleMuiIDs 200 | { 201 | NSSet *visibleItemsCopy = [_visibleItems copy]; 202 | for (UIView *itemView in visibleItemsCopy) { 203 | BOOL isToShow = [newVisibleMuiIDs containsObject:itemView.muiID]; 204 | if (!isToShow) { 205 | // Call didLeave. 206 | if ([itemView respondsToSelector:@selector(mui_didLeave)]){ 207 | [(UIView *)itemView mui_didLeave]; 208 | } 209 | if (itemView.reuseIdentifier.length > 0) { 210 | itemView.hidden = YES; 211 | [self.reusePool addItemView:itemView forReuseIdentifier:itemView.reuseIdentifier]; 212 | [_visibleItems removeObject:itemView]; 213 | } else if(isReload && itemView.muiID) { 214 | [_needReloadingMuiIDs addObject:itemView.muiID]; 215 | } 216 | } else if (isReload && itemView.muiID) { 217 | [_needReloadingMuiIDs addObject:itemView.muiID]; 218 | } 219 | } 220 | } 221 | 222 | - (void)generateItems:(BOOL)isReload 223 | { 224 | if (_newVisibleMuiIDs == nil || _newVisibleMuiIDs.count == 0) { 225 | return; 226 | } 227 | 228 | NSString *muiID = [_newVisibleMuiIDs anyObject]; 229 | BOOL hasLoadAnItem = NO; 230 | 231 | // 1. Item view is not visible. We should create or reuse an item view. 232 | // 2. Item view need to be reloaded. 233 | BOOL isVisible = [self isMuiIdVisible:muiID]; 234 | BOOL needReload = [_needReloadingMuiIDs containsObject:muiID]; 235 | if (isVisible == NO || needReload == YES) { 236 | if (self.dataSource) { 237 | hasLoadAnItem = YES; 238 | 239 | // If you call dequeue method in your dataSource, the currentReloadingMuiID 240 | // will be used for searching the best-matched reusable view. 241 | if (isVisible == YES) { 242 | _currentReloadingMuiID = muiID; 243 | } 244 | UIView *itemView = [self.dataSource scrollView:self itemByMuiID:muiID]; 245 | _currentReloadingMuiID = nil; 246 | 247 | if (itemView) { 248 | // Call afterGetView. 249 | if ([itemView respondsToSelector:@selector(mui_afterGetView)]) { 250 | [(UIView *)itemView mui_afterGetView]; 251 | } 252 | // Show the item view. 253 | itemView.muiID = muiID; 254 | itemView.hidden = NO; 255 | if (self.autoAddSubview) { 256 | if (itemView.superview != self) { 257 | [self addSubview:itemView]; 258 | } 259 | } 260 | // Add item view to visibleItems. 261 | if (isVisible == NO) { 262 | [_visibleItems addObject:itemView]; 263 | } 264 | } 265 | 266 | [_needReloadingMuiIDs removeObject:muiID]; 267 | } 268 | } 269 | 270 | // Call didEnterWithTimes. 271 | // didEnterWithTimes will only be called when item view enter the in screen 272 | // visible area, so we have to write the logic at here. 273 | if ([_lastInScreenVisibleMuiIDs containsObject:muiID] == NO 274 | && [_inScreenVisibleMuiIDs containsObject:muiID] == YES) { 275 | for (UIView *itemView in _visibleItems) { 276 | if ([itemView.muiID isEqualToString:muiID]) { 277 | if ([itemView respondsToSelector:@selector(mui_didEnterWithTimes:)]) { 278 | NSInteger times = [_enterTimesDict tm_integerForKey:itemView.muiID]; 279 | times++; 280 | [_enterTimesDict tm_safeSetObject:@(times) forKey:itemView.muiID]; 281 | [(UIView *)itemView mui_didEnterWithTimes:times]; 282 | } 283 | break; 284 | } 285 | } 286 | } 287 | 288 | [_newVisibleMuiIDs removeObject:muiID]; 289 | if (_newVisibleMuiIDs.count > 0) { 290 | if (isReload == YES || self.loadAllItemsImmediately == YES || hasLoadAnItem == NO) { 291 | [self generateItems:isReload]; 292 | } else { 293 | [self performSelector:@selector(generateItems:) 294 | withObject:@(isReload) 295 | afterDelay:0.0000001 296 | inModes:@[NSRunLoopCommonModes]]; 297 | } 298 | } 299 | } 300 | 301 | - (void)assembleSubviews:(BOOL)isReload minY:(CGFloat)minY maxY:(CGFloat)maxY 302 | { 303 | // Calculate which item views should be shown. 304 | // Calculating will cost some time, so here is a buffer for reducing 305 | // times of calculating. 306 | NSSet *newVisibleModels = [_modelBucket showingModelsFrom:minY - LazyBufferHeight 307 | to:maxY + LazyBufferHeight]; 308 | NSSet *newVisibleMuiIDs = [newVisibleModels valueForKey:@"muiID"]; 309 | 310 | // Find if item views are in visible area. 311 | // Recycle invisible item views. 312 | [self recycleItems:isReload newVisibleMuiIDs:newVisibleMuiIDs]; 313 | 314 | // Calculate the inScreenVisibleModels. 315 | _lastInScreenVisibleMuiIDs = [_inScreenVisibleMuiIDs copy]; 316 | [_inScreenVisibleMuiIDs removeAllObjects]; 317 | for (TMLazyItemModel *itemModel in newVisibleModels) { 318 | if (itemModel.top < maxY && itemModel.bottom > minY) { 319 | [_inScreenVisibleMuiIDs addObject:itemModel.muiID]; 320 | } 321 | } 322 | 323 | // Generate or reload visible item views. 324 | [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(generateItems:) object:@(NO)]; 325 | _newVisibleMuiIDs = [newVisibleMuiIDs mutableCopy]; 326 | [self generateItems:isReload]; 327 | } 328 | 329 | #pragma mark Reload 330 | 331 | - (void)storeItemModelsFromIndex:(NSInteger)startIndex 332 | { 333 | if (startIndex == 0) { 334 | _itemCount = 0; 335 | [_modelBucket clear]; 336 | } 337 | if (self.dataSource) { 338 | _itemCount = [self.dataSource numberOfItemsInScrollView:self]; 339 | for (NSInteger index = startIndex; index < _itemCount; index++) { 340 | TMLazyItemModel *itemModel = [self.dataSource scrollView:self itemModelAtIndex:index]; 341 | if (itemModel.muiID.length == 0) { 342 | itemModel.muiID = [NSString stringWithFormat:@"%zd", index]; 343 | } 344 | [_modelBucket addModel:itemModel]; 345 | } 346 | } 347 | } 348 | 349 | - (void)reloadData 350 | { 351 | [self storeItemModelsFromIndex:0]; 352 | [self assembleSubviews:YES]; 353 | } 354 | 355 | - (void)loadMoreData 356 | { 357 | [self storeItemModelsFromIndex:_itemCount]; 358 | [self assembleSubviews:NO]; 359 | } 360 | 361 | - (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier 362 | { 363 | return [self dequeueReusableItemWithIdentifier:identifier muiID:nil]; 364 | } 365 | 366 | - (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier muiID:(NSString *)muiID 367 | { 368 | UIView *result = nil; 369 | if (_currentReloadingMuiID) { 370 | for (UIView *item in _visibleItems) { 371 | if ([item.muiID isEqualToString:_currentReloadingMuiID] 372 | && [item.reuseIdentifier isEqualToString:identifier]) { 373 | result = item; 374 | break; 375 | } 376 | } 377 | } 378 | if (result == nil) { 379 | result = [self.reusePool dequeueItemViewForReuseIdentifier:identifier andMuiID:muiID]; 380 | } 381 | if (result) { 382 | if (self.autoClearGestures) { 383 | result.gestureRecognizers = nil; 384 | } 385 | if ([result respondsToSelector:@selector(mui_prepareForReuse)]) { 386 | [(UIView *)result mui_prepareForReuse]; 387 | } 388 | } 389 | return result; 390 | } 391 | 392 | #pragma mark Clear & Reset 393 | 394 | - (void)clearVisibleItems:(BOOL)enableRecycle 395 | { 396 | if (enableRecycle) { 397 | for (UIView *itemView in _visibleItems) { 398 | itemView.hidden = YES; 399 | if (itemView.reuseIdentifier.length > 0) { 400 | [self.reusePool addItemView:itemView forReuseIdentifier:itemView.reuseIdentifier]; 401 | } 402 | } 403 | } else { 404 | for (UIView *itemView in _visibleItems) { 405 | [itemView removeFromSuperview]; 406 | } 407 | } 408 | [_visibleItems removeAllObjects]; 409 | } 410 | 411 | - (void)removeAllLayouts 412 | { 413 | [self clearVisibleItems:YES]; 414 | } 415 | 416 | - (void)clearReuseItems 417 | { 418 | for (UIView *itemView in [self.reusePool allItemViews]) { 419 | [itemView removeFromSuperview]; 420 | } 421 | [self.reusePool clear]; 422 | } 423 | 424 | - (void)cleanRecycledView 425 | { 426 | [self clearReuseItems]; 427 | } 428 | 429 | - (void)resetItemsEnterTimes 430 | { 431 | [_enterTimesDict removeAllObjects]; 432 | } 433 | 434 | - (void)resetViewEnterTimes 435 | { 436 | [self resetItemsEnterTimes]; 437 | } 438 | 439 | - (void)resetAll 440 | { 441 | [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(generateItems:) object:@(NO)]; 442 | [self clearVisibleItems:NO]; 443 | [self clearReuseItems]; 444 | [self resetItemsEnterTimes]; 445 | } 446 | 447 | - (void)removeContentOffsetObserver 448 | { 449 | self.outerScrollView = nil; 450 | } 451 | 452 | - (void)reLayout 453 | { 454 | [self reloadData]; 455 | } 456 | 457 | #pragma mark Private 458 | 459 | - (BOOL)isMuiIdVisible:(NSString *)muiID 460 | { 461 | for (UIView *itemView in _visibleItems) { 462 | if ([itemView.muiID isEqualToString:muiID]) { 463 | return YES; 464 | } 465 | } 466 | return NO; 467 | } 468 | 469 | - (BOOL)isDataSourceValid:(id)dataSource 470 | { 471 | return dataSource 472 | && [dataSource respondsToSelector:@selector(numberOfItemsInScrollView:)] 473 | && [dataSource respondsToSelector:@selector(scrollView:itemModelAtIndex:)] 474 | && [dataSource respondsToSelector:@selector(scrollView:itemByMuiID:)]; 475 | } 476 | 477 | @end 478 | 479 | //**************************************************************** 480 | 481 | @implementation TMLazyOuterScrollViewObserver 482 | 483 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 484 | { 485 | if (context == LazyObserverContext && [keyPath isEqualToString:@"contentOffset"] && _lazyScrollView) { 486 | [_lazyScrollView outerScrollViewDidScroll]; 487 | } 488 | } 489 | 490 | @end 491 | -------------------------------------------------------------------------------- /LazyScrollViewDemo/LazyScrollViewDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9247FDBD205A9311004FB14E /* AsyncViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9247FDBC205A9310004FB14E /* AsyncViewController.m */; }; 11 | 927CAE3B2046B37700BD3B19 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 927CAE3A2046B37700BD3B19 /* AppDelegate.m */; }; 12 | 927CAE3E2046B37700BD3B19 /* ReuseViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 927CAE3D2046B37700BD3B19 /* ReuseViewController.m */; }; 13 | 927CAE432046B37700BD3B19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 927CAE422046B37700BD3B19 /* Assets.xcassets */; }; 14 | 927CAE462046B37700BD3B19 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 927CAE442046B37700BD3B19 /* LaunchScreen.storyboard */; }; 15 | 927CAE492046B37700BD3B19 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 927CAE482046B37700BD3B19 /* main.m */; }; 16 | 929CC56A20592B1400C2870B /* MoreViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 929CC56920592B1400C2870B /* MoreViewController.m */; }; 17 | 92F01C4720493C36000983CA /* MainViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 92F01C4620493C36000983CA /* MainViewController.m */; }; 18 | 92F01C4A20493D9C000983CA /* OuterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 92F01C4920493D9C000983CA /* OuterViewController.m */; }; 19 | D1AB44F5D8E0B6975BF3CDF3 /* libPods-LazyScrollViewDemo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CA97F266258FD7B207134C76 /* libPods-LazyScrollViewDemo.a */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 80196663B6BB20F868052CE9 /* Pods-LazyScrollViewDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LazyScrollViewDemo.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-LazyScrollViewDemo/Pods-LazyScrollViewDemo.debug.xcconfig"; sourceTree = ""; }; 24 | 9247FDBB205A9310004FB14E /* AsyncViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AsyncViewController.h; sourceTree = ""; }; 25 | 9247FDBC205A9310004FB14E /* AsyncViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AsyncViewController.m; sourceTree = ""; }; 26 | 927CAE362046B37700BD3B19 /* LazyScrollViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LazyScrollViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 927CAE392046B37700BD3B19 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 28 | 927CAE3A2046B37700BD3B19 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 29 | 927CAE3C2046B37700BD3B19 /* ReuseViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReuseViewController.h; sourceTree = ""; }; 30 | 927CAE3D2046B37700BD3B19 /* ReuseViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReuseViewController.m; sourceTree = ""; }; 31 | 927CAE422046B37700BD3B19 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 32 | 927CAE452046B37700BD3B19 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 33 | 927CAE472046B37700BD3B19 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | 927CAE482046B37700BD3B19 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 35 | 929CC56820592B1400C2870B /* MoreViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MoreViewController.h; sourceTree = ""; }; 36 | 929CC56920592B1400C2870B /* MoreViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MoreViewController.m; sourceTree = ""; }; 37 | 92F01C4520493C36000983CA /* MainViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainViewController.h; sourceTree = ""; }; 38 | 92F01C4620493C36000983CA /* MainViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainViewController.m; sourceTree = ""; }; 39 | 92F01C4820493D9C000983CA /* OuterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OuterViewController.h; sourceTree = ""; }; 40 | 92F01C4920493D9C000983CA /* OuterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OuterViewController.m; sourceTree = ""; }; 41 | AA72B4C430D44D71DA7D6BD2 /* Pods-LazyScrollViewDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LazyScrollViewDemo.release.xcconfig"; path = "../Pods/Target Support Files/Pods-LazyScrollViewDemo/Pods-LazyScrollViewDemo.release.xcconfig"; sourceTree = ""; }; 42 | CA97F266258FD7B207134C76 /* libPods-LazyScrollViewDemo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-LazyScrollViewDemo.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | 927CAE332046B37700BD3B19 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | D1AB44F5D8E0B6975BF3CDF3 /* libPods-LazyScrollViewDemo.a in Frameworks */, 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 54053CC9EA1F39E5076BC69F /* Pods */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 80196663B6BB20F868052CE9 /* Pods-LazyScrollViewDemo.debug.xcconfig */, 61 | AA72B4C430D44D71DA7D6BD2 /* Pods-LazyScrollViewDemo.release.xcconfig */, 62 | ); 63 | name = Pods; 64 | sourceTree = ""; 65 | }; 66 | 927CAE2D2046B37700BD3B19 = { 67 | isa = PBXGroup; 68 | children = ( 69 | 927CAE382046B37700BD3B19 /* LazyScrollViewDemo */, 70 | 927CAE372046B37700BD3B19 /* Products */, 71 | 54053CC9EA1F39E5076BC69F /* Pods */, 72 | A9F9DA53D9F091F220CF1FF4 /* Frameworks */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | 927CAE372046B37700BD3B19 /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 927CAE362046B37700BD3B19 /* LazyScrollViewDemo.app */, 80 | ); 81 | name = Products; 82 | sourceTree = ""; 83 | }; 84 | 927CAE382046B37700BD3B19 /* LazyScrollViewDemo */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 927CAE392046B37700BD3B19 /* AppDelegate.h */, 88 | 927CAE3A2046B37700BD3B19 /* AppDelegate.m */, 89 | 92F01C4520493C36000983CA /* MainViewController.h */, 90 | 92F01C4620493C36000983CA /* MainViewController.m */, 91 | 927CAE3C2046B37700BD3B19 /* ReuseViewController.h */, 92 | 927CAE3D2046B37700BD3B19 /* ReuseViewController.m */, 93 | 92F01C4820493D9C000983CA /* OuterViewController.h */, 94 | 92F01C4920493D9C000983CA /* OuterViewController.m */, 95 | 929CC56820592B1400C2870B /* MoreViewController.h */, 96 | 929CC56920592B1400C2870B /* MoreViewController.m */, 97 | 9247FDBB205A9310004FB14E /* AsyncViewController.h */, 98 | 9247FDBC205A9310004FB14E /* AsyncViewController.m */, 99 | 927CAE422046B37700BD3B19 /* Assets.xcassets */, 100 | 927CAE442046B37700BD3B19 /* LaunchScreen.storyboard */, 101 | 927CAE472046B37700BD3B19 /* Info.plist */, 102 | 927CAE482046B37700BD3B19 /* main.m */, 103 | ); 104 | path = LazyScrollViewDemo; 105 | sourceTree = ""; 106 | }; 107 | A9F9DA53D9F091F220CF1FF4 /* Frameworks */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | CA97F266258FD7B207134C76 /* libPods-LazyScrollViewDemo.a */, 111 | ); 112 | name = Frameworks; 113 | sourceTree = ""; 114 | }; 115 | /* End PBXGroup section */ 116 | 117 | /* Begin PBXNativeTarget section */ 118 | 927CAE352046B37700BD3B19 /* LazyScrollViewDemo */ = { 119 | isa = PBXNativeTarget; 120 | buildConfigurationList = 927CAE4C2046B37700BD3B19 /* Build configuration list for PBXNativeTarget "LazyScrollViewDemo" */; 121 | buildPhases = ( 122 | A810AF92003E824DBE229A21 /* [CP] Check Pods Manifest.lock */, 123 | 927CAE322046B37700BD3B19 /* Sources */, 124 | 927CAE332046B37700BD3B19 /* Frameworks */, 125 | 927CAE342046B37700BD3B19 /* Resources */, 126 | 2C0A0C8EE3C249225702BEF4 /* [CP] Embed Pods Frameworks */, 127 | 3B42DAA9E89AC5058947BA15 /* [CP] Copy Pods Resources */, 128 | ); 129 | buildRules = ( 130 | ); 131 | dependencies = ( 132 | ); 133 | name = LazyScrollViewDemo; 134 | productName = LazyScrollViewDemo; 135 | productReference = 927CAE362046B37700BD3B19 /* LazyScrollViewDemo.app */; 136 | productType = "com.apple.product-type.application"; 137 | }; 138 | /* End PBXNativeTarget section */ 139 | 140 | /* Begin PBXProject section */ 141 | 927CAE2E2046B37700BD3B19 /* Project object */ = { 142 | isa = PBXProject; 143 | attributes = { 144 | LastUpgradeCheck = 0920; 145 | ORGANIZATIONNAME = tmall; 146 | TargetAttributes = { 147 | 927CAE352046B37700BD3B19 = { 148 | CreatedOnToolsVersion = 9.2; 149 | ProvisioningStyle = Automatic; 150 | }; 151 | }; 152 | }; 153 | buildConfigurationList = 927CAE312046B37700BD3B19 /* Build configuration list for PBXProject "LazyScrollViewDemo" */; 154 | compatibilityVersion = "Xcode 8.0"; 155 | developmentRegion = en; 156 | hasScannedForEncodings = 0; 157 | knownRegions = ( 158 | en, 159 | Base, 160 | ); 161 | mainGroup = 927CAE2D2046B37700BD3B19; 162 | productRefGroup = 927CAE372046B37700BD3B19 /* Products */; 163 | projectDirPath = ""; 164 | projectRoot = ""; 165 | targets = ( 166 | 927CAE352046B37700BD3B19 /* LazyScrollViewDemo */, 167 | ); 168 | }; 169 | /* End PBXProject section */ 170 | 171 | /* Begin PBXResourcesBuildPhase section */ 172 | 927CAE342046B37700BD3B19 /* Resources */ = { 173 | isa = PBXResourcesBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | 927CAE462046B37700BD3B19 /* LaunchScreen.storyboard in Resources */, 177 | 927CAE432046B37700BD3B19 /* Assets.xcassets in Resources */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | /* End PBXResourcesBuildPhase section */ 182 | 183 | /* Begin PBXShellScriptBuildPhase section */ 184 | 2C0A0C8EE3C249225702BEF4 /* [CP] Embed Pods Frameworks */ = { 185 | isa = PBXShellScriptBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | ); 189 | inputPaths = ( 190 | ); 191 | name = "[CP] Embed Pods Frameworks"; 192 | outputPaths = ( 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | shellPath = /bin/sh; 196 | shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-LazyScrollViewDemo/Pods-LazyScrollViewDemo-frameworks.sh\"\n"; 197 | showEnvVarsInLog = 0; 198 | }; 199 | 3B42DAA9E89AC5058947BA15 /* [CP] Copy Pods Resources */ = { 200 | isa = PBXShellScriptBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | ); 204 | inputPaths = ( 205 | ); 206 | name = "[CP] Copy Pods Resources"; 207 | outputPaths = ( 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | shellPath = /bin/sh; 211 | shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-LazyScrollViewDemo/Pods-LazyScrollViewDemo-resources.sh\"\n"; 212 | showEnvVarsInLog = 0; 213 | }; 214 | A810AF92003E824DBE229A21 /* [CP] Check Pods Manifest.lock */ = { 215 | isa = PBXShellScriptBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | ); 219 | inputPaths = ( 220 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 221 | "${PODS_ROOT}/Manifest.lock", 222 | ); 223 | name = "[CP] Check Pods Manifest.lock"; 224 | outputPaths = ( 225 | "$(DERIVED_FILE_DIR)/Pods-LazyScrollViewDemo-checkManifestLockResult.txt", 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | shellPath = /bin/sh; 229 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 230 | showEnvVarsInLog = 0; 231 | }; 232 | /* End PBXShellScriptBuildPhase section */ 233 | 234 | /* Begin PBXSourcesBuildPhase section */ 235 | 927CAE322046B37700BD3B19 /* Sources */ = { 236 | isa = PBXSourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | 927CAE3E2046B37700BD3B19 /* ReuseViewController.m in Sources */, 240 | 929CC56A20592B1400C2870B /* MoreViewController.m in Sources */, 241 | 927CAE492046B37700BD3B19 /* main.m in Sources */, 242 | 92F01C4A20493D9C000983CA /* OuterViewController.m in Sources */, 243 | 927CAE3B2046B37700BD3B19 /* AppDelegate.m in Sources */, 244 | 92F01C4720493C36000983CA /* MainViewController.m in Sources */, 245 | 9247FDBD205A9311004FB14E /* AsyncViewController.m in Sources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXSourcesBuildPhase section */ 250 | 251 | /* Begin PBXVariantGroup section */ 252 | 927CAE442046B37700BD3B19 /* LaunchScreen.storyboard */ = { 253 | isa = PBXVariantGroup; 254 | children = ( 255 | 927CAE452046B37700BD3B19 /* Base */, 256 | ); 257 | name = LaunchScreen.storyboard; 258 | sourceTree = ""; 259 | }; 260 | /* End PBXVariantGroup section */ 261 | 262 | /* Begin XCBuildConfiguration section */ 263 | 927CAE4A2046B37700BD3B19 /* Debug */ = { 264 | isa = XCBuildConfiguration; 265 | buildSettings = { 266 | ALWAYS_SEARCH_USER_PATHS = NO; 267 | CLANG_ANALYZER_NONNULL = YES; 268 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 269 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 270 | CLANG_CXX_LIBRARY = "libc++"; 271 | CLANG_ENABLE_MODULES = YES; 272 | CLANG_ENABLE_OBJC_ARC = YES; 273 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 274 | CLANG_WARN_BOOL_CONVERSION = YES; 275 | CLANG_WARN_COMMA = YES; 276 | CLANG_WARN_CONSTANT_CONVERSION = YES; 277 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 278 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 279 | CLANG_WARN_EMPTY_BODY = YES; 280 | CLANG_WARN_ENUM_CONVERSION = YES; 281 | CLANG_WARN_INFINITE_RECURSION = YES; 282 | CLANG_WARN_INT_CONVERSION = YES; 283 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 285 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 286 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 287 | CLANG_WARN_STRICT_PROTOTYPES = YES; 288 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 289 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 290 | CLANG_WARN_UNREACHABLE_CODE = YES; 291 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 292 | CODE_SIGN_IDENTITY = "iPhone Developer"; 293 | COPY_PHASE_STRIP = NO; 294 | DEBUG_INFORMATION_FORMAT = dwarf; 295 | ENABLE_STRICT_OBJC_MSGSEND = YES; 296 | ENABLE_TESTABILITY = YES; 297 | GCC_C_LANGUAGE_STANDARD = gnu11; 298 | GCC_DYNAMIC_NO_PIC = NO; 299 | GCC_NO_COMMON_BLOCKS = YES; 300 | GCC_OPTIMIZATION_LEVEL = 0; 301 | GCC_PREPROCESSOR_DEFINITIONS = ( 302 | "DEBUG=1", 303 | "$(inherited)", 304 | ); 305 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 306 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 307 | GCC_WARN_UNDECLARED_SELECTOR = YES; 308 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 309 | GCC_WARN_UNUSED_FUNCTION = YES; 310 | GCC_WARN_UNUSED_VARIABLE = YES; 311 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 312 | MTL_ENABLE_DEBUG_INFO = YES; 313 | ONLY_ACTIVE_ARCH = YES; 314 | SDKROOT = iphoneos; 315 | }; 316 | name = Debug; 317 | }; 318 | 927CAE4B2046B37700BD3B19 /* Release */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ALWAYS_SEARCH_USER_PATHS = NO; 322 | CLANG_ANALYZER_NONNULL = YES; 323 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 324 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 325 | CLANG_CXX_LIBRARY = "libc++"; 326 | CLANG_ENABLE_MODULES = YES; 327 | CLANG_ENABLE_OBJC_ARC = YES; 328 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 329 | CLANG_WARN_BOOL_CONVERSION = YES; 330 | CLANG_WARN_COMMA = YES; 331 | CLANG_WARN_CONSTANT_CONVERSION = YES; 332 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 333 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 334 | CLANG_WARN_EMPTY_BODY = YES; 335 | CLANG_WARN_ENUM_CONVERSION = YES; 336 | CLANG_WARN_INFINITE_RECURSION = YES; 337 | CLANG_WARN_INT_CONVERSION = YES; 338 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 339 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 341 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 342 | CLANG_WARN_STRICT_PROTOTYPES = YES; 343 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 344 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 345 | CLANG_WARN_UNREACHABLE_CODE = YES; 346 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 347 | CODE_SIGN_IDENTITY = "iPhone Developer"; 348 | COPY_PHASE_STRIP = NO; 349 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 350 | ENABLE_NS_ASSERTIONS = NO; 351 | ENABLE_STRICT_OBJC_MSGSEND = YES; 352 | GCC_C_LANGUAGE_STANDARD = gnu11; 353 | GCC_NO_COMMON_BLOCKS = YES; 354 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 355 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 356 | GCC_WARN_UNDECLARED_SELECTOR = YES; 357 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 358 | GCC_WARN_UNUSED_FUNCTION = YES; 359 | GCC_WARN_UNUSED_VARIABLE = YES; 360 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 361 | MTL_ENABLE_DEBUG_INFO = NO; 362 | SDKROOT = iphoneos; 363 | VALIDATE_PRODUCT = YES; 364 | }; 365 | name = Release; 366 | }; 367 | 927CAE4D2046B37700BD3B19 /* Debug */ = { 368 | isa = XCBuildConfiguration; 369 | baseConfigurationReference = 80196663B6BB20F868052CE9 /* Pods-LazyScrollViewDemo.debug.xcconfig */; 370 | buildSettings = { 371 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 372 | CODE_SIGN_STYLE = Automatic; 373 | INFOPLIST_FILE = LazyScrollViewDemo/Info.plist; 374 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 375 | PRODUCT_BUNDLE_IDENTIFIER = tmall.LazyScrollViewDemo; 376 | PRODUCT_NAME = "$(TARGET_NAME)"; 377 | TARGETED_DEVICE_FAMILY = 1; 378 | }; 379 | name = Debug; 380 | }; 381 | 927CAE4E2046B37700BD3B19 /* Release */ = { 382 | isa = XCBuildConfiguration; 383 | baseConfigurationReference = AA72B4C430D44D71DA7D6BD2 /* Pods-LazyScrollViewDemo.release.xcconfig */; 384 | buildSettings = { 385 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 386 | CODE_SIGN_STYLE = Automatic; 387 | INFOPLIST_FILE = LazyScrollViewDemo/Info.plist; 388 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 389 | PRODUCT_BUNDLE_IDENTIFIER = tmall.LazyScrollViewDemo; 390 | PRODUCT_NAME = "$(TARGET_NAME)"; 391 | TARGETED_DEVICE_FAMILY = 1; 392 | }; 393 | name = Release; 394 | }; 395 | /* End XCBuildConfiguration section */ 396 | 397 | /* Begin XCConfigurationList section */ 398 | 927CAE312046B37700BD3B19 /* Build configuration list for PBXProject "LazyScrollViewDemo" */ = { 399 | isa = XCConfigurationList; 400 | buildConfigurations = ( 401 | 927CAE4A2046B37700BD3B19 /* Debug */, 402 | 927CAE4B2046B37700BD3B19 /* Release */, 403 | ); 404 | defaultConfigurationIsVisible = 0; 405 | defaultConfigurationName = Release; 406 | }; 407 | 927CAE4C2046B37700BD3B19 /* Build configuration list for PBXNativeTarget "LazyScrollViewDemo" */ = { 408 | isa = XCConfigurationList; 409 | buildConfigurations = ( 410 | 927CAE4D2046B37700BD3B19 /* Debug */, 411 | 927CAE4E2046B37700BD3B19 /* Release */, 412 | ); 413 | defaultConfigurationIsVisible = 0; 414 | defaultConfigurationName = Release; 415 | }; 416 | /* End XCConfigurationList section */ 417 | }; 418 | rootObject = 927CAE2E2046B37700BD3B19 /* Project object */; 419 | } 420 | --------------------------------------------------------------------------------