├── .gitignore ├── Docs ├── resources │ ├── viewTracker.png │ ├── viewtracker_after.png │ └── viewtracker_pre.png ├── viewtrack_performance.md ├── viewtrack_performance_CN.md └── viewtrack_principle_CN.md ├── Dummy ├── Dummy.h └── Dummy.m ├── LICENSE ├── Podfile ├── README.md ├── README_CN.md ├── TMViewTrackerSDK.podspec ├── TMViewTrackerSDK.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── TMViewTrackerSDK.xcscheme ├── TMViewTrackerSDK ├── Category │ ├── UIView+PageName.h │ ├── UIView+PageName.m │ ├── UIView+TMViewTracker.h │ ├── UIView+TMViewTracker.m │ ├── UIViewController+TMViewTracker.h │ └── UIViewController+TMViewTracker.m ├── Info.plist ├── Swizzler │ ├── Swizzler.h │ └── Swizzler.m ├── TMViewExposure │ ├── CALayer+TMViewExposure.h │ ├── CALayer+TMViewExposure.m │ ├── TMExposureManager.h │ ├── TMExposureManager.m │ ├── UIScrollView+TMViewTracker.h │ ├── UIScrollView+TMViewTracker.m │ ├── UIView+TMViewExposure.h │ ├── UIView+TMViewExposure.m │ ├── UIViewController+TMViewExposure.h │ └── UIViewController+TMViewExposure.m ├── TMViewTracker │ ├── TMEventManager.h │ ├── TMEventManager.m │ ├── UICollectionView+EventTracking.h │ ├── UICollectionView+EventTracking.m │ ├── UITableView+EventTracking.h │ ├── UITableView+EventTracking.m │ ├── UIView+EventTrackingFilter.h │ └── UIView+EventTrackingFilter.m ├── TMViewTrackerCommitProtocol.h ├── TMViewTrackerConfigModel.h ├── TMViewTrackerConfigModel.m ├── TMViewTrackerManager+ProjectPrivateMethods.h ├── TMViewTrackerManager.h ├── TMViewTrackerManager.m ├── TMViewTrackerSDK-Info.plist ├── TMViewTrackerSDK-Prefix.pch └── TMViewTrackerSDK.h └── ViewTrackerSDKDemo ├── Podfile ├── ViewTrackerSDKDemo.xcodeproj └── project.pbxproj ├── ViewTrackerSDKDemo ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── CollectionViewCell.h ├── CollectionViewCell.m ├── CycleScrollView.h ├── CycleScrollView.m ├── Info.plist ├── SubViewController.h ├── SubViewController.m ├── TableViewController.h ├── TableViewController.m ├── ViewController.h ├── ViewController.m ├── ViewTrackerProxy.h ├── ViewTrackerProxy.m ├── h1.jpg ├── h2.jpg ├── h3.jpg ├── h4.jpg └── main.m └── post-install.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude OS X folder attributes 2 | .DS_Store 3 | # Exclude user-specific XCode files 4 | *.xcworkspace 5 | xcuserdata 6 | xcuserdata/* 7 | 8 | Pods/ 9 | Podfile.lock 10 | -------------------------------------------------------------------------------- /Docs/resources/viewTracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/TMViewTrackerSDK/700243607328ede69dc111c6c986dda574ba1b75/Docs/resources/viewTracker.png -------------------------------------------------------------------------------- /Docs/resources/viewtracker_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/TMViewTrackerSDK/700243607328ede69dc111c6c986dda574ba1b75/Docs/resources/viewtracker_after.png -------------------------------------------------------------------------------- /Docs/resources/viewtracker_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/TMViewTrackerSDK/700243607328ede69dc111c6c986dda574ba1b75/Docs/resources/viewtracker_pre.png -------------------------------------------------------------------------------- /Docs/viewtrack_performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | ***FPS on iPhone6s:*** There isn't much difference to scroll tableView. 3 | 4 | 5 | Turn off the ViewTrackSDK switch: 6 | 7 | ![MacDown Screenshot](resources/viewtracker_pre.png) 8 | 9 | Turn on the ViewTrackSDK switch: 10 | 11 | ![MacDown Screenshot](resources/viewtracker_after.png) 12 | -------------------------------------------------------------------------------- /Docs/viewtrack_performance_CN.md: -------------------------------------------------------------------------------- 1 | # 性能 2 | ***FPS on iPhone6s:*** Demo中的tableView滑动的FPS数据如下(基本无太大差别): 3 | 4 | 5 | 关闭 ViewTrackSDK 开关,滑动tableView时的FPS如下: 6 | 7 | ![MacDown Screenshot](resources/viewtracker_pre.png) 8 | 9 | 打开 ViewTrackSDK 开关,滑动tableView时的开关如下: 10 | 11 | ![MacDown Screenshot](resources/viewtracker_after.png) 12 | -------------------------------------------------------------------------------- /Docs/viewtrack_principle_CN.md: -------------------------------------------------------------------------------- 1 | ViewTrackerSDK 技术文档 2 | ## 基本架构 3 | ![结构图](resources/viewTracker.png) 4 | 5 | ## 原理介绍 6 | ### 点击事件 7 | * 支持场景 8 | 1. UIControl的UIControlEvents事件监听, 目前仅关注UIControlEventTouchUpInside。 9 | 2. UIGestureRecognizer的拦截, 目前仅关注UITapGestureRecognizer。 10 | 3. UITableView和UICollectionView的didSelect回调。 11 | 12 | * 实现原理 13 | 在恰当的时机,判断是否需要为该控件增加监听: 14 | > 1. 在UIView的`willMoveToWindow:`中,判断手势和target是否需要监听。 15 | > 如果需要,则添加一个target来处理回调接口。 16 | > 2. 在UITableView和UICollectionView的`setDelegate:`方法中,hook Delegate的didSelect方法, hook函数中判断是否需要回调接口。 17 | 18 | ### 曝光事件 19 | * 支持场景 20 | 页面切换,前后台切换,scrollview的滚动及自动滚动(包括UITableView和UICollectionView)。 21 | 不支持view的覆盖,如UIAlertView、poplayer等。 22 | 23 | * 实现原理 24 | 拦截了UIView的`setAlpha:`、`setHidden:`、`didMoveToWindow`和`layoutSubviews`,以及CALayer的`setHidden:`方法。 25 | 在以上这些时机,判断目标view的可见状态是否改变,并记录为扩展属性。 26 | 当view的可见状态改变时,如果由不可见变为可见,则开始曝光,计算曝光开始时间; 27 | 如果由可见变为可见,则认为结束曝光,此时计算曝光的持续时间,并根据各种设置判断是否需要回调接口。 28 | view的可见状态是由view是否隐藏、透明度、位置等属性来判断的。 29 | 30 | 由于考虑到性能问题,未支持view遮挡的场景。 31 | -------------------------------------------------------------------------------- /Dummy/Dummy.h: -------------------------------------------------------------------------------- 1 | // 2 | // Dummy.h 3 | // Dummy 4 | // 5 | // Created by Dafeng Jin on 15/9/6. 6 | // Copyright (c) 2015年 Taobao lnc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface Dummy : NSObject 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Dummy/Dummy.m: -------------------------------------------------------------------------------- 1 | // 2 | // Dummy.m 3 | // Dummy 4 | // 5 | // Created by Dafeng Jin on 15/9/6. 6 | // Copyright (c) 2015年 Taobao lnc. All rights reserved. 7 | // 8 | 9 | #import "Dummy.h" 10 | 11 | @implementation Dummy 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | 2 | platform :ios 3 | 4 | target 'TMViewTrackerSDK' do 5 | platform:ios, '7.0' 6 | end 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ViewTracker - iOS 2 | 3 | ViewTracker is a tool to automatically collect exposure and click event data. 4 | 5 | Now just support Objective-C, not swift support. 6 | 7 | The system requirement for ViewTracker is iOS 7.0+ 8 | 9 | [中文文档](README_CN.md) 10 | 11 | ## Feature 12 | 13 | - Two platform support (iOS & Android, See ViewTracker-Android in Github for Android Version) 14 | - Automated Data Collection for exposure and click event. 15 | - Covering a variety of scenes , such as Tab、ScrollView、UIControlEventTouchUpInside、Page or App switch. 16 | - A good performance on Page FPS. 17 | - Compact API. 18 | 19 | ## Performance 20 | 21 | - Move to [Performance Test](Docs/viewtrack_performance.md) 22 | 23 | ## Install 24 | 25 | Use Cocoapods to Get latest version of ViewTracker 26 | 27 | ```ruby 28 | pod 'ViewTracker' 29 | ``` 30 | 31 | ## Getting Started 32 | 33 | ##### Set a Delegate to respond to processing exposure and click events. 34 | 35 | > feature/viewtrack-opensource 36 | 37 | ```objc 38 | #import "ViewTrackerProxy.h" 39 | #import 40 | 41 | ... 42 | [[TMViewTrackerManager sharedManager] setCommitProtocol:[ViewTrackerProxy new]]; 43 | ... 44 | ``` 45 | 46 | ViewTrackerProxy.h 47 | 48 | ```objc 49 | #import 50 | @interface ViewTrackerProxy : NSObject 51 | @end 52 | ``` 53 | 54 | ViewTrackerProxy.m 55 | 56 | ```objc 57 | #import "ViewTrackerProxy.h" 58 | 59 | @implementation ViewTrackerProxy 60 | - (instancetype)init 61 | { 62 | if (self = [super init]) { 63 | //init ViewTrack Config 64 | NSDictionary * dictionary = @{kExposureSwitch:@(1), 65 | kClickSwitch:@(1)}; 66 | 67 | [[TMViewTrackerManager sharedManager] setViewTrackerConfig:dictionary]; 68 | 69 | //register notification to handle changes of config from server. 70 | } 71 | return self; 72 | } 73 | - (void)ctrlClicked:(NSString*)controlName 74 | onPage:(NSString*)pageName 75 | args:(NSDictionary*)args 76 | { 77 | NSLog(@"Clicked on Page(%@), controlName(%@), with args(%@)", pageName, controlName, args); 78 | } 79 | 80 | - (void)module:(NSString*)moduleName 81 | showedOnPage:(NSString*)pageName 82 | duration:(NSUInteger)duration 83 | args:(NSDictionary *)args 84 | { 85 | 86 | NSLog(@"module on Page(%@), controlName(%@), duration(%lu), with args(%@)", pageName, moduleName, (unsigned long)duration, args); 87 | } 88 | @end 89 | ``` 90 | 91 | ##### Add the tag 'controlName' to the view 92 | 93 | ```objc 94 | #import 95 | 96 | ... 97 | view.controlName=@"banner-0"; 98 | view.args=@{@"picName":@"pic1"}; 99 | ... 100 | ``` 101 | 102 | ##### Set pageName in viewDidAppear.It is recommended to set it in the base class。 103 | 104 | ```objc 105 | #import 106 | 107 | ... 108 | - (void)viewDidAppear:(BOOL)animated 109 | { 110 | [super viewDidAppear:animated]; 111 | [TMViewTrackerManager setCurrentPageName:@"Tab-1"]; 112 | } 113 | ... 114 | ``` 115 | 116 | ## Author 117 | 118 | - @圆寸 119 | - @子央 120 | 121 | ## LICENSE 122 | 123 | ViewTracker is available under the Apache2.0 license. See the LICENSE file for more info. 124 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # ViewTracker - iOS 2 | 3 | `ViewTracker`是用于自动化的采集用户UI交互过程中的曝光和点击事件。 4 | 5 | 目前仅支持Objective-C,不支持swift。 6 | 7 | 系统要求:iOS 7.0以上 8 | 9 | [English Document](README.md) 10 | 11 | ## 特性 12 | 13 | - 支持两个平台 (iOS & Android, Android版本可以在Github中搜索ViewTracker-Android) 14 | - 曝光和点击事件的无痕采集 15 | - 覆盖多种场景, 例如 Tab、ScrollView、UIControlEventTouchUpInside、页面和APP的切换 16 | - 页面FPS这块性能良好,基本没有帧率影响 17 | - 简单易用的API 18 | 19 | 20 | ## 性能 21 | 22 | - 移步 [性能测试](Docs/viewtrack_performance_CN.md) 23 | 24 | ## 安装 25 | 26 | 使用 Cocoapods 来获取最新的 ViewTracker 版本 27 | 28 | ```ruby 29 | pod 'ViewTracker' 30 | ``` 31 | 32 | 33 | ## 接入 34 | 35 | ##### 设置代理,初始化开关和配置 36 | 37 | ```objc 38 | #import "ViewTrackerProxy.h" 39 | #import 40 | 41 | ... 42 | [[TMViewTrackerManager sharedManager] setCommitProtocol:[ViewTrackerProxy new]]; 43 | ... 44 | ``` 45 | 46 | ViewTrackerProxy.h 47 | 48 | ```objc 49 | #import 50 | @interface ViewTrackerProxy : NSObject 51 | @end 52 | ``` 53 | 54 | ViewTrackerProxy.m 55 | 56 | ```objc 57 | #import "ViewTrackerProxy.h" 58 | 59 | @implementation ViewTrackerProxy 60 | - (instancetype)init 61 | { 62 | if (self = [super init]) { 63 | //init ViewTrack Config 64 | NSDictionary * dictionary = @{kExposureSwitch:@(1), 65 | kClickSwitch:@(1)}; 66 | 67 | [[TMViewTrackerManager sharedManager] setViewTrackerConfig:dictionary]; 68 | 69 | //register notification to handle changes of config from server. 70 | } 71 | return self; 72 | } 73 | - (void)ctrlClicked:(NSString*)controlName 74 | onPage:(NSString*)pageName 75 | args:(NSDictionary*)args 76 | { 77 | NSLog(@"Clicked on Page(%@), controlName(%@), with args(%@)", pageName, controlName, args); 78 | } 79 | 80 | - (void)module:(NSString*)moduleName 81 | showedOnPage:(NSString*)pageName 82 | duration:(NSUInteger)duration 83 | args:(NSDictionary *)args 84 | { 85 | 86 | NSLog(@"module on Page(%@), controlName(%@), duration(%lu), with args(%@)", pageName, moduleName, (unsigned long)duration, args); 87 | } 88 | @end 89 | ``` 90 | 91 | ##### 给View 打tag 'controlName' 92 | 93 | ```objc 94 | #import 95 | 96 | ... 97 | view.controlName=@"banner-0"; 98 | view.args=@{@"picName":@"pic1"}; 99 | ... 100 | ``` 101 | 102 | ##### 在 viewDidAppear 设置 pageName,建议在UIViewController的基类里设置。 103 | 104 | ```objc 105 | #import 106 | 107 | ... 108 | - (void)viewDidAppear:(BOOL)animated 109 | { 110 | [super viewDidAppear:animated]; 111 | [TMViewTrackerManager setCurrentPageName:@"Tab-1"]; 112 | } 113 | ... 114 | ``` 115 | 116 | ## 原理 117 | 118 | - 详见 [ViewTracker原理](Docs/viewtrack_principle_CN.md) 119 | 120 | ## 作者 121 | 122 | - @圆寸 123 | - @子央 124 | 125 | ## License 126 | 127 | ViewTracker 采用 Apache2.0 协议。 详情请见 LICENSE 文件。 128 | -------------------------------------------------------------------------------- /TMViewTrackerSDK.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "TMViewTrackerSDK" 4 | s.version = "1.0.3.8" 5 | s.summary = "A short description of TMDataCollectionSDK." 6 | 7 | s.description = <<-DESC 8 | A longer description of TMClientCategory in Markdown format. 9 | 10 | * Think: Why did you write this? What is the focus? What does it do? 11 | * CocoaPods will be using this to generate tags, and improve search results. 12 | * Try to keep it short, snappy and to the point. 13 | * Finally, don't worry about the indent, CocoaPods strips it! 14 | DESC 15 | 16 | s.homepage = "http://www.tmall.com/TMClientCategory" 17 | 18 | s.license = { 19 | :type => 'Copyright', 20 | :text => <<-LICENSE 21 | Alibaba-INC copyright 22 | LICENSE 23 | } 24 | 25 | s.license = "MIT (example)" 26 | s.author = { } 27 | 28 | s.platform = :ios, "6.0" 29 | 30 | 31 | s.source = { } 32 | 33 | 34 | s.xcconfig = {'FRAMEWORK_SEARCH_PATHS' => '$(PODS_ROOT)/UserTrack'} 35 | s.source_files = "TMViewTrackerSDK", "TMViewTrackerSDK/**/*.{h,c,m}" 36 | end 37 | -------------------------------------------------------------------------------- /TMViewTrackerSDK.xcodeproj/xcshareddata/xcschemes/TMViewTrackerSDK.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/Category/UIView+PageName.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+PageName.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/12/28. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | 11 | @interface UIView (PageName) 12 | - (void)resetPageName; 13 | - (NSString*)pageName; 14 | 15 | /** 16 | * The view controller whose view contains this view. 17 | */ 18 | - (UIViewController*)ownerViewController; 19 | 20 | 21 | @end 22 | 23 | #pragma mark - debug funtions 24 | #ifdef DEBUG 25 | @interface UIView (EnumViews) 26 | // Start the tree recursion at level 0 with the root view 27 | - (NSString *) displayViews: (UIView *) aView; 28 | 29 | // Recursively travel down the view tree, increasing the indentation level for children 30 | - (void)dumpView:(UIView *)aView atIndent:(int)indent into:(NSMutableString *)outstring; 31 | @end 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/Category/UIView+PageName.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+PageName.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/12/28. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "UIView+PageName.h" 10 | #import 11 | 12 | #import "UIViewController+TMViewTracker.h" 13 | #import "TMViewTrackerManager.h" 14 | #import "UIView+TMViewTracker.h" 15 | #import "UIView+PageName.h" 16 | #import "UIViewController+TMViewExposure.h" 17 | 18 | //#import 19 | 20 | static const char* kPageName = "pageName"; 21 | 22 | @implementation UIView (PageName) 23 | - (void)resetPageName 24 | { 25 | objc_setAssociatedObject(self, kPageName, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 26 | } 27 | 28 | - (NSString *)pageName 29 | { 30 | NSString *page = objc_getAssociatedObject(self, kPageName); 31 | if (!page) { 32 | page = [TMViewTrackerManager currentPageName]; 33 | objc_setAssociatedObject(self, kPageName, page, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 34 | } 35 | 36 | return page; 37 | } 38 | 39 | - (UIViewController*)ownerViewController { 40 | for (UIView* next = [self superview]; next; next = next.superview) { 41 | UIResponder* nextResponder = [next nextResponder]; 42 | if ([nextResponder isKindOfClass:[UIViewController class]]) { 43 | return (UIViewController*)nextResponder; 44 | } 45 | } 46 | return nil; 47 | } 48 | @end 49 | 50 | #ifdef DEBUG 51 | @implementation UIView (EnumViews) 52 | // Start the tree recursion at level 0 with the root view 53 | - (NSString *) displayViews: (UIView *) aView 54 | { 55 | NSMutableString *outstring = [[NSMutableString alloc] init]; 56 | [self dumpView: aView.window atIndent:0 into:outstring]; 57 | return outstring; 58 | } 59 | 60 | // Recursively travel down the view tree, increasing the indentation level for children 61 | - (void)dumpView:(UIView *)aView atIndent:(int)indent into:(NSMutableString *)outstring 62 | { 63 | for (int i = 0; i < indent; i++) [outstring appendString:@"--"]; 64 | [outstring appendFormat:@"[%2d] %@ (%@,%@)\n", indent, [[aView class] description], aView.controlName, aView.minorControlName]; 65 | for (UIView *view in [aView subviews]) 66 | [self dumpView:view atIndent:indent + 1 into:outstring]; 67 | } 68 | @end 69 | #endif 70 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/Category/UIView+TMViewTracker.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+TMViewTracker.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2017/2/8. 6 | // Copyright © 2017年 Taobao lnc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef enum : NSUInteger { 12 | ECommitTypeBoth, 13 | ECommitTypeClick, 14 | ECommitTypeExposure, 15 | } ECommitType; 16 | 17 | @interface UIView (TMViewTracker) 18 | @property (nonatomic, strong) NSString *controlName; 19 | @property (nonatomic, strong) NSDictionary *args; 20 | @property (nonatomic, assign) ECommitType commitType; 21 | 22 | //特殊用途,用于处理曝光和点击事件不在同一view上的情况,请慎重使用,或联系@圆寸。 23 | @property (nonatomic, strong) NSString *minorControlName; 24 | @end 25 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/Category/UIView+TMViewTracker.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+TMViewTracker.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2017/2/8. 6 | // Copyright © 2017年 Taobao lnc. All rights reserved. 7 | // 8 | 9 | #import "UIView+TMViewTracker.h" 10 | 11 | #import 12 | 13 | @implementation UIView (TMViewTracker) 14 | 15 | static const char *dataCollectionControlName = "controlName"; 16 | static const char *dataCollectionMinorControlName = "minorControlName"; 17 | static const char *dataCollectionArgs = "dataCollectionArgs"; 18 | static const char *dataCollectionCommitType = "commitType"; 19 | 20 | #pragma mark - Setter And Getters 21 | - (void)setControlName:(NSString *)controlName 22 | { 23 | if ([controlName isKindOfClass:[NSString class]]) { 24 | objc_setAssociatedObject(self, dataCollectionControlName, controlName, OBJC_ASSOCIATION_RETAIN); 25 | } 26 | } 27 | - (NSString *)controlName 28 | { 29 | return objc_getAssociatedObject(self, dataCollectionControlName); 30 | } 31 | 32 | - (void)setMinorControlName:(NSString *)minorControlName 33 | { 34 | if ([minorControlName isKindOfClass:[NSString class]]) { 35 | objc_setAssociatedObject(self, dataCollectionMinorControlName, minorControlName, OBJC_ASSOCIATION_RETAIN); 36 | } 37 | } 38 | 39 | - (NSString *)minorControlName 40 | { 41 | return objc_getAssociatedObject(self, dataCollectionMinorControlName); 42 | } 43 | - (void)setArgs:(NSDictionary *)args 44 | { 45 | if ([args isKindOfClass:[NSDictionary class]]) { 46 | objc_setAssociatedObject(self, dataCollectionArgs, args, OBJC_ASSOCIATION_RETAIN); 47 | } 48 | } 49 | 50 | - (NSDictionary *)args 51 | { 52 | return objc_getAssociatedObject(self, dataCollectionArgs); 53 | } 54 | 55 | - (ECommitType)commitType 56 | { 57 | return [objc_getAssociatedObject(self, dataCollectionCommitType) unsignedIntegerValue]; 58 | } 59 | 60 | - (void)setCommitType:(ECommitType)type 61 | { 62 | objc_setAssociatedObject(self, dataCollectionCommitType, @(type), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 63 | } 64 | @end 65 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/Category/UIViewController+TMViewTracker.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+TMViewTracker.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 16/8/10. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | 11 | @interface UIViewController (TMViewTracker) 12 | @property (nonatomic, strong) NSDictionary *pageCommonArgs; 13 | @end 14 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/Category/UIViewController+TMViewTracker.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+TMViewTracker.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 16/8/10. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "UIViewController+TMViewTracker.h" 10 | 11 | #import 12 | 13 | static const char* kPageCommonArgs = "pageCommonArgs"; 14 | @implementation UIViewController (TMViewTracker) 15 | - (NSDictionary *)pageCommonArgs 16 | { 17 | return objc_getAssociatedObject(self, kPageCommonArgs); 18 | } 19 | 20 | - (void)setPageCommonArgs:(NSDictionary *)extArgs 21 | { 22 | if ([extArgs isKindOfClass:[NSDictionary class]]) { 23 | objc_setAssociatedObject(self, kPageCommonArgs, extArgs, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 24 | } 25 | } 26 | @end 27 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.taobao.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/Swizzler/Swizzler.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMSwizzler.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/10/31. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | 11 | 12 | 13 | /* 14 | * This File Provide two ways to swizzle a selector : 15 | * One is swizzle a method with another selector ; 16 | * The Other One is swizzle a method with a block wrapped by the provider block typed with MethodSwizzlerProvider. 17 | * 18 | * Both of the two ways support instance method and class method. 19 | * 20 | * In addition, use deswizzle to restore all swizzlers 21 | */ 22 | 23 | //------------- 24 | /* typedefs */ 25 | //------------- 26 | typedef id (^MethodSwizzlerProvider)(IMP original, __unsafe_unretained Class swizzledClass, SEL selector); 27 | 28 | //---------------- 29 | /* Deswizzling */ 30 | //---------------- 31 | 32 | /** 33 | Deswizzle all methods. 34 | 35 | @return \c YES if any methods have been deswizzled successfully. 36 | */ 37 | 38 | OBJC_EXTERN BOOL deswizzleAll(void); 39 | 40 | 41 | //----------------- 42 | /* Helper macros */ 43 | //----------------- 44 | 45 | #define MethodSwizzlerReplacement(returntype, selftype, ...) ^ returntype (__unsafe_unretained selftype self, ##__VA_ARGS__) 46 | #define MethodSwizzlerReplacementProviderBlock ^ id (IMP original, __unsafe_unretained Class swizzledClass, SEL _cmd) 47 | #define MethodSwizzlerOriginalImplementation(functype, ...) do{\ 48 | if (original)\ 49 | ((functype)original)(self, _cmd, ##__VA_ARGS__);\ 50 | }while(0); 51 | 52 | 53 | 54 | //--------------------------------------- 55 | /** @name Super easy method swizzling */ 56 | //--------------------------------------- 57 | 58 | @interface NSObject (MethodSwizzler) 59 | 60 | 61 | /** 62 | Swizzle the specified class method with a block. 63 | 64 | @param selector Selector of the method to swizzle. 65 | @param replacement The replacement block to use for swizzling the method. Its signature needs to be: return_type ^(id self, ...). 66 | 67 | */ 68 | 69 | + (void)swizzleClassMethod:(SEL)selector withReplacement:(MethodSwizzlerProvider)replacementProvider; 70 | 71 | 72 | /** 73 | Swizzle the specified instance method with a block. 74 | 75 | @param selector Selector of the method to swizzle. 76 | @param replacement The replacement block to use for swizzling the method. Its signature needs to be: return_type ^(id self, ...). 77 | 78 | */ 79 | 80 | + (void)swizzleInstanceMethod:(SEL)selector withReplacement:(MethodSwizzlerProvider)replacementProvider; 81 | 82 | 83 | /** 84 | Swizzle the specified instance method with another selector 85 | 86 | @param selector Selector of the method to swizzle. 87 | @param swizzledSelector Selector of the new method will be swizzled. 88 | */ 89 | + (void)swizzleInstanceMethod:(SEL)selector withSelector:(SEL)swizzledSelector; 90 | 91 | /** 92 | Swizzle the specified class method with another selector 93 | 94 | @param selector Selector of the method to swizzle. 95 | @param swizzledSelector Selector of the new method will be swizzled. 96 | */ 97 | + (void)swizzleClassMethod:(SEL)selector withSelector:(SEL)swizzledSelector; 98 | @end 99 | 100 | 101 | 102 | 103 | //--------------------------------------- 104 | /** @name Super easy method swizzling */ 105 | //--------------------------------------- 106 | 107 | @interface NSObject (MethodDeSwizzler) 108 | 109 | /** 110 | Restore the specified class method by removing all swizzles. 111 | 112 | @param selector Selector of the swizzled method. 113 | 114 | @return \c YES if the method was successfully restored, \c NO if the method has never been swizzled. 115 | 116 | */ 117 | 118 | + (BOOL)deswizzleClassMethod:(SEL)selector; 119 | 120 | 121 | 122 | 123 | /** 124 | Restore the specified class method by removing all swizzles. 125 | 126 | @param selector Selector of the swizzled method. 127 | 128 | @return \c YES if the method was successfully restored, \c NO if the method has never been swizzled. 129 | 130 | */ 131 | 132 | + (BOOL)deswizzleInstanceMethod:(SEL)selector; 133 | 134 | 135 | 136 | 137 | 138 | /** 139 | Restore all swizzled class methods. 140 | 141 | @return \c YES if the method was successfully restored, \c NO if no method has never been swizzled 142 | 143 | */ 144 | 145 | + (BOOL)deswizzleAllClassMethods; 146 | 147 | 148 | 149 | /** 150 | Restore all swizzled instance methods. 151 | 152 | @return \c YES if the method was successfully restored, \c NO if no method has never been swizzled. 153 | 154 | */ 155 | 156 | + (BOOL)deswizzleAllInstanceMethods; 157 | 158 | 159 | 160 | 161 | /** 162 | Restore all swizzled class and instance methods. 163 | 164 | @return \c YES if the method was successfully restored, \c NO if no method has never been swizzled. 165 | 166 | */ 167 | 168 | + (BOOL)deswizzleAllMethods; 169 | 170 | @end 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/Swizzler/Swizzler.m: -------------------------------------------------------------------------------- 1 | // 2 | // Swizzler.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/10/31. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "Swizzler.h" 10 | 11 | #import 12 | #import 13 | 14 | NS_INLINE void classSwizzleMethod(Class cls, Method method, IMP newImp) { 15 | if (!class_addMethod(cls, method_getName(method), newImp, method_getTypeEncoding(method))) { 16 | // class already has implementation, swizzle it instead 17 | method_setImplementation(method, newImp); 18 | } 19 | } 20 | 21 | #pragma mark - Original Implementations 22 | 23 | static OSSpinLock lock = OS_SPINLOCK_INIT; 24 | 25 | static NSMutableDictionary *originalClassMethods; 26 | static NSMutableDictionary *originalInstanceMethods; 27 | 28 | NS_INLINE IMP originalClassMethodImplementation(__unsafe_unretained Class class, SEL selector, BOOL fetchOnly) { 29 | if (!originalClassMethods) { 30 | originalClassMethods = [[NSMutableDictionary alloc] init]; 31 | } 32 | 33 | NSString *classKey = NSStringFromClass(class); 34 | NSString *selectorKey = NSStringFromSelector(selector); 35 | 36 | NSMutableDictionary *classSwizzles = originalClassMethods[classKey]; 37 | 38 | NSValue *pointerValue = classSwizzles[selectorKey]; 39 | 40 | if (!classSwizzles) { 41 | classSwizzles = [NSMutableDictionary dictionary]; 42 | 43 | originalClassMethods[classKey] = classSwizzles; 44 | } 45 | 46 | IMP orig = NULL; 47 | 48 | if (pointerValue) { 49 | orig = [pointerValue pointerValue]; 50 | 51 | if (fetchOnly) { 52 | if (classSwizzles.count == 1) { 53 | [originalClassMethods removeObjectForKey:classKey]; 54 | } 55 | else { 56 | [classSwizzles removeObjectForKey:selectorKey]; 57 | } 58 | } 59 | } 60 | else if (!fetchOnly) { 61 | orig = (IMP)[class methodForSelector:selector]; 62 | 63 | classSwizzles[selectorKey] = [NSValue valueWithPointer:orig]; 64 | } 65 | 66 | if (classSwizzles.count == 0) { 67 | [originalClassMethods removeObjectForKey:classKey]; 68 | } 69 | 70 | if (originalClassMethods.count == 0) { 71 | originalClassMethods = nil; 72 | } 73 | 74 | return orig; 75 | } 76 | 77 | NS_INLINE IMP originalInstanceMethodImplementation(__unsafe_unretained Class class, SEL selector, BOOL fetchOnly) { 78 | if (!originalInstanceMethods) { 79 | originalInstanceMethods = [[NSMutableDictionary alloc] init]; 80 | } 81 | 82 | NSString *classKey = NSStringFromClass(class); 83 | NSString *selectorKey = NSStringFromSelector(selector); 84 | 85 | NSMutableDictionary *classSwizzles = originalInstanceMethods[classKey]; 86 | 87 | NSValue *pointerValue = classSwizzles[selectorKey]; 88 | 89 | if (!classSwizzles) { 90 | classSwizzles = [NSMutableDictionary dictionary]; 91 | 92 | originalInstanceMethods[classKey] = classSwizzles; 93 | } 94 | 95 | IMP orig = NULL; 96 | 97 | if (pointerValue) { 98 | orig = [pointerValue pointerValue]; 99 | 100 | if (fetchOnly) { 101 | [classSwizzles removeObjectForKey:selectorKey]; 102 | if (classSwizzles.count == 0) { 103 | [originalInstanceMethods removeObjectForKey:classKey]; 104 | } 105 | } 106 | } 107 | else if (!fetchOnly) { 108 | orig = (IMP)[class instanceMethodForSelector:selector]; 109 | 110 | classSwizzles[selectorKey] = [NSValue valueWithPointer:orig]; 111 | } 112 | 113 | if (classSwizzles.count == 0) { 114 | [originalInstanceMethods removeObjectForKey:classKey]; 115 | } 116 | 117 | if (originalInstanceMethods.count == 0) { 118 | originalInstanceMethods = nil; 119 | } 120 | 121 | return orig; 122 | } 123 | 124 | #pragma mark - Deswizzling Global Swizzles 125 | 126 | 127 | NS_INLINE BOOL deswizzleClassMethod(__unsafe_unretained Class class, SEL selector) { 128 | OSSpinLockLock(&lock); 129 | 130 | IMP originalIMP = originalClassMethodImplementation(class, selector, YES); 131 | 132 | if (originalIMP) { 133 | method_setImplementation(class_getClassMethod(class, selector), (IMP)originalIMP); 134 | OSSpinLockUnlock(&lock); 135 | return YES; 136 | } 137 | else { 138 | OSSpinLockUnlock(&lock); 139 | return NO; 140 | } 141 | } 142 | 143 | 144 | NS_INLINE BOOL deswizzleInstanceMethod(__unsafe_unretained Class class, SEL selector) { 145 | OSSpinLockLock(&lock); 146 | 147 | IMP originalIMP = originalInstanceMethodImplementation(class, selector, YES); 148 | 149 | if (originalIMP) { 150 | method_setImplementation(class_getInstanceMethod(class, selector), (IMP)originalIMP); 151 | OSSpinLockUnlock(&lock); 152 | return YES; 153 | } 154 | else { 155 | OSSpinLockUnlock(&lock); 156 | return NO; 157 | } 158 | } 159 | 160 | 161 | NS_INLINE BOOL deswizzleAllClassMethods(__unsafe_unretained Class class) { 162 | OSSpinLockLock(&lock); 163 | BOOL success = NO; 164 | NSDictionary *d = [originalClassMethods[NSStringFromClass(class)] copy]; 165 | for (NSString *sel in d) { 166 | OSSpinLockUnlock(&lock); 167 | if (deswizzleClassMethod(class, NSSelectorFromString(sel))) { 168 | success = YES; 169 | } 170 | OSSpinLockLock(&lock); 171 | } 172 | OSSpinLockUnlock(&lock); 173 | return success; 174 | } 175 | 176 | 177 | NS_INLINE BOOL deswizzleAllInstanceMethods(__unsafe_unretained Class class) { 178 | OSSpinLockLock(&lock); 179 | BOOL success = NO; 180 | NSDictionary *d = [originalInstanceMethods[NSStringFromClass(class)] copy]; 181 | for (NSString *sel in d) { 182 | OSSpinLockUnlock(&lock); 183 | if (deswizzleInstanceMethod(class, NSSelectorFromString(sel))) { 184 | success = YES; 185 | } 186 | OSSpinLockLock(&lock); 187 | } 188 | OSSpinLockUnlock(&lock); 189 | return success; 190 | } 191 | 192 | 193 | #pragma mark - Global Swizzling 194 | 195 | NS_INLINE void swizzleClassMethod(__unsafe_unretained Class class, SEL selector, MethodSwizzlerProvider replacement) { 196 | 197 | OSSpinLockLock(&lock); 198 | 199 | Method originalMethod = class_getClassMethod(class, selector); 200 | 201 | IMP orig = originalClassMethodImplementation(class, selector, NO); 202 | 203 | id replaceBlock = replacement(orig, class, selector); 204 | 205 | Class meta = object_getClass(class); 206 | 207 | classSwizzleMethod(meta, originalMethod, imp_implementationWithBlock(replaceBlock)); 208 | 209 | OSSpinLockUnlock(&lock); 210 | } 211 | 212 | 213 | NS_INLINE void swizzleInstanceMethod(__unsafe_unretained Class class, SEL selector, MethodSwizzlerProvider replacement) { 214 | 215 | OSSpinLockLock(&lock); 216 | 217 | Method originalMethod = class_getInstanceMethod(class, selector); 218 | 219 | IMP orig = originalInstanceMethodImplementation(class, selector, NO); 220 | 221 | id replaceBlock = replacement(orig, class, selector); 222 | 223 | IMP replace = imp_implementationWithBlock(replaceBlock); 224 | 225 | classSwizzleMethod(class, originalMethod, replace); 226 | 227 | OSSpinLockUnlock(&lock); 228 | } 229 | 230 | #pragma mark - Public functions 231 | 232 | BOOL deswizzleAll(void) { 233 | BOOL success = NO; 234 | OSSpinLockLock(&lock); 235 | NSDictionary *d = originalClassMethods.copy; 236 | for (NSString *classKey in d) { 237 | OSSpinLockUnlock(&lock); 238 | BOOL ok = [NSClassFromString(classKey) deswizzleAllMethods]; 239 | OSSpinLockLock(&lock); 240 | if (success != ok) { 241 | success = YES; 242 | } 243 | } 244 | 245 | NSDictionary *d1 = originalInstanceMethods.copy; 246 | for (NSString *classKey in d1) { 247 | OSSpinLockUnlock(&lock); 248 | BOOL ok = [NSClassFromString(classKey) deswizzleAllMethods]; 249 | OSSpinLockLock(&lock); 250 | if (success != ok) { 251 | success = YES; 252 | } 253 | } 254 | OSSpinLockUnlock(&lock); 255 | 256 | return success; 257 | } 258 | 259 | #pragma mark - Category Implementations 260 | 261 | @implementation NSObject (MethodSwizzler) 262 | 263 | + (void)swizzleClassMethod:(SEL)selector withReplacement:(MethodSwizzlerProvider)replacementProvider { 264 | swizzleClassMethod(self, selector, replacementProvider); 265 | } 266 | 267 | + (void)swizzleInstanceMethod:(SEL)selector withReplacement:(MethodSwizzlerProvider)replacementProvider { 268 | swizzleInstanceMethod(self, selector, replacementProvider); 269 | } 270 | 271 | +(void)swizzleInstanceMethod:(SEL)originalSelector withSelector:(SEL)swizzledSelector 272 | { 273 | Method originalMethod = class_getInstanceMethod(self, originalSelector); 274 | Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); 275 | 276 | // When swizzling a class method, use the following: 277 | // Class class = object_getClass((id)self); 278 | // ... 279 | // Method originalMethod = class_getClassMethod(class, originalSelector); 280 | // Method swizzledMethod = class_getClassMethod(class, swizzledSelector); 281 | 282 | BOOL didAddMethod = 283 | class_addMethod(self, 284 | originalSelector, 285 | method_getImplementation(swizzledMethod), 286 | method_getTypeEncoding(swizzledMethod)); 287 | 288 | if (didAddMethod) { 289 | class_replaceMethod(self, 290 | swizzledSelector, 291 | method_getImplementation(originalMethod), 292 | method_getTypeEncoding(originalMethod)); 293 | } else { 294 | method_exchangeImplementations(originalMethod, swizzledMethod); 295 | } 296 | } 297 | 298 | +(void)swizzleClassMethod:(SEL)originalSelector withSelector:(SEL)swizzledSelector 299 | { 300 | Method originalMethod = class_getClassMethod(self, originalSelector); 301 | Method swizzledMethod = class_getClassMethod(self, swizzledSelector); 302 | 303 | BOOL didAddMethod = 304 | class_addMethod(self, 305 | originalSelector, 306 | method_getImplementation(swizzledMethod), 307 | method_getTypeEncoding(swizzledMethod)); 308 | 309 | if (didAddMethod) { 310 | class_replaceMethod(self, 311 | swizzledSelector, 312 | method_getImplementation(originalMethod), 313 | method_getTypeEncoding(originalMethod)); 314 | } else { 315 | method_exchangeImplementations(originalMethod, swizzledMethod); 316 | } 317 | } 318 | 319 | @end 320 | 321 | 322 | @implementation NSObject (MethodDeSwizzler) 323 | 324 | + (BOOL)deswizzleClassMethod:(SEL)selector { 325 | if(! deswizzleClassMethod(self, selector)) 326 | { 327 | 328 | } 329 | return YES; 330 | } 331 | 332 | + (BOOL)deswizzleInstanceMethod:(SEL)selector { 333 | return deswizzleInstanceMethod(self, selector); 334 | } 335 | 336 | + (BOOL)deswizzleAllClassMethods { 337 | return deswizzleAllClassMethods(self); 338 | } 339 | 340 | + (BOOL)deswizzleAllInstanceMethods { 341 | return deswizzleAllInstanceMethods(self); 342 | } 343 | 344 | + (BOOL)deswizzleAllMethods { 345 | BOOL c = [self deswizzleAllClassMethods]; 346 | BOOL i = [self deswizzleAllInstanceMethods]; 347 | return (c || i); 348 | } 349 | 350 | @end 351 | 352 | 353 | 354 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/CALayer+TMViewExposure.h: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+TMViewExposure.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 16/6/14. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | 11 | @interface CALayer (TMViewExposure) 12 | + (void)doSwizzle; 13 | @end 14 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/CALayer+TMViewExposure.m: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+TMViewExposure.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 16/6/14. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "CALayer+TMViewExposure.h" 10 | 11 | #import "Swizzler.h" 12 | #import "TMExposureManager.h" 13 | 14 | @implementation CALayer (TMViewExposure) 15 | 16 | + (void)doSwizzle 17 | { 18 | [self swizzleInstanceMethod:@selector(setHidden:) withSelector:@selector(swizzle_setHidden:)]; 19 | } 20 | 21 | -(void)swizzle_setHidden:(BOOL)hidden 22 | { 23 | BOOL orig = self.hidden; 24 | 25 | [self swizzle_setHidden:hidden]; 26 | 27 | if (orig != hidden) { 28 | id delegate = self.delegate; 29 | if (delegate && [delegate isKindOfClass:[UIView class]]) { 30 | UIView *view = delegate; 31 | 32 | [TMExposureManager adjustStateForView:view forType:TMViewTrackerAdjustTypeCALayerSetHidden]; 33 | } 34 | } 35 | } 36 | @end 37 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/TMExposureManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMViewExposureManager.h 3 | // TMViewTrackerSDK-Exposure 4 | // 5 | // Created by philip on 2017/3/8. 6 | // Copyright © 2017年 Taobao lnc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | typedef NS_ENUM(NSUInteger, TMViewVisibleType){ 13 | TMViewVisibleTypeUndefined = 0, 14 | TMViewVisibleTypeVisible, //1 15 | TMViewVisibleTypeInvisible //2 16 | }; 17 | 18 | 19 | typedef enum : NSUInteger { 20 | TMViewTrackerAdjustTypeCALayerSetHidden = 0, 21 | TMViewTrackerAdjustTypeUIViewSetHidden, 22 | TMViewTrackerAdjustTypeUIViewSetAlpha, 23 | TMViewTrackerAdjustTypeUIViewDidMoveToWindow, 24 | TMViewTrackerAdjustTypeUIScrollViewSetContentOffset, 25 | TMViewTrackerAdjustTypeForceExposure 26 | } TMViewTrackerAdjustType; 27 | 28 | @interface TMExposureManager : NSObject 29 | 30 | + (void)commitPolymerInfoForAllPage; 31 | + (void)commitPolymerInfoForPage:(NSString*)page; 32 | 33 | /** 34 | auto calc State and set for tagged view with controlName. 35 | 36 | if the given view doesn't have controlName, find in subviews and do setState. 37 | 38 | @param view given view 39 | @param type who calls the method 40 | */ 41 | + (void)adjustStateForView:(UIView*)view forType:(TMViewTrackerAdjustType)type; 42 | 43 | /** 44 | set State for tagged view with controlName 45 | 46 | @param state newState 47 | @param view given View 48 | */ 49 | + (void)setState:(NSUInteger)state forView:(UIView*)view; 50 | 51 | //call this method to set the stored index of control to zero. 52 | // the timing is , 53 | // 1. when the destViewController called viewDidAppear:, new appear, should reset index 54 | // 2. when the destViewController come back from Back, viewDidAppear: may not be called, here we should do it 55 | // 3. when the destViewController will relayout according an url's response, life cycle cant catch it, should call it. 56 | + (void)resetPageIndexForPage:(NSString*)pageName; 57 | @end 58 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/TMExposureManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // TMViewExposureManager.m 3 | // TMViewTrackerSDK-Exposure 4 | // 5 | // Created by philip on 2017/3/8. 6 | // Copyright © 2017年 Taobao lnc. All rights reserved. 7 | // 8 | 9 | #import "TMExposureManager.h" 10 | #import "UIView+TMViewTracker.h" 11 | #import "TMViewTrackerManager+ProjectPrivateMethods.h" 12 | #import "UIView+PageName.h" 13 | #import "UIViewController+TMViewTracker.h" 14 | 15 | 16 | #import "UIView+TMViewExposure.h" 17 | 18 | @interface TMUTExposureItem : NSObject 19 | @property (nonatomic, strong) NSString * pageName; // Page Name. 20 | @property (nonatomic, strong) NSString * uniqueControlName; // Control Name use to mark the unique view. 21 | // some special items need suffix. 22 | @property (nonatomic, strong) NSString * controlName; // Control Name. 23 | @property (nonatomic, strong) NSDictionary * args; // Control's extra args. 24 | 25 | @property (nonatomic, assign) NSUInteger times; // JionModel Exposure times. 26 | @property (nonatomic, assign) NSUInteger totalExposureTime; // total time of JionModel Exposure,ms. 27 | 28 | @property (nonatomic, assign) NSUInteger indexInApp; // current control exposure times in app. 29 | @property (nonatomic, assign) NSUInteger indexInPage; // current control exposure times in page. 30 | @end 31 | 32 | @implementation TMUTExposureItem 33 | @end 34 | 35 | @interface TMUTExposingItem : NSObject 36 | @property (nonatomic, strong) NSString * exposingControlName; // current control name, with view's ptr. 37 | 38 | @property (nonatomic, assign) BOOL visible; // current control visibility. 39 | @property (nonatomic ,strong) NSDate * beginTime; // the exposure beginTime of curren control. 40 | @end 41 | 42 | @implementation TMUTExposingItem 43 | @end 44 | 45 | //> 46 | typedef NSMutableDictionary*> TMUTExposureInfos; 47 | 48 | // 49 | typedef NSMutableDictionary TMUTExposingInfos; 50 | 51 | @interface TMExposureManager () 52 | { 53 | dispatch_queue_t _exposureSerialQueue; 54 | } 55 | 56 | // store all data. 57 | @property (nonatomic, strong) TMUTExposureInfos *datas; 58 | // store exposuring data. 59 | @property (nonatomic, strong) TMUTExposingInfos *exposingDatas; 60 | @end 61 | 62 | @implementation TMExposureManager 63 | + (instancetype)shareInstance 64 | { 65 | static TMExposureManager * instance = nil; 66 | static dispatch_once_t onceToken; 67 | dispatch_once(&onceToken, ^{ 68 | instance = [[TMExposureManager alloc] init]; 69 | }); 70 | 71 | return instance; 72 | } 73 | 74 | + (BOOL)isTargetViewForExposure:(UIView*)view 75 | { 76 | if ([view respondsToSelector:@selector(controlName)] && view.controlName) 77 | { 78 | if ([view respondsToSelector:@selector(commitType)]) { 79 | return (view.commitType == ECommitTypeBoth || view.commitType == ECommitTypeExposure); 80 | } 81 | return YES; 82 | } 83 | return NO; 84 | } 85 | #pragma mark - public class method 86 | 87 | /** 88 | * joinMode occasion : 89 | 1. page switch 90 | 2. app switch 91 | 3. sdk switch update. 92 | 4. user invoke. 93 | */ 94 | /** 95 | * commit joinMode data. 96 | */ 97 | + (void)commitPolymerInfoForAllPage 98 | { 99 | dispatch_async([[TMExposureManager shareInstance] getSerialQueue], ^{ 100 | if ([TMExposureManager isPolymerModeOn]) { 101 | NSArray *items = [[TMExposureManager shareInstance] _itemsForAllPage]; 102 | 103 | [items enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 104 | if ([obj isKindOfClass:[TMUTExposureItem class]]) { 105 | [self _commitItem:obj]; 106 | } 107 | }]; 108 | } 109 | }); 110 | } 111 | 112 | 113 | /** 114 | * commit joinMode data of page. 115 | 116 | @param page Page Name 117 | */ 118 | + (void)commitPolymerInfoForPage:(NSString*)page 119 | { 120 | dispatch_async([[TMExposureManager shareInstance] getSerialQueue], ^{ 121 | if ([TMExposureManager isPolymerModeOn]) { 122 | NSArray *items = [[TMExposureManager shareInstance] _itemsForPage:page]; 123 | 124 | [items enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 125 | if ([obj isKindOfClass:[TMUTExposureItem class]]) { 126 | [self _commitItem:obj]; 127 | } 128 | }]; 129 | } 130 | }); 131 | } 132 | 133 | + (void)adjustStateForView:(UIView*)view forType:(TMViewTrackerAdjustType)type 134 | { 135 | /** 136 | * only handle view changed in main thread, 137 | * if view changed not in main thread, maybe webview. 138 | */ 139 | if (![NSThread isMainThread]) { 140 | return; 141 | } 142 | 143 | if (type == TMViewTrackerAdjustTypeForceExposure){ 144 | [self findDestViewInSubviewsAndAdjustState:view recursive:YES]; 145 | return; 146 | } 147 | 148 | if ([[TMViewTrackerManager sharedManager] exposureNeedUpload:view]) { 149 | [self findDestViewInSubviewsAndAdjustState:view 150 | recursive:type!=TMViewTrackerAdjustTypeUIViewDidMoveToWindow]; 151 | } 152 | } 153 | 154 | + (void)setState:(NSUInteger)state forView:(UIView*)view 155 | { 156 | if (state == view.showing) return; 157 | 158 | // if view has controlName, recode to map. 159 | if ([TMExposureManager isTargetViewForExposure:view]) { 160 | // start visible,exposure begin. 161 | if (view.showing != TMViewVisibleTypeVisible && state == TMViewVisibleTypeVisible) { 162 | // get pageName after exposure end. 163 | [self view:view becomeVisible:view.controlName inPage:nil]; 164 | } 165 | // exposure end. 166 | else if(view.showing == TMViewVisibleTypeVisible && state == TMViewVisibleTypeInvisible){ 167 | [self view:view becomeInVisible:view.controlName inPage:[view pageName]]; 168 | } 169 | 170 | view.showing = state; 171 | } 172 | } 173 | 174 | #pragma mark - class method of tools 175 | + (void)_commitItem:(TMUTExposureItem *)item 176 | { 177 | id protocol = [TMViewTrackerManager sharedManager].commitProtocol; 178 | if (protocol && [protocol respondsToSelector:@selector(module:showedOnPage:duration:args:)]) { 179 | NSMutableDictionary *args = [NSMutableDictionary dictionary]; 180 | // add view's args. 181 | if (item.args) { 182 | [args addEntriesFromDictionary:item.args]; 183 | } 184 | 185 | // add exposure args. 186 | if ([TMExposureManager isPolymerModeOn] && item.times) { 187 | [args setObject:@(item.times) forKey:@"exposureTimes"]; 188 | } 189 | 190 | [args setObject:@(item.indexInApp) forKey:@"exposureIndex"]; 191 | 192 | if ([[TMViewTrackerManager sharedManager] isPageNameInExposureWhiteList:item.pageName]) { 193 | [protocol module:item.controlName 194 | showedOnPage:item.pageName 195 | duration:item.totalExposureTime 196 | args:[NSDictionary dictionaryWithDictionary:args]]; 197 | } 198 | 199 | [[TMExposureManager shareInstance] _clearItem:item]; 200 | } 201 | } 202 | 203 | + (void)findDestViewInSubviewsAndAdjustState:(UIView*)view recursive:(BOOL)recursive 204 | { 205 | if ([TMExposureManager isTargetViewForExposure:view]){ 206 | 207 | BOOL visible = [self isViewVisible:view]; 208 | TMViewVisibleType state = visible ? TMViewVisibleTypeVisible : TMViewVisibleTypeInvisible; 209 | 210 | [self setState:state forView:view]; 211 | //android端是一直遍历,找到所有打标controlName的view,去计算其曝光 212 | //之前在这里return是因为,为了节省效率,只要父view有congtrolName,就不去遍历子view了。 213 | //现在这里去掉return,如果当前view有controlName,仍然会向下遍历,直到找到所有设置了controlName的view。 214 | // return; 215 | } 216 | 217 | if (recursive) { 218 | for (UIView * subview in view.subviews) { 219 | [TMExposureManager findDestViewInSubviewsAndAdjustState:subview recursive:recursive]; 220 | } 221 | } 222 | } 223 | 224 | +(BOOL)isViewVisible:(UIView*)view 225 | { 226 | if (!view.window || view.hidden || view.layer.hidden || !view.alpha) { 227 | return NO; 228 | } 229 | 230 | UIView * current = view; 231 | while ([current isKindOfClass:[UIView class]]) { 232 | if (current.alpha <= 0 || current.hidden == YES) { 233 | return NO; 234 | } 235 | current = current.superview; 236 | } 237 | 238 | CGRect viewRectInWindow = [view convertRect:view.bounds toView:view.window]; 239 | BOOL isIntersects = CGRectIntersectsRect(view.window.bounds, viewRectInWindow); 240 | 241 | if (isIntersects) { 242 | 243 | if (![TMExposureManager isTargetViewForExposure:view]) { 244 | return YES; 245 | } 246 | 247 | CGRect intersectRect = CGRectIntersection(view.window.bounds, viewRectInWindow); 248 | if (intersectRect.size.width != 0.f && intersectRect.size.height != 0.f) { 249 | // modify size threshold,80% 250 | CGFloat dimThreshold = [TMViewTrackerManager sharedManager].exposureDimThreshold; 251 | if (intersectRect.size.width / viewRectInWindow.size.width > dimThreshold && 252 | intersectRect.size.height / viewRectInWindow.size.height > dimThreshold) { 253 | return YES; 254 | } 255 | } 256 | } 257 | return NO; 258 | } 259 | 260 | + (NSString*)uniqueExposingControlName:(NSString*)controlName suffix:(NSString*)suffix 261 | { 262 | return [NSString stringWithFormat:@"%@-%@", controlName, suffix]; 263 | } 264 | 265 | + (NSString*)uniqueControlName:(NSString*)controlName inPage:(NSString*)pageName withArgs:(NSDictionary*)args 266 | { 267 | id list = [TMViewTrackerManager sharedManager].config.exposureModifyTagList; 268 | if ([list isKindOfClass:[NSArray class]]) { 269 | for (id item in list) { 270 | if ([item isKindOfClass:[NSDictionary class]]) { 271 | NSString *destPageName = [item objectForKey:@"pageName"]; 272 | if ([destPageName isKindOfClass:[NSString class]]) { 273 | 274 | NSString *destId = [item objectForKey:@"argsId"]; 275 | if ([destId isKindOfClass:[NSString class]]) 276 | { 277 | if ([args isKindOfClass:[NSDictionary class]]) { 278 | NSString *suffix = [args objectForKey:destId]; 279 | 280 | if (suffix) { 281 | return [NSString stringWithFormat:@"%@_%@", controlName, suffix]; 282 | } 283 | } 284 | } 285 | } 286 | } 287 | } 288 | } 289 | 290 | return controlName; 291 | } 292 | 293 | + (void)view:(UIView*)view becomeVisible:(NSString*)controlName inPage:(NSString*)pageName 294 | { 295 | if (!view || !controlName.length){ 296 | return; 297 | } 298 | 299 | __block NSDate* currentDate = [NSDate date]; 300 | __block NSString *suffix = [NSString stringWithFormat:@"%p", view]; 301 | 302 | dispatch_async([[TMExposureManager shareInstance] getSerialQueue], ^{ 303 | NSString * exposingControlName = [TMExposureManager uniqueExposingControlName:controlName suffix:suffix]; 304 | 305 | TMUTExposingItem *item = [[TMExposureManager shareInstance] _exposingItemForControlName:exposingControlName]; 306 | if (!item) { 307 | item = [TMUTExposingItem new]; 308 | item.exposingControlName = exposingControlName; 309 | [[TMExposureManager shareInstance] _addExposingItem:item]; 310 | } 311 | item.visible = YES; 312 | item.beginTime = currentDate; 313 | }); 314 | } 315 | 316 | + (void)view:(UIView*)view becomeInVisible:(NSString*)controlName inPage:(NSString*)pageName 317 | { 318 | if (!view || !controlName.length ) {//|| !pageName.length 319 | return; 320 | } 321 | 322 | __block NSDate* currentDate = [NSDate date]; 323 | __block NSDictionary* args = [view.args copy]; 324 | __block NSString *suffix = [NSString stringWithFormat:@"%p", view]; 325 | 326 | dispatch_async([[TMExposureManager shareInstance] getSerialQueue], ^{ 327 | NSString * exposingControlName = [TMExposureManager uniqueExposingControlName:controlName suffix:suffix]; 328 | 329 | TMUTExposingItem *exposingItem = [[TMExposureManager shareInstance] _exposingItemForControlName:exposingControlName]; 330 | if (exposingItem && exposingItem.beginTime && exposingItem.visible) { 331 | 332 | NSUInteger constMS = ([currentDate timeIntervalSince1970] - [exposingItem.beginTime timeIntervalSince1970]) * 1000; 333 | 334 | // remove item when exposuring. 335 | [[TMExposureManager shareInstance] _removeExposingItemByControlName:exposingControlName]; 336 | 337 | if (pageName.length) { 338 | NSString *uniqueControlName = [TMExposureManager uniqueControlName:controlName inPage:pageName withArgs:args]; 339 | 340 | // search joinMode data. 341 | TMUTExposureItem* item = [[TMExposureManager shareInstance] _itemForUniqueControlName:uniqueControlName inPage:pageName]; 342 | 343 | // add item to joinMode data. 344 | if (!item) { 345 | item = [TMUTExposureItem new]; 346 | item.pageName = pageName; 347 | item.uniqueControlName = uniqueControlName; 348 | item.controlName = controlName; 349 | // item.args = args; 350 | item.times = 0; 351 | item.totalExposureTime = 0; 352 | item.indexInApp = 0; 353 | item.indexInPage = 0; 354 | 355 | [[TMExposureManager shareInstance] _addItem:item]; 356 | } 357 | 358 | //rewrite args when new exposure occours 359 | item.args = args; 360 | 361 | // judge threshold and sampling rate 362 | if (constMS >= [TMViewTrackerManager sharedManager].exposureTimeThreshold // threshold 363 | && [[TMViewTrackerManager sharedManager] isExposureHitSampling])// sampling rate 364 | { 365 | item.indexInApp++; 366 | item.indexInPage++; 367 | 368 | // commit upload right now. 369 | if ([TMExposureManager isPolymerModeOn]) { 370 | // modify item. 371 | item.totalExposureTime += constMS; 372 | item.times++; 373 | }else 374 | { 375 | item.times++; 376 | item.totalExposureTime = constMS; 377 | 378 | if ([TMExposureManager shouldUpload:item]) {// report once in page 379 | [TMExposureManager _commitItem:item]; 380 | } 381 | } 382 | } 383 | } 384 | } 385 | }); 386 | } 387 | 388 | + (void)resetPageIndexForPage:(NSString*)pageName 389 | { 390 | dispatch_async([[TMExposureManager shareInstance] getSerialQueue], ^{ 391 | TMExposureManager *mgr = [TMExposureManager shareInstance]; 392 | NSArray * items = [mgr _itemsForPage:pageName]; 393 | 394 | if ([items count]) { 395 | [items enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 396 | TMUTExposureItem* item = (TMUTExposureItem*)obj; 397 | item.indexInPage = 0; 398 | }]; 399 | 400 | } 401 | }); 402 | } 403 | 404 | #pragma mark - instance method 405 | - (instancetype)init 406 | { 407 | if (self =[super init]) { 408 | _exposureSerialQueue = dispatch_queue_create("exposure_handler_queue", DISPATCH_QUEUE_SERIAL); 409 | 410 | _datas = [NSMutableDictionary dictionary]; 411 | _exposingDatas = [NSMutableDictionary dictionary]; 412 | 413 | // register backgroud notification,to commit joinMode data. 414 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TMVEM_handlerNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil]; 415 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TMVEM_handlerNotification:) name:UIApplicationWillEnterForegroundNotification object:nil]; 416 | 417 | } 418 | return self; 419 | } 420 | 421 | - (void)TMVEM_handlerNotification:(NSNotification*)notify 422 | { 423 | if ([notify.name isEqualToString:UIApplicationDidEnterBackgroundNotification]) { 424 | if ([TMExposureManager isPolymerModeOn]) { 425 | [TMExposureManager commitPolymerInfoForAllPage]; 426 | } 427 | }else if ([notify.name isEqualToString:UIApplicationWillEnterForegroundNotification]) 428 | { 429 | //reset Page Index 430 | [TMExposureManager resetPageIndexForPage:[TMViewTrackerManager currentPageName]]; 431 | } 432 | } 433 | 434 | + (BOOL)isPolymerModeOn 435 | { 436 | if ([TMViewTrackerManager sharedManager].isDebugModeOn) { 437 | return NO; 438 | } 439 | return [TMViewTrackerManager sharedManager].config.exposureUploadMode == TMExposureDataUploadModePolymer; 440 | } 441 | 442 | + (BOOL) shouldUpload:(TMUTExposureItem*)item 443 | { 444 | if ([TMViewTrackerManager sharedManager].config.exposureUploadMode == TMExposureDataUploadModeNormal) { 445 | return YES; 446 | } 447 | 448 | if ([TMViewTrackerManager sharedManager].config.exposureUploadMode == TMExposureDataUploadModeSingleInPage && item.indexInPage == 1) { 449 | return YES; 450 | } 451 | 452 | return NO; 453 | } 454 | 455 | - (dispatch_queue_t)getSerialQueue 456 | { 457 | return _exposureSerialQueue; 458 | } 459 | 460 | #pragma mark - 以下函数,操作本地内存缓存,被commitPolymerInfoXXX和view:becomeXXX系列函数调用,异步串行队列。 461 | #pragma mark - get & set item in exposingDatas 462 | - (TMUTExposingItem*)_exposingItemForControlName:(NSString*)exposingControlName 463 | { 464 | if (exposingControlName.length) { 465 | return [self.exposingDatas objectForKey:exposingControlName]; 466 | } 467 | 468 | return nil; 469 | } 470 | 471 | - (void)_addExposingItem:(TMUTExposingItem*)item 472 | { 473 | if (item && item.exposingControlName.length) { 474 | [self.exposingDatas setObject:item forKey:item.exposingControlName]; 475 | } 476 | } 477 | 478 | - (void)_removeExposingItemByControlName:(NSString*)exposingControlName 479 | { 480 | if (exposingControlName.length){ 481 | [self.exposingDatas removeObjectForKey:exposingControlName]; 482 | } 483 | } 484 | 485 | - (void)_clearExposingItem:(TMUTExposingItem*)item 486 | { 487 | if (item && item.exposingControlName.length) { 488 | item.visible = NO; 489 | item.beginTime = nil; 490 | } 491 | } 492 | #pragma mark - get & set item in datas 493 | - (NSArray*)_itemsForAllPage 494 | { 495 | NSMutableArray *array = [NSMutableArray array]; 496 | 497 | for (NSDictionary * pageItem in self.datas.allValues) { 498 | [array arrayByAddingObjectsFromArray:pageItem.allValues]; 499 | } 500 | 501 | return array; 502 | } 503 | 504 | - (NSArray*)_itemsForPage:(NSString*)pageName 505 | { 506 | if (pageName.length) { 507 | NSDictionary * pageItem = [self.datas objectForKey:pageName]; 508 | if (pageItem) { 509 | return [pageItem allValues]; 510 | } 511 | } 512 | 513 | return nil; 514 | } 515 | 516 | - (TMUTExposureItem*)_itemForUniqueControlName:(NSString*)uniqueControlName inPage:(NSString*)pageName 517 | { 518 | if (uniqueControlName.length && pageName.length) { 519 | NSDictionary * pageItem = [self.datas objectForKey:pageName]; 520 | if (pageItem) { 521 | return [pageItem objectForKey:uniqueControlName]; 522 | } 523 | } 524 | 525 | return nil; 526 | } 527 | 528 | - (void)_addItem:(TMUTExposureItem*)item 529 | { 530 | if (item && item.pageName.length && item.uniqueControlName.length) { 531 | NSMutableDictionary * pageItem = [self.datas objectForKey:item.pageName]; 532 | if (!pageItem) { 533 | pageItem = [NSMutableDictionary dictionary]; 534 | [self.datas setObject:pageItem forKey:item.pageName]; 535 | } 536 | 537 | [pageItem setObject:item forKey:item.uniqueControlName]; 538 | } 539 | } 540 | 541 | //generally, dont need to remove any item, cause we need to record times in app. 542 | - (void)_removeItem:(TMUTExposureItem*)item 543 | { 544 | if (item && item.uniqueControlName.length && item.pageName.length) { 545 | NSMutableDictionary * pageItem = [self.datas objectForKey:item.pageName]; 546 | if (pageItem) { 547 | [pageItem removeObjectForKey:item.uniqueControlName]; 548 | } 549 | } 550 | } 551 | 552 | // clear item's status,after joinMode commit. 553 | - (void)_clearItem:(TMUTExposureItem*)item 554 | { 555 | if (item) { 556 | item.times = 0; 557 | item.totalExposureTime = 0; 558 | } 559 | } 560 | 561 | @end 562 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/UIScrollView+TMViewTracker.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+TMViewTracker.h 3 | // Pods 4 | // 5 | // Created by philip on 2017/5/11. 6 | // 7 | // 8 | 9 | 10 | #import 11 | 12 | @interface UIScrollView (TMViewTracker) 13 | + (void)doSwizzleForTMViewExposure; 14 | @end 15 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/UIScrollView+TMViewTracker.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+TMViewTracker.m 3 | // Pods 4 | // 5 | // Created by philip on 2017/5/11. 6 | // 7 | // 8 | 9 | #import "UIScrollView+TMViewTracker.h" 10 | 11 | #import 12 | #import "Swizzler.h" 13 | #import "TMViewTrackerManager+ProjectPrivateMethods.h" 14 | #import "TMExposureManager.h" 15 | 16 | static const char* kLastLayoutDate = "lastLayoutDate"; 17 | @implementation UIScrollView (TMViewTracker) 18 | 19 | + (void)doSwizzleForTMViewExposure 20 | { 21 | //for UIView's position and rect 22 | [self swizzleInstanceMethod:@selector(setContentOffset:) withSelector:@selector(swizzle_setContentOffset:)]; 23 | } 24 | 25 | -(NSDate*)lastLayoutDate 26 | { 27 | return objc_getAssociatedObject(self, kLastLayoutDate); 28 | } 29 | 30 | -(void)setLastLayoutDate:(NSDate*)date 31 | { 32 | objc_setAssociatedObject(self, kLastLayoutDate, date, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 33 | } 34 | 35 | - (void)swizzle_setContentOffset:(CGPoint)contentOffset 36 | { 37 | [self swizzle_setContentOffset:contentOffset]; 38 | 39 | //只有当应用在前台的时候才拦截setContentOffset 40 | if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { 41 | if ([self lastLayoutDate] && ([[NSDate date] timeIntervalSince1970] - [[self lastLayoutDate] timeIntervalSince1970])*1000 < [TMViewTrackerManager sharedManager].exposureTimeThreshold) { 42 | return; 43 | } 44 | 45 | [self setLastLayoutDate:[NSDate date]]; 46 | 47 | [TMExposureManager adjustStateForView:self forType:TMViewTrackerAdjustTypeUIScrollViewSetContentOffset]; 48 | } 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/UIView+TMViewExposure.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+TMViewExposure.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 16/6/14. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | 11 | #import "UIView+PageName.h" 12 | 13 | #import "TMExposureManager.h" 14 | 15 | @interface UIView (TMViewExposure) 16 | // view's visible type. 17 | @property (nonatomic) TMViewVisibleType showing; 18 | + (void)doSwizzleForTMViewExposure; 19 | @end 20 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/UIView+TMViewExposure.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+TMViewExposure.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 16/6/14. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "UIView+TMViewExposure.h" 10 | 11 | #import 12 | #import 13 | 14 | #import "UIView+TMViewTracker.h" 15 | #import "Swizzler.h" 16 | 17 | #import "TMViewTrackerManager+ProjectPrivateMethods.h" 18 | #import "UIViewController+TMViewTracker.h" 19 | #import "UIView+PageName.h" 20 | 21 | static const char* kShowing = "showing"; 22 | //static const char* kLastLayoutDate = "lastLayoutDate"; 23 | 24 | @implementation UIView (TMViewExposure) 25 | @dynamic showing; 26 | 27 | + (void)doSwizzleForTMViewExposure 28 | { 29 | //for UIView's position and rect 30 | //no need to hook layoutSubviews, hook UIScrollView's setContentOffset instead 31 | // [self swizzleInstanceMethod:@selector(layoutSubviews) withSelector:@selector(swizzle_layoutSubviews)]; 32 | 33 | //for UIView's hidden 34 | [self swizzleInstanceMethod:@selector(setHidden:) withSelector:@selector(swizzle_setHidden:)]; 35 | 36 | //for UIView's alpha 37 | [self swizzleInstanceMethod:@selector(setAlpha:) withSelector:@selector(swizzle_setAlpha:)]; 38 | 39 | //for UIViewController's switch 40 | [self swizzleInstanceMethod:@selector(didMoveToWindow) withSelector:@selector(swizzle_didMoveToWindow)]; 41 | // [self swizzleInstanceMethod:@selector(willMoveToWindow:) withSelector:@selector(swizzle_willMoveToWindow:)]; 42 | } 43 | 44 | #pragma mark - getter and setter for extra properties 45 | //-(NSDate*)lastLayoutDate 46 | //{ 47 | // return objc_getAssociatedObject(self, kLastLayoutDate); 48 | //} 49 | // 50 | //-(void)setLastLayoutDate:(NSDate*)date 51 | //{ 52 | // objc_setAssociatedObject(self, kLastLayoutDate, date, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 53 | //} 54 | 55 | -(TMViewVisibleType)showing 56 | { 57 | if (([self respondsToSelector:@selector(controlName)] && self.controlName)) { 58 | return [objc_getAssociatedObject(self, kShowing) integerValue]; 59 | } 60 | 61 | return TMViewVisibleTypeUndefined; 62 | } 63 | -(void)setShowing:(TMViewVisibleType)showing 64 | { 65 | if (showing == self.showing) return; 66 | 67 | if (([self respondsToSelector:@selector(controlName)] && self.controlName)) { 68 | // end show 69 | if( self.showing == TMViewVisibleTypeVisible && showing == TMViewVisibleTypeInvisible) 70 | { 71 | // remove observer. 72 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; 73 | } 74 | //begin show 75 | else if(self.showing != TMViewVisibleTypeVisible && showing == TMViewVisibleTypeVisible) 76 | { 77 | // add observer. 78 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TMVE_handlerNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil]; 79 | } 80 | 81 | objc_setAssociatedObject(self, kShowing, @(showing), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 82 | } 83 | } 84 | 85 | - (void)TMVE_handlerNotification:(NSNotification*)notify 86 | { 87 | if ([notify.name isEqualToString:UIApplicationDidEnterBackgroundNotification] ) { 88 | [TMExposureManager setState:TMViewVisibleTypeInvisible forView:self]; 89 | [[NSNotificationCenter defaultCenter] addObserver:self 90 | selector:@selector(TMVE_handlerNotification:) 91 | name:UIApplicationWillEnterForegroundNotification 92 | object:nil]; 93 | } 94 | else if ([notify.name isEqualToString:UIApplicationWillEnterForegroundNotification]) { 95 | [TMExposureManager setState:TMViewVisibleTypeVisible forView:self]; 96 | [[NSNotificationCenter defaultCenter] removeObserver:self 97 | name:UIApplicationWillEnterForegroundNotification 98 | object:nil]; 99 | } 100 | } 101 | 102 | #pragma mark - swizzle method 103 | -(void)swizzle_setHidden:(BOOL)hidden 104 | { 105 | BOOL orig = self.hidden; 106 | [self swizzle_setHidden:hidden]; 107 | 108 | if (orig != hidden) { 109 | [TMExposureManager adjustStateForView:self forType:TMViewTrackerAdjustTypeUIViewSetHidden]; 110 | } 111 | } 112 | 113 | -(void)swizzle_setAlpha:(CGFloat)alpha 114 | { 115 | CGFloat orig = self.alpha; 116 | [self swizzle_setAlpha:alpha]; 117 | if (!(orig == alpha || (orig > 0.f && alpha >0.f))) { 118 | [TMExposureManager adjustStateForView:self forType:TMViewTrackerAdjustTypeUIViewSetAlpha]; 119 | } 120 | } 121 | 122 | //- (void)swizzle_willMoveToWindow:(UIWindow *)newWindow 123 | //{ 124 | // [self swizzle_willMoveToWindow:newWindow]; 125 | //} 126 | 127 | /** 128 | * do things in didMoveToWindow: not willMoveToWindow: 129 | */ 130 | -(void)swizzle_didMoveToWindow 131 | { 132 | [self swizzle_didMoveToWindow]; 133 | 134 | if (!self.window) { 135 | [[NSNotificationCenter defaultCenter] removeObserver:self 136 | name:UIApplicationDidEnterBackgroundNotification 137 | object:nil]; 138 | 139 | [[NSNotificationCenter defaultCenter] removeObserver:self 140 | name:UIApplicationWillEnterForegroundNotification 141 | object:nil]; 142 | } 143 | [TMExposureManager adjustStateForView:self forType:TMViewTrackerAdjustTypeUIViewDidMoveToWindow]; 144 | } 145 | 146 | //-(void)swizzle_layoutSubviews 147 | //{ 148 | // [self swizzle_layoutSubviews]; 149 | // 150 | // //just for UIScrollView 151 | // if ([self isKindOfClass:[UIScrollView class]] ) { 152 | // if ([self lastLayoutDate] && ([[NSDate date] timeIntervalSince1970] - [[self lastLayoutDate] timeIntervalSince1970])*1000 < [TMViewTrackerManager sharedManager].exposureTimeThreshold) { 153 | // return; 154 | // } 155 | // 156 | // [self setLastLayoutDate:[NSDate date]]; 157 | // 158 | // [TMExposureManager adjustStateForView:self]; 159 | // } 160 | //} 161 | 162 | @end 163 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/UIViewController+TMViewExposure.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+TMViewExposure.h 3 | // Pods 4 | // 5 | // Created by philip on 2017/3/29. 6 | // 7 | // 8 | 9 | #import 10 | 11 | /** 12 | * hook UIViewController's (viewDidDisappear:) and (viewDidAppear:). 13 | * two usage: 14 | * 1. on viewDidAppear:,commit page's 2201 joinMode data. 15 | * 2. before viewDidAppear:,didMoveToWindow's views, modify pageName. 16 | */ 17 | @interface UIViewController (TMViewExposure) 18 | + (void)doSwizzleForTMViewExposure; 19 | @end 20 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewExposure/UIViewController+TMViewExposure.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+TMViewExposure.m 3 | // Pods 4 | // 5 | // Created by philip on 2017/3/29. 6 | // 7 | // 8 | 9 | #import "UIViewController+TMViewExposure.h" 10 | #import "Swizzler.h" 11 | #import "TMExposureManager.h" 12 | 13 | #import "TMViewTrackerManager.h" 14 | #import "UIViewController+TMViewTracker.h" 15 | #import 16 | 17 | @implementation UIViewController (TMViewExposure) 18 | + (void)doSwizzleForTMViewExposure 19 | { 20 | [self swizzleInstanceMethod:@selector(viewDidDisappear:) withSelector:@selector(swizzle_viewDidDisappear:)]; 21 | [self swizzleInstanceMethod:@selector(viewDidAppear:) withSelector:@selector(swizzle_viewDidAppear:)]; 22 | } 23 | 24 | - (void)swizzle_viewDidDisappear:(BOOL)animated 25 | { 26 | // [TMExposureManager commitPolymerInfoForPage:[TMViewTrackerManager currentPageName]]; 27 | [TMExposureManager commitPolymerInfoForAllPage]; 28 | 29 | [self swizzle_viewDidDisappear:animated]; 30 | } 31 | 32 | - (void)swizzle_viewDidAppear:(BOOL)animated 33 | { 34 | [self swizzle_viewDidAppear:animated]; 35 | 36 | //reset Page Index 37 | [TMExposureManager resetPageIndexForPage:[TMViewTrackerManager currentPageName]]; 38 | } 39 | @end 40 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTracker/TMEventManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMEventManager.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/10/21. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | 11 | 12 | /** 13 | 点击事件的白名单判断,与曝光不同。 14 | 15 | 1. 普通view(非TableView或CollectionView的cell), 能拦截到的生命周期为willMoveToSuperview:\didMoveToSuperview\willMoveToWindow\didMoveToWindow, 16 | 这些事件,均发生在ViewController的 viewDidAppear之前。 17 | 18 | 如果在viewDidAppear内设置页面名称,并判断其白名单,则此时拿到的pageName为上一页页面名称,并不能为按钮添加监听器。 19 | 20 | 所以不能再添加target之前判断白名单,而应该在target的处理回调内判断页面名称是否在白名单。 21 | 22 | 2. 非TableView或CollectionView的cell, 同样能拦截到生命周期willMoveToSuperview:\didMoveToSuperview\willMoveToWindow\didMoveToWindow, 23 | 这些事件,均发生在ViewController的 viewDidAppear之前。 24 | 25 | 但是可复用view的点击事件,在本案中是通过hook其Delegate的didSelect函数来实现的。 26 | 27 | 综上,对于UIView,拦截时,不判断白名单,在上报时才去判断白名单。 28 | */ 29 | @interface TMEventManager : NSObject 30 | 31 | 32 | + (void)registerFilterHandlerForView:(UIView*)view; 33 | + (void)uploadEventTrackingInfoForView:(UIView*)view; 34 | 35 | + (UIView*) targetViewForUITableViewCell:(UITableViewCell*)cell; 36 | + (UIView*) targetViewForUICollectionViewCell:(UICollectionViewCell*)cell; 37 | @end 38 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTracker/TMEventManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // TMEventManager.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/10/21. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "TMEventManager.h" 10 | 11 | #import 12 | #import "TMViewTrackerManager+ProjectPrivateMethods.h" 13 | 14 | #import "UIViewController+TMViewTracker.h" 15 | #import "UIView+TMViewTracker.h" 16 | #import "UIView+EventTrackingFilter.m" 17 | 18 | #import "UIView+PageName.h" 19 | 20 | @interface TMEventManager () 21 | @end 22 | 23 | @implementation TMEventManager 24 | // add register handler for view(controlName is not nil,or minorControlName is not nil) 25 | + (void)registerFilterHandlerForView:(UIView *)view 26 | { 27 | if ([TMEventManager isTagetView:view]) { 28 | if (view.userInteractionEnabled) { 29 | // if view is UIControl,add target to intercept events. 30 | if ([view isKindOfClass:[UIControl class]]) { 31 | [self addFilterTargetsForControl:(UIControl*)view]; 32 | } 33 | 34 | // add extra gesture event. 35 | [self addFilterTargetForGestureView:view]; 36 | } 37 | [view.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 38 | [self registerFilterHandlerForView:obj]; 39 | }]; 40 | } 41 | } 42 | 43 | + (UIView*) targetViewForUITableViewCell:(UITableViewCell*)cell 44 | { 45 | if ([TMEventManager targetViewForView:cell]) { 46 | return cell; 47 | } 48 | 49 | if ([TMEventManager targetViewForView:cell.contentView]) { 50 | return cell.contentView; 51 | } 52 | 53 | return nil; 54 | } 55 | + (UIView*) targetViewForUICollectionViewCell:(UICollectionViewCell*)cell 56 | { 57 | if ([TMEventManager targetViewForView:cell]) { 58 | return cell; 59 | } 60 | 61 | if ([TMEventManager targetViewForView:cell.contentView]) { 62 | return cell.contentView; 63 | } 64 | 65 | return nil; 66 | } 67 | #pragma mark - determine if need to add filter for view 68 | + (BOOL)isTagetView:(UIView *)view 69 | { 70 | return ([self targetViewForView:view] != nil); 71 | } 72 | 73 | + (BOOL)isTargetViewForClick:(UIView*)view 74 | { 75 | if ([view respondsToSelector:@selector(controlName)] && view.controlName) 76 | { 77 | if ([view respondsToSelector:@selector(commitType)]) { 78 | return (view.commitType == ECommitTypeBoth || view.commitType == ECommitTypeClick); 79 | } 80 | return YES; 81 | } 82 | return NO; 83 | } 84 | 85 | // if view's controlName is not nil,return the controlName; 86 | // otherwise,traversal super view's controlName,return the controlName; 87 | + (UIView*)targetViewForView:(UIView*)view 88 | { 89 | if (view) { 90 | if ([TMEventManager isTargetViewForClick:view]) { 91 | return view; 92 | } 93 | 94 | if ([view respondsToSelector:@selector(minorControlName)] && view.minorControlName) { 95 | return view; 96 | } 97 | } 98 | 99 | return nil; 100 | } 101 | 102 | + (NSString*)controlNameForView:(UIView*)view 103 | { 104 | 105 | if (view) { 106 | if ([TMEventManager isTargetViewForClick:view]) { 107 | return view.controlName; 108 | } 109 | 110 | if ([view respondsToSelector:@selector(minorControlName)] && view.minorControlName) { 111 | return view.minorControlName; 112 | } 113 | } 114 | 115 | return nil; 116 | } 117 | #pragma mark - add filter for view 118 | + (void)addFilterTargetForGestureView:(UIView*)view 119 | { 120 | // now just support UITapGestureRecognizer 121 | [view.gestureRecognizers enumerateObjectsUsingBlock:^(__kindof UIGestureRecognizer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 122 | if ([obj isKindOfClass:[UITapGestureRecognizer class]] && ![obj.view isKindOfClass:[UIWindow class]]) { 123 | [obj addTarget:self action:@selector(uploadEventTrackingInfoForGestureRecognizer:)]; 124 | } 125 | }]; 126 | } 127 | 128 | 129 | + (void)addFilterTargetsForControl:(UIControl*)ctrl 130 | { 131 | // now just support UIControlEventTouchUpInside 132 | UIControlEvents currentEvents = ctrl.allControlEvents; 133 | if (currentEvents & UIControlEventTouchUpInside) { 134 | // directly invoke addTarget:action:forControlEvents: , avoid subclass overwrite the method. 135 | IMP aMethodImp = (IMP)class_getMethodImplementation([UIControl class], @selector(addTarget:action:forControlEvents:)); 136 | ((void(*)(id, SEL, id, SEL, UIControlEvents))aMethodImp)(ctrl, @selector(addTarget:action:forControlEvents:), self, @selector(_uploadEventTrackingInfo:), UIControlEventTouchUpInside); 137 | } 138 | } 139 | 140 | #pragma mark - upload method 141 | //UITapGestureRecognizer's action 142 | + (void)uploadEventTrackingInfoForGestureRecognizer:(UIGestureRecognizer*)sender 143 | { 144 | if ([sender isKindOfClass:[UITapGestureRecognizer class]]) { 145 | [self _uploadEventTrackingInfo:sender.view]; 146 | } 147 | } 148 | 149 | //UIControl's action 150 | + (void)_uploadEventTrackingInfo:(UIView *)view 151 | { 152 | if ([[TMViewTrackerManager sharedManager] clickNeedUploadWithWhiteList:view]) { 153 | [self uploadEventTrackingInfoForView:view]; 154 | } 155 | } 156 | 157 | //read upload selector for UIControl、TapGesture、[TableView & CollectionView's didSelected] 158 | + (void)uploadEventTrackingInfoForView:(UIView*)view 159 | { 160 | if (![TMEventManager isTagetView:view]) { 161 | return; 162 | } 163 | // downgrade switch. 164 | if ([[TMViewTrackerManager sharedManager] isClickHitSampling]) { 165 | id protocol = [TMViewTrackerManager sharedManager].commitProtocol; 166 | if (protocol && [protocol respondsToSelector:@selector(ctrlClicked:onPage:args:)]) { 167 | UIView* targetView = [TMEventManager targetViewForView:view]; 168 | 169 | NSString *controlName = [TMEventManager controlNameForView:targetView]; 170 | if (!controlName) { 171 | return; 172 | } 173 | 174 | NSMutableDictionary *args = [NSMutableDictionary dictionary]; 175 | 176 | // add pageCommonArgs 177 | UIViewController *vc = targetView.ownerViewController; 178 | if ([vc respondsToSelector:@selector(pageCommonArgs)]) { 179 | id extPageArgs = vc.pageCommonArgs; 180 | if (extPageArgs) { 181 | [args addEntriesFromDictionary:extPageArgs]; 182 | } 183 | } 184 | 185 | // add args 186 | if (args) { 187 | NSDictionary *oriArgs = nil; 188 | if (targetView.controlName) { 189 | oriArgs = targetView.args; 190 | }else if (targetView.minorControlName) 191 | { 192 | oriArgs = targetView.args; 193 | if (!oriArgs) { 194 | UIView *superView = targetView.superview; 195 | while (superView) { 196 | if (superView.controlName) { 197 | oriArgs = superView.args; 198 | break; 199 | } 200 | 201 | superView = superView.superview; 202 | } 203 | } 204 | } 205 | 206 | [args addEntriesFromDictionary:oriArgs]; 207 | } 208 | 209 | // add viewTracker's sign. 210 | [args addEntriesFromDictionary:@{@"isFromViewTracker":@(1)}]; 211 | 212 | [protocol ctrlClicked:controlName 213 | onPage:[view pageName] 214 | args:[NSDictionary dictionaryWithDictionary:args]]; 215 | } 216 | } 217 | } 218 | @end 219 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTracker/UICollectionView+EventTracking.h: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+EventTracking.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/10/31. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | 11 | @interface UICollectionView (EventTracking) 12 | + (void)doSwizzle; 13 | @end 14 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTracker/UICollectionView+EventTracking.m: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+EventTracking.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/10/31. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "UICollectionView+EventTracking.h" 10 | #import "Swizzler.h" 11 | 12 | #import "Swizzler.h" 13 | #import "TMEventManager.h" 14 | #import "TMViewTrackerManager+ProjectPrivateMethods.h" 15 | 16 | @implementation UICollectionView (EventTracking) 17 | + (void)doSwizzle 18 | { 19 | [self swizzleInstanceMethod:@selector(setDelegate:) withSelector:@selector(swizzle_etf_setDelegate:)]; 20 | } 21 | 22 | - (void)swizzle_etf_setDelegate:(id)delegate 23 | { 24 | if (delegate && ![delegate isProxy] && [delegate respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)]) { 25 | __weak UIView *weakSelf = self; 26 | [delegate.class swizzleInstanceMethod:@selector(collectionView:didSelectItemAtIndexPath:) 27 | withReplacement:^id(IMP original, __unsafe_unretained Class swizzledClass, SEL selector) { 28 | return MethodSwizzlerReplacement(void, id, UICollectionView *collectionView, NSIndexPath *indexPath) 29 | { 30 | MethodSwizzlerOriginalImplementation(void(*)(id, SEL, id, id), collectionView, indexPath); 31 | __strong UIView *strongSelf = weakSelf; 32 | if ([[TMViewTrackerManager sharedManager] clickNeedUploadWithWhiteList:strongSelf]) { 33 | //fix bug 34 | if (indexPath.section < [collectionView numberOfSections] && 35 | indexPath.item < [collectionView numberOfItemsInSection:indexPath.section]) { 36 | UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; 37 | 38 | UIView *view = [TMEventManager targetViewForUICollectionViewCell:cell]; 39 | [TMEventManager uploadEventTrackingInfoForView:view]; 40 | } 41 | } 42 | }; 43 | }]; 44 | } 45 | 46 | [self swizzle_etf_setDelegate:delegate]; 47 | } 48 | @end 49 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTracker/UITableView+EventTracking.h: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+EventTracking.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/10/31. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | 11 | @interface UITableView (EventTracking) 12 | + (void)doSwizzle; 13 | @end 14 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTracker/UITableView+EventTracking.m: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+EventTracking.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/10/31. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "UITableView+EventTracking.h" 10 | #import "Swizzler.h" 11 | 12 | #import "Swizzler.h" 13 | #import "TMEventManager.h" 14 | #import "TMViewTrackerManager+ProjectPrivateMethods.h" 15 | 16 | @implementation UITableView (EventTracking) 17 | + (void)doSwizzle 18 | { 19 | [self swizzleInstanceMethod:@selector(setDelegate:) withSelector:@selector(swizzle_etf_setDelegate:)]; 20 | } 21 | 22 | - (void)swizzle_etf_setDelegate:(id)delegate 23 | { 24 | if (delegate && ![delegate isProxy] && [delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]){ 25 | __weak UIView *weakSelf = self; 26 | [delegate.class swizzleInstanceMethod:@selector(tableView:didSelectRowAtIndexPath:) 27 | withReplacement:^id(IMP original, __unsafe_unretained Class swizzledClass, SEL selector) { 28 | return MethodSwizzlerReplacement(void, id, UITableView *tableView, NSIndexPath *indexPath){ 29 | MethodSwizzlerOriginalImplementation(void(*)(id, SEL, id, id), tableView, indexPath); 30 | __strong UIView *strongSelf = weakSelf; 31 | if ([[TMViewTrackerManager sharedManager] clickNeedUploadWithWhiteList:strongSelf]) { 32 | 33 | if (indexPath.section < [tableView numberOfSections] && 34 | indexPath.row < [tableView numberOfRowsInSection:indexPath.section]) { 35 | UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; 36 | UIView *view = [TMEventManager targetViewForUITableViewCell:cell]; 37 | [TMEventManager uploadEventTrackingInfoForView:view]; 38 | } 39 | } 40 | }; 41 | }]; 42 | 43 | } 44 | 45 | [self swizzle_etf_setDelegate:delegate]; 46 | } 47 | @end 48 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTracker/UIView+EventTrackingFilter.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+EventTrackingFilter.h 3 | // Pods 4 | // 5 | // Created by philip on 2016/10/21. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface UIView (EventTrackingFilter) 12 | + (void)doSwizzleForEventTrackingFilter; 13 | @end 14 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTracker/UIView+EventTrackingFilter.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+EventTrackingFilter.m 3 | // Pods 4 | // 5 | // Created by philip on 2016/10/21. 6 | // 7 | // 8 | 9 | #import "UIView+EventTrackingFilter.h" 10 | #import "Swizzler.h" 11 | 12 | #import "TMEventManager.h" 13 | #import "UIView+TMViewTracker.h" 14 | #import "UIView+PageName.h" 15 | #import "TMViewTrackerManager+ProjectPrivateMethods.h" 16 | 17 | @implementation UIView (EventTrackingFilter) 18 | + (void)doSwizzleForEventTrackingFilter 19 | { 20 | [self swizzleInstanceMethod:@selector(willMoveToSuperview:) withSelector:@selector(swizzle_etf_willMoveToSuperview:)]; 21 | [self swizzleInstanceMethod:@selector(didMoveToWindow) withSelector:@selector(swizzle_etf_didMoveToWindow)]; 22 | } 23 | 24 | - (void)swizzle_etf_willMoveToSuperview:(UIView *)newSuperview 25 | { 26 | [self swizzle_etf_willMoveToSuperview:newSuperview]; 27 | 28 | if ([[TMViewTrackerManager sharedManager] clickNeedUpload:newSuperview]) { 29 | if (newSuperview.controlName) { 30 | self.minorControlName = newSuperview.controlName; 31 | }else if (newSuperview.minorControlName) 32 | { 33 | self.minorControlName = newSuperview.minorControlName; 34 | } 35 | } 36 | } 37 | 38 | - (void)setMinorControlNameForSubviews:(NSString *)minorControlName 39 | { 40 | [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 41 | if (!obj.minorControlName) { 42 | obj.minorControlName = minorControlName; 43 | } 44 | [obj setMinorControlNameForSubviews:minorControlName]; 45 | }]; 46 | } 47 | 48 | - (void)swizzle_etf_didMoveToWindow 49 | { 50 | [self swizzle_etf_didMoveToWindow]; 51 | 52 | if (self.window) { 53 | if ([[TMViewTrackerManager sharedManager] clickNeedUpload:self]) { 54 | if (self.controlName) { 55 | [self setMinorControlNameForSubviews:self.controlName]; 56 | }else if (self.minorControlName) 57 | { 58 | [self setMinorControlNameForSubviews:self.minorControlName]; 59 | } 60 | 61 | [TMEventManager registerFilterHandlerForView:self]; 62 | } 63 | 64 | }else{ 65 | // self.minorControlName = nil; 66 | 67 | //remove pageName 68 | // [self resetPageName]; 69 | } 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTrackerCommitProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMViewTrackerCommitProtocol.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/12/28. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @protocol TMViewTrackerCommitProtocol 13 | @required 14 | - (void)ctrlClicked:(NSString*)controlName 15 | onPage:(NSString*)pageName 16 | args:(NSDictionary*)args; 17 | 18 | - (void)module:(NSString*)moduleName 19 | showedOnPage:(NSString*)pageName 20 | duration:(NSUInteger)duration 21 | args:(NSDictionary *)args; 22 | 23 | @optional 24 | - (NSString *)currentPageName; 25 | @end 26 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTrackerConfigModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMViewTrackerConfigModel.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/12/28. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | 11 | typedef enum : NSUInteger { 12 | TMExposureDataUploadModeNormal = 0, 13 | TMExposureDataUploadModePolymer = 1, 14 | TMExposureDataUploadModeSingleInPage = 2, 15 | } TMExposureDataUploadMode; 16 | 17 | @interface TMViewTrackerConfigModel : NSObject 18 | 19 | /** 20 | * info upload mode,such as Common Mode, JoinMode. 21 | */ 22 | @property (nonatomic, assign) TMExposureDataUploadMode exposureUploadMode; 23 | 24 | /** 25 | used to indicate the view, when multi Views has the same controlName 26 | current used in Page_SearchResult 27 | */ 28 | @property (nonatomic, strong) NSArray *exposureModifyTagList; 29 | 30 | /** 31 | * click switch. 32 | */ 33 | @property (nonatomic, assign) BOOL clickSwitch; 34 | 35 | /** 36 | * click event whitelist, if pageName is in the whitelist,sdk will upload data. 37 | */ 38 | @property (nonatomic, strong) NSDictionary *clickWhiteList; 39 | 40 | /** 41 | * click sampling rate (0-10000),default 10000 42 | */ 43 | @property (nonatomic, assign) NSUInteger clickSampling; 44 | 45 | /** 46 | * exposure switch. 47 | */ 48 | @property (nonatomic, assign) BOOL exposureSwitch; 49 | 50 | /** 51 | * time of exposure , larger than the exposure time,sdk will upload data. 52 | */ 53 | @property (nonatomic, assign) NSUInteger exposureTimeThreshold; 54 | 55 | /** 56 | * view's exposure area threshold. If it is large than the threshold,sdk will upload data. 57 | */ 58 | @property (nonatomic, assign) float exposureDimThreshold; 59 | 60 | /** 61 | * exposure event whitelist. 62 | */ 63 | @property (nonatomic, strong) NSDictionary *exposureWhiteList; 64 | 65 | /** 66 | * exposure sampling rate (0-10000),default 10000(100%) 67 | */ 68 | @property (nonatomic, assign) NSUInteger exposureSampling; 69 | 70 | 71 | /** 72 | * update ConfigModel With dict. 73 | * 74 | * @param dict 75 | */ 76 | - (void)updateWithJSONDictionary:(NSDictionary *)dict; 77 | @end 78 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTrackerConfigModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // TMViewTrackerConfigModel.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 2016/12/28. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "TMViewTrackerConfigModel.h" 10 | 11 | #import 12 | 13 | @interface ViewTrackerPropertyModel : NSObject 14 | @property (nonatomic, strong) NSString * typeInfo; 15 | @property (nonatomic, strong) Class propertyCls; 16 | @property (nonatomic, strong) NSString * name; 17 | @end 18 | 19 | @implementation ViewTrackerPropertyModel 20 | @end 21 | 22 | @interface TMViewTrackerConfigModel () 23 | @property (nonatomic, strong) NSDictionary * propertyMap; 24 | @end 25 | 26 | @implementation TMViewTrackerConfigModel 27 | 28 | 29 | - (instancetype)init 30 | { 31 | if (self = [super init]) { 32 | _exposureUploadMode = TMExposureDataUploadModeNormal; 33 | 34 | _clickSwitch = NO; 35 | _clickWhiteList = nil; 36 | _clickSampling = 10000; 37 | 38 | _exposureSwitch = NO; //Default NO 39 | _exposureTimeThreshold = 100; //ms 40 | _exposureDimThreshold = 0.8f; 41 | _exposureWhiteList = nil; 42 | _exposureSampling = 10000; 43 | 44 | _exposureModifyTagList = @[ 45 | @{ 46 | @"pageName" : @"Page_SearchResult", 47 | @"argsId": @"item_id" 48 | } 49 | ]; 50 | 51 | [self _initPropertyMap]; 52 | } 53 | return self; 54 | } 55 | 56 | - (void)updateWithJSONDictionary:(NSDictionary *)dict 57 | { 58 | //对JSON所有字段进行遍历,到 keyMapper 中查看是否有映射的属性名,然后再对属性进行设置 59 | [dict enumerateKeysAndObjectsUsingBlock:^(NSString *jsonKey, id jsonValue, BOOL *stop) { 60 | ViewTrackerPropertyModel *propertyModel = [self.propertyMap valueForKey:jsonKey]; 61 | 62 | if (propertyModel) { 63 | if (propertyModel.propertyCls && ![jsonValue isKindOfClass:propertyModel.propertyCls]) { 64 | 65 | }else{ 66 | [self setValue:jsonValue forKey:jsonKey]; 67 | } 68 | } 69 | }]; 70 | } 71 | 72 | - (void)_initPropertyMap 73 | { 74 | self.propertyMap = [NSMutableDictionary dictionary]; 75 | unsigned int propertyCount; 76 | objc_property_t *properties = class_copyPropertyList(self.class, &propertyCount); 77 | for (unsigned int i = 0; i < propertyCount; i++) { 78 | 79 | objc_property_t property = properties[i]; 80 | const char *cPropertyName = property_getName(property); 81 | NSString *propertyName = [NSString stringWithUTF8String:cPropertyName]; 82 | 83 | if (propertyName && propertyName.length ) { 84 | //属性的相关属性都在propertyAttrs中,包括其类型,protocol,存取修饰符等信息 85 | const char *propertyAttrs = property_getAttributes(property); 86 | NSString *typeString = [NSString stringWithUTF8String:propertyAttrs]; 87 | 88 | ViewTrackerPropertyModel *model = [TMViewTrackerConfigModel _getClassInfoFromTypeString:typeString forPropertyName:propertyName]; 89 | 90 | if (model) { 91 | [self.propertyMap setValue:model forKey:propertyName]; 92 | } 93 | } 94 | } 95 | free(properties); 96 | } 97 | 98 | + (ViewTrackerPropertyModel*)_getClassInfoFromTypeString:(NSString*)typeString forPropertyName:(NSString*)propertyName 99 | { 100 | if (!typeString || !typeString.length) { 101 | return nil; 102 | } 103 | NSArray * attributes = [typeString componentsSeparatedByString:@","]; 104 | NSString * typeAttribute = [attributes objectAtIndex:0]; 105 | NSString * propertyType = [typeAttribute substringFromIndex:1]; 106 | const char * rawPropertyType = [propertyType UTF8String]; 107 | if (rawPropertyType == NULL) { 108 | return nil; 109 | } 110 | 111 | NSLog(@"propertyType : %s", rawPropertyType); 112 | 113 | // Q NSUInteger 114 | // B BOOL 115 | // d double/CGFloat 116 | // f float 117 | 118 | ViewTrackerPropertyModel * model = [ViewTrackerPropertyModel new]; 119 | model.name = propertyName; 120 | NSString *propertyTypeString = [NSString stringWithUTF8String:rawPropertyType]; 121 | char t = rawPropertyType[0]; 122 | switch (t) { 123 | case 'Q': 124 | model.typeInfo = propertyTypeString; 125 | break; 126 | case 'B': 127 | model.typeInfo = propertyTypeString; 128 | break; 129 | case 'd': 130 | model.typeInfo = propertyTypeString; 131 | break; 132 | case 'f': 133 | model.typeInfo = propertyTypeString; 134 | break; 135 | case '@': 136 | model.typeInfo = propertyTypeString; 137 | if (strlen(rawPropertyType) != 1) { 138 | NSString *cls = [propertyTypeString substringWithRange:NSMakeRange(2, propertyTypeString.length-3)]; 139 | 140 | model.propertyCls = NSClassFromString(cls); 141 | } 142 | break; 143 | 144 | default: 145 | break; 146 | } 147 | 148 | return model; 149 | } 150 | @end 151 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTrackerManager+ProjectPrivateMethods.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMViewTrackerManager+ProjectPrivateMethods.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 16/9/8. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | 10 | #import "TMViewTrackerManager.h" 11 | #import "TMViewTrackerConfigModel.h" 12 | 13 | @interface TMViewTrackerManager (ProjectPrivateMethods) 14 | @property (nonatomic, strong) TMViewTrackerConfigModel *config; 15 | @property (nonatomic, assign) BOOL isDebugModeOn; 16 | 17 | - (BOOL)clickNeedUpload:(UIView*)view; 18 | - (BOOL)clickNeedUploadWithWhiteList:(UIView*)view; 19 | 20 | - (BOOL)exposureNeedUpload:(UIView*)view; 21 | - (BOOL)exposureNeedUploadWithWhiteList:(UIView*)view; 22 | - (BOOL)isPageNameInExposureWhiteList:(NSString*)pageName; 23 | 24 | - (NSUInteger)exposureTimeThreshold; 25 | - (CGFloat)exposureDimThreshold; 26 | 27 | - (BOOL)isClickHitSampling; 28 | - (BOOL)isExposureHitSampling; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTrackerManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMViewTrackerManager.h 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 16/8/15. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import 10 | #import 11 | 12 | /** 13 | * 可通过发送TMViewTrackerManagerInitSwitchsNotification通知,来初始化开关 14 | * 15 | */ 16 | UIKIT_EXTERN NSString *const TMViewTrackerInitSwitchesNotification; 17 | 18 | //ViewTrackerConfig的key值 19 | UIKIT_EXTERN NSString *const kExposureSwitch; //BOOL 20 | UIKIT_EXTERN NSString *const kExposureBatchOpen; 21 | UIKIT_EXTERN NSString *const kExposureUploadMode; //NSUInteger, 0, normal; 1. PolyerUpload; 2. Single In Page 22 | UIKIT_EXTERN NSString *const kExposureTimeThreshold;//NSUInteger (ms) 23 | UIKIT_EXTERN NSString *const kExposureDimThreshold; //float 24 | UIKIT_EXTERN NSString *const kExposureWhiteList; //NSArray 25 | UIKIT_EXTERN NSString *const kExposureSampling; //NSUInteger, 0-10000 26 | 27 | UIKIT_EXTERN NSString *const kClickSwitch; //BOOL 28 | UIKIT_EXTERN NSString *const kClickWhiteList; //NSArray 29 | UIKIT_EXTERN NSString *const kClickSampling; //NSUInteger, 0-10000 30 | 31 | 32 | #import 33 | 34 | @interface TMViewTrackerManager : NSObject 35 | @property (nonatomic, strong) id commitProtocol; 36 | + (instancetype)sharedManager; 37 | 38 | + (void)setCurrentPageName:(NSString*)pageName; 39 | + (NSString*)currentPageName; 40 | 41 | 42 | + (void)turnOnDebugMode; 43 | + (void)turnOffDebugMode; 44 | 45 | /** 46 | set config for switches 47 | 48 | @param config key为上面的值。 49 | */ 50 | - (void)setViewTrackerConfig:(NSDictionary*)config; 51 | 52 | //特殊用途,用于开关设置较晚,不能拿到view曝光初始状态时,调用该函数强制将某个view及子view中满足条件的view置为开始曝光。 53 | //该方法会下层遍历,直到找到满足条件的叶子view。 54 | + (void)forceBeginExposureForView:(UIView*)view; 55 | 56 | + (void)resetPageIndexOnCurrentPage; 57 | @end 58 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTrackerManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // TMViewTrackerManager.m 3 | // TMViewTrackerSDK 4 | // 5 | // Created by philip on 16/8/15. 6 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 7 | // 8 | 9 | #import "TMViewTrackerManager+ProjectPrivateMethods.h" 10 | 11 | #import "UIView+PageName.h" 12 | #import "TMViewTrackerConfigModel.h" 13 | 14 | #import "CALayer+TMViewExposure.h" 15 | #import "UITableView+EventTracking.h" 16 | #import "UICollectionView+EventTracking.h" 17 | #import "UIView+EventTrackingFilter.h" 18 | #import "UIView+TMViewExposure.h" 19 | #import "UIViewController+TMViewExposure.h" 20 | #import "UIScrollView+TMViewTracker.h" 21 | 22 | //default values 23 | NSString *const TMViewTrackerInitSwitchesNotification = @"TMViewTrackerManagerInitSwitchsNotification"; 24 | 25 | NSString *const kExposureSwitch = @"exposureSwitch"; 26 | NSString *const kExposureUploadMode = @"exposureUploadMode"; 27 | NSString *const kExposureBatchOpen = @"batchOpen"; 28 | NSString *const kExposureTimeThreshold = @"exposureTimeThreshold"; 29 | NSString *const kExposureDimThreshold = @"exposureDimThreshold"; 30 | NSString *const kExposureWhiteList = @"exposureWhiteList"; 31 | NSString *const kExposureSampling = @"exposureSampling"; 32 | 33 | NSString *const kClickSwitch = @"clickSwitch"; 34 | NSString *const kClickWhiteList = @"clickWhiteList"; 35 | NSString *const kClickSampling = @"clickSampling"; 36 | 37 | 38 | static TMViewTrackerManager *manager = nil; 39 | static id defaultProtocol = nil; 40 | 41 | @interface TMViewTrackerManager () 42 | @property (nonatomic, assign) BOOL isDebugModeOn; 43 | 44 | @property (nonatomic, strong) TMViewTrackerConfigModel *config; 45 | @property (nonatomic, strong) NSString *currentPageName; 46 | @end 47 | 48 | @implementation TMViewTrackerManager 49 | +(instancetype)sharedManager 50 | { 51 | static dispatch_once_t onceToken; 52 | dispatch_once(&onceToken, ^{ 53 | manager = [[TMViewTrackerManager alloc] init]; 54 | }); 55 | 56 | return manager; 57 | } 58 | 59 | - (instancetype)init 60 | { 61 | if (self = [super init]) { 62 | _config = [[TMViewTrackerConfigModel alloc] init]; 63 | _isDebugModeOn = NO; 64 | [[NSNotificationCenter defaultCenter] addObserver:self 65 | selector:@selector(TMVTM_handlerNotification:) 66 | name:TMViewTrackerInitSwitchesNotification 67 | object:nil]; 68 | } 69 | 70 | return self; 71 | } 72 | 73 | + (void)turnOnDebugMode 74 | { 75 | [TMViewTrackerManager sharedManager].isDebugModeOn = YES; 76 | } 77 | 78 | + (void)turnOffDebugMode 79 | { 80 | [TMViewTrackerManager sharedManager].isDebugModeOn = NO; 81 | } 82 | 83 | + (void)setCurrentPageName:(NSString*)pageName 84 | { 85 | [TMViewTrackerManager sharedManager].currentPageName = pageName; 86 | } 87 | 88 | + (NSString*)currentPageName 89 | { 90 | NSString *current = [TMViewTrackerManager sharedManager].currentPageName; 91 | if (!current) { 92 | id delegate = [TMViewTrackerManager sharedManager].commitProtocol; 93 | if ( delegate && [delegate respondsToSelector:@selector(currentPageName)]) { 94 | return [delegate currentPageName]; 95 | } 96 | } 97 | 98 | return current; 99 | } 100 | #pragma mark - public methods 101 | - (NSUInteger)exposureTimeThreshold 102 | { 103 | return _config.exposureTimeThreshold; 104 | } 105 | 106 | - (CGFloat)exposureDimThreshold 107 | { 108 | return _config.exposureDimThreshold; 109 | } 110 | 111 | - (BOOL)clickNeedUpload:(UIView*)view 112 | { 113 | if (!view) return NO; 114 | return _config.clickSwitch; 115 | } 116 | 117 | - (BOOL)clickNeedUploadWithWhiteList:(UIView*)view 118 | { 119 | if (!view) return NO; 120 | if (_config.clickSwitch) { 121 | id obj = [_config.clickWhiteList objectForKey:[TMViewTrackerManager currentPageName]]; 122 | if ([obj isKindOfClass:[NSNumber class]]) { 123 | return [obj boolValue]; 124 | } 125 | } 126 | 127 | return NO; 128 | } 129 | 130 | - (BOOL)exposureNeedUpload:(UIView*)view 131 | { 132 | if (!view) return NO; 133 | 134 | return _config.exposureSwitch; 135 | } 136 | - (BOOL)exposureNeedUploadWithWhiteList:(UIView*)view 137 | { 138 | if (!view) return NO; 139 | return [self isPageNameInExposureWhiteList:[TMViewTrackerManager currentPageName]]; 140 | } 141 | - (BOOL)isPageNameInExposureWhiteList:(NSString*)pageName 142 | { 143 | if (_config.exposureSwitch) { 144 | id obj = [_config.exposureWhiteList objectForKey:pageName]; 145 | if ([obj isKindOfClass:[NSNumber class]]) { 146 | return [obj boolValue]; 147 | } 148 | } 149 | return NO; 150 | } 151 | 152 | - (BOOL)isClickHitSampling 153 | { 154 | if (_config.clickSampling == 10000) { 155 | return YES; 156 | } 157 | 158 | NSUInteger rand = arc4random() % 10000; 159 | if (rand <= _config.clickSampling) { 160 | return YES; 161 | } 162 | 163 | return NO; 164 | } 165 | - (BOOL)isExposureHitSampling 166 | { 167 | if (_config.exposureSampling == 10000) { 168 | return YES; 169 | } 170 | 171 | NSUInteger rand = arc4random() % 10000; 172 | if (rand <= _config.exposureSampling) { 173 | return YES; 174 | } 175 | 176 | return NO; 177 | } 178 | 179 | - (void)setViewTrackerConfig:(NSDictionary*)config 180 | { 181 | if (config && config.count) { 182 | [self _setupTMViewTrackerSDK:config]; 183 | } 184 | } 185 | #pragma mark - notification handler 186 | - (void)TMVTM_handlerNotification:(NSNotification*)notify 187 | { 188 | if ([notify.name isEqualToString:TMViewTrackerInitSwitchesNotification]) { 189 | [self _setupTMViewTrackerSDK:notify.userInfo]; 190 | } 191 | } 192 | 193 | - (void)_setupTMViewTrackerSDK:(NSDictionary*)config 194 | { 195 | static dispatch_once_t onceToken; 196 | dispatch_once(&onceToken, ^{ 197 | //do hook 198 | [UIView doSwizzleForTMViewExposure]; 199 | [UIView doSwizzleForEventTrackingFilter]; 200 | [UITableView doSwizzle]; 201 | [UICollectionView doSwizzle]; 202 | [CALayer doSwizzle]; 203 | [UIScrollView doSwizzleForTMViewExposure]; 204 | 205 | [UIViewController doSwizzleForTMViewExposure]; 206 | }); 207 | 208 | // upload config. 209 | NSMutableDictionary *mutableConfig = [NSMutableDictionary dictionaryWithDictionary:config]; 210 | 211 | id clickWhiteList = [config objectForKey:kClickWhiteList]; 212 | if ([clickWhiteList isKindOfClass:[NSArray class]]) { 213 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 214 | for (id item in clickWhiteList) { 215 | if ([item isKindOfClass:[NSString class]]) { 216 | [dict setObject:[NSNumber numberWithBool:YES] forKey:item]; 217 | } 218 | } 219 | [mutableConfig setObject:dict forKey:kClickWhiteList]; 220 | } 221 | 222 | id exposureWhiteList = [config objectForKey:kExposureWhiteList]; 223 | if ([exposureWhiteList isKindOfClass:[NSArray class]]) { 224 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 225 | for (id item in exposureWhiteList) { 226 | if ([item isKindOfClass:[NSString class]]) { 227 | [dict setObject:[NSNumber numberWithBool:YES] forKey:item]; 228 | } 229 | } 230 | [mutableConfig setObject:dict forKey:kExposureWhiteList]; 231 | } 232 | 233 | [self.config updateWithJSONDictionary:mutableConfig]; 234 | } 235 | #pragma mark - addition method 236 | + (void)forceBeginExposureForView:(UIView*)view 237 | { 238 | [TMExposureManager adjustStateForView:view forType:TMViewTrackerAdjustTypeForceExposure]; 239 | } 240 | 241 | + (void)resetPageIndexOnCurrentPage 242 | { 243 | [TMExposureManager resetPageIndexForPage:[TMViewTrackerManager currentPageName]]; 244 | } 245 | @end 246 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTrackerSDK-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | 6 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTrackerSDK-Prefix.pch: -------------------------------------------------------------------------------- 1 | #ifndef DEBUG 2 | 3 | #define NSLog(...) {} 4 | 5 | #endif 6 | -------------------------------------------------------------------------------- /TMViewTrackerSDK/TMViewTrackerSDK.h: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // TMViewTracker.h 4 | // TMViewTrackerSDK 5 | // 6 | // Created by philip on 16/8/15. 7 | // Copyright (C)2010-2017 Alibaba Group Holding Limited 8 | // 9 | 10 | #ifndef TMViewTracker_h 11 | #define TMViewTracker_h 12 | 13 | #import 14 | 15 | #import 16 | 17 | #import 18 | #import 19 | 20 | #endif /* TMViewTracker_h */ 21 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/Podfile: -------------------------------------------------------------------------------- 1 | 2 | platform :ios 3 | 4 | target 'ViewTrackerSDKDemo' do 5 | platform:ios, '7.0' 6 | 7 | #pod 'TBJSONModel', '0.1.15' 8 | pod 'TBJSONModel', :path=>'/Users/philip/workspace/gitlab/TBJSONModel' 9 | pod 'TMViewTrackerSDK', :path=>'/Users/philip/workspace/tmall_ios/TMViewTrackerSDK' 10 | end 11 | 12 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | archiveVersion 6 | 1 7 | classes 8 | 9 | objectVersion 10 | 46 11 | objects 12 | 13 | 0C9A30C74CD821C21AB5CE82 14 | 15 | children 16 | 17 | 26911DFD1EB7982918FDF260 18 | 19 | isa 20 | PBXGroup 21 | name 22 | Frameworks 23 | sourceTree 24 | <group> 25 | 26 | 26911DFD1EB7982918FDF260 27 | 28 | explicitFileType 29 | archive.ar 30 | includeInIndex 31 | 0 32 | isa 33 | PBXFileReference 34 | path 35 | libPods-ViewTrackerSDKDemo.a 36 | sourceTree 37 | BUILT_PRODUCTS_DIR 38 | 39 | 2AE0922A063C0606393FEA02 40 | 41 | fileRef 42 | 26911DFD1EB7982918FDF260 43 | isa 44 | PBXBuildFile 45 | 46 | 32AB2FD534BBBE01FA2D3680 47 | 48 | buildActionMask 49 | 2147483647 50 | files 51 | 52 | inputPaths 53 | 54 | isa 55 | PBXShellScriptBuildPhase 56 | name 57 | Embed Pods Frameworks 58 | outputPaths 59 | 60 | runOnlyForDeploymentPostprocessing 61 | 0 62 | shellPath 63 | /bin/sh 64 | shellScript 65 | "${SRCROOT}/Pods/Target Support Files/Pods-ViewTrackerSDKDemo/Pods-ViewTrackerSDKDemo-frameworks.sh" 66 | 67 | showEnvVarsInLog 68 | 0 69 | 70 | 3BE056B75ADABAD1C06B6829 71 | 72 | buildActionMask 73 | 2147483647 74 | files 75 | 76 | inputPaths 77 | 78 | isa 79 | PBXShellScriptBuildPhase 80 | name 81 | Check Pods Manifest.lock 82 | outputPaths 83 | 84 | runOnlyForDeploymentPostprocessing 85 | 0 86 | shellPath 87 | /bin/sh 88 | shellScript 89 | diff "${PODS_ROOT}/../Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null 90 | if [[ $? != 0 ]] ; then 91 | cat << EOM 92 | error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation. 93 | EOM 94 | exit 1 95 | fi 96 | 97 | showEnvVarsInLog 98 | 0 99 | 100 | 4FC387EFB1A19A1A70FB79E0 101 | 102 | includeInIndex 103 | 1 104 | isa 105 | PBXFileReference 106 | lastKnownFileType 107 | text.xcconfig 108 | name 109 | Pods-ViewTrackerSDKDemo.release.xcconfig 110 | path 111 | Pods/Target Support Files/Pods-ViewTrackerSDKDemo/Pods-ViewTrackerSDKDemo.release.xcconfig 112 | sourceTree 113 | <group> 114 | 115 | 9B3391A20AAEBAE3E6A57FB9 116 | 117 | buildActionMask 118 | 2147483647 119 | files 120 | 121 | inputPaths 122 | 123 | isa 124 | PBXShellScriptBuildPhase 125 | name 126 | Copy Pods Resources 127 | outputPaths 128 | 129 | runOnlyForDeploymentPostprocessing 130 | 0 131 | shellPath 132 | /bin/sh 133 | shellScript 134 | "${SRCROOT}/Pods/Target Support Files/Pods-ViewTrackerSDKDemo/Pods-ViewTrackerSDKDemo-resources.sh" 135 | 136 | showEnvVarsInLog 137 | 0 138 | 139 | BCDADEF191DCC78B98424C83 140 | 141 | includeInIndex 142 | 1 143 | isa 144 | PBXFileReference 145 | lastKnownFileType 146 | text.xcconfig 147 | name 148 | Pods-ViewTrackerSDKDemo.debug.xcconfig 149 | path 150 | Pods/Target Support Files/Pods-ViewTrackerSDKDemo/Pods-ViewTrackerSDKDemo.debug.xcconfig 151 | sourceTree 152 | <group> 153 | 154 | D3D20E0EA96F1F2CD2809C95 155 | 156 | children 157 | 158 | BCDADEF191DCC78B98424C83 159 | 4FC387EFB1A19A1A70FB79E0 160 | 161 | isa 162 | PBXGroup 163 | name 164 | Pods 165 | sourceTree 166 | <group> 167 | 168 | D426ECB91E9B7CCC0033DB57 169 | 170 | fileEncoding 171 | 4 172 | isa 173 | PBXFileReference 174 | lastKnownFileType 175 | sourcecode.c.h 176 | path 177 | TableViewController.h 178 | sourceTree 179 | <group> 180 | 181 | D426ECBA1E9B7CCC0033DB57 182 | 183 | fileEncoding 184 | 4 185 | isa 186 | PBXFileReference 187 | lastKnownFileType 188 | sourcecode.c.objc 189 | path 190 | TableViewController.m 191 | sourceTree 192 | <group> 193 | 194 | D426ECBB1E9B7CCC0033DB57 195 | 196 | fileRef 197 | D426ECBA1E9B7CCC0033DB57 198 | isa 199 | PBXBuildFile 200 | 201 | D426ECBF1E9B81C20033DB57 202 | 203 | fileEncoding 204 | 4 205 | isa 206 | PBXFileReference 207 | lastKnownFileType 208 | sourcecode.c.h 209 | path 210 | SubViewController.h 211 | sourceTree 212 | <group> 213 | 214 | D426ECC01E9B81C20033DB57 215 | 216 | fileEncoding 217 | 4 218 | isa 219 | PBXFileReference 220 | lastKnownFileType 221 | sourcecode.c.objc 222 | path 223 | SubViewController.m 224 | sourceTree 225 | <group> 226 | 227 | D426ECC11E9B81C20033DB57 228 | 229 | fileRef 230 | D426ECC01E9B81C20033DB57 231 | isa 232 | PBXBuildFile 233 | 234 | D426ECC21E9B8B660033DB57 235 | 236 | fileEncoding 237 | 4 238 | isa 239 | PBXFileReference 240 | lastKnownFileType 241 | sourcecode.c.h 242 | path 243 | ViewTrackerProxy.h 244 | sourceTree 245 | <group> 246 | 247 | D426ECC31E9B8B660033DB57 248 | 249 | fileEncoding 250 | 4 251 | isa 252 | PBXFileReference 253 | lastKnownFileType 254 | sourcecode.c.objc 255 | path 256 | ViewTrackerProxy.m 257 | sourceTree 258 | <group> 259 | 260 | D426ECC41E9B8B660033DB57 261 | 262 | fileRef 263 | D426ECC31E9B8B660033DB57 264 | isa 265 | PBXBuildFile 266 | 267 | D426ECC51E9C723E0033DB57 268 | 269 | isa 270 | PBXFileReference 271 | lastKnownFileType 272 | image.jpeg 273 | path 274 | h1.jpg 275 | sourceTree 276 | <group> 277 | 278 | D426ECC61E9C723E0033DB57 279 | 280 | isa 281 | PBXFileReference 282 | lastKnownFileType 283 | image.jpeg 284 | path 285 | h3.jpg 286 | sourceTree 287 | <group> 288 | 289 | D426ECC71E9C723E0033DB57 290 | 291 | isa 292 | PBXFileReference 293 | lastKnownFileType 294 | image.jpeg 295 | path 296 | h2.jpg 297 | sourceTree 298 | <group> 299 | 300 | D426ECC81E9C723E0033DB57 301 | 302 | isa 303 | PBXFileReference 304 | lastKnownFileType 305 | image.jpeg 306 | path 307 | h4.jpg 308 | sourceTree 309 | <group> 310 | 311 | D426ECC91E9C723E0033DB57 312 | 313 | fileRef 314 | D426ECC51E9C723E0033DB57 315 | isa 316 | PBXBuildFile 317 | 318 | D426ECCA1E9C723E0033DB57 319 | 320 | fileRef 321 | D426ECC61E9C723E0033DB57 322 | isa 323 | PBXBuildFile 324 | 325 | D426ECCB1E9C723E0033DB57 326 | 327 | fileRef 328 | D426ECC71E9C723E0033DB57 329 | isa 330 | PBXBuildFile 331 | 332 | D426ECCC1E9C723E0033DB57 333 | 334 | fileRef 335 | D426ECC81E9C723E0033DB57 336 | isa 337 | PBXBuildFile 338 | 339 | D426ECCD1E9C72440033DB57 340 | 341 | children 342 | 343 | D426ECC51E9C723E0033DB57 344 | D426ECC61E9C723E0033DB57 345 | D426ECC71E9C723E0033DB57 346 | D426ECC81E9C723E0033DB57 347 | 348 | isa 349 | PBXGroup 350 | name 351 | Resources 352 | sourceTree 353 | <group> 354 | 355 | D426ECCE1E9C778C0033DB57 356 | 357 | fileEncoding 358 | 4 359 | isa 360 | PBXFileReference 361 | lastKnownFileType 362 | sourcecode.c.h 363 | path 364 | CollectionViewCell.h 365 | sourceTree 366 | <group> 367 | 368 | D426ECCF1E9C778C0033DB57 369 | 370 | fileEncoding 371 | 4 372 | isa 373 | PBXFileReference 374 | lastKnownFileType 375 | sourcecode.c.objc 376 | path 377 | CollectionViewCell.m 378 | sourceTree 379 | <group> 380 | 381 | D426ECD01E9C778C0033DB57 382 | 383 | fileEncoding 384 | 4 385 | isa 386 | PBXFileReference 387 | lastKnownFileType 388 | sourcecode.c.h 389 | path 390 | CycleScrollView.h 391 | sourceTree 392 | <group> 393 | 394 | D426ECD11E9C778C0033DB57 395 | 396 | fileEncoding 397 | 4 398 | isa 399 | PBXFileReference 400 | lastKnownFileType 401 | sourcecode.c.objc 402 | path 403 | CycleScrollView.m 404 | sourceTree 405 | <group> 406 | 407 | D426ECD21E9C778C0033DB57 408 | 409 | fileRef 410 | D426ECCF1E9C778C0033DB57 411 | isa 412 | PBXBuildFile 413 | 414 | D426ECD31E9C778C0033DB57 415 | 416 | fileRef 417 | D426ECD11E9C778C0033DB57 418 | isa 419 | PBXBuildFile 420 | 421 | D45C1DF61E9B6A85008285A4 422 | 423 | children 424 | 425 | D45C1E011E9B6A85008285A4 426 | D45C1E001E9B6A85008285A4 427 | D3D20E0EA96F1F2CD2809C95 428 | 0C9A30C74CD821C21AB5CE82 429 | 430 | isa 431 | PBXGroup 432 | sourceTree 433 | <group> 434 | 435 | D45C1DF71E9B6A85008285A4 436 | 437 | attributes 438 | 439 | LastUpgradeCheck 440 | 0800 441 | ORGANIZATIONNAME 442 | ViewTracker 443 | TargetAttributes 444 | 445 | D45C1DFE1E9B6A85008285A4 446 | 447 | CreatedOnToolsVersion 448 | 8.0 449 | DevelopmentTeam 450 | U6MYM755PN 451 | ProvisioningStyle 452 | Automatic 453 | 454 | 455 | 456 | buildConfigurationList 457 | D45C1DFA1E9B6A85008285A4 458 | compatibilityVersion 459 | Xcode 3.2 460 | developmentRegion 461 | English 462 | hasScannedForEncodings 463 | 0 464 | isa 465 | PBXProject 466 | knownRegions 467 | 468 | en 469 | Base 470 | 471 | mainGroup 472 | D45C1DF61E9B6A85008285A4 473 | productRefGroup 474 | D45C1E001E9B6A85008285A4 475 | projectDirPath 476 | 477 | projectReferences 478 | 479 | projectRoot 480 | 481 | targets 482 | 483 | D45C1DFE1E9B6A85008285A4 484 | 485 | 486 | D45C1DFA1E9B6A85008285A4 487 | 488 | buildConfigurations 489 | 490 | D45C1E141E9B6A85008285A4 491 | D45C1E151E9B6A85008285A4 492 | 493 | defaultConfigurationIsVisible 494 | 0 495 | defaultConfigurationName 496 | Release 497 | isa 498 | XCConfigurationList 499 | 500 | D45C1DFB1E9B6A85008285A4 501 | 502 | buildActionMask 503 | 2147483647 504 | files 505 | 506 | D426ECC11E9B81C20033DB57 507 | D45C1E0A1E9B6A85008285A4 508 | D426ECD21E9C778C0033DB57 509 | D426ECC41E9B8B660033DB57 510 | D45C1E071E9B6A85008285A4 511 | D45C1E041E9B6A85008285A4 512 | D426ECBB1E9B7CCC0033DB57 513 | D426ECD31E9C778C0033DB57 514 | 515 | isa 516 | PBXSourcesBuildPhase 517 | runOnlyForDeploymentPostprocessing 518 | 0 519 | 520 | D45C1DFC1E9B6A85008285A4 521 | 522 | buildActionMask 523 | 2147483647 524 | files 525 | 526 | 2AE0922A063C0606393FEA02 527 | 528 | isa 529 | PBXFrameworksBuildPhase 530 | runOnlyForDeploymentPostprocessing 531 | 0 532 | 533 | D45C1DFD1E9B6A85008285A4 534 | 535 | buildActionMask 536 | 2147483647 537 | files 538 | 539 | D426ECCA1E9C723E0033DB57 540 | D45C1E121E9B6A85008285A4 541 | D45C1E0F1E9B6A85008285A4 542 | D426ECCB1E9C723E0033DB57 543 | D426ECCC1E9C723E0033DB57 544 | D426ECC91E9C723E0033DB57 545 | D45C1E0D1E9B6A85008285A4 546 | 547 | isa 548 | PBXResourcesBuildPhase 549 | runOnlyForDeploymentPostprocessing 550 | 0 551 | 552 | D45C1DFE1E9B6A85008285A4 553 | 554 | buildConfigurationList 555 | D45C1E161E9B6A85008285A4 556 | buildPhases 557 | 558 | 3BE056B75ADABAD1C06B6829 559 | D45C1DFB1E9B6A85008285A4 560 | D45C1DFC1E9B6A85008285A4 561 | D45C1DFD1E9B6A85008285A4 562 | 32AB2FD534BBBE01FA2D3680 563 | 9B3391A20AAEBAE3E6A57FB9 564 | 565 | buildRules 566 | 567 | dependencies 568 | 569 | isa 570 | PBXNativeTarget 571 | name 572 | ViewTrackerSDKDemo 573 | productName 574 | ViewTrackerSDKDemo 575 | productReference 576 | D45C1DFF1E9B6A85008285A4 577 | productType 578 | com.apple.product-type.application 579 | 580 | D45C1DFF1E9B6A85008285A4 581 | 582 | explicitFileType 583 | wrapper.application 584 | includeInIndex 585 | 0 586 | isa 587 | PBXFileReference 588 | path 589 | ViewTrackerSDKDemo.app 590 | sourceTree 591 | BUILT_PRODUCTS_DIR 592 | 593 | D45C1E001E9B6A85008285A4 594 | 595 | children 596 | 597 | D45C1DFF1E9B6A85008285A4 598 | 599 | isa 600 | PBXGroup 601 | name 602 | Products 603 | sourceTree 604 | <group> 605 | 606 | D45C1E011E9B6A85008285A4 607 | 608 | children 609 | 610 | D426ECCE1E9C778C0033DB57 611 | D426ECCF1E9C778C0033DB57 612 | D426ECD01E9C778C0033DB57 613 | D426ECD11E9C778C0033DB57 614 | D426ECB91E9B7CCC0033DB57 615 | D426ECBA1E9B7CCC0033DB57 616 | D45C1E051E9B6A85008285A4 617 | D45C1E061E9B6A85008285A4 618 | D426ECC21E9B8B660033DB57 619 | D426ECC31E9B8B660033DB57 620 | D45C1E081E9B6A85008285A4 621 | D45C1E091E9B6A85008285A4 622 | D426ECBF1E9B81C20033DB57 623 | D426ECC01E9B81C20033DB57 624 | D426ECCD1E9C72440033DB57 625 | D45C1E0B1E9B6A85008285A4 626 | D45C1E0E1E9B6A85008285A4 627 | D45C1E101E9B6A85008285A4 628 | D45C1E131E9B6A85008285A4 629 | D45C1E021E9B6A85008285A4 630 | 631 | isa 632 | PBXGroup 633 | path 634 | ViewTrackerSDKDemo 635 | sourceTree 636 | <group> 637 | 638 | D45C1E021E9B6A85008285A4 639 | 640 | children 641 | 642 | D45C1E031E9B6A85008285A4 643 | 644 | isa 645 | PBXGroup 646 | name 647 | Supporting Files 648 | sourceTree 649 | <group> 650 | 651 | D45C1E031E9B6A85008285A4 652 | 653 | isa 654 | PBXFileReference 655 | lastKnownFileType 656 | sourcecode.c.objc 657 | path 658 | main.m 659 | sourceTree 660 | <group> 661 | 662 | D45C1E041E9B6A85008285A4 663 | 664 | fileRef 665 | D45C1E031E9B6A85008285A4 666 | isa 667 | PBXBuildFile 668 | 669 | D45C1E051E9B6A85008285A4 670 | 671 | isa 672 | PBXFileReference 673 | lastKnownFileType 674 | sourcecode.c.h 675 | path 676 | AppDelegate.h 677 | sourceTree 678 | <group> 679 | 680 | D45C1E061E9B6A85008285A4 681 | 682 | isa 683 | PBXFileReference 684 | lastKnownFileType 685 | sourcecode.c.objc 686 | path 687 | AppDelegate.m 688 | sourceTree 689 | <group> 690 | 691 | D45C1E071E9B6A85008285A4 692 | 693 | fileRef 694 | D45C1E061E9B6A85008285A4 695 | isa 696 | PBXBuildFile 697 | 698 | D45C1E081E9B6A85008285A4 699 | 700 | isa 701 | PBXFileReference 702 | lastKnownFileType 703 | sourcecode.c.h 704 | path 705 | ViewController.h 706 | sourceTree 707 | <group> 708 | 709 | D45C1E091E9B6A85008285A4 710 | 711 | isa 712 | PBXFileReference 713 | lastKnownFileType 714 | sourcecode.c.objc 715 | path 716 | ViewController.m 717 | sourceTree 718 | <group> 719 | 720 | D45C1E0A1E9B6A85008285A4 721 | 722 | fileRef 723 | D45C1E091E9B6A85008285A4 724 | isa 725 | PBXBuildFile 726 | 727 | D45C1E0B1E9B6A85008285A4 728 | 729 | children 730 | 731 | D45C1E0C1E9B6A85008285A4 732 | 733 | isa 734 | PBXVariantGroup 735 | name 736 | Main.storyboard 737 | sourceTree 738 | <group> 739 | 740 | D45C1E0C1E9B6A85008285A4 741 | 742 | isa 743 | PBXFileReference 744 | lastKnownFileType 745 | file.storyboard 746 | name 747 | Base 748 | path 749 | Base.lproj/Main.storyboard 750 | sourceTree 751 | <group> 752 | 753 | D45C1E0D1E9B6A85008285A4 754 | 755 | fileRef 756 | D45C1E0B1E9B6A85008285A4 757 | isa 758 | PBXBuildFile 759 | 760 | D45C1E0E1E9B6A85008285A4 761 | 762 | isa 763 | PBXFileReference 764 | lastKnownFileType 765 | folder.assetcatalog 766 | path 767 | Assets.xcassets 768 | sourceTree 769 | <group> 770 | 771 | D45C1E0F1E9B6A85008285A4 772 | 773 | fileRef 774 | D45C1E0E1E9B6A85008285A4 775 | isa 776 | PBXBuildFile 777 | 778 | D45C1E101E9B6A85008285A4 779 | 780 | children 781 | 782 | D45C1E111E9B6A85008285A4 783 | 784 | isa 785 | PBXVariantGroup 786 | name 787 | LaunchScreen.storyboard 788 | sourceTree 789 | <group> 790 | 791 | D45C1E111E9B6A85008285A4 792 | 793 | isa 794 | PBXFileReference 795 | lastKnownFileType 796 | file.storyboard 797 | name 798 | Base 799 | path 800 | Base.lproj/LaunchScreen.storyboard 801 | sourceTree 802 | <group> 803 | 804 | D45C1E121E9B6A85008285A4 805 | 806 | fileRef 807 | D45C1E101E9B6A85008285A4 808 | isa 809 | PBXBuildFile 810 | 811 | D45C1E131E9B6A85008285A4 812 | 813 | isa 814 | PBXFileReference 815 | lastKnownFileType 816 | text.plist.xml 817 | path 818 | Info.plist 819 | sourceTree 820 | <group> 821 | 822 | D45C1E141E9B6A85008285A4 823 | 824 | buildSettings 825 | 826 | ALWAYS_SEARCH_USER_PATHS 827 | NO 828 | CLANG_ANALYZER_NONNULL 829 | YES 830 | CLANG_CXX_LANGUAGE_STANDARD 831 | gnu++0x 832 | CLANG_CXX_LIBRARY 833 | libc++ 834 | CLANG_ENABLE_MODULES 835 | YES 836 | CLANG_ENABLE_OBJC_ARC 837 | YES 838 | CLANG_WARN_BOOL_CONVERSION 839 | YES 840 | CLANG_WARN_CONSTANT_CONVERSION 841 | YES 842 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE 843 | YES_ERROR 844 | CLANG_WARN_DOCUMENTATION_COMMENTS 845 | YES 846 | CLANG_WARN_EMPTY_BODY 847 | YES 848 | CLANG_WARN_ENUM_CONVERSION 849 | YES 850 | CLANG_WARN_INFINITE_RECURSION 851 | YES 852 | CLANG_WARN_INT_CONVERSION 853 | YES 854 | CLANG_WARN_OBJC_ROOT_CLASS 855 | YES_ERROR 856 | CLANG_WARN_SUSPICIOUS_MOVES 857 | YES 858 | CLANG_WARN_UNREACHABLE_CODE 859 | YES 860 | CLANG_WARN__DUPLICATE_METHOD_MATCH 861 | YES 862 | CODE_SIGN_IDENTITY[sdk=iphoneos*] 863 | iPhone Developer 864 | COPY_PHASE_STRIP 865 | NO 866 | DEBUG_INFORMATION_FORMAT 867 | dwarf 868 | ENABLE_STRICT_OBJC_MSGSEND 869 | YES 870 | ENABLE_TESTABILITY 871 | YES 872 | GCC_C_LANGUAGE_STANDARD 873 | gnu99 874 | GCC_DYNAMIC_NO_PIC 875 | NO 876 | GCC_NO_COMMON_BLOCKS 877 | YES 878 | GCC_OPTIMIZATION_LEVEL 879 | 0 880 | GCC_PREPROCESSOR_DEFINITIONS 881 | 882 | DEBUG=1 883 | $(inherited) 884 | 885 | GCC_WARN_64_TO_32_BIT_CONVERSION 886 | YES 887 | GCC_WARN_ABOUT_RETURN_TYPE 888 | YES_ERROR 889 | GCC_WARN_UNDECLARED_SELECTOR 890 | YES 891 | GCC_WARN_UNINITIALIZED_AUTOS 892 | YES_AGGRESSIVE 893 | GCC_WARN_UNUSED_FUNCTION 894 | YES 895 | GCC_WARN_UNUSED_VARIABLE 896 | YES 897 | IPHONEOS_DEPLOYMENT_TARGET 898 | 10.0 899 | MTL_ENABLE_DEBUG_INFO 900 | YES 901 | ONLY_ACTIVE_ARCH 902 | YES 903 | SDKROOT 904 | iphoneos 905 | TARGETED_DEVICE_FAMILY 906 | 1,2 907 | 908 | isa 909 | XCBuildConfiguration 910 | name 911 | Debug 912 | 913 | D45C1E151E9B6A85008285A4 914 | 915 | buildSettings 916 | 917 | ALWAYS_SEARCH_USER_PATHS 918 | NO 919 | CLANG_ANALYZER_NONNULL 920 | YES 921 | CLANG_CXX_LANGUAGE_STANDARD 922 | gnu++0x 923 | CLANG_CXX_LIBRARY 924 | libc++ 925 | CLANG_ENABLE_MODULES 926 | YES 927 | CLANG_ENABLE_OBJC_ARC 928 | YES 929 | CLANG_WARN_BOOL_CONVERSION 930 | YES 931 | CLANG_WARN_CONSTANT_CONVERSION 932 | YES 933 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE 934 | YES_ERROR 935 | CLANG_WARN_DOCUMENTATION_COMMENTS 936 | YES 937 | CLANG_WARN_EMPTY_BODY 938 | YES 939 | CLANG_WARN_ENUM_CONVERSION 940 | YES 941 | CLANG_WARN_INFINITE_RECURSION 942 | YES 943 | CLANG_WARN_INT_CONVERSION 944 | YES 945 | CLANG_WARN_OBJC_ROOT_CLASS 946 | YES_ERROR 947 | CLANG_WARN_SUSPICIOUS_MOVES 948 | YES 949 | CLANG_WARN_UNREACHABLE_CODE 950 | YES 951 | CLANG_WARN__DUPLICATE_METHOD_MATCH 952 | YES 953 | CODE_SIGN_IDENTITY[sdk=iphoneos*] 954 | iPhone Developer 955 | COPY_PHASE_STRIP 956 | NO 957 | DEBUG_INFORMATION_FORMAT 958 | dwarf-with-dsym 959 | ENABLE_NS_ASSERTIONS 960 | NO 961 | ENABLE_STRICT_OBJC_MSGSEND 962 | YES 963 | GCC_C_LANGUAGE_STANDARD 964 | gnu99 965 | GCC_NO_COMMON_BLOCKS 966 | YES 967 | GCC_WARN_64_TO_32_BIT_CONVERSION 968 | YES 969 | GCC_WARN_ABOUT_RETURN_TYPE 970 | YES_ERROR 971 | GCC_WARN_UNDECLARED_SELECTOR 972 | YES 973 | GCC_WARN_UNINITIALIZED_AUTOS 974 | YES_AGGRESSIVE 975 | GCC_WARN_UNUSED_FUNCTION 976 | YES 977 | GCC_WARN_UNUSED_VARIABLE 978 | YES 979 | IPHONEOS_DEPLOYMENT_TARGET 980 | 10.0 981 | MTL_ENABLE_DEBUG_INFO 982 | NO 983 | SDKROOT 984 | iphoneos 985 | TARGETED_DEVICE_FAMILY 986 | 1,2 987 | VALIDATE_PRODUCT 988 | YES 989 | 990 | isa 991 | XCBuildConfiguration 992 | name 993 | Release 994 | 995 | D45C1E161E9B6A85008285A4 996 | 997 | buildConfigurations 998 | 999 | D45C1E171E9B6A85008285A4 1000 | D45C1E181E9B6A85008285A4 1001 | 1002 | defaultConfigurationIsVisible 1003 | 0 1004 | defaultConfigurationName 1005 | Release 1006 | isa 1007 | XCConfigurationList 1008 | 1009 | D45C1E171E9B6A85008285A4 1010 | 1011 | baseConfigurationReference 1012 | BCDADEF191DCC78B98424C83 1013 | buildSettings 1014 | 1015 | ASSETCATALOG_COMPILER_APPICON_NAME 1016 | AppIcon 1017 | CODE_SIGN_IDENTITY[sdk=iphoneos*] 1018 | iPhone Developer 1019 | DEVELOPMENT_TEAM 1020 | 1021 | ENABLE_BITCODE 1022 | NO 1023 | INFOPLIST_FILE 1024 | ViewTrackerSDKDemo/Info.plist 1025 | IPHONEOS_DEPLOYMENT_TARGET 1026 | 7.0 1027 | LD_RUNPATH_SEARCH_PATHS 1028 | $(inherited) @executable_path/Frameworks 1029 | PRODUCT_BUNDLE_IDENTIFIER 1030 | com.ViewTrackerSDKDemo 1031 | PRODUCT_NAME 1032 | $(TARGET_NAME) 1033 | PROVISIONING_PROFILE 1034 | 1035 | PROVISIONING_PROFILE_SPECIFIER 1036 | 1037 | 1038 | isa 1039 | XCBuildConfiguration 1040 | name 1041 | Debug 1042 | 1043 | D45C1E181E9B6A85008285A4 1044 | 1045 | baseConfigurationReference 1046 | 4FC387EFB1A19A1A70FB79E0 1047 | buildSettings 1048 | 1049 | ASSETCATALOG_COMPILER_APPICON_NAME 1050 | AppIcon 1051 | CODE_SIGN_IDENTITY[sdk=iphoneos*] 1052 | iPhone Developer 1053 | DEVELOPMENT_TEAM 1054 | 1055 | ENABLE_BITCODE 1056 | NO 1057 | INFOPLIST_FILE 1058 | ViewTrackerSDKDemo/Info.plist 1059 | IPHONEOS_DEPLOYMENT_TARGET 1060 | 7.0 1061 | LD_RUNPATH_SEARCH_PATHS 1062 | $(inherited) @executable_path/Frameworks 1063 | PRODUCT_BUNDLE_IDENTIFIER 1064 | com.ViewTrackerSDKDemo 1065 | PRODUCT_NAME 1066 | $(TARGET_NAME) 1067 | PROVISIONING_PROFILE 1068 | 1069 | PROVISIONING_PROFILE_SPECIFIER 1070 | 1071 | 1072 | isa 1073 | XCBuildConfiguration 1074 | name 1075 | Release 1076 | 1077 | 1078 | rootObject 1079 | D45C1DF71E9B6A85008285A4 1080 | 1081 | 1082 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. 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 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | #import "ViewController.h" 12 | #import "TableViewController.h" 13 | #import "ViewTrackerProxy.h" 14 | 15 | #import 16 | 17 | @interface AppDelegate () 18 | 19 | @end 20 | 21 | @implementation AppDelegate 22 | 23 | 24 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 25 | // Override point for customization after application launch. 26 | 27 | 28 | // init ViewTrackerSDK 29 | [[TMViewTrackerManager sharedManager] setCommitProtocol:[ViewTrackerProxy new]]; 30 | 31 | // init UI 32 | UITabBarController *tabBarVC = [[UITabBarController alloc] init]; 33 | tabBarVC.tabBar.alpha = 0.5f; 34 | 35 | ViewController *tab1 = [ViewController new]; 36 | tab1.title = @"tab1"; 37 | 38 | TableViewController *tab2 =[TableViewController new]; 39 | tab2.title = @"tab2"; 40 | 41 | [tabBarVC setViewControllers:@[[[UINavigationController alloc] initWithRootViewController:tab1], 42 | [[UINavigationController alloc] initWithRootViewController:tab2]]]; 43 | 44 | self.window.rootViewController = tabBarVC; 45 | return YES; 46 | } 47 | 48 | 49 | - (void)applicationWillResignActive:(UIApplication *)application { 50 | // 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. 51 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 52 | } 53 | 54 | 55 | - (void)applicationDidEnterBackground:(UIApplication *)application { 56 | // 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. 57 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 58 | } 59 | 60 | 61 | - (void)applicationWillEnterForeground:(UIApplication *)application { 62 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 63 | } 64 | 65 | 66 | - (void)applicationDidBecomeActive:(UIApplication *)application { 67 | // 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. 68 | } 69 | 70 | 71 | - (void)applicationWillTerminate:(UIApplication *)application { 72 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 73 | } 74 | 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/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 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/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 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/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 | 27 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/CollectionViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCell.h 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CollectionViewCell : UICollectionViewCell 12 | 13 | @property (nonatomic, strong) UIImageView *imageView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/CollectionViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCell.m 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import "CollectionViewCell.h" 10 | 11 | @implementation CollectionViewCell 12 | 13 | - (instancetype)initWithFrame:(CGRect)frame 14 | { 15 | if (self = [super initWithFrame:frame]) { 16 | _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; 17 | [self.contentView addSubview:_imageView]; 18 | } 19 | 20 | return self; 21 | } 22 | @end 23 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/CycleScrollView.h: -------------------------------------------------------------------------------- 1 | // 2 | // CycleScrollView.h 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //typedef enum { 12 | // SDCycleScrollViewPageContolAlimentRight, 13 | // SDCycleScrollViewPageContolAlimentCenter 14 | //} SDCycleScrollViewPageContolAliment; 15 | // 16 | //typedef enum { 17 | // SDCycleScrollViewPageContolStyleClassic, // 系统自带经典样式 18 | // SDCycleScrollViewPageContolStyleAnimated, // 动画效果pagecontrol 19 | // SDCycleScrollViewPageContolStyleNone // 不显示pagecontrol 20 | //} SDCycleScrollViewPageContolStyle; 21 | 22 | @class CycleScrollView; 23 | 24 | @protocol CycleScrollViewDelegate 25 | 26 | @optional 27 | 28 | /** 点击图片回调 */ 29 | - (void)cycleScrollView:(CycleScrollView *)cycleScrollView didSelectItemAtIndex:(NSInteger)index; 30 | 31 | /** 图片滚动回调 */ 32 | - (void)cycleScrollView:(CycleScrollView *)cycleScrollView didScrollToIndex:(NSInteger)index; 33 | 34 | @end 35 | 36 | @interface CycleScrollView : UIView 37 | 38 | /** 本地图片轮播初始化方式 */ 39 | + (instancetype)cycleScrollViewWithFrame:(CGRect)frame imageNamesGroup:(NSArray *)imageNamesGroup; 40 | 41 | /** 本地图片轮播初始化方式2,infiniteLoop:是否无限循环 */ 42 | + (instancetype)cycleScrollViewWithFrame:(CGRect)frame shouldInfiniteLoop:(BOOL)infiniteLoop imageNamesGroup:(NSArray *)imageNamesGroup; 43 | 44 | /** 本地图片数组 */ 45 | @property (nonatomic, strong) NSArray *localizationImageNamesGroup; 46 | 47 | /** 自动滚动间隔时间,默认2s */ 48 | @property (nonatomic, assign) CGFloat autoScrollTimeInterval; 49 | 50 | /** 是否无限循环,默认Yes */ 51 | @property (nonatomic,assign) BOOL infiniteLoop; 52 | 53 | /** 是否自动滚动,默认Yes */ 54 | @property (nonatomic,assign) BOOL autoScroll; 55 | 56 | /** 图片滚动方向,默认为水平滚动 */ 57 | @property (nonatomic, assign) UICollectionViewScrollDirection scrollDirection; 58 | 59 | @property (nonatomic, weak) id delegate; 60 | 61 | /** 解决viewWillAppear时出现时轮播图卡在一半的问题,在控制器viewWillAppear时调用此方法 */ 62 | - (void)adjustWhenControllerViewWillAppera; 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/CycleScrollView.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | #import "CycleScrollView.h" 9 | #import "CollectionViewCell.h" 10 | 11 | #import 12 | 13 | static NSString * const ID = @"CycleScrollView_CollectionViewCell"; 14 | @interface CycleScrollView () 15 | 16 | 17 | @property (nonatomic, strong) UICollectionView *collectionView; 18 | @property (nonatomic, strong) UICollectionViewFlowLayout *flowLayout; 19 | 20 | @property (nonatomic, strong) NSArray *imagePathsGroup; 21 | @property (nonatomic, strong) NSTimer *timer; 22 | 23 | @property (nonatomic, assign) NSInteger totalItemsCount; 24 | @property (nonatomic, strong) UIPageControl *pageControl; 25 | @end 26 | 27 | @implementation CycleScrollView 28 | 29 | - (instancetype)initWithFrame:(CGRect)frame 30 | { 31 | if (self = [super initWithFrame:frame]) { 32 | [self initialization]; 33 | [self setupcollectionView]; 34 | } 35 | return self; 36 | } 37 | 38 | - (void)initialization 39 | { 40 | _autoScrollTimeInterval = 5.0; 41 | _autoScroll = YES; 42 | _infiniteLoop = YES; 43 | 44 | self.backgroundColor = [UIColor lightGrayColor]; 45 | } 46 | 47 | + (instancetype)cycleScrollViewWithFrame:(CGRect)frame imageNamesGroup:(NSArray *)imageNamesGroup 48 | { 49 | CycleScrollView *cycleScrollView = [[self alloc] initWithFrame:frame]; 50 | cycleScrollView.localizationImageNamesGroup = [NSMutableArray arrayWithArray:imageNamesGroup]; 51 | return cycleScrollView; 52 | } 53 | 54 | + (instancetype)cycleScrollViewWithFrame:(CGRect)frame shouldInfiniteLoop:(BOOL)infiniteLoop imageNamesGroup:(NSArray *)imageNamesGroup 55 | { 56 | CycleScrollView *cycleScrollView = [[self alloc] initWithFrame:frame]; 57 | cycleScrollView.infiniteLoop = infiniteLoop; 58 | cycleScrollView.localizationImageNamesGroup = [NSMutableArray arrayWithArray:imageNamesGroup]; 59 | return cycleScrollView; 60 | } 61 | 62 | // 设置显示图片的collectionView 63 | - (void)setupcollectionView 64 | { 65 | _flowLayout = [[UICollectionViewFlowLayout alloc] init]; 66 | _flowLayout.minimumLineSpacing = 0; 67 | _flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; 68 | _flowLayout.itemSize = CGSizeMake(self.bounds.size.width, self.bounds.size.height); 69 | 70 | 71 | _collectionView = [[UICollectionView alloc] initWithFrame:self.bounds collectionViewLayout:_flowLayout]; 72 | _collectionView.backgroundColor = [UIColor clearColor]; 73 | _collectionView.pagingEnabled = YES; 74 | _collectionView.showsHorizontalScrollIndicator = NO; 75 | _collectionView.showsVerticalScrollIndicator = NO; 76 | [_collectionView registerClass:[CollectionViewCell class] forCellWithReuseIdentifier:ID]; 77 | _collectionView.dataSource = self; 78 | _collectionView.delegate = self; 79 | _collectionView.scrollsToTop = NO; 80 | [self addSubview:_collectionView]; 81 | 82 | 83 | _pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(self.bounds.size.width -100, self.bounds.size.height-50, 100, 50)]; 84 | _pageControl.numberOfPages = self.imagePathsGroup.count; 85 | _pageControl.currentPage = 0; 86 | _pageControl.hidesForSinglePage = YES; 87 | _pageControl.pageIndicatorTintColor = [UIColor blueColor]; 88 | _pageControl.currentPageIndicatorTintColor = [UIColor redColor]; 89 | [self addSubview:_pageControl]; 90 | } 91 | 92 | #pragma mark - properties 93 | 94 | - (void)setInfiniteLoop:(BOOL)infiniteLoop 95 | { 96 | _infiniteLoop = infiniteLoop; 97 | 98 | if (self.imagePathsGroup.count) { 99 | self.imagePathsGroup = self.imagePathsGroup; 100 | } 101 | } 102 | 103 | -(void)setAutoScroll:(BOOL)autoScroll{ 104 | _autoScroll = autoScroll; 105 | 106 | [self invalidateTimer]; 107 | 108 | if (_autoScroll) { 109 | [self setupTimer]; 110 | } 111 | } 112 | 113 | - (void)setAutoScrollTimeInterval:(CGFloat)autoScrollTimeInterval 114 | { 115 | _autoScrollTimeInterval = autoScrollTimeInterval; 116 | 117 | [self setAutoScroll:self.autoScroll]; 118 | } 119 | 120 | - (void)setImagePathsGroup:(NSArray *)imagePathsGroup 121 | { 122 | if (imagePathsGroup.count < _imagePathsGroup.count) { 123 | [_collectionView setContentOffset:CGPointZero animated:NO]; 124 | } 125 | 126 | _imagePathsGroup = imagePathsGroup; 127 | 128 | _totalItemsCount = self.infiniteLoop ? self.imagePathsGroup.count * 100 : self.imagePathsGroup.count; 129 | _pageControl.numberOfPages = self.imagePathsGroup.count; 130 | 131 | if (imagePathsGroup.count != 1) { 132 | self.collectionView.scrollEnabled = YES; 133 | [self setAutoScroll:self.autoScroll]; 134 | } else { 135 | [self invalidateTimer]; 136 | self.collectionView.scrollEnabled = NO; 137 | } 138 | 139 | [self.collectionView reloadData]; 140 | 141 | } 142 | 143 | - (void)setLocalizationImageNamesGroup:(NSArray *)localizationImageNamesGroup 144 | { 145 | _localizationImageNamesGroup = localizationImageNamesGroup; 146 | self.imagePathsGroup = [localizationImageNamesGroup copy]; 147 | } 148 | 149 | #pragma mark - actions 150 | 151 | - (void)setupTimer 152 | { 153 | NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:self.autoScrollTimeInterval target:self selector:@selector(automaticScroll) userInfo:nil repeats:YES]; 154 | _timer = timer; 155 | [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 156 | } 157 | 158 | - (void)invalidateTimer 159 | { 160 | [_timer invalidate]; 161 | _timer = nil; 162 | } 163 | 164 | - (void)automaticScroll 165 | { 166 | if (0 == _totalItemsCount) return; 167 | int currentIndex = [self currentIndex]; 168 | int targetIndex = currentIndex + 1; 169 | if (targetIndex >= _totalItemsCount) { 170 | if (self.infiniteLoop) { 171 | targetIndex = _totalItemsCount * 0.5; 172 | [_collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:NO]; 173 | } 174 | return; 175 | } 176 | [_collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:YES]; 177 | } 178 | 179 | - (int)currentIndex 180 | { 181 | int index = 0; 182 | if (_flowLayout.scrollDirection == UICollectionViewScrollDirectionHorizontal) { 183 | index = (_collectionView.contentOffset.x + _flowLayout.itemSize.width * 0.5) / _flowLayout.itemSize.width; 184 | } else { 185 | index = (_collectionView.contentOffset.y + _flowLayout.itemSize.height * 0.5) / _flowLayout.itemSize.height; 186 | } 187 | return MAX(0, index); 188 | } 189 | 190 | #pragma mark - life circles 191 | //解决当父View释放时,当前视图因为被Timer强引用而不能释放的问题 192 | - (void)willMoveToSuperview:(UIView *)newSuperview 193 | { 194 | if (!newSuperview) { 195 | [self invalidateTimer]; 196 | } 197 | } 198 | 199 | //解决当timer释放后 回调scrollViewDidScroll时访问野指针导致崩溃 200 | - (void)dealloc { 201 | _collectionView.delegate = nil; 202 | _collectionView.dataSource = nil; 203 | } 204 | 205 | #pragma mark - public actions 206 | 207 | - (void)adjustWhenControllerViewWillAppera 208 | { 209 | long targetIndex = [self currentIndex]; 210 | if (targetIndex < _totalItemsCount) { 211 | [_collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:NO]; 212 | } 213 | } 214 | 215 | 216 | #pragma mark - UICollectionViewDataSource 217 | 218 | - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 219 | { 220 | return _totalItemsCount; 221 | } 222 | 223 | - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 224 | { 225 | CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath]; 226 | long itemIndex = indexPath.item % self.imagePathsGroup.count; 227 | 228 | NSString *imagePath = self.imagePathsGroup[itemIndex]; 229 | 230 | if ([imagePath isKindOfClass:[NSString class]]) { 231 | UIImage *image = [UIImage imageNamed:imagePath]; 232 | if (!image) { 233 | image = [UIImage imageWithContentsOfFile:imagePath]; 234 | } 235 | cell.imageView.contentMode = UIViewContentModeScaleAspectFit; 236 | cell.imageView.image = image; 237 | cell.controlName = [NSString stringWithFormat:@"banner-%ld", itemIndex]; 238 | } 239 | 240 | return cell; 241 | } 242 | 243 | - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath 244 | { 245 | if ([self.delegate respondsToSelector:@selector(cycleScrollView:didSelectItemAtIndex:)]) { 246 | [self.delegate cycleScrollView:self didSelectItemAtIndex:indexPath.item % self.imagePathsGroup.count]; 247 | } 248 | } 249 | 250 | 251 | #pragma mark - UIScrollViewDelegate 252 | 253 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView 254 | { 255 | if (!self.imagePathsGroup.count) return; 256 | int itemIndex = [self currentIndex]; 257 | int indexOnPageControl = itemIndex % self.imagePathsGroup.count; 258 | 259 | if ([self.pageControl isKindOfClass:[UIPageControl class]]) { 260 | UIPageControl *pageControl = (UIPageControl *)_pageControl; 261 | pageControl.currentPage = indexOnPageControl; 262 | } 263 | } 264 | 265 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView 266 | { 267 | if (self.autoScroll) { 268 | [self invalidateTimer]; 269 | } 270 | } 271 | 272 | - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate 273 | { 274 | if (self.autoScroll) { 275 | [self setupTimer]; 276 | } 277 | } 278 | 279 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView 280 | { 281 | [self scrollViewDidEndScrollingAnimation:self.collectionView]; 282 | } 283 | 284 | - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView 285 | { 286 | if (!self.imagePathsGroup.count) return; // 解决清除timer时偶尔会出现的问题 287 | int itemIndex = [self currentIndex]; 288 | int indexOnPageControl = itemIndex % self.imagePathsGroup.count; 289 | 290 | if ([self.delegate respondsToSelector:@selector(cycleScrollView:didScrollToIndex:)]) { 291 | [self.delegate cycleScrollView:self didScrollToIndex:indexOnPageControl]; 292 | } 293 | } 294 | @end 295 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/SubViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SubViewController.h 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SubViewController : UIViewController 12 | @property (nonatomic, strong) NSString *imagePath; 13 | @end 14 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/SubViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SubViewController.m 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import "SubViewController.h" 10 | 11 | #import 12 | 13 | @interface SubViewController () 14 | @property (nonatomic, strong) UIButton *button; 15 | @property (nonatomic, strong) UIImageView *imageView; 16 | @end 17 | 18 | @implementation SubViewController 19 | 20 | - (void)viewDidLoad { 21 | [super viewDidLoad]; 22 | // Do any additional setup after loading the view. 23 | if (!self.title) 24 | self.title = @"SubViewController"; 25 | 26 | self.view.backgroundColor = [UIColor lightGrayColor]; 27 | 28 | _button = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 50)]; 29 | [_button setTintColor:[UIColor blueColor]]; 30 | [_button setTitleColor:[UIColor redColor] forState:UIControlStateNormal]; 31 | [_button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside]; 32 | 33 | NSString *btnTitle = nil; 34 | if ([self isPushed]) { 35 | btnTitle = @"popViewController"; 36 | }else{ 37 | btnTitle = @"dismissViewController"; 38 | } 39 | 40 | [_button setTitle:btnTitle forState:UIControlStateNormal]; 41 | _button.controlName = btnTitle; 42 | 43 | [self.view addSubview:_button]; 44 | 45 | 46 | _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 150, self.view.bounds.size.width, 300)]; 47 | _imageView.contentMode = UIViewContentModeScaleAspectFit; 48 | [self.view addSubview:_imageView]; 49 | 50 | _imageView.controlName = _imagePath; 51 | [self setImagePath:_imagePath]; 52 | 53 | } 54 | 55 | - (void)setImagePath:(NSString *)imagePath 56 | { 57 | _imagePath = imagePath; 58 | if (imagePath.length) { 59 | UIImage *image = [UIImage imageNamed:imagePath]; 60 | if (!image) { 61 | image = [UIImage imageWithContentsOfFile:imagePath]; 62 | } 63 | 64 | _imageView.image = image; 65 | _imageView.controlName = _imagePath; 66 | } 67 | } 68 | - (void)viewDidAppear:(BOOL)animated 69 | { 70 | [super viewDidAppear:animated]; 71 | 72 | [TMViewTrackerManager setCurrentPageName:self.title]; 73 | } 74 | 75 | - (void)buttonClicked:(id)sender 76 | { 77 | if ([sender isEqual:_button]) { 78 | if ([self isPushed]) { 79 | [self.navigationController popViewControllerAnimated:YES]; 80 | }else{ 81 | [self dismissViewControllerAnimated:YES completion:^{ 82 | 83 | NSLog(@"SubViewController : dismiss SubViewController complete"); 84 | }]; 85 | } 86 | } 87 | } 88 | 89 | - (BOOL)isPushed 90 | { 91 | NSArray *viewcontrollers=self.navigationController.viewControllers; 92 | if (viewcontrollers.count>1) { 93 | if ([viewcontrollers objectAtIndex:viewcontrollers.count-1]==self) { 94 | //push方式 95 | return YES; 96 | } 97 | } 98 | 99 | return NO; 100 | } 101 | 102 | - (void)didReceiveMemoryWarning { 103 | [super didReceiveMemoryWarning]; 104 | // Dispose of any resources that can be recreated. 105 | } 106 | 107 | - (void)viewWillAppear:(BOOL)animated 108 | { 109 | [super viewWillAppear:animated]; 110 | self.tabBarController.tabBar.hidden = YES; 111 | } 112 | 113 | /* 114 | #pragma mark - Navigation 115 | 116 | // In a storyboard-based application, you will often want to do a little preparation before navigation 117 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { 118 | // Get the new view controller using [segue destinationViewController]. 119 | // Pass the selected object to the new view controller. 120 | } 121 | */ 122 | 123 | @end 124 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/TableViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.h 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface TableViewController : UITableViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/TableViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.m 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import "TableViewController.h" 10 | #import "SubViewController.h" 11 | 12 | #import 13 | 14 | @interface TableViewController () 15 | 16 | @end 17 | 18 | @implementation TableViewController 19 | 20 | - (void)viewDidLoad { 21 | [super viewDidLoad]; 22 | 23 | self.title = @"Tab2"; 24 | self.view.backgroundColor = [UIColor lightGrayColor]; 25 | 26 | // Uncomment the following line to preserve selection between presentations. 27 | // self.clearsSelectionOnViewWillAppear = NO; 28 | 29 | // Uncomment the following line to display an Edit button in the navigation bar for this view controller. 30 | // self.navigationItem.rightBarButtonItem = self.editButtonItem; 31 | } 32 | 33 | - (void)viewWillAppear:(BOOL)animated 34 | { 35 | [super viewWillAppear:animated]; 36 | self.tabBarController.tabBar.hidden = NO; 37 | } 38 | 39 | - (void)viewDidAppear:(BOOL)animated 40 | { 41 | [super viewDidAppear:animated]; 42 | 43 | [TMViewTrackerManager setCurrentPageName:self.title]; 44 | } 45 | 46 | - (void)didReceiveMemoryWarning { 47 | [super didReceiveMemoryWarning]; 48 | // Dispose of any resources that can be recreated. 49 | } 50 | 51 | #pragma mark - Table view data source 52 | 53 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 54 | return 1; 55 | } 56 | 57 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 58 | return 100; 59 | } 60 | 61 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 62 | { 63 | static NSString *CellIdentifier = @"TableViewController_CellIdentifier"; 64 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 65 | if (!cell) { 66 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; 67 | cell.backgroundColor = [UIColor lightGrayColor]; 68 | } 69 | 70 | cell.controlName = [NSString stringWithFormat:@"Cell-%ld", (long)indexPath.row]; 71 | cell.textLabel.text = [NSString stringWithFormat:@"Index : %ld", (long)indexPath.row]; 72 | return cell; 73 | } 74 | 75 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 76 | { 77 | static SubViewController *subVC = nil; 78 | subVC = [SubViewController new]; 79 | subVC.title = [NSString stringWithFormat:@"SubViewController-%ld", (long)indexPath.row]; 80 | [self.navigationController pushViewController:subVC animated:YES]; 81 | } 82 | 83 | /* 84 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 85 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:<#@"reuseIdentifier"#> forIndexPath:indexPath]; 86 | 87 | // Configure the cell... 88 | 89 | return cell; 90 | } 91 | */ 92 | 93 | /* 94 | // Override to support conditional editing of the table view. 95 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 96 | // Return NO if you do not want the specified item to be editable. 97 | return YES; 98 | } 99 | */ 100 | 101 | /* 102 | // Override to support editing the table view. 103 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 104 | if (editingStyle == UITableViewCellEditingStyleDelete) { 105 | // Delete the row from the data source 106 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; 107 | } else if (editingStyle == UITableViewCellEditingStyleInsert) { 108 | // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view 109 | } 110 | } 111 | */ 112 | 113 | /* 114 | // Override to support rearranging the table view. 115 | - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { 116 | } 117 | */ 118 | 119 | /* 120 | // Override to support conditional rearranging of the table view. 121 | - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { 122 | // Return NO if you do not want the item to be re-orderable. 123 | return YES; 124 | } 125 | */ 126 | 127 | /* 128 | #pragma mark - Navigation 129 | 130 | // In a storyboard-based application, you will often want to do a little preparation before navigation 131 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { 132 | // Get the new view controller using [segue destinationViewController]. 133 | // Pass the selected object to the new view controller. 134 | } 135 | */ 136 | 137 | @end 138 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "SubViewController.h" 11 | 12 | #import 13 | #import "CycleScrollView.h" 14 | 15 | @interface ViewController () 16 | 17 | @property (nonatomic, strong) CycleScrollView *banner; 18 | 19 | @property (nonatomic, strong) UIScrollView *scrollView; 20 | 21 | @property (nonatomic, strong) UIButton *button1; 22 | @property (nonatomic, strong) UIButton *button2; 23 | @property (nonatomic, strong) UIButton *button3; 24 | 25 | @end 26 | 27 | @implementation ViewController 28 | - (void)viewDidLoad { 29 | [super viewDidLoad]; 30 | // Do any additional setup after loading the view, typically from a nib. 31 | 32 | self.title = @"Tab1"; 33 | self.view.backgroundColor = [UIColor lightGrayColor]; 34 | 35 | CGFloat w = self.view.bounds.size.width; 36 | 37 | _banner = [CycleScrollView cycleScrollViewWithFrame:CGRectMake(0, 0, w, 300) shouldInfiniteLoop:YES imageNamesGroup:self.imageNames]; 38 | _banner.delegate = self; 39 | [self.view addSubview:_banner]; 40 | _banner.scrollDirection = UICollectionViewScrollDirectionVertical; 41 | 42 | self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 300, w, [UIScreen mainScreen].bounds.size.height - 300.f -44.f)]; 43 | self.scrollView.contentSize = CGSizeMake(w, 400.f); 44 | [self.view addSubview:_scrollView]; 45 | 46 | _button1 = [[UIButton alloc] initWithFrame:CGRectMake(50, 10, 200, 50)]; 47 | [_button1 setTintColor:[UIColor blueColor]]; 48 | [_button1 setTitleColor:[UIColor redColor] forState:UIControlStateNormal]; 49 | [_button1 setTitle:@"pushToSubVC1" forState:UIControlStateNormal]; 50 | [_button1 addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside]; 51 | 52 | _button1.controlName = @"pushToSubVC1"; 53 | _button1.commitType = ECommitTypeClick; 54 | [_scrollView addSubview:_button1]; 55 | 56 | _button2 = [[UIButton alloc] initWithFrame:CGRectMake(50, 70, 200, 50)]; 57 | [_button2 setTintColor:[UIColor blueColor]]; 58 | [_button2 setTitleColor:[UIColor redColor] forState:UIControlStateNormal]; 59 | [_button2 setTitle:@"presentSubVC2" forState:UIControlStateNormal]; 60 | [_button2 addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside]; 61 | _button2.controlName = @"presentSubVC2"; 62 | _button2.commitType = ECommitTypeClick; 63 | [_scrollView addSubview:_button2]; 64 | 65 | _button3 = [[UIButton alloc] initWithFrame:CGRectMake(50, 140, 200, 50)]; 66 | [_button3 setTintColor:[UIColor blueColor]]; 67 | [_button3 setTitleColor:[UIColor redColor] forState:UIControlStateNormal]; 68 | [_button3 setTitle:@"showAlertView" forState:UIControlStateNormal]; 69 | [_button3 addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside]; 70 | 71 | _button3.controlName = @"showAlertView"; 72 | _button3.commitType = ECommitTypeExposure; 73 | [_scrollView addSubview:_button3]; 74 | 75 | self.automaticallyAdjustsScrollViewInsets = NO; 76 | } 77 | 78 | - (void)viewWillAppear:(BOOL)animated 79 | { 80 | [super viewWillAppear:animated]; 81 | self.tabBarController.tabBar.hidden = NO; 82 | } 83 | 84 | - (void)viewDidAppear:(BOOL)animated 85 | { 86 | [super viewDidAppear:animated]; 87 | 88 | [TMViewTrackerManager setCurrentPageName:self.title]; 89 | } 90 | 91 | - (void)buttonClicked:(id)sender 92 | { 93 | UIViewController *vc = nil; 94 | if ([sender isEqual:_button1]){ 95 | vc = [SubViewController new]; 96 | vc.title = @"pushedSubViewController"; 97 | [self.navigationController pushViewController:vc animated:NO]; 98 | }else if ([sender isEqual:_button2]) 99 | { 100 | vc = [SubViewController new]; 101 | vc.title = @"presentedSubViewController"; 102 | [self presentViewController:vc animated:YES completion:^{ 103 | NSLog(@"ViewController : present SubViewController complete"); 104 | }]; 105 | }else if ([sender isEqual:_button3]) 106 | { 107 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" 108 | message:@"Alert" 109 | preferredStyle:UIAlertControllerStyleAlert]; 110 | [alert addAction:[UIAlertAction actionWithTitle:@"确定" 111 | style:UIAlertActionStyleCancel 112 | handler:^(UIAlertAction * _Nonnull action) { 113 | [alert dismissViewControllerAnimated:YES completion:^{ 114 | 115 | }]; 116 | }]]; 117 | 118 | [self presentViewController:alert animated:YES completion:^{}]; 119 | } 120 | 121 | } 122 | - (void)didReceiveMemoryWarning { 123 | [super didReceiveMemoryWarning]; 124 | // Dispose of any resources that can be recreated. 125 | } 126 | 127 | - (NSArray*)imageNames{ 128 | return @[@"h1.jpg", @"h2.jpg", @"h3.jpg", @"h4.jpg"]; 129 | } 130 | #pragma mark - delegate 131 | - (void)cycleScrollView:(CycleScrollView *)cycleScrollView didSelectItemAtIndex:(NSInteger)index 132 | { 133 | SubViewController *subVC = [[SubViewController alloc] init]; 134 | subVC.title = [NSString stringWithFormat:@"SubVC_For_Banner-%ld", index]; 135 | NSInteger itemIndex = index % [self imageNames].count; 136 | subVC.imagePath = [self imageNames][itemIndex]; 137 | [self.navigationController pushViewController:subVC animated:YES]; 138 | } 139 | @end 140 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/ViewTrackerProxy.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewTrackerProxy.h 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | @interface ViewTrackerProxy : NSObject 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/ViewTrackerProxy.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewTrackerProxy.m 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. All rights reserved. 7 | // 8 | 9 | #import "ViewTrackerProxy.h" 10 | 11 | @implementation ViewTrackerProxy 12 | - (instancetype)init 13 | { 14 | if (self = [super init]) { 15 | //init ViewTrack Config 16 | NSDictionary * dictionary = @{kExposureSwitch:@(1), 17 | kExposureWhiteList:@[@"Tab1",@"SubViewController"], 18 | kClickSwitch:@(1), 19 | kClickWhiteList:@[@"Tab1",@"SubViewController"]}; 20 | 21 | [[TMViewTrackerManager sharedManager] setViewTrackerConfig:dictionary]; 22 | 23 | //register notification to handle changes of config from server. 24 | } 25 | return self; 26 | } 27 | - (void)ctrlClicked:(NSString*)controlName 28 | onPage:(NSString*)pageName 29 | args:(NSDictionary*)args 30 | { 31 | NSLog(@"Clicked on Page(%@), controlName(%@), with args(%@)", pageName, controlName, args); 32 | } 33 | 34 | - (void)module:(NSString*)moduleName 35 | showedOnPage:(NSString*)pageName 36 | duration:(NSUInteger)duration 37 | args:(NSDictionary *)args 38 | { 39 | 40 | NSLog(@"module on Page(%@), controlName(%@), duration(%lu), with args(%@)", pageName, moduleName, (unsigned long)duration, args); 41 | } 42 | @end 43 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/h1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/TMViewTrackerSDK/700243607328ede69dc111c6c986dda574ba1b75/ViewTrackerSDKDemo/ViewTrackerSDKDemo/h1.jpg -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/h2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/TMViewTrackerSDK/700243607328ede69dc111c6c986dda574ba1b75/ViewTrackerSDKDemo/ViewTrackerSDKDemo/h2.jpg -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/h3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/TMViewTrackerSDK/700243607328ede69dc111c6c986dda574ba1b75/ViewTrackerSDKDemo/ViewTrackerSDKDemo/h3.jpg -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/h4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/TMViewTrackerSDK/700243607328ede69dc111c6c986dda574ba1b75/ViewTrackerSDKDemo/ViewTrackerSDKDemo/h4.jpg -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/ViewTrackerSDKDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // ViewTrackerSDKDemo 4 | // 5 | // Created by philip on 2017/4/10. 6 | // Copyright © 2017年 ViewTracker. 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 | -------------------------------------------------------------------------------- /ViewTrackerSDKDemo/post-install.rb: -------------------------------------------------------------------------------- 1 | # 2 | #在Podfifle中添加以下代码,实现post_install hook。 3 | # begin 4 | # cocoapods_post_install if require './cocoapods-post-install.rb' 5 | # rescue LoadError 6 | # end 7 | 8 | 9 | # 该脚本只是为了兼容0.39,增加了HEADER_SEARCH_PATHS。 10 | 11 | def cocoapods_post_install 12 | # cocoapods 0.39.0将public header中的下一层级目录去掉了,所以对于framework中头文件的引用需要改为import 的方式引入,这样是更规范,但是修改成本太高,且在framework与源码切换调试后,头文件有可能再报找不到。所以此处增加该脚本,将header search path改为和0.38一样,增加了下一层目录,这样业务方就不需要修改。对脚本有任何问题请@晨燕 13 | 14 | if Pod::VERSION >= '0.39.0' 15 | ENV["COCOAPODS_DISABLE_STATS"] = "1" 16 | Pod::Config.instance.deterministic_uuids = false 17 | post_install do |installer| 18 | installer.pods_project.targets.each do |target| 19 | # 设置Pods-Target的xcconfig 20 | if target.name.include?"Pods-" 21 | target.build_configurations.each do |config| 22 | xcconfig_path = config.base_configuration_reference.real_path 23 | build_settings = Hash[*File.read(xcconfig_path).lines.map{|x| x.split(/\s*=\s*/, 2)}.flatten] 24 | 25 | header_search_path_array = build_settings['HEADER_SEARCH_PATHS'].split(" ") 26 | append_search_path_array = Array.new 27 | header_search_path_array.each do |path| 28 | append_search_path_array << path 29 | if path.include? "${PODS_ROOT}/Headers/Public/" 30 | # 获取真实路径 31 | realpath = path.gsub('${PODS_ROOT}',installer.sandbox.root.to_s).tr('\"\'','') 32 | if Dir.exist?(realpath) 33 | # 对路径中的下一层级进行遍历 34 | Dir.entries(realpath).each do |entry| 35 | # 如果是文件夹 36 | if File.directory?(realpath+'/'+entry) and !(entry =='.' || entry == '..') 37 | # 组成一个新的路径 38 | new_path = '\''+path.tr('\"\'','')+'/'+entry+'\' ' 39 | # 将新路径加入到数组中 40 | append_search_path_array << new_path 41 | end 42 | end 43 | end 44 | end 45 | end 46 | build_settings['HEADER_SEARCH_PATHS'] = append_search_path_array.join(" ") 47 | 48 | # write build_settings dictionary to xcconfig 49 | File.open(xcconfig_path, "w") 50 | build_settings.each do |key,value| 51 | File.open(xcconfig_path, "a") {|file| file.puts "#{key} = #{value}"} 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | 60 | --------------------------------------------------------------------------------