├── .gitignore ├── Classes ├── LazyScrollView.h └── LazyScrollView.m ├── LICENSE ├── LazyScrollView.podspec ├── LazyScrollView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ ├── Tsing.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ └── history.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ ├── Tsing.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ ├── LazyScrollView.xcscheme │ │ └── xcschememanagement.plist │ └── history.xcuserdatad │ └── xcschemes │ ├── LazyScrollView.xcscheme │ └── xcschememanagement.plist ├── LazyScrollView ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── SingleView.h ├── SingleView.m ├── ViewController.h ├── ViewController.m └── main.m └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/screenshots 54 | 55 | #Code Injection 56 | # 57 | # After new code Injection tools there's a generated folder /iOSInjectionProject 58 | # https://github.com/johnno1962/injectionforxcode 59 | 60 | iOSInjectionProject/ 61 | -------------------------------------------------------------------------------- /Classes/LazyScrollView.h: -------------------------------------------------------------------------------- 1 | // 2 | // LazyScrollView.h 3 | // LazyScrollView 4 | // 5 | // Created by Zhang Qing on 16/11/30. 6 | // Copyright © 2016年 Tsing. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class LSVRectModel; 12 | @class LazyScrollView; 13 | 14 | typedef NS_ENUM(NSUInteger, LazyScrollViewDirection) { 15 | LazyScrollViewDirectionHorizontal, 16 | LazyScrollViewDirectionVertical, 17 | }; 18 | 19 | @protocol LazyScrollViewDataSource 20 | @required 21 | // ScrollView一共展示多少个item 22 | - (NSUInteger)numberOfItemInScrollView:(LazyScrollView *)scrollView; 23 | // 要求根据index直接返回RectModel 24 | - (LSVRectModel *)scrollView:(LazyScrollView *)scrollView rectModelAtIndex:(NSUInteger)index; 25 | // 返回下标所对应的view 26 | - (UIView *)scrollView:(LazyScrollView *)scrollView itemByLsvId:(NSString *)lsvId; 27 | 28 | @end 29 | 30 | @protocol LazyScrollViewDelegate 31 | 32 | @optional 33 | - (void)scrollView:(LazyScrollView *)scrollView didClickItemAtLsvId:(NSString *)lsvId; 34 | 35 | @end 36 | 37 | @interface LazyScrollView : UIScrollView 38 | @property (nonatomic, weak) id dataSource; 39 | @property (nonatomic, weak) id delegate; 40 | /** 41 | * 滚动方向 42 | * 暂时只支持 `LazyScrollViewDirectionVertical` 43 | */ 44 | //@property (nonatomic, assign) LazyScrollViewDirection direction; 45 | - (void)reloadData; 46 | - (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier; 47 | - (void)registerClass:(Class)viewClass forViewReuseIdentifier:(NSString *)identifier; 48 | @end 49 | 50 | 51 | 52 | @interface UIView (LSV) 53 | // 索引过的标识,在LazyScrollView范围内唯一 54 | @property (nonatomic, copy) NSString *lsvId; 55 | // 重用的ID 56 | @property (nonatomic, copy) NSString *reuseIdentifier; 57 | @end 58 | 59 | @interface LSVRectModel : NSObject 60 | // 转换后的绝对值rect 61 | @property (nonatomic, assign) CGRect absRect; 62 | // 业务下标 63 | @property (nonatomic, copy) NSString *lsvId; 64 | + (instancetype)modelWithRect:(CGRect)rect lsvId:(NSString *)lsvId; 65 | @end 66 | 67 | 68 | -------------------------------------------------------------------------------- /Classes/LazyScrollView.m: -------------------------------------------------------------------------------- 1 | // 2 | // LazyScrollView.m 3 | // LazyScrollView 4 | // 5 | // Created by Zhang Qing on 16/11/30. 6 | // Copyright © 2016年 Tsing. All rights reserved. 7 | // 8 | 9 | #import "LazyScrollView.h" 10 | #import 11 | 12 | static CGFloat const kBufferSize = 20; 13 | 14 | @interface LazyScrollView () 15 | { 16 | 17 | } 18 | @property (nonatomic, strong) NSMutableDictionary *reuseViews; 19 | @property (nonatomic, strong) NSMutableSet<__kindof UIView *> *visibleViews; 20 | @property (nonatomic, strong) NSMutableArray *allRects; 21 | @property (nonatomic, assign) NSUInteger numberOfItems; 22 | @property (nonatomic, strong) NSMutableDictionary *registerClass; 23 | @end 24 | 25 | @implementation LazyScrollView 26 | @dynamic delegate; 27 | 28 | - (void)setDataSource:(id)dataSource { 29 | if (dataSource != _dataSource) { 30 | _dataSource = dataSource; 31 | } 32 | if (_dataSource) { 33 | [self reloadData]; 34 | } 35 | } 36 | 37 | - (void)layoutSubviews { 38 | [super layoutSubviews]; 39 | // TOOD: 此处算法待优化 40 | NSMutableArray *newVisibleViews = [self visiableViewModels].mutableCopy; 41 | NSMutableArray *newVisibleLsvIds = [newVisibleViews valueForKey:@"lsvId"]; 42 | NSMutableArray *removeViews = [NSMutableArray array]; 43 | for (UIView *view in self.visibleViews) { 44 | if (![newVisibleLsvIds containsObject:view.lsvId]) { 45 | [removeViews addObject:view]; 46 | } 47 | } 48 | for (UIView *view in removeViews) { 49 | [self.visibleViews removeObject:view]; 50 | [self enqueueReusableView:view]; 51 | [view removeFromSuperview]; 52 | } 53 | 54 | NSMutableArray *alreadyVisibles = [self.visibleViews valueForKey:@"lsvId"]; 55 | 56 | for (LSVRectModel *model in newVisibleViews) { 57 | if ([alreadyVisibles containsObject:model.lsvId]) { 58 | continue; 59 | } 60 | UIView *view = [self.dataSource scrollView:self itemByLsvId:model.lsvId]; 61 | view.frame = model.absRect; 62 | view.lsvId = model.lsvId; 63 | 64 | [self.visibleViews addObject:view]; 65 | [self addSubview:view]; 66 | } 67 | 68 | } 69 | 70 | - (void)reloadData { 71 | 72 | [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; 73 | [self.visibleViews removeAllObjects]; 74 | 75 | [self updateAllRects]; 76 | } 77 | 78 | - (void)enqueueReusableView:(UIView *)view { 79 | if (!view.reuseIdentifier) { 80 | return; 81 | } 82 | NSString *identifier = view.reuseIdentifier; 83 | NSMutableSet *reuseSet = self.reuseViews[identifier]; 84 | if (!reuseSet) { 85 | reuseSet = [NSMutableSet set]; 86 | [self.reuseViews setValue:reuseSet forKey:identifier]; 87 | } 88 | [reuseSet addObject:view]; 89 | } 90 | - (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier { 91 | if (!identifier) { 92 | return nil; 93 | } 94 | NSMutableSet *reuseSet = self.reuseViews[identifier]; 95 | UIView *view = [reuseSet anyObject]; 96 | if (view) { 97 | [reuseSet removeObject:view]; 98 | return view; 99 | } 100 | else { 101 | Class viewClass = [self.registerClass objectForKey:identifier]; 102 | view = [viewClass new]; 103 | 104 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapItemAction:)]; 105 | [view addGestureRecognizer:tap]; 106 | view.userInteractionEnabled = YES; 107 | 108 | view.reuseIdentifier = identifier; 109 | return view; 110 | } 111 | } 112 | 113 | - (void)registerClass:(Class)viewClass forViewReuseIdentifier:(NSString *)identifier { 114 | [self.registerClass setValue:viewClass forKey:identifier]; 115 | } 116 | 117 | #pragma mark - 118 | - (CGFloat)minEdgeOffset { 119 | CGFloat min = self.contentOffset.y; 120 | return MAX(min - kBufferSize, 0); 121 | } 122 | - (CGFloat)maxEdgeOffset { 123 | CGFloat max = self.contentOffset.y + CGRectGetHeight(self.bounds); 124 | return MIN(max + kBufferSize, self.contentSize.height); 125 | } 126 | - (NSMutableSet *)findSetWithMinEdge:(CGFloat)minEdge { 127 | NSArray *ascendingEdgeArray = 128 | [self.allRects sortedArrayUsingComparator:^NSComparisonResult(LSVRectModel *obj1, LSVRectModel *obj2) { 129 | return CGRectGetMinY(obj1.absRect) > CGRectGetMinY(obj2.absRect) ? NSOrderedDescending : NSOrderedAscending; 130 | }]; 131 | 132 | // TOOD: 此处待优化 133 | // 二分法 134 | NSInteger minIndex = 0; 135 | NSInteger maxIndex = ascendingEdgeArray.count - 1; 136 | NSInteger midIndex = (minIndex + maxIndex) / 2; 137 | LSVRectModel *model = ascendingEdgeArray[midIndex]; 138 | while (minIndex < maxIndex - 1) { 139 | if (CGRectGetMinY(model.absRect) > minEdge) { 140 | maxIndex = midIndex; 141 | } 142 | else { 143 | minIndex = midIndex; 144 | } 145 | midIndex = (minIndex + maxIndex) / 2; 146 | model = ascendingEdgeArray[midIndex]; 147 | } 148 | midIndex = MAX(midIndex - 1, 0); 149 | NSArray *array = [ascendingEdgeArray subarrayWithRange:NSMakeRange(midIndex, ascendingEdgeArray.count - midIndex)]; 150 | return [NSMutableSet setWithArray:array]; 151 | } 152 | - (NSMutableSet *)findSetWithMaxEdge:(CGFloat)maxEdge { 153 | 154 | NSArray *descendingEdgeArray = 155 | [self.allRects sortedArrayUsingComparator:^NSComparisonResult(LSVRectModel *obj1, LSVRectModel *obj2) { 156 | return CGRectGetMaxY(obj1.absRect) < CGRectGetMaxY(obj2.absRect) ? NSOrderedDescending : NSOrderedAscending; 157 | }]; 158 | // TOOD: 此处待优化 159 | // 二分法 160 | NSInteger minIndex = 0; 161 | NSInteger maxIndex = descendingEdgeArray.count - 1; 162 | NSInteger midIndex = (minIndex + maxIndex) / 2; 163 | LSVRectModel *model = descendingEdgeArray[midIndex]; 164 | while (minIndex < maxIndex - 1) { 165 | if (CGRectGetMaxY(model.absRect) < maxEdge) { 166 | maxIndex = midIndex; 167 | } 168 | else { 169 | minIndex = midIndex; 170 | } 171 | midIndex = (minIndex + maxIndex) / 2; 172 | model = descendingEdgeArray[midIndex]; 173 | } 174 | midIndex = MAX(midIndex - 1, 0); 175 | NSArray *array = [descendingEdgeArray subarrayWithRange:NSMakeRange(midIndex, descendingEdgeArray.count - midIndex)]; 176 | return [NSMutableSet setWithArray:array]; 177 | } 178 | - (NSArray *)visiableViewModels { 179 | NSMutableSet *ascendSet = [self findSetWithMinEdge:[self minEdgeOffset]]; 180 | NSMutableSet *descendSet = [self findSetWithMaxEdge:[self maxEdgeOffset]]; 181 | [ascendSet intersectSet:descendSet]; 182 | NSMutableArray *result = [NSMutableArray arrayWithArray:ascendSet.allObjects]; 183 | return result; 184 | } 185 | - (void)updateAllRects { 186 | [self.allRects removeAllObjects]; 187 | _numberOfItems = [self.dataSource numberOfItemInScrollView:self]; 188 | 189 | for (NSInteger index = 0; index < _numberOfItems; ++ index) { 190 | LSVRectModel *model = [self.dataSource scrollView:self rectModelAtIndex:index]; 191 | [self.allRects addObject:model]; 192 | } 193 | LSVRectModel *model = self.allRects.lastObject; 194 | self.contentSize = CGSizeMake(CGRectGetWidth(self.bounds), CGRectGetMaxY(model.absRect)); 195 | } 196 | 197 | - (void)tapItemAction:(UITapGestureRecognizer *)gesture { 198 | if ([self.delegate respondsToSelector:@selector(scrollView:didClickItemAtLsvId:)]) { 199 | UIView *view = [gesture view]; 200 | [self.delegate scrollView:self didClickItemAtLsvId:view.lsvId]; 201 | } 202 | } 203 | 204 | #pragma mark - getter 205 | - (NSMutableDictionary *)reuseViews { 206 | if (!_reuseViews) { 207 | _reuseViews = @{}.mutableCopy; 208 | } 209 | return _reuseViews; 210 | } 211 | - (NSMutableArray *)allRects { 212 | if (!_allRects) { 213 | _allRects = @[].mutableCopy; 214 | } 215 | return _allRects; 216 | } 217 | - (NSMutableDictionary *)registerClass { 218 | if (!_registerClass) { 219 | _registerClass = @{}.mutableCopy; 220 | } 221 | return _registerClass; 222 | } 223 | - (NSMutableSet *)visibleViews { 224 | if (!_visibleViews) { 225 | _visibleViews = [NSMutableSet set]; 226 | } 227 | return _visibleViews; 228 | } 229 | @end 230 | 231 | static char kAssociatedObjectKeylsvId; 232 | static char kAssociatedObjectKeyReuseIdentifier; 233 | 234 | @implementation UIView (LSV) 235 | - (NSString *)lsvId { 236 | return objc_getAssociatedObject(self, &kAssociatedObjectKeylsvId); 237 | } 238 | - (void)setLsvId:(NSString *)lsvId { 239 | objc_setAssociatedObject(self, &kAssociatedObjectKeylsvId, lsvId, OBJC_ASSOCIATION_COPY_NONATOMIC); 240 | } 241 | - (NSString *)reuseIdentifier { 242 | return objc_getAssociatedObject(self, &kAssociatedObjectKeyReuseIdentifier); 243 | } 244 | - (void)setReuseIdentifier:(NSString *)reuseIdentifier { 245 | objc_setAssociatedObject(self, &kAssociatedObjectKeyReuseIdentifier, reuseIdentifier, OBJC_ASSOCIATION_COPY_NONATOMIC); 246 | } 247 | @end 248 | 249 | @implementation LSVRectModel 250 | + (instancetype)modelWithRect:(CGRect)rect lsvId:(NSString *)lsvId { 251 | LSVRectModel *model = [[LSVRectModel alloc] init]; 252 | model.absRect = rect; 253 | model.lsvId = lsvId; 254 | return model; 255 | } 256 | @end 257 | 258 | 259 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 让历史重演 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /LazyScrollView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "LazyScrollView" 3 | s.version = "1.0.0" 4 | s.summary = "iOS 高性能异构滚动视图构建方案." 5 | s.homepage = "https://github.com/HistoryZhang/LazyScrollView" 6 | s.license = { :type => 'MIT License', :file => 'LICENSE' } 7 | s.author = { "TsingZhang" => "history_zq@163.com" } 8 | s.source = { :git => "https://github.com/HistoryZhang/LazyScrollView.git", :tag => "1.0.0" } 9 | s.platform = :ios, '7.0' 10 | s.source_files = 'Classes/*.{h,m}' 11 | s.requires_arc = true 12 | end -------------------------------------------------------------------------------- /LazyScrollView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0301AB7B1DF3B8E600F798AB /* LazyScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0301AB7A1DF3B8E600F798AB /* LazyScrollView.m */; }; 11 | 97A4784D1DEEAD67008395A0 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97A4784C1DEEAD67008395A0 /* main.m */; }; 12 | 97A478501DEEAD67008395A0 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 97A4784F1DEEAD67008395A0 /* AppDelegate.m */; }; 13 | 97A478531DEEAD67008395A0 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97A478521DEEAD67008395A0 /* ViewController.m */; }; 14 | 97A478561DEEAD67008395A0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97A478541DEEAD67008395A0 /* Main.storyboard */; }; 15 | 97A478581DEEAD67008395A0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97A478571DEEAD67008395A0 /* Assets.xcassets */; }; 16 | 97A4785B1DEEAD67008395A0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97A478591DEEAD67008395A0 /* LaunchScreen.storyboard */; }; 17 | 97A478731DF004EA008395A0 /* SingleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 97A478721DF004EA008395A0 /* SingleView.m */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 0301AB791DF3B8E600F798AB /* LazyScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LazyScrollView.h; sourceTree = ""; }; 22 | 0301AB7A1DF3B8E600F798AB /* LazyScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LazyScrollView.m; sourceTree = ""; }; 23 | 97A478481DEEAD67008395A0 /* LazyScrollView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LazyScrollView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | 97A4784C1DEEAD67008395A0 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 25 | 97A4784E1DEEAD67008395A0 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 26 | 97A4784F1DEEAD67008395A0 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 27 | 97A478511DEEAD67008395A0 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 28 | 97A478521DEEAD67008395A0 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 29 | 97A478551DEEAD67008395A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 30 | 97A478571DEEAD67008395A0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 31 | 97A4785A1DEEAD67008395A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 32 | 97A4785C1DEEAD67008395A0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 33 | 97A478711DF004EA008395A0 /* SingleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SingleView.h; sourceTree = ""; }; 34 | 97A478721DF004EA008395A0 /* SingleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SingleView.m; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 97A478451DEEAD67008395A0 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 0301AB781DF3B8E600F798AB /* Classes */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 0301AB791DF3B8E600F798AB /* LazyScrollView.h */, 52 | 0301AB7A1DF3B8E600F798AB /* LazyScrollView.m */, 53 | ); 54 | path = Classes; 55 | sourceTree = SOURCE_ROOT; 56 | }; 57 | 97A4783F1DEEAD66008395A0 = { 58 | isa = PBXGroup; 59 | children = ( 60 | 97A4784A1DEEAD67008395A0 /* LazyScrollView */, 61 | 97A478491DEEAD67008395A0 /* Products */, 62 | ); 63 | sourceTree = ""; 64 | }; 65 | 97A478491DEEAD67008395A0 /* Products */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 97A478481DEEAD67008395A0 /* LazyScrollView.app */, 69 | ); 70 | name = Products; 71 | sourceTree = ""; 72 | }; 73 | 97A4784A1DEEAD67008395A0 /* LazyScrollView */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 0301AB781DF3B8E600F798AB /* Classes */, 77 | 97A4784E1DEEAD67008395A0 /* AppDelegate.h */, 78 | 97A4784F1DEEAD67008395A0 /* AppDelegate.m */, 79 | 97A478511DEEAD67008395A0 /* ViewController.h */, 80 | 97A478521DEEAD67008395A0 /* ViewController.m */, 81 | 97A478541DEEAD67008395A0 /* Main.storyboard */, 82 | 97A478571DEEAD67008395A0 /* Assets.xcassets */, 83 | 97A478591DEEAD67008395A0 /* LaunchScreen.storyboard */, 84 | 97A4785C1DEEAD67008395A0 /* Info.plist */, 85 | 97A4784B1DEEAD67008395A0 /* Supporting Files */, 86 | 97A478711DF004EA008395A0 /* SingleView.h */, 87 | 97A478721DF004EA008395A0 /* SingleView.m */, 88 | ); 89 | path = LazyScrollView; 90 | sourceTree = ""; 91 | }; 92 | 97A4784B1DEEAD67008395A0 /* Supporting Files */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 97A4784C1DEEAD67008395A0 /* main.m */, 96 | ); 97 | name = "Supporting Files"; 98 | sourceTree = ""; 99 | }; 100 | /* End PBXGroup section */ 101 | 102 | /* Begin PBXNativeTarget section */ 103 | 97A478471DEEAD67008395A0 /* LazyScrollView */ = { 104 | isa = PBXNativeTarget; 105 | buildConfigurationList = 97A4785F1DEEAD67008395A0 /* Build configuration list for PBXNativeTarget "LazyScrollView" */; 106 | buildPhases = ( 107 | 97A478441DEEAD67008395A0 /* Sources */, 108 | 97A478451DEEAD67008395A0 /* Frameworks */, 109 | 97A478461DEEAD67008395A0 /* Resources */, 110 | ); 111 | buildRules = ( 112 | ); 113 | dependencies = ( 114 | ); 115 | name = LazyScrollView; 116 | productName = LazyScrollView; 117 | productReference = 97A478481DEEAD67008395A0 /* LazyScrollView.app */; 118 | productType = "com.apple.product-type.application"; 119 | }; 120 | /* End PBXNativeTarget section */ 121 | 122 | /* Begin PBXProject section */ 123 | 97A478401DEEAD66008395A0 /* Project object */ = { 124 | isa = PBXProject; 125 | attributes = { 126 | LastUpgradeCheck = 0710; 127 | ORGANIZATIONNAME = Tsing; 128 | TargetAttributes = { 129 | 97A478471DEEAD67008395A0 = { 130 | CreatedOnToolsVersion = 7.1; 131 | DevelopmentTeam = YW6S526SLB; 132 | }; 133 | }; 134 | }; 135 | buildConfigurationList = 97A478431DEEAD66008395A0 /* Build configuration list for PBXProject "LazyScrollView" */; 136 | compatibilityVersion = "Xcode 3.2"; 137 | developmentRegion = English; 138 | hasScannedForEncodings = 0; 139 | knownRegions = ( 140 | en, 141 | Base, 142 | ); 143 | mainGroup = 97A4783F1DEEAD66008395A0; 144 | productRefGroup = 97A478491DEEAD67008395A0 /* Products */; 145 | projectDirPath = ""; 146 | projectRoot = ""; 147 | targets = ( 148 | 97A478471DEEAD67008395A0 /* LazyScrollView */, 149 | ); 150 | }; 151 | /* End PBXProject section */ 152 | 153 | /* Begin PBXResourcesBuildPhase section */ 154 | 97A478461DEEAD67008395A0 /* Resources */ = { 155 | isa = PBXResourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | 97A4785B1DEEAD67008395A0 /* LaunchScreen.storyboard in Resources */, 159 | 97A478581DEEAD67008395A0 /* Assets.xcassets in Resources */, 160 | 97A478561DEEAD67008395A0 /* Main.storyboard in Resources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXResourcesBuildPhase section */ 165 | 166 | /* Begin PBXSourcesBuildPhase section */ 167 | 97A478441DEEAD67008395A0 /* Sources */ = { 168 | isa = PBXSourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | 97A478731DF004EA008395A0 /* SingleView.m in Sources */, 172 | 0301AB7B1DF3B8E600F798AB /* LazyScrollView.m in Sources */, 173 | 97A478531DEEAD67008395A0 /* ViewController.m in Sources */, 174 | 97A478501DEEAD67008395A0 /* AppDelegate.m in Sources */, 175 | 97A4784D1DEEAD67008395A0 /* main.m in Sources */, 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | }; 179 | /* End PBXSourcesBuildPhase section */ 180 | 181 | /* Begin PBXVariantGroup section */ 182 | 97A478541DEEAD67008395A0 /* Main.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | 97A478551DEEAD67008395A0 /* Base */, 186 | ); 187 | name = Main.storyboard; 188 | sourceTree = ""; 189 | }; 190 | 97A478591DEEAD67008395A0 /* LaunchScreen.storyboard */ = { 191 | isa = PBXVariantGroup; 192 | children = ( 193 | 97A4785A1DEEAD67008395A0 /* Base */, 194 | ); 195 | name = LaunchScreen.storyboard; 196 | sourceTree = ""; 197 | }; 198 | /* End PBXVariantGroup section */ 199 | 200 | /* Begin XCBuildConfiguration section */ 201 | 97A4785D1DEEAD67008395A0 /* Debug */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 206 | CLANG_CXX_LIBRARY = "libc++"; 207 | CLANG_ENABLE_MODULES = YES; 208 | CLANG_ENABLE_OBJC_ARC = YES; 209 | CLANG_WARN_BOOL_CONVERSION = YES; 210 | CLANG_WARN_CONSTANT_CONVERSION = YES; 211 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 212 | CLANG_WARN_EMPTY_BODY = YES; 213 | CLANG_WARN_ENUM_CONVERSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 216 | CLANG_WARN_UNREACHABLE_CODE = YES; 217 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 218 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 219 | COPY_PHASE_STRIP = NO; 220 | DEBUG_INFORMATION_FORMAT = dwarf; 221 | ENABLE_STRICT_OBJC_MSGSEND = YES; 222 | ENABLE_TESTABILITY = YES; 223 | GCC_C_LANGUAGE_STANDARD = gnu99; 224 | GCC_DYNAMIC_NO_PIC = NO; 225 | GCC_NO_COMMON_BLOCKS = YES; 226 | GCC_OPTIMIZATION_LEVEL = 0; 227 | GCC_PREPROCESSOR_DEFINITIONS = ( 228 | "DEBUG=1", 229 | "$(inherited)", 230 | ); 231 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 232 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 233 | GCC_WARN_UNDECLARED_SELECTOR = YES; 234 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 235 | GCC_WARN_UNUSED_FUNCTION = YES; 236 | GCC_WARN_UNUSED_VARIABLE = YES; 237 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 238 | MTL_ENABLE_DEBUG_INFO = YES; 239 | ONLY_ACTIVE_ARCH = YES; 240 | SDKROOT = iphoneos; 241 | }; 242 | name = Debug; 243 | }; 244 | 97A4785E1DEEAD67008395A0 /* Release */ = { 245 | isa = XCBuildConfiguration; 246 | buildSettings = { 247 | ALWAYS_SEARCH_USER_PATHS = NO; 248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 249 | CLANG_CXX_LIBRARY = "libc++"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_WARN_BOOL_CONVERSION = YES; 253 | CLANG_WARN_CONSTANT_CONVERSION = YES; 254 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INT_CONVERSION = YES; 258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 259 | CLANG_WARN_UNREACHABLE_CODE = YES; 260 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 261 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 262 | COPY_PHASE_STRIP = NO; 263 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 264 | ENABLE_NS_ASSERTIONS = NO; 265 | ENABLE_STRICT_OBJC_MSGSEND = YES; 266 | GCC_C_LANGUAGE_STANDARD = gnu99; 267 | GCC_NO_COMMON_BLOCKS = YES; 268 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 269 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 270 | GCC_WARN_UNDECLARED_SELECTOR = YES; 271 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 272 | GCC_WARN_UNUSED_FUNCTION = YES; 273 | GCC_WARN_UNUSED_VARIABLE = YES; 274 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 275 | MTL_ENABLE_DEBUG_INFO = NO; 276 | SDKROOT = iphoneos; 277 | VALIDATE_PRODUCT = YES; 278 | }; 279 | name = Release; 280 | }; 281 | 97A478601DEEAD67008395A0 /* Debug */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 285 | DEVELOPMENT_TEAM = YW6S526SLB; 286 | INFOPLIST_FILE = LazyScrollView/Info.plist; 287 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 288 | PRODUCT_BUNDLE_IDENTIFIER = com.tsing.LazyScrollView; 289 | PRODUCT_NAME = "$(TARGET_NAME)"; 290 | }; 291 | name = Debug; 292 | }; 293 | 97A478611DEEAD67008395A0 /* Release */ = { 294 | isa = XCBuildConfiguration; 295 | buildSettings = { 296 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 297 | DEVELOPMENT_TEAM = YW6S526SLB; 298 | INFOPLIST_FILE = LazyScrollView/Info.plist; 299 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 300 | PRODUCT_BUNDLE_IDENTIFIER = com.tsing.LazyScrollView; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | }; 303 | name = Release; 304 | }; 305 | /* End XCBuildConfiguration section */ 306 | 307 | /* Begin XCConfigurationList section */ 308 | 97A478431DEEAD66008395A0 /* Build configuration list for PBXProject "LazyScrollView" */ = { 309 | isa = XCConfigurationList; 310 | buildConfigurations = ( 311 | 97A4785D1DEEAD67008395A0 /* Debug */, 312 | 97A4785E1DEEAD67008395A0 /* Release */, 313 | ); 314 | defaultConfigurationIsVisible = 0; 315 | defaultConfigurationName = Release; 316 | }; 317 | 97A4785F1DEEAD67008395A0 /* Build configuration list for PBXNativeTarget "LazyScrollView" */ = { 318 | isa = XCConfigurationList; 319 | buildConfigurations = ( 320 | 97A478601DEEAD67008395A0 /* Debug */, 321 | 97A478611DEEAD67008395A0 /* Release */, 322 | ); 323 | defaultConfigurationIsVisible = 0; 324 | defaultConfigurationName = Release; 325 | }; 326 | /* End XCConfigurationList section */ 327 | }; 328 | rootObject = 97A478401DEEAD66008395A0 /* Project object */; 329 | } 330 | -------------------------------------------------------------------------------- /LazyScrollView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LazyScrollView.xcodeproj/project.xcworkspace/xcuserdata/Tsing.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KelvinQQ/LazyScrollView/3a5838eed32a34d084ad500341eafcd1408e104d/LazyScrollView.xcodeproj/project.xcworkspace/xcuserdata/Tsing.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /LazyScrollView.xcodeproj/project.xcworkspace/xcuserdata/history.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KelvinQQ/LazyScrollView/3a5838eed32a34d084ad500341eafcd1408e104d/LazyScrollView.xcodeproj/project.xcworkspace/xcuserdata/history.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /LazyScrollView.xcodeproj/xcuserdata/Tsing.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /LazyScrollView.xcodeproj/xcuserdata/Tsing.xcuserdatad/xcschemes/LazyScrollView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /LazyScrollView.xcodeproj/xcuserdata/Tsing.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LazyScrollView.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 97A478471DEEAD67008395A0 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LazyScrollView.xcodeproj/xcuserdata/history.xcuserdatad/xcschemes/LazyScrollView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /LazyScrollView.xcodeproj/xcuserdata/history.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LazyScrollView.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 97A478471DEEAD67008395A0 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LazyScrollView/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // LazyScrollView 4 | // 5 | // Created by Zhang Qing on 16/11/30. 6 | // Copyright © 2016年 Tsing. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /LazyScrollView/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // LazyScrollView 4 | // 5 | // Created by Zhang Qing on 16/11/30. 6 | // Copyright © 2016年 Tsing. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | - (void)applicationWillResignActive:(UIApplication *)application { 24 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 25 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 26 | } 27 | 28 | - (void)applicationDidEnterBackground:(UIApplication *)application { 29 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 31 | } 32 | 33 | - (void)applicationWillEnterForeground:(UIApplication *)application { 34 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 35 | } 36 | 37 | - (void)applicationDidBecomeActive:(UIApplication *)application { 38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 39 | } 40 | 41 | - (void)applicationWillTerminate:(UIApplication *)application { 42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /LazyScrollView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /LazyScrollView/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 | -------------------------------------------------------------------------------- /LazyScrollView/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LazyScrollView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /LazyScrollView/SingleView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SingleView.h 3 | // LazyScrollView 4 | // 5 | // Created by Zhang Qing on 16/12/1. 6 | // Copyright © 2016年 Tsing. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | extern NSString * const kViewIdfSingle; 12 | extern NSString * const kViewIdfSingleOther; 13 | 14 | @interface SingleView : UIView 15 | @property (nonatomic, copy) NSString *data; 16 | @end 17 | -------------------------------------------------------------------------------- /LazyScrollView/SingleView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SingleView.m 3 | // LazyScrollView 4 | // 5 | // Created by Zhang Qing on 16/12/1. 6 | // Copyright © 2016年 Tsing. All rights reserved. 7 | // 8 | 9 | #import "SingleView.h" 10 | 11 | NSString * const kViewIdfSingle = @"kViewIdfSingle"; 12 | NSString * const kViewIdfSingleOther = @"kViewIdfSingleOther"; 13 | 14 | @interface SingleView () 15 | @property (nonatomic, strong) UILabel *title; 16 | @end 17 | 18 | @implementation SingleView 19 | 20 | - (instancetype)initWithFrame:(CGRect)frame { 21 | if (self = [super initWithFrame:frame]) { 22 | [self setupUI]; 23 | } 24 | return self; 25 | } 26 | 27 | - (void)setupUI { 28 | 29 | [self addSubview:self.title]; 30 | 31 | self.title.frame = CGRectMake(0, 0, 100, 50); 32 | } 33 | 34 | - (UIColor *)randomColor { 35 | CGFloat hue = ( arc4random() % 256 / 256.0 ); 36 | CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; 37 | CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; 38 | return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; 39 | } 40 | 41 | #pragma mark - setter 42 | 43 | - (void)setData:(NSString *)data { 44 | _data = data; 45 | self.title.text = data; 46 | self.backgroundColor = [self randomColor]; 47 | } 48 | 49 | #pragma mark - getter 50 | 51 | - (UILabel *)title { 52 | if (!_title) { 53 | _title = [UILabel new]; 54 | _title.font = [UIFont systemFontOfSize:13.f]; 55 | _title.textColor = [UIColor whiteColor]; 56 | } 57 | return _title; 58 | } 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /LazyScrollView/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // LazyScrollView 4 | // 5 | // Created by Zhang Qing on 16/11/30. 6 | // Copyright © 2016年 Tsing. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /LazyScrollView/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // LazyScrollView 4 | // 5 | // Created by Zhang Qing on 16/11/30. 6 | // Copyright © 2016年 Tsing. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "LazyScrollView.h" 11 | #import "SingleView.h" 12 | 13 | @interface ViewController () 14 | @property (strong, nonatomic) LazyScrollView *lazyScrollView; 15 | @property (copy, nonatomic) NSArray *rectDatas; 16 | @property (copy, nonatomic) NSDictionary *viewsData; 17 | @end 18 | 19 | @implementation ViewController 20 | - (void)loadView { 21 | [super loadView]; 22 | 23 | [self loadDatas]; 24 | [self setupUI]; 25 | } 26 | - (void)viewDidLoad { 27 | [super viewDidLoad]; 28 | // Do any additional setup after loading the view, typically from a nib. 29 | } 30 | 31 | - (void)setupUI { 32 | 33 | self.view.backgroundColor = [UIColor whiteColor]; 34 | 35 | [self.view addSubview:self.lazyScrollView]; 36 | self.lazyScrollView.frame = self.view.bounds; 37 | 38 | } 39 | 40 | - (void)didReceiveMemoryWarning { 41 | [super didReceiveMemoryWarning]; 42 | // Dispose of any resources that can be recreated. 43 | } 44 | 45 | - (NSUInteger)numberOfItemInScrollView:(LazyScrollView *)scrollView { 46 | return self.rectDatas.count; 47 | } 48 | 49 | - (LSVRectModel *)scrollView:(LazyScrollView *)scrollView rectModelAtIndex:(NSUInteger)index { 50 | 51 | return self.rectDatas[index]; 52 | } 53 | 54 | - (UIView *)scrollView:(LazyScrollView *)scrollView itemByLsvId:(NSString *)lsvId { 55 | NSInteger index = [[[lsvId componentsSeparatedByString:@"/"] valueForKeyPath:@"@sum.integerValue"] integerValue]; 56 | if (index % 3 == 1 || index % 5 == 2) { 57 | SingleView *view = (SingleView *)[self.lazyScrollView dequeueReusableItemWithIdentifier:kViewIdfSingleOther]; 58 | view.data = [NSString stringWithFormat:@"Single2 - %@", self.viewsData[lsvId]]; 59 | return view; 60 | } 61 | else { 62 | SingleView *view = (SingleView *)[self.lazyScrollView dequeueReusableItemWithIdentifier:kViewIdfSingle]; 63 | view.data = [NSString stringWithFormat:@"Single1 - %@", self.viewsData[lsvId]]; 64 | return view; 65 | } 66 | } 67 | 68 | - (void)loadDatas { 69 | 70 | NSMutableArray *array = @[].mutableCopy; 71 | NSMutableDictionary *dictionary = @{}.mutableCopy; 72 | for (NSInteger index = 0; index < 5000; ++ index) { 73 | NSString *lsvId = [NSString stringWithFormat:@"%@/%@", @(index / 10), @(index % 10)]; 74 | LSVRectModel *model = [LSVRectModel modelWithRect:CGRectMake(10 + (index % 2) * 120, (index / 2) * 120, 110, 110) lsvId:lsvId]; 75 | [array addObject:model]; 76 | [dictionary setObject:lsvId forKey:lsvId]; 77 | } 78 | self.rectDatas = array; 79 | self.viewsData = dictionary; 80 | 81 | } 82 | 83 | - (void)scrollView:(LazyScrollView *)scrollView didClickItemAtLsvId:(NSString *)lsvId { 84 | NSLog(@"%@", lsvId); 85 | } 86 | 87 | #pragma mark - getter 88 | 89 | - (LazyScrollView *)lazyScrollView { 90 | if (!_lazyScrollView) { 91 | _lazyScrollView = [LazyScrollView new]; 92 | _lazyScrollView.dataSource = self; 93 | _lazyScrollView.delegate = self; 94 | [_lazyScrollView registerClass:[SingleView class] forViewReuseIdentifier:kViewIdfSingle]; 95 | [_lazyScrollView registerClass:[SingleView class] forViewReuseIdentifier:kViewIdfSingleOther]; 96 | } 97 | return _lazyScrollView; 98 | } 99 | 100 | - (NSArray *)rectDatas { 101 | if (!_rectDatas) { 102 | _rectDatas = [NSArray array]; 103 | } 104 | return _rectDatas; 105 | } 106 | 107 | - (NSDictionary *)viewsData { 108 | if (!_viewsData) { 109 | _viewsData = [NSDictionary dictionary]; 110 | } 111 | return _viewsData; 112 | } 113 | @end 114 | -------------------------------------------------------------------------------- /LazyScrollView/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // LazyScrollView 4 | // 5 | // Created by Zhang Qing on 16/11/30. 6 | // Copyright © 2016年 Tsing. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##`LazyScrollView`简介 2 | >LazyScrollView 继承自ScrollView,目标是解决异构(与TableView的同构对比)滚动视图的复用回收问题。它可以支持跨View层的复用,用易用方式来生成一个高性能的滚动视图。此方案最先在天猫iOS客户端的首页落地。 3 | 4 | >----[苹果核 - iOS 高性能异构滚动视图构建方案 —— LazyScrollView](http://pingguohe.net/2016/01/31/lazyscroll.html) 5 | 6 | 在[这篇文章](http://pingguohe.net/2016/01/31/lazyscroll.html)中,博主详细介绍了LazyScrollView的使用和实现方案,但是并没有给出具体DEMO,这里只是站在巨人的肩膀上,给一个DEMO,同时也希望可以抛砖引玉。 7 | 8 | ##`LazyScrollView`使用 9 | 暂时的实现比较简陋,目前只有一个`id dataSource;`,需要实现下面三个接口: 10 | ``` 11 | @protocol LazyScrollViewDataSource 12 | @required 13 | // ScrollView一共展示多少个item 14 | - (NSUInteger)numberOfItemInScrollView:(LazyScrollView *)scrollView; 15 | // 要求根据index直接返回RectModel 16 | - (LSVRectModel *)scrollView:(LazyScrollView *)scrollView rectModelAtIndex:(NSUInteger)index; 17 | // 返回下标所对应的view 18 | - (UIView *)scrollView:(LazyScrollView *)scrollView itemByLsvId:(NSString *)lsvId; 19 | @end 20 | ``` 21 | 其中`LSVRectModel`就是原文中的`TMMuiRectModel`: 22 | ``` 23 | @interface LSVRectModel : NSObject 24 | // 转换后的绝对值rect 25 | @property (nonatomic, assign) CGRect absRect; 26 | // 业务下标 27 | @property (nonatomic, copy) NSString *lsvId; 28 | + (instancetype)modelWithRect:(CGRect)rect lsvId:(NSString *)lsvId; 29 | @end 30 | ``` 31 | 三个接口都很简单,和`UITableView`很类似,如果有不清楚,可以在底部查看DEMO或者原文。 32 | 33 | 另外,``LazyScrollView``提供了三个接口,也都是仿照`UITableView`来的,所以整个`LazyScrollView`的使用应该是很容易上手的: 34 | ``` 35 | - (void)reloadData; 36 | - (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier; 37 | - (void)registerClass:(Class)viewClass forViewReuseIdentifier:(NSString *)identifier; 38 | ``` 39 | ##`LazyScrollView`实现 40 | 最主要的思路就是复用,所以有两个`View`池: 41 | ``` 42 | @property (nonatomic, strong) NSMutableDictionary *reuseViews; 43 | @property (nonatomic, strong) NSMutableSet<__kindof UIView *> *visibleViews; 44 | ``` 45 | 由于每个`View`可能对应不同的identifier,所以`reuseViews`是一个`NSMutableDictionary`。 46 | 当一个`View`滑出可见区域之后,会将它先从`visibleViews`中移除,然后添加到`reuseViews`中,并从`LazyScrollView`中 *remove*,即调用`removeFromSuperview`。这个地方在原文中作者的表述可能让大家误会了。 47 | >LazyScrollView中有一个Dictionary,key是reuseIdentifier,Value是对应reuseIdentifier被回收的View,当LazyScrollView得知这个View不该再出现了,会把View放在这里,并且把这个View hidden掉。 48 | 49 | 这里作者用的是`hidden掉`,但是我们知道,`hidden`只是控制显隐,`View`本身还是在那里,也无法去复用。 50 | 51 | 而当一个View滑到可见区域内时,需要先从`reuseViews`中复用,如果`reuseViews`没有,则重新创建一个。相关实现请看`- (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier;`。 52 | 53 | 最后一个问题就是如何判断一个`View`是在可见区域内的。这里原文中说的很清晰,还有图片配合。建议大家还是移步原文。这里我简单说一下,找到顶边大于`contentOffset.y - BUFFER_HEIGHT`,底边小于`contentOffset.y+CGRectGetHeight(self.bounds) + BUFFER_HEIGHT`,然后两个集合取交集就是需要显示的`View`集合了。 54 | 当然,这里有一些处理算法: 55 | * 对 **顶边** 做升序处理得到一个集合,对 **底边** 降序处理得到一个集合。 56 | * 采用二分法查找合适的位置,然后再对上一步得到的集合取子集即可。 57 | 58 | 好了,说了这么多,先放出DEMO地址吧,希望大家可以帮助完善,也希望可以给个Star。[https://github.com/HistoryZhang/LazyScrollView](https://github.com/HistoryZhang/LazyScrollView)。 59 | 60 | 原文地址:[苹果核 - iOS 高性能异构滚动视图构建方案 —— LazyScrollView](http://pingguohe.net/2016/01/31/lazyscroll.html)(里面还有很多干货)。 61 | 62 | 最后说一下目前写的几个问题,希望大家可以一起来优化: 63 | 64 | - [x] 没有处理`View`点击事件,即没有写`delegate`回调。 65 | 66 | - [ ] 二分法查找合适位置的时候算法待优化。 67 | 68 | - [ ] 从旧的`visibleViews`中移除被滑出的`View`算法待优化。 69 | 70 | 贴一段第二个问题的代码: 71 | ``` 72 | - (NSMutableSet *)findSetWithMinEdge:(CGFloat)minEdge { 73 |     NSArray *ascendingEdgeArray = 74 |     [self.allRects sortedArrayUsingComparator:^NSComparisonResult(LSVRectModel *obj1, LSVRectModel *obj2) { 75 |         return CGRectGetMinY(obj1.absRect) > CGRectGetMinY(obj2.absRect) ? NSOrderedDescending : NSOrderedAscending; 76 |     }]; 77 |      78 |     // TOOD: 此处待优化 79 |     // 二分法 80 |     NSInteger minIndex = 0; 81 |     NSInteger maxIndex = ascendingEdgeArray.count - 1; 82 |     NSInteger midIndex = (minIndex + maxIndex) / 2; 83 |     LSVRectModel *model = ascendingEdgeArray[midIndex]; 84 |     while (minIndex < maxIndex - 1) { 85 |         if (CGRectGetMinY(model.absRect) > minEdge) { 86 |             maxIndex = midIndex; 87 |         } 88 |         else { 89 |             minIndex = midIndex; 90 |         } 91 |         midIndex = (minIndex + maxIndex) / 2; 92 |         model = ascendingEdgeArray[midIndex]; 93 |     } 94 |     midIndex = MAX(midIndex - 1, 0); 95 |     NSArray *array = [ascendingEdgeArray subarrayWithRange:NSMakeRange(midIndex, ascendingEdgeArray.count - midIndex)]; 96 |     return [NSMutableSet setWithArray:array]; 97 | } 98 | ``` 99 | 100 | 再贴一段第三个问题的代码: 101 | ``` 102 |     NSMutableArray *newVisibleViews = [self visiableViewModels].mutableCopy; 103 |     NSMutableArray *newVisibleLsvIds = [newVisibleViews valueForKey:@"lsvId"]; 104 |     NSMutableArray *removeViews = [NSMutableArray array]; 105 |     for (UIView *view in self.visibleViews) { 106 |         if (![newVisibleLsvIds containsObject:view.lsvId]) { 107 |             [removeViews addObject:view]; 108 |         } 109 |     } 110 |     for (UIView *view in removeViews) { 111 |         [self.visibleViews removeObject:view]; 112 |         [self enqueueReusableView:view]; 113 |         [view removeFromSuperview]; 114 |     } 115 | ``` 116 | 117 | ##项目引用 118 | 已经支持`cocoapods`,在`Podfile`中添加 `pod 'LazyScrollView'`然后`pod update`即可。 119 | 120 | ##更新记录 121 | * **2016.12.27 新增`delegate`** 122 | 123 | 新增了`@protocol LazyScrollViewDelegate `。其中有一个接口: 124 | 125 | ``` 126 | @optional 127 | - (void)scrollView:(LazyScrollView *)scrollView didClickItemAtLsvId:(NSString *)lsvId; 128 | ``` 129 | 由于`lsvId`在`ScrollView`中是唯一了,这里就没有使用`index`了。 130 | 131 | * **2016.12.04 实现基本功能** --------------------------------------------------------------------------------