├── .gitignore ├── AppHostExample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── AppHostExample.xcscheme └── xcuserdata │ └── hite.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── AppHostExample ├── AppDelegate.h ├── AppDelegate.m ├── AppHost.framework │ ├── AppHost │ ├── Headers │ │ ├── AHDebugServerManager.h │ │ ├── AHResponseManager.h │ │ ├── AHSchemeTaskDelegate.h │ │ ├── AppHost.h │ │ ├── AppHostEnum.h │ │ ├── AppHostProtocol.h │ │ ├── AppHostResponse.h │ │ ├── AppHostViewController+Dispatch.h │ │ ├── AppHostViewController+Extend.h │ │ ├── AppHostViewController+Scripts.h │ │ └── AppHostViewController.h │ ├── Info.plist │ ├── Modules │ │ └── module.modulemap │ ├── _CodeSignature │ │ └── CodeResources │ ├── app-access.txt │ ├── appHost_version_1.5.0.js │ ├── components │ │ └── tool-panel.js │ ├── eval.js │ ├── images │ │ ├── mobile to pc.png │ │ └── pc to mobile.png │ ├── profile │ │ ├── pageTiming.js │ │ ├── pageTiming_for_mac.js │ │ ├── profiler.js │ │ └── profiler_for_mac.js │ ├── renderjson.css │ ├── renderjson.js │ ├── server.css │ ├── server.html │ ├── server.js │ ├── testcase.tmpl │ ├── thirdParty │ │ └── weinreSupport.js │ └── vue.js ├── AppHostExample-Bridging-Header.h ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── BaseViewController.h ├── BaseViewController.m ├── DescTableViewCell.h ├── DescTableViewCell.m ├── HUDResponse.h ├── HUDResponse.m ├── Info.plist ├── MasterViewController.h ├── MasterViewController.m ├── Preview-Header.h ├── UIView+Toast.h ├── UIView+Toast.m ├── UIViewPreview.swift ├── ViewController_Preview.swift ├── View_Preview.swift ├── WebViewViewController.h ├── WebViewViewController.m ├── main.m └── resources │ └── TestCase │ ├── index.css │ ├── index.html │ ├── index.js │ └── index.png ├── AppHostExampleUITests ├── AppHostExampleUITests.swift └── Info.plist └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /AppHostExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AppHostExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AppHostExample.xcodeproj/xcshareddata/xcschemes/AppHostExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 79 | 80 | 81 | 82 | 83 | 84 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /AppHostExample.xcodeproj/xcuserdata/hite.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | AppHostExample.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 481A06A72265710700DD57BB 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /AppHostExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. 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 | -------------------------------------------------------------------------------- /AppHostExample/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | 21 | return YES; 22 | } 23 | 24 | 25 | - (void)applicationWillResignActive:(UIApplication *)application { 26 | // 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. 27 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 28 | } 29 | 30 | 31 | - (void)applicationDidEnterBackground:(UIApplication *)application { 32 | // 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. 33 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 34 | } 35 | 36 | 37 | - (void)applicationWillEnterForeground:(UIApplication *)application { 38 | // 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. 39 | } 40 | 41 | 42 | - (void)applicationDidBecomeActive:(UIApplication *)application { 43 | // 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. 44 | } 45 | 46 | 47 | - (void)applicationWillTerminate:(UIApplication *)application { 48 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 49 | } 50 | 51 | 52 | #pragma mark - Split view 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/AppHost: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hite/AppHostExample/9f691bd3974f5237a6754734609c6e781c1fcfd9/AppHostExample/AppHost.framework/AppHost -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AHDebugServerManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // AHDebugServerManager.h 3 | // AppHost 4 | // 5 | // Created by liang on 2018/12/29. 6 | // Copyright © 2018 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface AHDebugServerManager : NSObject 14 | 15 | + (instancetype)sharedInstance; 16 | 17 | - (void)start; 18 | 19 | - (void)stop; 20 | 21 | - (void)showDebugWindow; 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AHResponseManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // AHResponseManager.h 3 | // AppHost 4 | // 5 | // Created by liang on 2019/1/22. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppHostResponse.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface AHResponseManager : NSObject 15 | 16 | /** 17 | 自定义response类 18 | */ 19 | @property (nonatomic, strong, readonly) NSMutableArray *customResponseClasses; 20 | 21 | + (instancetype)defaultManager; 22 | 23 | #ifdef DEBUG 24 | 25 | /** 26 | 获取所有注册的 Response 的接口 27 | 28 | @return 返回所有 class 支持的 methods,以 class 为 key。key 对应的数据包含所有这个 class 支持的方法 29 | */ 30 | - (NSDictionary *)allResponseMethods; 31 | 32 | #endif 33 | #pragma mark - 自定义 Response 区域 34 | /** 35 | 注册自定义的 Response 36 | 37 | @param cls 可以处理响应的子类 class,其符合 AppHostProtocol 38 | */ 39 | - (void)addCustomResponse:(Class)cls; 40 | 41 | - (id)responseForAction:(NSString *)action withAppHost:(AppHostViewController * _Nonnull)appHost; 42 | 43 | - (Class)responseForAction:(NSString *)action; 44 | @end 45 | 46 | NS_ASSUME_NONNULL_END 47 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AHSchemeTaskDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AHSchemeTaskResponse.h 3 | // AppHost 4 | // 5 | // Created by liang on 2018/12/29. 6 | // Copyright © 2018 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppHostEnum.h" 11 | 12 | @import WebKit; 13 | 14 | typedef NSData*_Nonnull(^bSchemeTaskHandler)(WKWebView *_Nonnull, id _Nonnull, NSString *_Nullable * _Nullable mime); 15 | 16 | NS_ASSUME_NONNULL_BEGIN 17 | 18 | @interface AHSchemeTaskDelegate : NSObject 19 | 20 | /** 21 | 添加自定义的处理逻辑 22 | */ 23 | - (void)addHandler:(bSchemeTaskHandler)handler forDomain:(NSString */* js */)domain; 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AppHost.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppHost.h 3 | // AppHost 4 | // 5 | // Created by liang on 2018/12/27. 6 | // Copyright © 2018 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for AppHost. 12 | FOUNDATION_EXPORT double AppHostVersionNumber; 13 | 14 | //! Project version string for AppHost. 15 | FOUNDATION_EXPORT const unsigned char AppHostVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | #import 20 | #import 21 | #import 22 | #import 23 | #import 24 | #import 25 | #import 26 | #import 27 | #import 28 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AppHostEnum.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppHostEnum.h 3 | // AppHost 4 | // 5 | // Created by liang on 2018/12/28. 6 | // Copyright © 2018 liang. All rights reserved. 7 | // 8 | 9 | #ifndef AppHostEnum_h 10 | #define AppHostEnum_h 11 | 12 | // 创建一个超级厉害的宏,https://www.jianshu.com/p/cbb6b71d925d 13 | // 在 debug 模式下打印带前缀的日志,非 debug 模式下,不输出。 14 | #if !defined(AHLog) 15 | #ifdef DEBUG 16 | #define AHLog(format, ...) do {\ 17 | (NSLog)((@"[AppHost] " format), ##__VA_ARGS__); \ 18 | } while (0) 19 | #else 20 | #define AHLog(format, ...) 21 | #endif 22 | #endif 23 | 24 | //获取设备的物理高度 25 | #define AH_SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height 26 | //获取设备的物理宽度 27 | #define AH_SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width 28 | #define AH_IS_SCREEN_HEIGHT_X (AH_SCREEN_HEIGHT == 812.0f || AH_SCREEN_HEIGHT == 896.0f) 29 | 30 | #define AH_PURE_NAVBAR_HEIGHT 44 //单纯的导航的高度 31 | #define AH_NAVIGATION_BAR_HEIGHT (AH_PURE_NAVBAR_HEIGHT + [[UIApplication sharedApplication] statusBarFrame].size.height) //顶部(导航+状态栏)的高度 32 | 33 | #define AHColorFromRGB(rgbValue) [UIColor \ 34 | colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 \ 35 | green:((float)((rgbValue & 0x00FF00) >> 8))/255.0 \ 36 | blue:((float)(rgbValue & 0x0000FF))/255.0 \ 37 | alpha:1.0] 38 | 39 | #define AHColorFromRGBA(rgbValue, alphaValue) [UIColor \ 40 | colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 \ 41 | green:((float)((rgbValue & 0x00FF00) >> 8))/255.0 \ 42 | blue:((float)(rgbValue & 0x0000FF))/255.0 \ 43 | alpha:alphaValue] 44 | 45 | // 定义 oc-doc,为自动化生成测试代码和自动化注释做准备 46 | // 凡是可能多行的文字描述都用 @result,而不是@#result 47 | 48 | #define ah_concat(A, B) A##B 49 | #define ah_doc_log_prefix @"ah_doc_for_" 50 | 51 | #define ah_doc_begin(log, desc) +(NSDictionary *)ah_concat(ah_doc_for_, log)\ 52 | {\ 53 | return @{\ 54 | @"discuss":@desc, 55 | 56 | #define ah_doc_code(code) "code":@#code, 57 | //ah_doc_code_result 是为了给 ah_doc_code 的代码执行后结果的描述, 58 | #define ah_doc_code_result(result) "codeResult":@result, 59 | 60 | #define ah_doc_param(paramName, paramDesc) "param":@{@#paramName:@paramDesc}, 61 | 62 | #define ah_doc_return(type, desc) "return":@{@#type:@desc} 63 | 64 | #define ah_doc_end };\ 65 | } 66 | // oc-doc 结束 67 | 68 | #endif /* AppHostEnum_h */ 69 | 70 | #ifdef AH_VIEWCONTROLLER_BASE 71 | #define AH_VC_BASE_NAME AH_VIEWCONTROLLER_BASE 72 | #else 73 | #define AH_VC_BASE_NAME UIViewController 74 | #endif 75 | 76 | #define NOW_TIME [[NSDate date] timeIntervalSince1970] * 1000 77 | 78 | // 为了解决 webview Cookie 而需要提前加载的页面 79 | extern NSString * _Nonnull kFakeCookieWebPageURLWithQueryString; 80 | // 设置进度条的颜色,如 "0xff00ff"; 81 | extern long long kWebViewProgressTintColorRGB; 82 | // 是否打开 debug server 的日志。 83 | extern BOOL kGCDWebServer_logging_enabled; 84 | 85 | static NSString * _Nonnull kAHLogoutNotification = @"kAHLogoutNotification"; 86 | static NSString * _Nonnull kAHLoginSuccessNotification = @"kAHLoginSuccessNotification"; 87 | 88 | static NSString * _Nonnull kAppHostEventDismissalFromPresented = @"kAppHostEventDismissalFromPresented"; 89 | // core 90 | static NSString * _Nonnull kAHActionKey = @"action"; 91 | static NSString * _Nonnull kAHParamKey = @"param"; 92 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AppHostProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // MKAppHost.h 3 | // 4 | // Created by liang on 05/01/2018. 5 | // Copyright © 2018 smilly.co All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @import WebKit; 11 | @class AppHostViewController; 12 | 13 | static NSString *const kAppHostURLScheme = @"apphost"; 14 | static NSString *const kAppHostURLProtocal = @"apphost://"; 15 | static NSString *const kAppHostURLImageHost = @"image.apphost.hite.me"; 16 | static NSString *const kAppHostURLScriptHost = @"js.apphost.hite.me"; 17 | static NSString *const kAppHostURLStyleHost = @"css.apphost.hite.me"; 18 | 19 | #define AppHostURLScriptServer [kAppHostURLProtocal stringByAppendingString:kAppHostURLScriptHost] 20 | #define AppHostURLStyleServer [kAppHostURLProtocal stringByAppendingString:kAppHostURLStyleHost] 21 | #define AppHostURLImageServer [kAppHostURLProtocal stringByAppendingString:kAppHostURLImageHost] 22 | 23 | @protocol AppHostProtocol 24 | 25 | // 以下为 从AppHostViewController 里获得的 只读类属性 26 | @property (nonatomic, weak, readonly) UINavigationController *navigationController; 27 | 28 | @property (nonatomic, weak, readonly) WKWebView *webView; 29 | 30 | @property (nonatomic, weak, readonly) AppHostViewController *appHost; 31 | 32 | @required 33 | 34 | - (instancetype)initWithAppHost:(AppHostViewController *)appHost; 35 | 36 | /** 37 | 尝试处理来自 h5 的请求,如果不能处理,则返回 NO。 38 | 39 | @param action h5 的 actionName 40 | @param paramDict 本次请求的参数 41 | @param callbackKey js 端匿名回调 42 | @return YES 表示可以处理,已处理; 43 | */ 44 | - (BOOL)handleAction:(NSString *)action withParam:(NSDictionary *)paramDict callbackKey:(NSString *)callbackKey; 45 | 46 | /** 47 | 类方法。表示当前请类型是否支持 48 | 49 | @param actionName action 的名词 50 | @return YES 表示支持,请注意 51 | */ 52 | + (BOOL)isSupportedAction:(NSString *)actionName; 53 | 54 | /** 55 | 返回接口的支持情况, 申明为类方法是为了用同步的方法 返回给 appHost,作为 JS 的属性。 56 | 57 | @return 形如, 58 | { 59 | @"alert": @"1", 60 | @"confrim": @"1" 61 | } 62 | */ 63 | + (NSDictionary *)supportActionList; 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AppHostResponse.h: -------------------------------------------------------------------------------- 1 | // 2 | // MKAppHostResponse.h 3 | 4 | // 5 | // Created by liang on 05/01/2018. 6 | // Copyright © 2018 smilly.co All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppHostProtocol.h" 11 | #import "AppHostEnum.h" 12 | 13 | @interface AppHostResponse : NSObject 14 | 15 | /** 16 | * 调用 callback 的函数,这个函数是 js 端调用方法时,注册在 js 端的 block。 17 | * 这里传入的第一个参数是 和这个 js 端 block 相关联的 key。js 根据这个 key 找到这个 block 并且执行 18 | */ 19 | - (void)fireCallback:(NSString *)callbackKey param:(NSDictionary *)paramDict; 20 | 21 | /** 22 | * 辅助方法,转发到 appHost 的接口 23 | */ 24 | - (void)fire:(NSString *)actionName param:(NSDictionary *)paramDict; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AppHostViewController+Dispatch.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppHostViewController+Dispatch.h 3 | // AppHost 4 | // 5 | // Created by liang on 2019/3/23. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import "AppHostViewController.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface AppHostViewController (Dispatch) 14 | 15 | /** 16 | * 核心的h5调用native接口的分发器; 17 | * @return 是否已经被处理,YES 表示可被处理; 18 | */ 19 | - (BOOL)callNative:(NSString *)action parameter:(NSDictionary *)paramDict; 20 | 21 | #pragma mark - like private 22 | 23 | - (void)dispatchParsingParameter:(NSDictionary *)contentJSON; 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AppHostViewController+Extend.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppHostViewController+Extend.h 3 | // AppHost 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | 这个分类的意义在于为 AppHost 调用方, 15 | 有对 webview 什么周期有特殊需要的时候,可以继承 AppHostViewController,并重载相应的方法实现自有逻辑。 16 | 注意:一旦重载之后,在方法体里,需要调用 super 方法。 17 | 18 | 重要提示: 为了让调用方可以自定义逻辑,有两种方式, 19 | 一种是开放 super,使用继承的方式; 20 | 一种是新增一个代理类,在不同的回调里,让调用方自行实现需要的方法。 21 | 22 | 此外,我们也使用过 webviewjsbridge 对 UIWebview 和 WKWebview 上面封装的方式来提供接口分离,解耦。 23 | 然后在实际的使用过程中,发现这是一种非常丑陋的提供灵活的方式。 24 | 综合考虑,我们使用第一种,更灵活,缺点是开放了较多的接口,所以如非必要,不要继承 AppHostViewController 类。 25 | */ 26 | @interface AppHostViewController (Extend) 27 | 28 | - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler; 29 | 30 | - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler; 31 | 32 | - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation; 33 | 34 | - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(nonnull WKNavigationResponse *)navigationResponse decisionHandler:(nonnull void (^)(WKNavigationResponsePolicy))decisionHandler; 35 | 36 | - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation; 37 | 38 | - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation; 39 | 40 | - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation; 41 | 42 | - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error; 43 | 44 | - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error; 45 | 46 | @end 47 | 48 | NS_ASSUME_NONNULL_END 49 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AppHostViewController+Scripts.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppHostViewController+Scripts.h 3 | // AppHost 4 | // 5 | // Created by liang on 2019/3/23. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import "AppHostViewController.h" 10 | 11 | @class WKUserContentController; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface AppHostViewController (Scripts) 16 | 17 | /** 18 | * 调用 callback 的函数,这个函数是 js 端调用方法时,注册在 js 端的 block。 19 | * 这里传入的第一个参数是 和这个 js 端 block 相关联的 key。js 根据这个 key 找到这个 block 并且执行 20 | */ 21 | - (void)fireCallback:(NSString *)actionName param:(NSDictionary *)paramDict; 22 | /** 23 | * 对应,监听了事件的接口的调用 24 | */ 25 | - (void)fire:(NSString *)actionName param:(NSDictionary *)paramDict; 26 | 27 | /** 28 | 无返回值的执行 js 代码 29 | 30 | @param javaScriptString 可执行的 js 代码 31 | */ 32 | - (void)executeJavaScriptString:(NSString *)javaScriptString; 33 | /** 34 | 需要返回值的 js 代码。可以返回例如 document 之类的,JSValue 无法映射的数据对象 35 | 36 | @param jsCode 可执行的 js 代码,注意:如果有引号,需要使用双引号。 37 | @param completion 返回的回调 38 | */ 39 | - (void)evalExpression:(NSString *)jsCode completion:(void (^)(id result, NSString *err))completion; 40 | /** 41 | 设置在 userscript 里要加载的脚本。如果已经打开了 webview,则这些设置需要在下次执行时生效 42 | 43 | @param script 注入的脚本,string\ url,两种类型 44 | @param injectTime 注入时机 45 | @param key 这段脚本的标识,为了后续的删除 46 | */ 47 | + (void)prepareJavaScript:(id)script when:(WKUserScriptInjectionTime)injectTime key:(NSString *)key; 48 | + (void)removeJavaScriptForKey:(NSString *)key; 49 | 50 | #pragma mark - like private 51 | - (void)insertData:(NSDictionary *)json intoPageWithVarName:(NSString *)appProperty; 52 | 53 | - (void)injectScriptsToUserContent:(WKUserContentController *)userContent; 54 | 55 | @end 56 | 57 | NS_ASSUME_NONNULL_END 58 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Headers/AppHostViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppHostViewController.h 3 | 4 | // 5 | // Created by hite on 9/22/15. 6 | // Copyright © 2015 smilly.co All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppHostProtocol.h" 11 | #import "AHSchemeTaskDelegate.h" 12 | #import "AppHostEnum.h" 13 | 14 | static NSString *kAppHostInvokeRequestEvent = @"kAppHostInvokeRequestEvent"; 15 | static NSString *kAppHostInvokeResponseEvent = @"kAppHostInvokeResponseEvent"; 16 | static NSString *kAppHostInvokeDebugEvent = @"kAppHostInvokeDebugEvent"; 17 | 18 | @class AppHostViewController; 19 | 20 | /** 21 | 监听 Response 里的事件; 22 | */ 23 | @protocol AppHostViewControllerDelegate 24 | 25 | - (void)onResponseEventOccurred:(NSString *)eventName response:(id)response; 26 | 27 | @end 28 | 29 | @interface AppHostViewController : AH_VC_BASE_NAME 30 | 31 | @property (nonatomic, copy) NSString *pageTitle; 32 | 33 | /** 34 | 当使用 url 地址加载页面时,url 代表了初始的 url。当载入初始 url 后,页面的地址还可能发生变化,此时不等于此 url。 35 | */ 36 | @property (nonatomic, copy) NSString *url; 37 | /** 38 | * 右上角的文案 39 | */ 40 | @property (nonatomic, copy) NSString *rightActionBarTitle; 41 | 42 | // 43 | @property (nonatomic, strong, readonly) WKWebView *webView; 44 | 45 | /** 46 | 定制状态栏的配色 47 | */ 48 | @property (nonatomic, assign) UIStatusBarStyle navBarStyle; 49 | /** 50 | 不容许进度条 51 | */ 52 | @property (nonatomic, assign) BOOL disabledProgressor; 53 | 54 | /** 55 | 取消记住上次浏览历史的特性 56 | */ 57 | @property (nonatomic, assign) BOOL disableScrollPositionMemory; 58 | /** 59 | * 指,当点击导航栏的back按钮时候,执行的跳转,并且这个跳转到这个链接 60 | */ 61 | @property (nonatomic, strong) NSDictionary *backPageParameter; 62 | 63 | // 处理 Response 内部发送的事件,这些事件,除了 h5 关心之外,可能 native 本身也很关心 64 | @property (nonatomic, weak) id appHostDelegate; 65 | //核心的函数分发机制。可以继承, 66 | 67 | /** 68 | 是否是被presented 69 | */ 70 | @property (nonatomic, assign) BOOL fromPresented; 71 | 72 | @property (nonatomic, strong, readonly) AHSchemeTaskDelegate *taskDelegate; 73 | 74 | #pragma mark - 使用缓存渲染界面 75 | /** 76 | 加载本地 html 资源,支持发送 xhr 请求 77 | 78 | @param url 打开的文件路径 79 | @param baseDomain 发送 xhr 请求的主域名地址,如 http://you.163.com 80 | */ 81 | - (void)loadLocalFile:(NSURL *)url domain:(NSString *)baseDomain; 82 | 83 | /** 84 | 加载本地文件夹。文件夹只支持 HTML,JS,CSS 文件。 85 | 在 iOS 11 以上使用 taskscheme,iOS 8+ 以上使用文件合并,不支持本地图片; 86 | 87 | @param fileName 主 HTML 文件的文件名,是个相对路径。 html 文件里应用的内部 js、css 文件都是相对于 directory 参数的 88 | @param directory 相对路径,包含 HTML,JS,CSS 文件 89 | @param baseDomain 为了解决相对路径 发送 xhr 请求的主域名地址,如 http://you.163.com 90 | */ 91 | - (void)loadIndexFile:(NSString *)fileName inDirectory:(NSURL *)directory domain:(NSString *)baseDomain; 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hite/AppHostExample/9f691bd3974f5237a6754734609c6e781c1fcfd9/AppHostExample/AppHost.framework/Info.plist -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module AppHost { 2 | umbrella header "AppHost.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Headers/AHDebugServerManager.h 8 | 9 | 63yfqK8N5X/6qL4qAHk/mpjJcr0= 10 | 11 | Headers/AHResponseManager.h 12 | 13 | 1efPT5Eac0DCyMKpEqpkiW5tKq4= 14 | 15 | Headers/AHSchemeTaskDelegate.h 16 | 17 | qs9xLNxz1XVqd6DOCUlFhZ9JNxc= 18 | 19 | Headers/AppHost.h 20 | 21 | W0zTKIPw1GaKGH0GKLxMReVLZsw= 22 | 23 | Headers/AppHostEnum.h 24 | 25 | RlZNosNXbp7S4j3drKgQIP8utkI= 26 | 27 | Headers/AppHostProtocol.h 28 | 29 | xjX8m6wOn33kqJKcVPyKg8btGB8= 30 | 31 | Headers/AppHostResponse.h 32 | 33 | 9IPDe/JUcp7XMzVfvygSDdq4D10= 34 | 35 | Headers/AppHostViewController+Dispatch.h 36 | 37 | 6nkdfZXLGJto2NutiOp5Z/3Lfo0= 38 | 39 | Headers/AppHostViewController+Extend.h 40 | 41 | QJjHvQxurjkfE+qubsZhRhcPAIY= 42 | 43 | Headers/AppHostViewController+Scripts.h 44 | 45 | Wk7ROn0UcP9Dhsj2cOCdx1uid6g= 46 | 47 | Headers/AppHostViewController.h 48 | 49 | gzCACUTQIqYU1lpwCppXC/uyCC8= 50 | 51 | Info.plist 52 | 53 | cEGggp+qh8jccjffgYGxggpOAxw= 54 | 55 | Modules/module.modulemap 56 | 57 | 6vKkX1FSk+NU7mq+r/G1jHFa+pc= 58 | 59 | app-access.txt 60 | 61 | CfBwfDl8H76fZZm7YmIzuHU51Z8= 62 | 63 | appHost_version_1.5.0.js 64 | 65 | jZAx9iidone+7zuNLaYvFK2mbhM= 66 | 67 | components/tool-panel.js 68 | 69 | r5rAAF6tgSF8/rc+cqtk173DL74= 70 | 71 | eval.js 72 | 73 | G9PpRi7QOcJ0NlA7jDPliHHzy98= 74 | 75 | images/mobile to pc.png 76 | 77 | +EeM0M9sa/ot+UxrlxaLdcOua5k= 78 | 79 | images/pc to mobile.png 80 | 81 | mY/9ACUG5L6l0fWv35SG8R7d6cM= 82 | 83 | profile/pageTiming.js 84 | 85 | fQ2VtLTgabul2cXOmXWs4sekCio= 86 | 87 | profile/pageTiming_for_mac.js 88 | 89 | G6GBjhOVeM9pB08YtILkLCvBiMU= 90 | 91 | profile/profiler.js 92 | 93 | dNXjng9ptIHI05t64kAwYDs38SE= 94 | 95 | profile/profiler_for_mac.js 96 | 97 | CE5GuTaWAYlwCWg9eZ/gp4A33ZM= 98 | 99 | renderjson.css 100 | 101 | R54n2ooHKFBgdS3TZKzjvu+o8J8= 102 | 103 | renderjson.js 104 | 105 | GRuXs+mWeg4LqZIrsqX+toxU0iQ= 106 | 107 | server.css 108 | 109 | LakAkj3NOVsHR/nxuZsO3Wn7LVE= 110 | 111 | server.html 112 | 113 | jZNYYSS46nLgbEJn9hkaGusQ5aY= 114 | 115 | server.js 116 | 117 | 74BEjpYURUYSmcOAeWzFYihDoXE= 118 | 119 | testcase.tmpl 120 | 121 | QPEDHdrM8fZ1/46eOa/H0vWtE5Q= 122 | 123 | thirdParty/weinreSupport.js 124 | 125 | itMEFEuoWIPvoHUk/pig3Zy++nM= 126 | 127 | vue.js 128 | 129 | hIxnt6PGllAGXyiz3BRCyjyA/+g= 130 | 131 | 132 | files2 133 | 134 | Headers/AHDebugServerManager.h 135 | 136 | hash2 137 | 138 | whNpKqsCMD0wCNXUDflfL/GjHV8B3OyXB7oBlO8P52o= 139 | 140 | 141 | Headers/AHResponseManager.h 142 | 143 | hash2 144 | 145 | nU1ERvmLiDjan9YkfwObI7jdIOQaYYBUy5zuB6W/aS4= 146 | 147 | 148 | Headers/AHSchemeTaskDelegate.h 149 | 150 | hash2 151 | 152 | 6TgsWxPYlfJVX+54s8kX87P9VEsi2HQyTWPMXpMwZoE= 153 | 154 | 155 | Headers/AppHost.h 156 | 157 | hash2 158 | 159 | efkQtaa7PmZiEaLirOsA6Q9EQAs5IQoiS3hoaw9ZjGg= 160 | 161 | 162 | Headers/AppHostEnum.h 163 | 164 | hash2 165 | 166 | MQoAChGl9hRVDPLeozE108tRCoUfwXcEpWsp/RHY8BE= 167 | 168 | 169 | Headers/AppHostProtocol.h 170 | 171 | hash2 172 | 173 | VPtMfmeqDWJ8D0EV1yscDTPxwprbfjmZQ0x9qJK3bYo= 174 | 175 | 176 | Headers/AppHostResponse.h 177 | 178 | hash2 179 | 180 | 2w4M0a+agPzpCEVjNYVIY3xF0s/vAKHwIDHpPjqHNMk= 181 | 182 | 183 | Headers/AppHostViewController+Dispatch.h 184 | 185 | hash2 186 | 187 | 4aGPn3Vn9bl1nz+3SHtl60wRQ3FRwlqhT6zWtmo8mNU= 188 | 189 | 190 | Headers/AppHostViewController+Extend.h 191 | 192 | hash2 193 | 194 | qekTBg/8qlrfsqq7VCddLcca/byJfz6yt4KmUU24rGU= 195 | 196 | 197 | Headers/AppHostViewController+Scripts.h 198 | 199 | hash2 200 | 201 | cUrYBgboESlpS7jtvDzPlZ5s7u2nzhwVc2FD+eJDl80= 202 | 203 | 204 | Headers/AppHostViewController.h 205 | 206 | hash2 207 | 208 | B9Zb0LgQxwXGlK+QNjzWWmRk6m02EX/5Li7dxQ+XwV0= 209 | 210 | 211 | Modules/module.modulemap 212 | 213 | hash2 214 | 215 | 9ePgnZlMIioEdbztpHkDf2LTlMzz71TbaUpmQgMTRVY= 216 | 217 | 218 | app-access.txt 219 | 220 | hash2 221 | 222 | gnL58d9VrHTgm7jt7SQDGp6gT3eTrOpOHbS2c8xROHc= 223 | 224 | 225 | appHost_version_1.5.0.js 226 | 227 | hash2 228 | 229 | Sd15hqvm+mgu/dIHc1Imfq9EGmH/gcwXWrNxk1K2rFY= 230 | 231 | 232 | components/tool-panel.js 233 | 234 | hash2 235 | 236 | +W8A0R7FJiIfx967YCZWidveCe7IRWBa5R5HBGQl75o= 237 | 238 | 239 | eval.js 240 | 241 | hash2 242 | 243 | pgZza3013F2d8fceJlgE6/YZbpsKI45pc0nPkC840Wk= 244 | 245 | 246 | images/mobile to pc.png 247 | 248 | hash2 249 | 250 | WVnN8l2fh8MnxcPZghHZHpZ5iBhY9Anck6KhYeFy4Do= 251 | 252 | 253 | images/pc to mobile.png 254 | 255 | hash2 256 | 257 | Sb99eIydGTR5vH8roRy0NY9AnXoolV8AeZQNOqfJMZg= 258 | 259 | 260 | profile/pageTiming.js 261 | 262 | hash2 263 | 264 | QdjxWLQocEYgSG64z6kz/m2HZ7UmhAYyDj5Ow2MZGp4= 265 | 266 | 267 | profile/pageTiming_for_mac.js 268 | 269 | hash2 270 | 271 | dtu3AZ3xxtTyVByr5OqjHszgV9bG2urXHJ97om7uvIA= 272 | 273 | 274 | profile/profiler.js 275 | 276 | hash2 277 | 278 | ujZ6ZqZKlITMDM9eVkjp9+Vz3wPZ1dptWQrf8QybRI4= 279 | 280 | 281 | profile/profiler_for_mac.js 282 | 283 | hash2 284 | 285 | 9Jq2BvNikZrzaKxd2XZTH8WXehyRk7eCTsLF9ek/vEY= 286 | 287 | 288 | renderjson.css 289 | 290 | hash2 291 | 292 | Pc+LmsZfHzTWP9H1oZLns1jCex4YZUoNQsknDZrNzx8= 293 | 294 | 295 | renderjson.js 296 | 297 | hash2 298 | 299 | mUzyWR2xj0bkcTDN+XJey10bO77gsZyoCpKVs6Flh8Y= 300 | 301 | 302 | server.css 303 | 304 | hash2 305 | 306 | KXZ465JaUgEQen6Y9zUyww4+xO8CrnIDcYTS1rLSxKA= 307 | 308 | 309 | server.html 310 | 311 | hash2 312 | 313 | v/2fbnlIfoZPPm4JmFeULrO0Ke1N5wGwU2yjfjsF5L8= 314 | 315 | 316 | server.js 317 | 318 | hash2 319 | 320 | rBN5M4C5uMCMHYXDNp2m6nTEU8Q9JVgljSD9Tksy0a4= 321 | 322 | 323 | testcase.tmpl 324 | 325 | hash2 326 | 327 | Xhs4rESeAM3B5dnFCuvs5Qx+8t3KeuSW6bwxtOJYapY= 328 | 329 | 330 | thirdParty/weinreSupport.js 331 | 332 | hash2 333 | 334 | 1IdTYRV5RMBJg/+y0FFMD7xwzLlkwnI0ezlPAcvCH+A= 335 | 336 | 337 | vue.js 338 | 339 | hash2 340 | 341 | GmgFjvxj3EgNe3TECaFoMnNKxVc3zY7Ouiyd+K4tqc8= 342 | 343 | 344 | 345 | rules 346 | 347 | ^.* 348 | 349 | ^.*\.lproj/ 350 | 351 | optional 352 | 353 | weight 354 | 1000 355 | 356 | ^.*\.lproj/locversion.plist$ 357 | 358 | omit 359 | 360 | weight 361 | 1100 362 | 363 | ^Base\.lproj/ 364 | 365 | weight 366 | 1010 367 | 368 | ^version.plist$ 369 | 370 | 371 | rules2 372 | 373 | .*\.dSYM($|/) 374 | 375 | weight 376 | 11 377 | 378 | ^(.*/)?\.DS_Store$ 379 | 380 | omit 381 | 382 | weight 383 | 2000 384 | 385 | ^.* 386 | 387 | ^.*\.lproj/ 388 | 389 | optional 390 | 391 | weight 392 | 1000 393 | 394 | ^.*\.lproj/locversion.plist$ 395 | 396 | omit 397 | 398 | weight 399 | 1100 400 | 401 | ^Base\.lproj/ 402 | 403 | weight 404 | 1010 405 | 406 | ^Info\.plist$ 407 | 408 | omit 409 | 410 | weight 411 | 20 412 | 413 | ^PkgInfo$ 414 | 415 | omit 416 | 417 | weight 418 | 20 419 | 420 | ^embedded\.provisionprofile$ 421 | 422 | weight 423 | 20 424 | 425 | ^version\.plist$ 426 | 427 | weight 428 | 20 429 | 430 | 431 | 432 | 433 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/app-access.txt: -------------------------------------------------------------------------------- 1 | #协议和h5接口权限开放白名单 2 | schema-open-url: 3 | you.163.com 4 | *.mail.163.com 5 | 6 | 7 | apphost: 8 | you.163.com 9 | *.mail.163.com 10 | 11 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/appHost_version_1.5.0.js: -------------------------------------------------------------------------------- 1 | !function() { 2 | window.appHost = { 3 | version: "1.5.1" 4 | }; 5 | 6 | var callbackPool = {}; 7 | var ack_no = 1; 8 | window.appHost.invoke = function(_action, _data, _callback) { 9 | var rndKey = 'cbk_' + new Date().getTime(); 10 | var fullParam = { 11 | action: _action, 12 | param: _data 13 | }; 14 | if (_callback) { //如果有回调函数。 15 | var rndKey = 'cbk_' + ack_no++; 16 | fullParam.callbackKey = rndKey; 17 | callbackPool[rndKey] = _callback; 18 | } 19 | 20 | window.webkit.messageHandlers.kAHScriptHandlerName.postMessage(fullParam) 21 | } 22 | var reqs = {}; 23 | window.appHost.on = function(_action, _callback) { 24 | reqs[_action + ""] = _callback; 25 | } 26 | window.appHost.__fire = function(_action, _data) { 27 | var func = reqs[_action + ""]; 28 | if (typeof func == 'function') { 29 | func(_data); 30 | } 31 | } 32 | window.appHost.__callback = function(_callbackKey, _param) { 33 | var func = callbackPool[_callbackKey]; 34 | if (typeof func == 'function') { 35 | func(_param); 36 | // 释放,只用一次 37 | callbackPool[_callbackKey] = nil; 38 | } 39 | } 40 | }(window); 41 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/components/tool-panel.js: -------------------------------------------------------------------------------- 1 | window.ah_env = { 2 | // 表示当前命令行执行的环境,isMobile = true 表示所有的语句是在远端的 webview 里执行的; false 表示是当前浏览器 3 | isMobile: false 4 | }; 5 | 6 | Vue.component("tool-panel", { 7 | data: function () { 8 | return { 9 | dataSource: [ 10 | { 11 | action: "switch", 12 | clsName: "w-tool-item w-tool-switch", 13 | title: "点击进入 mobile", 14 | text: "Off Mobile" 15 | }, 16 | { 17 | action: "help", 18 | clsName: "w-tool-item w-tool-help", 19 | title: "look for help", 20 | text: "Help" 21 | }, 22 | { 23 | action: "list", 24 | clsName: "w-tool-item w-tool-docs", 25 | title: "documents for api", 26 | text: "Docs" 27 | }, 28 | { 29 | action: "timing", 30 | clsName: "w-tool-item w-tool-timing", 31 | title: "Audit", 32 | text: "Timing" 33 | } 34 | ] 35 | }; 36 | }, 37 | methods: { 38 | useTool: function (e) { 39 | var _real_switch_env = function(){ 40 | Vue.set(this.dataSource, 0, { 41 | action: "switch", 42 | clsName: "w-tool-item w-tool-switch", 43 | title: window.ah_env.isMobile ? "点击退出 mobile" : "点击进入 mobile", 44 | text: window.ah_env.isMobile ? "On Mobile" : "Off Mobile" 45 | }); 46 | // 使用原生的方法操作 ele,切换 输入栏处的图标 47 | document.getElementsByClassName('j-mobile2pc')[0].style = window.ah_env.isMobile ? 'display:block' : 'display:none'; 48 | document.getElementsByClassName('j-pc2mobile')[0].style = window.ah_env.isMobile ? 'display:none' : 'display:block'; 49 | var runBtn = document.getElementById('command'); 50 | if (window.ah_env.isMobile) { 51 | runBtn.placeholder = '在这里输入脚本,点击 Run 执行'; 52 | } else { 53 | runBtn.placeholder = '输入命令,如. :help'; 54 | } 55 | }.bind(this); 56 | 57 | var ele = e.target; 58 | var action = ele.dataset.action; 59 | switch (action) { 60 | case "switch": 61 | { 62 | window.ah_env.isMobile = !window.ah_env.isMobile; 63 | _real_switch_env(); 64 | } 65 | break; 66 | case 'help': 67 | case 'list': 68 | case 'timing': 69 | { 70 | window.ah_env.isMobile = false; 71 | _real_switch_env(); 72 | _run_command(':' + action); 73 | } 74 | break; 75 | } 76 | } 77 | }, 78 | template: "#tool-panel-template" 79 | }); -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/eval.js: -------------------------------------------------------------------------------- 1 | // 本文件为了替换 self.webView evaluateJavaScript:javaScriptString completionHandler:nil 2 | 3 | !(function (window) { 4 | // https://stackoverflow.com/questions/7893776/the-most-accurate-way-to-check-js-objects-type 5 | window.ah_typeof = function (global) { 6 | var cache = {}; 7 | return function (obj) { 8 | var key; 9 | return obj === null? "null" // null 10 | : obj === global? "global" // window in browser or global in nodejs 11 | : (key = typeof obj) !== "object"? key // basic: string, boolean, number, undefined, function 12 | : obj.nodeType? "DOMElement" // DOM element 13 | : cache[(key = {}.toString.call(obj))] || // cached. date, regexp, error, object, array, math 14 | (cache[key] = key.slice(8, -1).toLowerCase()); // get XXXX from [object XXXX], and cache it 15 | }; 16 | }(window); 17 | // 18 | var safeCopy = function(_origin){ 19 | if (!_origin) return; 20 | var r = {}; 21 | for (var key in _origin) { 22 | if (_origin.hasOwnProperty(key)) { 23 | var val = _origin[key]; 24 | if(val == null) break; 25 | 26 | var objType = window.ah_typeof(val); 27 | if (['object','array'].indexOf(objType) > -1){ 28 | r[key] = Object.prototype.toString.call(val); 29 | } else if('string, boolean, number,date'.indexOf(objType) > -1) { 30 | r[key] = _origin[key]; 31 | } 32 | } 33 | } 34 | return r; 35 | }; 36 | 37 | var serialize = function(obj){ 38 | var r = null; 39 | switch(ah_typeof(obj)){ 40 | case 'null': 41 | case 'global': 42 | case 'string': 43 | case 'boolean': 44 | case 'number': 45 | case 'undefined': 46 | case 'date': 47 | case 'array': 48 | // 以上不处理 49 | r = obj; 50 | break; 51 | case 'location': 52 | case 'window': 53 | r = safeCopy(obj); 54 | break; 55 | case 'regexp': 56 | r = Object.toString.call(obj); 57 | break; 58 | case 'DOMElement': 59 | r = { 60 | nodeType: obj.nodeType, 61 | nodeName: obj.nodeName, 62 | html: obj.outerHTML.length > 100 ? obj.outerHTML.substring(0,100): obj.outerHTML 63 | }; 64 | break; 65 | default: 66 | if(typeof obj.toJSON === 'function'){ 67 | r = obj.toJSON(); 68 | } else { 69 | r = 'Unsupported Type: ' + obj; 70 | } 71 | 72 | } 73 | return r; 74 | }; 75 | // https://weblogs.asp.net/yuanjian/json-performance-comparison-of-eval-new-function-and-json 76 | window.ah_eval = function (r) { 77 | var err = null; 78 | 79 | r = serialize(r); 80 | if (err == null && r == null){ 81 | return {}; 82 | } else if (err == null && r != null){ 83 | return {'result': r}; 84 | } else if (err != null && r == null){ 85 | return {'err': err}; 86 | } else { 87 | return {'result': r, 'err': err}; 88 | } 89 | }; 90 | })(this); 91 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/images/mobile to pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hite/AppHostExample/9f691bd3974f5237a6754734609c6e781c1fcfd9/AppHostExample/AppHost.framework/images/mobile to pc.png -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/images/pc to mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hite/AppHostExample/9f691bd3974f5237a6754734609c6e781c1fcfd9/AppHostExample/AppHost.framework/images/pc to mobile.png -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/profile/pageTiming.js: -------------------------------------------------------------------------------- 1 | !function() { 2 | if (!window.__profiler || window.__profiler.scriptLoaded !== true) { 3 | var d = document, 4 | h = d.getElementsByTagName('head')[0], 5 | 6 | l = d.createElement('div'), 7 | c = function () { 8 | if (l) { 9 | d.body.removeChild(l); 10 | } 11 | window.__profiler = window.__profiler || new __Profiler(); 12 | window.__profiler.init(); 13 | __profiler.scriptLoaded = true; 14 | }, 15 | t = new Date(); 16 | 17 | l.style.cssText = 'z-index:999;position:fixed;top:10px;left:10px;display:inline;width:auto;font-size:14px;line-height:1.5em;font-family:Helvetica,Calibri,Arial,sans-serif;text-shadow:none;padding:3px 10px 0;background:#FFFDF2;box-shadow:0 0 0 3px rgba(0,0,0,.25),0 0 5px 5px rgba(0,0,0,.25); border-radius:1px'; 18 | l.innerHTML = 'Just a moment'; 19 | d.body.appendChild(l); 20 | l.style.display = 'none'; 21 | // 当处于 AppHost 环境时,接收命令显示。 22 | if (window.appHost) { 23 | window.appHost.on('requestToTiming', function () { 24 | l.style.display = 'block'; 25 | c(); 26 | }); 27 | } else { 28 | c(); 29 | } 30 | } else if (window.__profiler instanceof __Profiler) { 31 | window.__profiler.init(); 32 | } 33 | }(this); 34 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/profile/pageTiming_for_mac.js: -------------------------------------------------------------------------------- 1 | function ah_timing(_timing) { 2 | window.mobile_performance_timing = _timing; 3 | 4 | if (!window.__profiler || window.__profiler.scriptLoaded !== true) { 5 | var d = document, h = d.getElementsByTagName('head')[0], l = d.createElement('div'), c = function () { 6 | if (l) { 7 | d.body.removeChild(l); 8 | } 9 | window.__profiler = window.__profiler || new __Profiler(); 10 | window.__profiler.init(); 11 | __profiler.scriptLoaded = true; 12 | }, t = new Date(); 13 | l.style.cssText = 'z-index:999;position:fixed;top:10px;left:10px;display:inline;width:auto;font-size:14px;line-height:1.5em;font-family:Helvetica,Calibri,Arial,sans-serif;text-shadow:none;padding:3px 10px 0;background:#FFFDF2;box-shadow:0 0 0 3px rgba(0,0,0,.25),0 0 5px 5px rgba(0,0,0,.25); border-radius:1px'; 14 | l.innerHTML = 'Just a moment'; 15 | d.body.appendChild(l); 16 | c(); 17 | } 18 | else if (window.__profiler instanceof __Profiler) { 19 | window.__profiler.init(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/profile/profiler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Timing - Presents visually the timing of different 3 | * page loading phases by a browser. (https://github.com/kaaes/timing) 4 | * Copyright (c) 2011-2013, Kasia Drzyzga. (FreeBSD License) 5 | */ 6 | function __Profiler() { 7 | this.totalTime = 0; 8 | 9 | this.barHeight = 18; 10 | this.timeLabelWidth = 50; 11 | this.nameLabelWidth = 160; 12 | this.textSpace = this.timeLabelWidth + this.nameLabelWidth; 13 | this.spacing = 1.2; 14 | this.unit = 1; 15 | this.fontStyle = "11.5px Arial"; 16 | this.containerPadding = 20; 17 | 18 | this.container = null; 19 | this.customElement = false; 20 | 21 | this.timingData = []; 22 | this.sections = []; 23 | }; 24 | 25 | /** 26 | * The order of the events is important, 27 | * store it here. 28 | */ 29 | __Profiler.prototype.eventsOrder = [ 30 | 'navigationStart', 'redirectStart', 'redirectStart', 31 | 'redirectEnd', 'fetchStart', 'domainLookupStart', 32 | 'domainLookupEnd', 'connectStart', 'secureConnectionStart', 33 | 'connectEnd', 'requestStart', 'responseStart', 'responseEnd', 34 | 'unloadEventStart', 'unloadEventEnd', 'domLoading', 35 | 'domInteractive', 'msFirstPaint', 'domContentLoadedEventStart', 36 | 'domContentLoadedEventEnd', 'domContentLoaded', 'domComplete', 37 | 'loadEventStart', 'loadEventEnd' 38 | ]; 39 | 40 | /** 41 | * CSS strings for various parts of the chart 42 | */ 43 | __Profiler.prototype.cssReset = 'font-size:12px;line-height:1em;z-index:99999;text-align:left;' + 44 | 'font-family:Calibri,\'Lucida Grande\',Arial,sans-serif;text-shadow:none;box-' + 45 | 'shadow:none;display:inline-block;color:#444;font-' + 46 | 'weight:normal;border:none;margin:0;padding:0;background:none;'; 47 | 48 | __Profiler.prototype.elementCss = 'position:fixed;margin:0 auto;top:' + 49 | '0;left:0;right:0;border-bottom:solid 1px #EFCEA1;box-shadow:0 2px 5px rgba(0,0,0,.1);'; 50 | 51 | __Profiler.prototype.containerCss = 'background:#FFFDF2;background:rgba(255,253,242,.99);padding:20px;display:block;'; 52 | 53 | __Profiler.prototype.headerCss = 'font-size:16px;font-weight:normal;margin:0 0 1em 0;width:auto'; 54 | 55 | __Profiler.prototype.buttonCss = 'float:right;background:none;border-radius:5px;padding:3px 10px' + 56 | ';font-size:12px;line-height:130%;width:auto;margin:-7px -10px 0 0;cursor:pointer'; 57 | 58 | __Profiler.prototype.infoLinkCss = 'color:#1D85B8;margin:1em 0 0 0;'; 59 | 60 | /** 61 | * Retrieves performance object keys. 62 | * Helper function to cover browser 63 | * inconsistencies. 64 | * 65 | * @param {PerformanceTiming} Object holding time data 66 | * @return {Array} list of PerformanceTiming properties names 67 | */ 68 | __Profiler.prototype._getPerfObjKeys = function(obj) { 69 | var keys = Object.keys(obj); 70 | return keys.length ? keys : Object.keys(Object.getPrototypeOf(obj)); 71 | } 72 | 73 | /** 74 | * Sets unit used in measurements on canvas. 75 | * Depends on the lenght of text labels and total 76 | * time of the page loading. 77 | */ 78 | __Profiler.prototype._setUnit = function(canvas) { 79 | this.unit = (canvas.width - this.textSpace) / this.totalTime; 80 | } 81 | 82 | /** 83 | * Defines sections of the chart. 84 | * According to specs there are three: 85 | * network, server and browser. 86 | * 87 | * @return {Array} chart sections. 88 | */ 89 | __Profiler.prototype._getSections = function() { 90 | return Array.prototype.indexOf ? [{ 91 | name: 'network', 92 | color: [224, 84, 63], 93 | firstEventIndex: this.eventsOrder.indexOf('navigationStart'), 94 | lastEventIndex: this.eventsOrder.indexOf('connectEnd'), 95 | startTime: 0, 96 | endTime: 0 97 | }, { 98 | name: 'server', 99 | color: [255, 188, 0], 100 | firstEventIndex: this.eventsOrder.indexOf('requestStart'), 101 | lastEventIndex: this.eventsOrder.indexOf('responseEnd'), 102 | startTime: 0, 103 | endTime: 0 104 | }, { 105 | name: 'browser', 106 | color: [16, 173, 171], 107 | firstEventIndex: this.eventsOrder.indexOf('unloadEventStart'), 108 | lastEventIndex: this.eventsOrder.indexOf('loadEventEnd'), 109 | startTime: 0, 110 | endTime: 0 111 | }] : []; 112 | } 113 | 114 | /** 115 | * Creates main container 116 | * @return {HTMLElement} container element 117 | */ 118 | __Profiler.prototype._createContainer = function() { 119 | var container = document.createElement('div'); 120 | var header = this._createHeader(); 121 | var button = this._createCloseButton(); 122 | 123 | button.onclick = function(e){ 124 | button.onclick = null; 125 | container.parentNode.removeChild(container); 126 | }; // DOM level 0 used to avoid implementing this twice for IE & the rest 127 | 128 | container.style.cssText = this.cssReset + this.containerCss; 129 | 130 | if (!this.customElement) { 131 | container.style.cssText += this.elementCss; 132 | } 133 | 134 | header.appendChild(button); 135 | container.appendChild(header); 136 | return container; 137 | } 138 | 139 | /** 140 | * Creates header 141 | * @return {HTMLElement} header element 142 | */ 143 | __Profiler.prototype._createHeader = function() { 144 | var c = document.createElement('div'); 145 | var h = document.createElement('h1'); 146 | var sectionStr = '/ '; 147 | 148 | for(var i = 0, l = this.sections.length; i < l; i++) { 149 | sectionStr += '' + this.sections[i].name + ' / '; 150 | } 151 | 152 | h.innerHTML = 'Page Load Time Breakdown ' + sectionStr; 153 | h.style.cssText = this.cssReset + this.headerCss; 154 | 155 | c.appendChild(h); 156 | 157 | return c; 158 | } 159 | 160 | /** 161 | * Creates close buttonr 162 | * @return {HTMLElement} button element 163 | */ 164 | __Profiler.prototype._createCloseButton = function() { 165 | var b = document.createElement('button'); 166 | 167 | b.innerHTML = 'close this box ×'; 168 | b.style.cssText = this.cssReset + this.buttonCss; 169 | 170 | return b; 171 | } 172 | 173 | /** 174 | * Creates info link 175 | * @return {HTMLElement} link element 176 | */ 177 | __Profiler.prototype._createInfoLink = function() { 178 | var a = document.createElement('a'); 179 | a.href = 'http://kaaes.github.com/timing/info.html'; 180 | a.target = '_blank'; 181 | a.innerHTML = 'What does that mean?'; 182 | a.style.cssText = this.cssReset + this.infoLinkCss; 183 | 184 | return a; 185 | } 186 | 187 | /** 188 | * Creates information when performance.timing is not supported 189 | * @return {HTMLElement} message element 190 | */ 191 | __Profiler.prototype._createNotSupportedInfo = function() { 192 | var p = document.createElement('p'); 193 | p.innerHTML = 'Navigation Timing API is not supported by your browser'; 194 | return p; 195 | } 196 | 197 | /** 198 | * Creates main bar chart 199 | * @return {HTMLElement} chart container. 200 | */ 201 | __Profiler.prototype._createChart = function() { 202 | var chartContainer = document.createElement('div'); 203 | 204 | var canvas = document.createElement('canvas'); 205 | canvas.width = this.container.clientWidth - this.containerPadding * 2; 206 | 207 | var infoLink = this._createInfoLink(); 208 | 209 | this._drawChart(canvas); 210 | 211 | chartContainer.appendChild(canvas); 212 | chartContainer.appendChild(infoLink); 213 | 214 | return chartContainer; 215 | } 216 | 217 | /** 218 | * Prepare draw function. 219 | * 220 | * @param {HTMLCanvasElement} canvas Canvas to draw on 221 | * @param {String} mode Either 'block' or 'point' for events 222 | * that have start and end or the ones that just happen. 223 | * @param {Object} eventData Additional event information. 224 | */ 225 | __Profiler.prototype._prepareDraw = function(canvas, mode, eventData) { 226 | var sectionData = this.sections[eventData.sectionIndex]; 227 | 228 | var barOptions = { 229 | color : sectionData.color, 230 | sectionTimeBounds : [sectionData.startTime, sectionData.endTime], 231 | eventTimeBounds : [eventData.time, eventData.timeEnd], 232 | label : eventData.label 233 | } 234 | 235 | return this._drawBar(mode, canvas, canvas.width, barOptions); 236 | } 237 | 238 | /** 239 | * Draws a single bar on the canvas 240 | * 241 | * @param {String} mode Either 'block' or 'point' for events 242 | * that have start and end or the ones that just happen. 243 | * @param {HTMLCanvasElement} canvas Canvas to draw on. 244 | * @param {Number} barWidth Width of the bar. 245 | * @param {Object} options Other bar options. 246 | * param {Array} options.color The color to use for rendering 247 | * the section. 248 | * param {Array} options.sectionTImeBounds Start and end times 249 | * for the section. Used to draw semi-transparent 250 | * section bar. 251 | * param {Array} options.eventTImeBounds Start and end times for 252 | * the event itself. Used to draw event bar. 253 | * param {String} options.label Name of the event to show next to 254 | * the bars. 255 | */ 256 | __Profiler.prototype._drawBar = function(mode, canvas, barWidth, options) { 257 | var start; 258 | var stop; 259 | var width; 260 | var timeLabel; 261 | var metrics; 262 | var color = options.color; 263 | var sectionStart = options.sectionTimeBounds[0]; 264 | var sectionStop = options.sectionTimeBounds[1]; 265 | var nameLabel = options.label; 266 | var context = canvas.getContext('2d'); 267 | 268 | if (mode === 'block') { 269 | start = options.eventTimeBounds[0]; 270 | stop = options.eventTimeBounds[1]; 271 | timeLabel = start + '-' + stop; 272 | } else { 273 | start = options.eventTimeBounds[0]; 274 | timeLabel = start; 275 | } 276 | timeLabel += 'ms'; 277 | 278 | metrics = context.measureText(timeLabel); 279 | if(metrics.width > this.timeLabelWidth) { 280 | this.timeLabelWidth = metrics.width + 10; 281 | this.textSpace = this.timeLabelWidth + this.nameLabelWidth; 282 | this._setUnit(canvas); 283 | } 284 | 285 | return function(context) { 286 | if(mode === 'block') { 287 | width = Math.round((stop - start) * this.unit); 288 | width = width === 0 ? 1 : width; 289 | } else { 290 | width = 1; 291 | } 292 | 293 | // row background 294 | context.strokeStyle = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',.3)'; 295 | context.lineWidth = 1; 296 | context.fillStyle = 'rgba(255,255,255,0)'; 297 | context.fillRect(0, 0, barWidth - this.textSpace, this.barHeight); 298 | context.fillStyle = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',.05)'; 299 | context.fillRect(0, 0, barWidth - this.textSpace, this.barHeight); 300 | // context.strokeRect(.5, .5, Math.round(barWidth - this.textSpace -1), Math.round(this.barHeight)); 301 | 302 | 303 | // section bar 304 | context.shadowColor = 'white'; 305 | context.fillStyle = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',.2)'; 306 | context.fillRect(Math.round(this.unit * sectionStart), 2, Math.round(this.unit * (sectionStop - sectionStart)), this.barHeight - 4); 307 | 308 | // event marker 309 | context.fillStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')'; 310 | context.fillRect(Math.round(this.unit * start), 2, width, this.barHeight - 4); 311 | 312 | // label 313 | context.fillText(timeLabel, barWidth - this.textSpace + 10, 2 * this.barHeight / 3); 314 | context.fillText(nameLabel, barWidth - this.textSpace + this.timeLabelWidth + 15, 2 * this.barHeight / 3); 315 | } 316 | } 317 | 318 | /** 319 | * Draws the chart on the canvas 320 | */ 321 | __Profiler.prototype._drawChart = function(canvas) { 322 | var time; 323 | var eventName; 324 | var options; 325 | var skipEvents = []; 326 | var drawFns = []; 327 | 328 | var context = canvas.getContext('2d'); 329 | 330 | // needs to be set here for proper text measurement... 331 | context.font = this.fontStyle; 332 | 333 | this._setUnit(canvas); 334 | 335 | for (var i = 0, l = this.eventsOrder.length; i < l; i++) { 336 | var evt = this.eventsOrder[i]; 337 | 338 | if (!this.timingData.hasOwnProperty(evt)) { 339 | continue; 340 | } 341 | 342 | var item = this.timingData[evt]; 343 | var startIndex = evt.indexOf('Start'); 344 | var isBlockStart = startIndex > -1; 345 | var hasBlockEnd = false; 346 | 347 | if (isBlockStart) { 348 | eventName = evt.substr(0, startIndex); 349 | hasBlockEnd = this.eventsOrder.indexOf(eventName + 'End') > -1; 350 | } 351 | 352 | if (isBlockStart && hasBlockEnd) { 353 | item.label = eventName; 354 | item.timeEnd = this.timingData[eventName + 'End'].time; 355 | drawFns.push(this._prepareDraw(canvas, 'block', item)); 356 | skipEvents.push(eventName + 'End'); 357 | } else if (skipEvents.indexOf(evt) < 0) { 358 | item.label = evt; 359 | drawFns.push(this._prepareDraw(canvas, 'point', item)); 360 | } 361 | } 362 | 363 | canvas.height = this.spacing * this.barHeight * drawFns.length; 364 | 365 | // setting canvas height resets font, has to be re-set 366 | context.font = this.fontStyle; 367 | 368 | var step = Math.round(this.barHeight * this.spacing); 369 | 370 | drawFns.forEach(function(draw) { 371 | draw.call(this, context); 372 | context.translate(0, step); 373 | }, this); 374 | } 375 | 376 | /** 377 | * Matches events with the section they belong to 378 | * i.e. network, server or browser and sets 379 | * info about time bounds for the sections. 380 | */ 381 | __Profiler.prototype._matchEventsWithSections = function() { 382 | var data = this.timingData; 383 | 384 | var sections = this.sections; 385 | 386 | for (var i = 0, len = sections.length; i < len; i++) { 387 | var firstEventIndex = sections[i].firstEventIndex; 388 | var lastEventIndex = sections[i].lastEventIndex; 389 | 390 | var sectionOrder = this.eventsOrder.slice(firstEventIndex, lastEventIndex + 1); 391 | var sectionEvents = sectionOrder.filter(function(el){ 392 | return data.hasOwnProperty(el); 393 | }); 394 | 395 | sectionEvents.sort(function(a, b){ 396 | return data[a].time - data[b].time; 397 | }) 398 | 399 | firstEventIndex = sectionEvents[0]; 400 | lastEventIndex = sectionEvents[sectionEvents.length - 1]; 401 | 402 | sections[i].startTime = data[firstEventIndex].time; 403 | sections[i].endTime = data[lastEventIndex].time; 404 | 405 | for(var j = 0, flen = sectionEvents.length; j < flen; j++) { 406 | var item = sectionEvents[j]; 407 | if(data[item]) { 408 | data[item].sectionIndex = i; 409 | } 410 | } 411 | } 412 | } 413 | 414 | /** 415 | * Gets timing data and calculates 416 | * when events occured as the original 417 | * object contains only timestamps. 418 | * 419 | * @return {Object} Hashmap of the event names 420 | * and times when they occured relatvely to 421 | * the page load start. 422 | */ 423 | __Profiler.prototype._getData = function() { 424 | if (!window.performance) { 425 | return; 426 | } 427 | 428 | var data = window.performance; 429 | var timingData = data.timing; 430 | var eventNames = this._getPerfObjKeys(timingData); 431 | var events = {}; 432 | 433 | var startTime = timingData.navigationStart || 0; 434 | var eventTime = 0; 435 | var totalTime = 0; 436 | 437 | for(var i = 0, l = eventNames.length; i < l; i++) { 438 | var evt = timingData[eventNames[i]]; 439 | 440 | if (evt && evt > 0) { 441 | eventTime = evt - startTime; 442 | events[eventNames[i]] = { time: eventTime }; 443 | 444 | if (eventTime > totalTime) { 445 | totalTime = eventTime; 446 | } 447 | } 448 | } 449 | 450 | this.totalTime = totalTime; 451 | 452 | return events; 453 | } 454 | 455 | /** 456 | * Actually init the chart 457 | */ 458 | __Profiler.prototype._init = function() { 459 | this.timingData = this._getData(); 460 | this.sections = this._getSections(); 461 | this.container = this._createContainer(); 462 | 463 | if (this.customElement) { 464 | this.customElement.appendChild(this.container); 465 | } else { 466 | document.body.appendChild(this.container); 467 | } 468 | 469 | var content; 470 | 471 | if (this.timingData && this.sections.length) { 472 | this._matchEventsWithSections(); 473 | content = this._createChart(); 474 | } else { 475 | content = this._createNotSupportedInfo(); 476 | } 477 | 478 | this.container.appendChild(content); 479 | } 480 | 481 | /** 482 | * Build the overlay with the timing chart 483 | * @param {?HTMLElement} element If provided 484 | * the chart will be render in the container. 485 | * If not provided, container element will be created 486 | * and appended to the page. 487 | * @param {?Number} timeout Optional timeout to execute 488 | * timing info. Can be used to catch all events. 489 | * if not provided will be executed immediately. 490 | */ 491 | __Profiler.prototype.init = function(element, timeout) { 492 | 493 | if (element instanceof HTMLElement) { 494 | this.customElement = element; 495 | } 496 | 497 | if (timeout && parseInt(timeout, 10) > 0) { 498 | var self = this; 499 | setTimeout(function() { 500 | self._init(); 501 | }, timeout); 502 | } else { 503 | this._init(); 504 | } 505 | } -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/profile/profiler_for_mac.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Timing - Presents visually the timing of different 3 | * page loading phases by a browser. (https://github.com/kaaes/timing) 4 | * Copyright (c) 2011-2013, Kasia Drzyzga. (FreeBSD License) 5 | */ 6 | function __Profiler() { 7 | this.totalTime = 0; 8 | 9 | this.barHeight = 18; 10 | this.timeLabelWidth = 50; 11 | this.nameLabelWidth = 160; 12 | this.textSpace = this.timeLabelWidth + this.nameLabelWidth; 13 | this.spacing = 1.2; 14 | this.unit = 1; 15 | this.fontStyle = "11.5px Arial"; 16 | this.containerPadding = 20; 17 | 18 | this.container = null; 19 | this.customElement = false; 20 | 21 | this.timingData = []; 22 | this.sections = []; 23 | }; 24 | 25 | /** 26 | * The order of the events is important, 27 | * store it here. 28 | */ 29 | __Profiler.prototype.eventsOrder = [ 30 | 'navigationStart', 'redirectStart', 'redirectStart', 31 | 'redirectEnd', 'fetchStart', 'domainLookupStart', 32 | 'domainLookupEnd', 'connectStart', 'secureConnectionStart', 33 | 'connectEnd', 'requestStart', 'responseStart', 'responseEnd', 34 | 'unloadEventStart', 'unloadEventEnd', 'domLoading', 35 | 'domInteractive', 'msFirstPaint', 'domContentLoadedEventStart', 36 | 'domContentLoadedEventEnd', 'domContentLoaded', 'domComplete', 37 | 'loadEventStart', 'loadEventEnd' 38 | ]; 39 | 40 | /** 41 | * CSS strings for various parts of the chart 42 | */ 43 | __Profiler.prototype.cssReset = 'font-size:12px;line-height:1em;z-index:99999;text-align:left;' + 44 | 'font-family:Calibri,\'Lucida Grande\',Arial,sans-serif;text-shadow:none;box-' + 45 | 'shadow:none;display:inline-block;color:#444;font-' + 46 | 'weight:normal;border:none;margin:0;padding:0;background:none;'; 47 | 48 | __Profiler.prototype.elementCss = 'position:fixed;margin:0 auto;top:' + 49 | '0;left:0;right:0;border-bottom:solid 1px #EFCEA1;box-shadow:0 2px 5px rgba(0,0,0,.1);'; 50 | 51 | __Profiler.prototype.containerCss = 'background:#FFFDF2;background:rgba(255,253,242,.99);padding:20px;display:block;'; 52 | 53 | __Profiler.prototype.headerCss = 'font-size:16px;font-weight:normal;margin:0 0 1em 0;width:auto'; 54 | 55 | __Profiler.prototype.buttonCss = 'float:right;background:none;border-radius:5px;padding:3px 10px' + 56 | ';font-size:12px;line-height:130%;width:auto;margin:-7px -10px 0 0;cursor:pointer'; 57 | 58 | __Profiler.prototype.infoLinkCss = 'color:#1D85B8;margin:1em 0 0 0;'; 59 | 60 | /** 61 | * Retrieves performance object keys. 62 | * Helper function to cover browser 63 | * inconsistencies. 64 | * 65 | * @param {PerformanceTiming} Object holding time data 66 | * @return {Array} list of PerformanceTiming properties names 67 | */ 68 | __Profiler.prototype._getPerfObjKeys = function(obj) { 69 | var keys = Object.keys(obj); 70 | return keys.length ? keys : Object.keys(Object.getPrototypeOf(obj)); 71 | } 72 | 73 | /** 74 | * Sets unit used in measurements on canvas. 75 | * Depends on the lenght of text labels and total 76 | * time of the page loading. 77 | */ 78 | __Profiler.prototype._setUnit = function(canvas) { 79 | this.unit = (canvas.width - this.textSpace) / this.totalTime; 80 | } 81 | 82 | /** 83 | * Defines sections of the chart. 84 | * According to specs there are three: 85 | * network, server and browser. 86 | * 87 | * @return {Array} chart sections. 88 | */ 89 | __Profiler.prototype._getSections = function() { 90 | return Array.prototype.indexOf ? [{ 91 | name: 'network', 92 | color: [224, 84, 63], 93 | firstEventIndex: this.eventsOrder.indexOf('navigationStart'), 94 | lastEventIndex: this.eventsOrder.indexOf('connectEnd'), 95 | startTime: 0, 96 | endTime: 0 97 | }, { 98 | name: 'server', 99 | color: [255, 188, 0], 100 | firstEventIndex: this.eventsOrder.indexOf('requestStart'), 101 | lastEventIndex: this.eventsOrder.indexOf('responseEnd'), 102 | startTime: 0, 103 | endTime: 0 104 | }, { 105 | name: 'browser', 106 | color: [16, 173, 171], 107 | firstEventIndex: this.eventsOrder.indexOf('unloadEventStart'), 108 | lastEventIndex: this.eventsOrder.indexOf('loadEventEnd'), 109 | startTime: 0, 110 | endTime: 0 111 | }] : []; 112 | } 113 | 114 | /** 115 | * Creates main container 116 | * @return {HTMLElement} container element 117 | */ 118 | __Profiler.prototype._createContainer = function() { 119 | var container = document.createElement('div'); 120 | var header = this._createHeader(); 121 | var button = this._createCloseButton(); 122 | 123 | button.onclick = function(e){ 124 | button.onclick = null; 125 | container.parentNode.removeChild(container); 126 | }; // DOM level 0 used to avoid implementing this twice for IE & the rest 127 | 128 | container.style.cssText = this.cssReset + this.containerCss; 129 | 130 | if (!this.customElement) { 131 | container.style.cssText += this.elementCss; 132 | } 133 | 134 | header.appendChild(button); 135 | container.appendChild(header); 136 | return container; 137 | } 138 | 139 | /** 140 | * Creates header 141 | * @return {HTMLElement} header element 142 | */ 143 | __Profiler.prototype._createHeader = function() { 144 | var c = document.createElement('div'); 145 | var h = document.createElement('h1'); 146 | var sectionStr = '/ '; 147 | 148 | for(var i = 0, l = this.sections.length; i < l; i++) { 149 | sectionStr += '' + this.sections[i].name + ' / '; 150 | } 151 | 152 | h.innerHTML = 'Page Load Time Breakdown ' + sectionStr; 153 | h.style.cssText = this.cssReset + this.headerCss; 154 | 155 | c.appendChild(h); 156 | 157 | return c; 158 | } 159 | 160 | /** 161 | * Creates close buttonr 162 | * @return {HTMLElement} button element 163 | */ 164 | __Profiler.prototype._createCloseButton = function() { 165 | var b = document.createElement('button'); 166 | 167 | b.innerHTML = 'close this box ×'; 168 | b.style.cssText = this.cssReset + this.buttonCss; 169 | 170 | return b; 171 | } 172 | 173 | /** 174 | * Creates info link 175 | * @return {HTMLElement} link element 176 | */ 177 | __Profiler.prototype._createInfoLink = function() { 178 | var a = document.createElement('a'); 179 | a.href = 'http://kaaes.github.com/timing/info.html'; 180 | a.target = '_blank'; 181 | a.innerHTML = 'What does that mean?'; 182 | a.style.cssText = this.cssReset + this.infoLinkCss; 183 | 184 | return a; 185 | } 186 | 187 | /** 188 | * Creates information when performance.timing is not supported 189 | * @return {HTMLElement} message element 190 | */ 191 | __Profiler.prototype._createNotSupportedInfo = function() { 192 | var p = document.createElement('p'); 193 | p.innerHTML = 'Navigation Timing API is not supported by your browser'; 194 | return p; 195 | } 196 | 197 | /** 198 | * Creates main bar chart 199 | * @return {HTMLElement} chart container. 200 | */ 201 | __Profiler.prototype._createChart = function() { 202 | var chartContainer = document.createElement('div'); 203 | 204 | var canvas = document.createElement('canvas'); 205 | canvas.width = this.container.clientWidth - this.containerPadding * 2; 206 | 207 | var infoLink = this._createInfoLink(); 208 | 209 | this._drawChart(canvas); 210 | 211 | chartContainer.appendChild(canvas); 212 | chartContainer.appendChild(infoLink); 213 | 214 | return chartContainer; 215 | } 216 | 217 | /** 218 | * Prepare draw function. 219 | * 220 | * @param {HTMLCanvasElement} canvas Canvas to draw on 221 | * @param {String} mode Either 'block' or 'point' for events 222 | * that have start and end or the ones that just happen. 223 | * @param {Object} eventData Additional event information. 224 | */ 225 | __Profiler.prototype._prepareDraw = function(canvas, mode, eventData) { 226 | var sectionData = this.sections[eventData.sectionIndex]; 227 | 228 | var barOptions = { 229 | color : sectionData.color, 230 | sectionTimeBounds : [sectionData.startTime, sectionData.endTime], 231 | eventTimeBounds : [eventData.time, eventData.timeEnd], 232 | label : eventData.label 233 | } 234 | 235 | return this._drawBar(mode, canvas, canvas.width, barOptions); 236 | } 237 | 238 | /** 239 | * Draws a single bar on the canvas 240 | * 241 | * @param {String} mode Either 'block' or 'point' for events 242 | * that have start and end or the ones that just happen. 243 | * @param {HTMLCanvasElement} canvas Canvas to draw on. 244 | * @param {Number} barWidth Width of the bar. 245 | * @param {Object} options Other bar options. 246 | * param {Array} options.color The color to use for rendering 247 | * the section. 248 | * param {Array} options.sectionTImeBounds Start and end times 249 | * for the section. Used to draw semi-transparent 250 | * section bar. 251 | * param {Array} options.eventTImeBounds Start and end times for 252 | * the event itself. Used to draw event bar. 253 | * param {String} options.label Name of the event to show next to 254 | * the bars. 255 | */ 256 | __Profiler.prototype._drawBar = function(mode, canvas, barWidth, options) { 257 | var start; 258 | var stop; 259 | var width; 260 | var timeLabel; 261 | var metrics; 262 | var color = options.color; 263 | var sectionStart = options.sectionTimeBounds[0]; 264 | var sectionStop = options.sectionTimeBounds[1]; 265 | var nameLabel = options.label; 266 | var context = canvas.getContext('2d'); 267 | 268 | if (mode === 'block') { 269 | start = options.eventTimeBounds[0]; 270 | stop = options.eventTimeBounds[1]; 271 | timeLabel = start + '-' + stop; 272 | } else { 273 | start = options.eventTimeBounds[0]; 274 | timeLabel = start; 275 | } 276 | timeLabel += 'ms'; 277 | 278 | metrics = context.measureText(timeLabel); 279 | if(metrics.width > this.timeLabelWidth) { 280 | this.timeLabelWidth = metrics.width + 10; 281 | this.textSpace = this.timeLabelWidth + this.nameLabelWidth; 282 | this._setUnit(canvas); 283 | } 284 | 285 | return function(context) { 286 | if(mode === 'block') { 287 | width = Math.round((stop - start) * this.unit); 288 | width = width === 0 ? 1 : width; 289 | } else { 290 | width = 1; 291 | } 292 | 293 | // row background 294 | context.strokeStyle = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',.3)'; 295 | context.lineWidth = 1; 296 | context.fillStyle = 'rgba(255,255,255,0)'; 297 | context.fillRect(0, 0, barWidth - this.textSpace, this.barHeight); 298 | context.fillStyle = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',.05)'; 299 | context.fillRect(0, 0, barWidth - this.textSpace, this.barHeight); 300 | // context.strokeRect(.5, .5, Math.round(barWidth - this.textSpace -1), Math.round(this.barHeight)); 301 | 302 | 303 | // section bar 304 | context.shadowColor = 'white'; 305 | context.fillStyle = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',.2)'; 306 | context.fillRect(Math.round(this.unit * sectionStart), 2, Math.round(this.unit * (sectionStop - sectionStart)), this.barHeight - 4); 307 | 308 | // event marker 309 | context.fillStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')'; 310 | context.fillRect(Math.round(this.unit * start), 2, width, this.barHeight - 4); 311 | 312 | // label 313 | context.fillText(timeLabel, barWidth - this.textSpace + 10, 2 * this.barHeight / 3); 314 | context.fillText(nameLabel, barWidth - this.textSpace + this.timeLabelWidth + 15, 2 * this.barHeight / 3); 315 | } 316 | } 317 | 318 | /** 319 | * Draws the chart on the canvas 320 | */ 321 | __Profiler.prototype._drawChart = function(canvas) { 322 | var time; 323 | var eventName; 324 | var options; 325 | var skipEvents = []; 326 | var drawFns = []; 327 | 328 | var context = canvas.getContext('2d'); 329 | 330 | // needs to be set here for proper text measurement... 331 | context.font = this.fontStyle; 332 | 333 | this._setUnit(canvas); 334 | 335 | for (var i = 0, l = this.eventsOrder.length; i < l; i++) { 336 | var evt = this.eventsOrder[i]; 337 | 338 | if (!this.timingData.hasOwnProperty(evt)) { 339 | continue; 340 | } 341 | 342 | var item = this.timingData[evt]; 343 | var startIndex = evt.indexOf('Start'); 344 | var isBlockStart = startIndex > -1; 345 | var hasBlockEnd = false; 346 | 347 | if (isBlockStart) { 348 | eventName = evt.substr(0, startIndex); 349 | hasBlockEnd = this.eventsOrder.indexOf(eventName + 'End') > -1; 350 | } 351 | 352 | if (isBlockStart && hasBlockEnd) { 353 | item.label = eventName; 354 | item.timeEnd = this.timingData[eventName + 'End'].time; 355 | drawFns.push(this._prepareDraw(canvas, 'block', item)); 356 | skipEvents.push(eventName + 'End'); 357 | } else if (skipEvents.indexOf(evt) < 0) { 358 | item.label = evt; 359 | drawFns.push(this._prepareDraw(canvas, 'point', item)); 360 | } 361 | } 362 | 363 | canvas.height = this.spacing * this.barHeight * drawFns.length; 364 | 365 | // setting canvas height resets font, has to be re-set 366 | context.font = this.fontStyle; 367 | 368 | var step = Math.round(this.barHeight * this.spacing); 369 | 370 | drawFns.forEach(function(draw) { 371 | draw.call(this, context); 372 | context.translate(0, step); 373 | }, this); 374 | } 375 | 376 | /** 377 | * Matches events with the section they belong to 378 | * i.e. network, server or browser and sets 379 | * info about time bounds for the sections. 380 | */ 381 | __Profiler.prototype._matchEventsWithSections = function() { 382 | var data = this.timingData; 383 | 384 | var sections = this.sections; 385 | 386 | for (var i = 0, len = sections.length; i < len; i++) { 387 | var firstEventIndex = sections[i].firstEventIndex; 388 | var lastEventIndex = sections[i].lastEventIndex; 389 | 390 | var sectionOrder = this.eventsOrder.slice(firstEventIndex, lastEventIndex + 1); 391 | var sectionEvents = sectionOrder.filter(function(el){ 392 | return data.hasOwnProperty(el); 393 | }); 394 | 395 | sectionEvents.sort(function(a, b){ 396 | return data[a].time - data[b].time; 397 | }) 398 | 399 | firstEventIndex = sectionEvents[0]; 400 | lastEventIndex = sectionEvents[sectionEvents.length - 1]; 401 | 402 | sections[i].startTime = data[firstEventIndex].time; 403 | sections[i].endTime = data[lastEventIndex].time; 404 | 405 | for(var j = 0, flen = sectionEvents.length; j < flen; j++) { 406 | var item = sectionEvents[j]; 407 | if(data[item]) { 408 | data[item].sectionIndex = i; 409 | } 410 | } 411 | } 412 | } 413 | 414 | /** 415 | * Gets timing data and calculates 416 | * when events occured as the original 417 | * object contains only timestamps. 418 | * 419 | * @return {Object} Hashmap of the event names 420 | * and times when they occured relatvely to 421 | * the page load start. 422 | */ 423 | __Profiler.prototype._getData = function() { 424 | // 修改点 2. 425 | if (!window.mobile_performance_timing) { 426 | return; 427 | } 428 | 429 | var timingData = window.mobile_performance_timing; 430 | var eventNames = this._getPerfObjKeys(timingData); 431 | var events = {}; 432 | 433 | var startTime = timingData.navigationStart || 0; 434 | var eventTime = 0; 435 | var totalTime = 0; 436 | 437 | for(var i = 0, l = eventNames.length; i < l; i++) { 438 | var evt = timingData[eventNames[i]]; 439 | 440 | if (evt && evt > 0) { 441 | eventTime = evt - startTime; 442 | events[eventNames[i]] = { time: eventTime }; 443 | 444 | if (eventTime > totalTime) { 445 | totalTime = eventTime; 446 | } 447 | } 448 | } 449 | 450 | this.totalTime = totalTime; 451 | 452 | return events; 453 | } 454 | 455 | /** 456 | * Actually init the chart 457 | */ 458 | __Profiler.prototype._init = function() { 459 | this.timingData = this._getData(); 460 | this.sections = this._getSections(); 461 | this.container = this._createContainer(); 462 | 463 | if (this.customElement) { 464 | this.customElement.appendChild(this.container); 465 | } else { 466 | document.body.appendChild(this.container); 467 | } 468 | 469 | var content; 470 | 471 | if (this.timingData && this.sections.length) { 472 | this._matchEventsWithSections(); 473 | content = this._createChart(); 474 | } else { 475 | content = this._createNotSupportedInfo(); 476 | } 477 | 478 | this.container.appendChild(content); 479 | } 480 | 481 | /** 482 | * Build the overlay with the timing chart 483 | * @param {?HTMLElement} element If provided 484 | * the chart will be render in the container. 485 | * If not provided, container element will be created 486 | * and appended to the page. 487 | * @param {?Number} timeout Optional timeout to execute 488 | * timing info. Can be used to catch all events. 489 | * if not provided will be executed immediately. 490 | */ 491 | __Profiler.prototype.init = function(element, timeout) { 492 | 493 | if (element instanceof HTMLElement) { 494 | this.customElement = element; 495 | } 496 | 497 | if (timeout && parseInt(timeout, 10) > 0) { 498 | var self = this; 499 | setTimeout(function() { 500 | self._init(); 501 | }, timeout); 502 | } else { 503 | this._init(); 504 | } 505 | } -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/renderjson.css: -------------------------------------------------------------------------------- 1 | .renderjson a { text-decoration: none; } 2 | .renderjson .disclosure { color: #555; 3 | font-size: 150%; } 4 | .renderjson .syntax { color: grey; } 5 | .renderjson .string { color: #af0404; } 6 | .renderjson .number { color: #af0404; } 7 | .renderjson .boolean { color: #af0404; } 8 | .renderjson .key { color: #333; } 9 | .renderjson .keyword { color: lightgoldenrodyellow; } 10 | .renderjson .object.syntax { color: lightseagreen; } 11 | .renderjson .array.syntax { color: #af0404; } -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/renderjson.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2013-2014 David Caldwell 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted, provided that the above 5 | // copyright notice and this permission notice appear in all copies. 6 | // 7 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 10 | // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 12 | // OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 13 | // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | // Usage 16 | // ----- 17 | // The module exports one entry point, the `renderjson()` function. It takes in 18 | // the JSON you want to render as a single argument and returns an HTML 19 | // element. 20 | // 21 | // Options 22 | // ------- 23 | // renderjson.set_icons("+", "-") 24 | // This Allows you to override the disclosure icons. 25 | // 26 | // renderjson.set_show_to_level(level) 27 | // Pass the number of levels to expand when rendering. The default is 0, which 28 | // starts with everything collapsed. As a special case, if level is the string 29 | // "all" then it will start with everything expanded. 30 | // 31 | // renderjson.set_max_string_length(length) 32 | // Strings will be truncated and made expandable if they are longer than 33 | // `length`. As a special case, if `length` is the string "none" then 34 | // there will be no truncation. The default is "none". 35 | // 36 | // renderjson.set_sort_objects(sort_bool) 37 | // Sort objects by key (default: false) 38 | // 39 | // Theming 40 | // ------- 41 | // The HTML output uses a number of classes so that you can theme it the way 42 | // you'd like: 43 | // .disclosure ("⊕", "⊖") 44 | // .syntax (",", ":", "{", "}", "[", "]") 45 | // .string (includes quotes) 46 | // .number 47 | // .boolean 48 | // .key (object key) 49 | // .keyword ("null", "undefined") 50 | // .object.syntax ("{", "}") 51 | // .array.syntax ("[", "]") 52 | 53 | var module; 54 | (module||{}).exports = renderjson = (function() { 55 | var themetext = function(/* [class, text]+ */) { 56 | var spans = []; 57 | while (arguments.length) 58 | spans.push(append(span(Array.prototype.shift.call(arguments)), 59 | text(Array.prototype.shift.call(arguments)))); 60 | return spans; 61 | }; 62 | var append = function(/* el, ... */) { 63 | var el = Array.prototype.shift.call(arguments); 64 | for (var a=0; a 0) 110 | show(); 111 | return el; 112 | }; 113 | 114 | if (json === null) return themetext(null, my_indent, "keyword", "null"); 115 | if (json === void 0) return themetext(null, my_indent, "keyword", "undefined"); 116 | 117 | if (typeof(json) == "string" && json.length > max_string) 118 | return disclosure('"', json.substr(0,max_string)+" ...", '"', "string", function () { 119 | return append(span("string"), themetext(null, my_indent, "string", JSON.stringify(json))); 120 | }); 121 | 122 | if (typeof(json) != "object") // Strings, numbers and bools 123 | return themetext(null, my_indent, typeof(json), JSON.stringify(json)); 124 | 125 | if (json.constructor == Array) { 126 | if (json.length == 0) return themetext(null, my_indent, "array syntax", "[]"); 127 | 128 | return disclosure("[", " ... ", "]", "array", function () { 129 | var as = append(span("array"), themetext("array syntax", "[", null, "\n")); 130 | for (var i=0; i 2 | 3 | 4 | 5 | 6 | AppHost Remote Debugger 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

欢迎使用 AppHost Remote Debugger

21 |

** 调试之前,首先打开一个 h5 页面 **

22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 99 | 100 | 108 | 109 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/server.js: -------------------------------------------------------------------------------- 1 | window.appHost = { 2 | invoke: function (action, param) { 3 | var xhr = new XMLHttpRequest(); 4 | xhr.open("POST", "/command.do", true); 5 | xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 6 | xhr.onload = function () { 7 | console.log(xhr.responseURL); // http://example.com/test 8 | }; 9 | 10 | xhr.send( 11 | "action=" + action + "¶m=" + encodeURIComponent(window.JSON.stringify(param)) 12 | ); 13 | } 14 | }; 15 | 16 | function _renderLogs(logs) { 17 | if (!logs) return; 18 | 19 | for (var i = 0; i < logs.length; i++) { 20 | var log = window.JSON.parse(logs[i]); 21 | var logType = log.type; 22 | var logVal = log.value; 23 | 24 | // 查询所有的接口,显示需要特殊处理下 25 | if (logVal.action === "requestToTiming_on_mac"){ 26 | ah_timing(logVal.param); 27 | } else if (logVal.action === "list") { 28 | var apis = []; 29 | for (var key in logVal.param) { 30 | if (logVal.param.hasOwnProperty(key)) { 31 | var response = logVal.param[key]; 32 | if (response.length > 0) { 33 | apis.push({ 34 | type: "group", 35 | value: key + " 的方法包括;" 36 | }); 37 | for (var k = 0; k < response.length; k++) { 38 | apis.push({ 39 | type: "api", 40 | value: " - " + response[k] 41 | }); 42 | } 43 | } 44 | } 45 | } 46 | addStore({ 47 | type: "list", 48 | apis: apis 49 | }); 50 | } else if (logVal.action.indexOf("apropos.") >= 0) { 51 | // 特殊处理 API 接口的显示 52 | var doc = logVal.param; 53 | addStore({ 54 | type: "apropos_item", 55 | doc: doc 56 | }); 57 | } else if (logVal.action == 'eval') { 58 | var r = ''; 59 | if (logVal.param){ 60 | r = logVal.param.result || logVal.param.err; 61 | } 62 | addStore({ 63 | type: "evalResult", 64 | message: r?r:'(空)' 65 | }); 66 | } else if (logVal.action == 'console.log') { 67 | addStore({ 68 | type: "console.log", 69 | message: logVal.param.text 70 | }); 71 | } else { 72 | // 先显示日志类型, 73 | var eleId = "eid" + window.kLogIndex++; 74 | /** 75 | * 先初始化一个带 id 的 div,然后在确认渲染成功后,使用 dom 原生的方法,把 renderjson 对象加上去. 76 | * 注意 renderjson 对象是带事件的,如果直接渲染为 HTML 会出现丢失事件的情况 77 | * 78 | * */ 79 | 80 | var preEle = renderjson.set_icons("+", "-").set_show_to_level(2)(logVal); 81 | 82 | var metaFunc = function (_id, e, d) { 83 | return function () { 84 | var ele = d.getElementById(_id); 85 | ele.appendChild(e); 86 | }; 87 | }; 88 | addStore( 89 | { 90 | type: "log", 91 | message: logType, 92 | eid: eleId 93 | }, 94 | metaFunc(eleId, preEle, document) 95 | ); 96 | } 97 | } 98 | } 99 | 100 | window.kLogIndex = 1; 101 | function loop() { 102 | var xhr = new XMLHttpRequest(); 103 | xhr.open("POST", "/react_log.do", true); 104 | xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 105 | xhr.responseType = "json"; 106 | xhr.onload = function () { 107 | // console.log(xhr.response); 108 | var json = xhr.response; 109 | if (json.code == "OK") { 110 | var data = json.data; 111 | var logs = data ? data.logs : []; 112 | _renderLogs(logs); 113 | } 114 | }; 115 | xhr.send(""); 116 | } 117 | 118 | function scrollToBottom() { 119 | // scroll to bottom 120 | var output = document.getElementById("app"); 121 | output.scrollTop = output.scrollHeight; 122 | } 123 | 124 | // 如果可以处理,且已经处理完毕,则返回 null 125 | // 无法处理的则抛到外部 126 | function _parseCommand(com) { 127 | if (com.indexOf(":") == 0) { 128 | var args; 129 | if (com == ":testcase") { 130 | com = "window.appHost.invoke('testcase', {})"; 131 | } else if (com.indexOf(":list") >= 0) { 132 | com = "window.appHost.invoke('list', {})"; 133 | } else if (com.indexOf(":apropos") >= 0) { 134 | args = com.split(" "); 135 | if (args.length == 2) { 136 | com = "window.appHost.invoke('apropos', {name:'" + args[1] + "'})"; 137 | } else { 138 | console.log("参数出错 " + com); 139 | com = null; 140 | } 141 | } else if (com.indexOf(":weinre") >= 0) { 142 | var url = com.replace(':weinre','').trim(); 143 | if (url.length > 0) { 144 | if (url === "disable") { 145 | com = "window.appHost.invoke('weinre', {disabled:true})"; 146 | } else { 147 | com = "window.appHost.invoke('weinre', {url:'" + url + "'})"; 148 | } 149 | } else { 150 | console.log("参数出错 " + com); 151 | com = null; 152 | } 153 | } else if (com.indexOf(":timing") >= 0) { 154 | var mobile = com.replace(":timing", "").trim(); 155 | if (mobile.length > 0){ 156 | com = "window.appHost.invoke('timing', {mobile:true})"; 157 | } else { 158 | com = "window.appHost.invoke('timing', {})"; 159 | } 160 | } else if (com.indexOf(":eval") >= 0) { 161 | var code = com.replace(":eval", "").trim(); 162 | if (code.length > 0) { 163 | var p = window.JSON.stringify({ code: code.trim() }); 164 | com = "window.appHost.invoke('eval', " + p + ")"; 165 | } else { 166 | console.log("参数出错 " + com); 167 | com = null; 168 | } 169 | } else { 170 | window.alert("不支持的命令 " + com); 171 | com = null; 172 | } 173 | } 174 | 175 | return com; 176 | } 177 | // vue 178 | var store = { 179 | debug: true, 180 | state: { 181 | dataSource: [] 182 | }, 183 | setMessageAction: function (newValue) { 184 | if (this.debug) console.log("setMessageAction triggered with", newValue); 185 | this.state.message = newValue; 186 | }, 187 | clearMessageAction: function () { 188 | if (this.debug) console.log("clearMessageAction triggered"); 189 | this.state.message = ""; 190 | } 191 | }; 192 | 193 | function addStore(_obj, _domreadyblock) { 194 | store.state.dataSource.push(_obj); 195 | Vue.nextTick(function () { 196 | if (_domreadyblock && typeof _domreadyblock === "function") { 197 | _domreadyblock(); 198 | } 199 | scrollToBottom(); 200 | }); 201 | } 202 | // 输入命令和点击按钮区域 203 | var clientStorage = window.localStorage; 204 | var COMMOND_HISTORY = 'command_history'; 205 | var history_header_cursor = clientStorage.length; 206 | var history_search_cursor = history_header_cursor; 207 | var MAX_HISTORY = 100; 208 | 209 | function _run_command(com) { 210 | if (com.length === 0) { 211 | alert("请输入命令"); 212 | return; 213 | } 214 | // 先处理对控制台的控制的命令,然后处理需要获取业务数据的命令 215 | if (com == ":clear") { 216 | store.state.dataSource.splice(0); 217 | com = null; 218 | } else if (com == ":history") { 219 | var cm = []; 220 | var len = clientStorage.length; 221 | for (var i = len - 1; i >= 0; i--){ 222 | cm.push(clientStorage.getItem(COMMOND_HISTORY + i)); 223 | } 224 | addStore({ 225 | type: "history", 226 | data: cm 227 | }); 228 | } else if (com == ":help") { 229 | addStore({ 230 | type: "help", 231 | message: "" 232 | }); 233 | } else { 234 | addStore({ 235 | type: "command", 236 | message: com 237 | }); 238 | try { 239 | var newCom = _parseCommand(com); 240 | if (newCom && newCom.length > 0) { 241 | var r = window.eval(newCom); 242 | if (r) { 243 | addStore({ 244 | type: "evalResult", 245 | message: r.toString() 246 | }); 247 | } 248 | } 249 | } catch (error) { 250 | if (error) { 251 | addStore({ 252 | type: "error", 253 | message: error.message 254 | }); 255 | } 256 | } 257 | } 258 | } 259 | 260 | Vue.component("command-value", { 261 | data: function () { 262 | return { 263 | command: ":help" 264 | }; 265 | }, 266 | template: "#command-value-template", 267 | methods: { 268 | submit: function () { 269 | this.$refs.run.click(); 270 | }, 271 | history: function(up){ 272 | if (up){ 273 | history_search_cursor--; 274 | } else { 275 | history_search_cursor++; 276 | } 277 | history_search_cursor = history_search_cursor % MAX_HISTORY; 278 | history_search_cursor = Math.max(0, history_search_cursor); 279 | history_search_cursor = Math.min(history_header_cursor, history_search_cursor); 280 | var n = clientStorage.getItem(COMMOND_HISTORY + history_search_cursor); 281 | if (n){ 282 | document.getElementById('command').value = n; 283 | this.command = n; 284 | } 285 | }, 286 | run: function () { 287 | var com = this.command; 288 | var oldCom = com; 289 | 290 | if(window.ah_env.isMobile){ 291 | com = ':eval ' + com; 292 | } 293 | _run_command(com); 294 | command.value = ''; 295 | this.command = ''; 296 | // 297 | clientStorage.setItem(COMMOND_HISTORY + (history_header_cursor++), oldCom); 298 | history_search_cursor = history_header_cursor; 299 | } 300 | } 301 | }); 302 | 303 | // 执行结果或者服务器推送的结果区域 304 | Vue.component("command-output", { 305 | data: function () { 306 | return { 307 | dataSource: store.state.dataSource 308 | }; 309 | }, 310 | methods: { 311 | useHistoryCommand: function(e){ 312 | var ele = e.target; 313 | var com = ele.dataset.command; 314 | _run_command(com); 315 | } 316 | }, 317 | template: "#command-output-template" 318 | }); 319 | 320 | document.addEventListener( 321 | "DOMContentLoaded", 322 | function (event) { 323 | console.log("DOM ready!"); 324 | var app = new Vue({ 325 | el: "#app", 326 | created: function () { 327 | console.log("App goes"); 328 | }, 329 | mounted: function () { 330 | window.setInterval(loop, 2000); 331 | } 332 | }); 333 | }, 334 | false 335 | ); 336 | 337 | function jdb(line) { 338 | // do a thing, possibly async, then… 339 | if (window.__bri == line) { 340 | window.alert("Stop at Debugger;"); 341 | } else { 342 | console.log("Skip at line " + line); 343 | } 344 | } 345 | document.addEventListener("readystatechange", function (event) { 346 | console.log("readystatechange!" + document.readyState); 347 | if (document.readyState == "complete") { 348 | // jsdebugger 349 | window.__bri = -1; 350 | 351 | var command = document.getElementById("command"); 352 | // jdb(0); 353 | // var run = document.getElementById('run'); 354 | // jdb(1); 355 | // var a = 10; 356 | // jdb(2) 357 | // var c = 9; 358 | // jdb(3) 359 | // a = a + ~c + 1; 360 | // jdb(4) 361 | // console.log(a); 362 | // jdb(5) 363 | 364 | // run.onclick = function (e) { 365 | // var com = command.value; jdb(6); 366 | // if (com.length > 0) { 367 | // eval(com); jdb(7); 368 | // } else { 369 | // alert('请输入命令'); jdb(8); 370 | // } 371 | // } 372 | } 373 | }); 374 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/testcase.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 60 | appHost 接口测试 61 | 81 | 82 | 83 | 84 |
85 |
86 | 测试webview浏览器 87 |
    88 |
  1. 89 | 90 |
    91 | 92 | 使用webview页面,加载以上链接 93 |
  2. 94 |
95 |
96 | {{ALL_DOCS}} 97 |
98 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /AppHostExample/AppHost.framework/thirdParty/weinreSupport.js: -------------------------------------------------------------------------------- 1 | if (window.appHost) { 2 | window.appHost.on('weinre.enable', function () { 3 | // 加载完成的页面要使用远程调试需要重新加载 webview 才行 4 | window.location.reload(); 5 | }); 6 | } else { 7 | console.log('无 AppHost 对象'); 8 | } 9 | -------------------------------------------------------------------------------- /AppHostExample/AppHostExample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #include "Preview-Header.h" 6 | -------------------------------------------------------------------------------- /AppHostExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /AppHostExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /AppHostExample/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 | -------------------------------------------------------------------------------- /AppHostExample/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 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 51 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /AppHostExample/BaseViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.h 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface BaseViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /AppHostExample/BaseViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.m 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import "BaseViewController.h" 10 | 11 | @interface BaseViewController () 12 | 13 | @end 14 | 15 | @implementation BaseViewController 16 | 17 | - (void)viewDidLoad { 18 | [super viewDidLoad]; 19 | // Do any additional setup after loading the view. 20 | } 21 | 22 | /* 23 | #pragma mark - Navigation 24 | 25 | // In a storyboard-based application, you will often want to do a little preparation before navigation 26 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { 27 | // Get the new view controller using [segue destinationViewController]. 28 | // Pass the selected object to the new view controller. 29 | } 30 | */ 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /AppHostExample/DescTableViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // DescTableViewCell.h 3 | // AppHostExample 4 | // 5 | // Created by liang on 2020/1/2. 6 | // Copyright © 2020 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface DescTableViewCell : UITableViewCell 14 | 15 | - (void)configureWithTitle:(NSString *)title desc:(NSString *)desc; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /AppHostExample/DescTableViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // DescTableViewCell.m 3 | // AppHostExample 4 | // 5 | // Created by liang on 2020/1/2. 6 | // Copyright © 2020 liang. All rights reserved. 7 | // 8 | 9 | #import "DescTableViewCell.h" 10 | 11 | @interface DescTableViewCell () 12 | 13 | @end 14 | 15 | @implementation DescTableViewCell 16 | 17 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{ 18 | if (self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier]) { 19 | self.detailTextLabel.numberOfLines = -1; 20 | self.detailTextLabel.font = [UIFont systemFontOfSize:16]; 21 | self.detailTextLabel.text = @"这里显示描述,可以多行,支持换行哦。\n,z在🏠写文字看看效果;"; 22 | 23 | self.textLabel.numberOfLines = -1; 24 | self.textLabel.font = [UIFont systemFontOfSize:22]; 25 | self.textLabel.textColor = [UIColor blueColor]; 26 | self.textLabel.text = @"这里显示标题"; 27 | 28 | // self.backgroundColor = [UIColor yellowColor]; 29 | } 30 | return self; 31 | } 32 | 33 | - (void)configureWithTitle:(NSString *)title desc:(NSString *)desc{ 34 | 35 | self.textLabel.text = title; 36 | self.detailTextLabel.text = desc; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /AppHostExample/HUDResponse.h: -------------------------------------------------------------------------------- 1 | // 2 | // HUDResponse.h 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface HUDResponse : AppHostResponse 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /AppHostExample/HUDResponse.m: -------------------------------------------------------------------------------- 1 | // 2 | // HUDResponse.m 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import "HUDResponse.h" 10 | #import "UIView+Toast.h" 11 | 12 | @implementation HUDResponse 13 | 14 | + (NSDictionary *)supportActionList 15 | { 16 | return @{ 17 | @"toast_":@"1", 18 | @"showLoading_":@"1", 19 | @"hideLoading":@"1" 20 | }; 21 | } 22 | 23 | #pragma mark - override 24 | ah_doc_begin(hideLoading, "隐藏 loading 的 HUD 动画,UIView+Toast实现。") 25 | ah_doc_code(window.appHost.invoke("hideLoading")) 26 | ah_doc_code_expect("在有 loading 动画的情况下,调用此接口,会隐藏 loading。") 27 | ah_doc_end 28 | - (void)hideLoading 29 | { 30 | [self.appHost.view hideToastActivity]; 31 | } 32 | ah_doc_begin(showLoading_, "loading 的 HUD 动画,UIView+Toast实现。") 33 | ah_doc_param(text, "字符串,设置和 loading 动画一起显示的文案") 34 | ah_doc_code(window.appHost.invoke("showLoading",{"text":"请稍等..."})) 35 | ah_doc_code_expect("UIView+Toast 只会在屏幕上出现 loading 动画,不显示文字") 36 | ah_doc_end 37 | - (void)showLoading:(NSDictionary *)paramDict 38 | { 39 | // display toast with an activity spinner 40 | [self.appHost.view makeToastActivity:CSToastPositionCenter]; 41 | } 42 | ah_doc_begin(toast_, "显示居中的提示,过几秒后消失,UIView+Toast实现。") 43 | ah_doc_param(text, "字符串,显示的文案,可多行") 44 | ah_doc_code(window.appHost.invoke("toast",{"text":"请稍\n等..."})) 45 | ah_doc_code_expect("在屏幕上出现 '请稍等...',多次调用此接口,会出现多个") 46 | ah_doc_end 47 | - (void)toast:(NSDictionary *)paramDict 48 | { 49 | CGFloat delay = [[paramDict objectForKey:@"delay"] floatValue]; 50 | [self showTextTip:[paramDict objectForKey:@"text"] delay:delay]; 51 | } 52 | 53 | - (void)showTextTip:(NSString *)tip delay:(CGFloat)delay 54 | { 55 | // create a new style 56 | CSToastStyle *style = [[CSToastStyle alloc] initWithDefaultStyle]; 57 | 58 | // this is just one of many style options 59 | style.messageColor = [UIColor orangeColor]; 60 | 61 | // present the toast with the new style 62 | [self.appHost.view makeToast:tip 63 | duration:delay>0?delay:3 64 | position:CSToastPositionBottom 65 | style:style]; 66 | } 67 | @end 68 | -------------------------------------------------------------------------------- /AppHostExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarTintParameters 32 | 33 | UINavigationBar 34 | 35 | Style 36 | UIBarStyleDefault 37 | Translucent 38 | 39 | 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | NSAppTransportSecurity 55 | 56 | NSAllowsArbitraryLoads 57 | 58 | NSAllowsArbitraryLoadsInWebContent 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /AppHostExample/MasterViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MasterViewController.h 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class DetailViewController; 12 | 13 | @interface MasterViewController : UITableViewController 14 | 15 | @property (strong, nonatomic) DetailViewController *detailViewController; 16 | 17 | 18 | @end 19 | 20 | -------------------------------------------------------------------------------- /AppHostExample/MasterViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MasterViewController.m 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import "MasterViewController.h" 10 | #import "WebViewViewController.h" 11 | #import 12 | #import "HUDResponse.h" 13 | #import "DescTableViewCell.h" 14 | 15 | @interface MasterViewController () 16 | 17 | @property NSArray *objects; 18 | @end 19 | 20 | @implementation MasterViewController 21 | 22 | - (void)viewDidLoad { 23 | [super viewDidLoad]; 24 | 25 | 26 | kWebViewProgressTintColorRGB = 0xdcb000; 27 | kFakeCookieWebPageURLWithQueryString = @"https://you.163.com?26u-KQa-fKQ-3BD"; 28 | kGCDWebServer_logging_enabled = YES; 29 | [[AHDebugServerManager sharedInstance] showDebugWindow]; 30 | [[AHDebugServerManager sharedInstance] start]; 31 | // 添加新的 Response,提供新的接口能力 32 | [[AHResponseManager defaultManager] addCustomResponse:HUDResponse.class]; 33 | 34 | // Do any additional setup after loading the view. 35 | // https://h5.m.jd.com/babelDiy/Zeus/2eWUit9hhREhCRJqkBCB1VXCTvw9/index.html?channel=5&lng=120.186718&lat=30.189141&un_area=15_1213_1215_50115&sid=67b5c76977573adac5a0b17718113f7w#/ 36 | NSArray *dataSource = [NSArray arrayWithObjects: 37 | @{@"name":@"加载京东页面,拦截京东 JSBridge 协议", 38 | @"url": @"https://item.m.jd.com/ware/view.action?wareId=5904827&sid=null", 39 | @"desc":@"本用例展示:AppHost 不仅提供了内置的 JSBridge 协议,还可以和原有的协议共存。\n 通过继承 AppHostViewController,重载了 decidePolicy 来实现这一点。保持内聚的同时,也具备一定的灵活性。\na.另外,可以看到 AppHostRespone 作为业务逻辑实现类的角色,不仅可以被 h5 调用,AppHost 也可以让 native 主动调用此能力。将前后端能力统一。\nb. 操作步骤;\n, 点击顶部的立即下载,此时弹出一个 toast,内容是京东 JSBridge 接口参数。\n 另外这里的 toast 是由 AppHostRespone 的扩展类 HUDResponse 来实现具体功能的,展示灵活的业务扩展能力。\nPS: 通过 AppHost 提供的 debugger,可以看到线上的代码里还有 console.log 日志" 40 | }, 41 | @{@"name":@"加载严选移动端首页,观察其性能参数", 42 | @"url": @"https://m.you.163.com", 43 | @"desc": @"本用例展示:AppHost 的定制能力和 profile 工具。这次加载的进度条是金色的,颜色是可配置的,当遇到某个请求 302 时,进度条也可以正常显示。\n查看 profile 步骤,运行之后,按照 XCode 日志里的提示(或者点击 App 里右上角一个 AH 样的图标,展开后的日志了有 url,长按复制或者在浏览器输入),用电脑浏览器打开调试页面,按照提示或者点击左侧快捷菜单、或直接输入 :timing 接口查看" 44 | }, 45 | @{@"name":@"加载严选酒水专题页面,使用 weinre 调试样式", 46 | @"url": @"http://m.you.163.com/item/list?categoryId=1005002&style=pd", 47 | @"desc":@"本用例展示:AppHost 的扩展能力,可以接入现有的工具,增强调试能力。\n 在命令行里输入,本地启动的 weinre 目标调试脚本,不需要修改目标调试页面即可实现对此页面的调试,而且对后续的所有页面都有效。\n注意:因为浏览器 CSP 的限制 https 的页面无法加载 http 的资源。如果需要调试 https,你可能需要安装 ngrok,输入命令,:weinre https://3c2c9d94.ngrok.io/target/target-script-min.js#anonymous" 48 | }, 49 | @{@"name":@"加载本地文件夹,测试接口参数", 50 | @"fileName":@"/index.html", 51 | @"dir": @"TestCase", 52 | @"domain": @"https://m.you.163.com", 53 | @"desc": @"本用例展示:AppHost 加载本地文件夹资源的能力。可以加载 html,以及 html 里引用的相同目录下的 图片资源\\javascript\\css 文件 3 类资源(不支持字体等)。\n如果要加载的主域是 http 的,可以使用嵌套性的资源引用,如 css 文件里引用了一个相对路径的图标。" 54 | }, 55 | nil]; 56 | self.objects = dataSource; 57 | 58 | [self.tableView registerClass:DescTableViewCell.class forCellReuseIdentifier:@"Cell"]; 59 | self.tableView.rowHeight = UITableViewAutomaticDimension; 60 | } 61 | 62 | #pragma mark - Table View 63 | 64 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 65 | return 1; 66 | } 67 | 68 | - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(nonnull NSIndexPath *)indexPath{ 69 | return 100; 70 | } 71 | 72 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 73 | return self.objects.count; 74 | } 75 | 76 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 77 | DescTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; 78 | 79 | NSDictionary *object = self.objects[indexPath.row]; 80 | 81 | [cell configureWithTitle:[NSString stringWithFormat:@"%ld - %@", (long)indexPath.row + 1, [object objectForKey:@"name"]] desc:[object objectForKey:@"desc"]]; 82 | return cell; 83 | } 84 | 85 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 86 | { 87 | NSDictionary *object = self.objects[indexPath.row]; 88 | NSString *url = [object objectForKey:@"url"]; 89 | NSString *fileName = [object objectForKey:@"fileName"]; 90 | 91 | if (url.length > 0) { 92 | WebViewViewController *vc = [WebViewViewController new]; 93 | if (![vc.url isEqualToString:url]) { 94 | vc.url = url; 95 | } 96 | [self.navigationController pushViewController:vc animated:YES]; 97 | 98 | } else if(fileName.length > 0){ 99 | WebViewViewController *vc = [WebViewViewController new]; 100 | NSString *dir = [object objectForKey:@"dir"]; 101 | NSURL * _Nonnull mainURL = [[NSBundle mainBundle] bundleURL]; 102 | NSString* domain = [object objectForKey:@"domain"]; 103 | if (dir.length > 0) { 104 | NSURL *url = [mainURL URLByAppendingPathComponent:dir]; 105 | [vc loadIndexFile:fileName inDirectory:url domain:domain]; 106 | } else { 107 | [vc loadLocalFile:[mainURL URLByAppendingPathComponent:fileName] domain:domain]; 108 | } 109 | 110 | [self.navigationController pushViewController:vc animated:YES]; 111 | } 112 | } 113 | 114 | @end 115 | -------------------------------------------------------------------------------- /AppHostExample/Preview-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Preview-Header.h 3 | // AppHostExample 4 | // 5 | // Created by liang on 2020/1/2. 6 | // Copyright © 2020 liang. All rights reserved. 7 | // 8 | #import "MasterViewController.h" 9 | #import "DescTableViewCell.h" 10 | -------------------------------------------------------------------------------- /AppHostExample/UIView+Toast.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Toast.h 3 | // Toast 4 | // 5 | // Copyright (c) 2011-2017 Charles Scalesse. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a 8 | // copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | #import 27 | 28 | extern const NSString * CSToastPositionTop; 29 | extern const NSString * CSToastPositionCenter; 30 | extern const NSString * CSToastPositionBottom; 31 | 32 | @class CSToastStyle; 33 | 34 | /** 35 | Toast is an Objective-C category that adds toast notifications to the UIView 36 | object class. It is intended to be simple, lightweight, and easy to use. Most 37 | toast notifications can be triggered with a single line of code. 38 | 39 | The `makeToast:` methods create a new view and then display it as toast. 40 | 41 | The `showToast:` methods display any view as toast. 42 | 43 | */ 44 | @interface UIView (Toast) 45 | 46 | /** 47 | Creates and presents a new toast view with a message and displays it with the 48 | default duration and position. Styled using the shared style. 49 | 50 | @param message The message to be displayed 51 | */ 52 | - (void)makeToast:(NSString *)message; 53 | 54 | /** 55 | Creates and presents a new toast view with a message. Duration and position 56 | can be set explicitly. Styled using the shared style. 57 | 58 | @param message The message to be displayed 59 | @param duration The toast duration 60 | @param position The toast's center point. Can be one of the predefined CSToastPosition 61 | constants or a `CGPoint` wrapped in an `NSValue` object. 62 | */ 63 | - (void)makeToast:(NSString *)message 64 | duration:(NSTimeInterval)duration 65 | position:(id)position; 66 | 67 | /** 68 | Creates and presents a new toast view with a message. Duration, position, and 69 | style can be set explicitly. 70 | 71 | @param message The message to be displayed 72 | @param duration The toast duration 73 | @param position The toast's center point. Can be one of the predefined CSToastPosition 74 | constants or a `CGPoint` wrapped in an `NSValue` object. 75 | @param style The style. The shared style will be used when nil 76 | */ 77 | - (void)makeToast:(NSString *)message 78 | duration:(NSTimeInterval)duration 79 | position:(id)position 80 | style:(CSToastStyle *)style; 81 | 82 | /** 83 | Creates and presents a new toast view with a message, title, and image. Duration, 84 | position, and style can be set explicitly. The completion block executes when the 85 | toast view completes. `didTap` will be `YES` if the toast view was dismissed from 86 | a tap. 87 | 88 | @param message The message to be displayed 89 | @param duration The toast duration 90 | @param position The toast's center point. Can be one of the predefined CSToastPosition 91 | constants or a `CGPoint` wrapped in an `NSValue` object. 92 | @param title The title 93 | @param image The image 94 | @param style The style. The shared style will be used when nil 95 | @param completion The completion block, executed after the toast view disappears. 96 | didTap will be `YES` if the toast view was dismissed from a tap. 97 | */ 98 | - (void)makeToast:(NSString *)message 99 | duration:(NSTimeInterval)duration 100 | position:(id)position 101 | title:(NSString *)title 102 | image:(UIImage *)image 103 | style:(CSToastStyle *)style 104 | completion:(void(^)(BOOL didTap))completion; 105 | 106 | /** 107 | Creates a new toast view with any combination of message, title, and image. 108 | The look and feel is configured via the style. Unlike the `makeToast:` methods, 109 | this method does not present the toast view automatically. One of the showToast: 110 | methods must be used to present the resulting view. 111 | 112 | @warning if message, title, and image are all nil, this method will return nil. 113 | 114 | @param message The message to be displayed 115 | @param title The title 116 | @param image The image 117 | @param style The style. The shared style will be used when nil 118 | @return The newly created toast view 119 | */ 120 | - (UIView *)toastViewForMessage:(NSString *)message 121 | title:(NSString *)title 122 | image:(UIImage *)image 123 | style:(CSToastStyle *)style; 124 | 125 | /** 126 | Hides the active toast. If there are multiple toasts active in a view, this method 127 | hides the oldest toast (the first of the toasts to have been presented). 128 | 129 | @see `hideAllToasts` to remove all active toasts from a view. 130 | 131 | @warning This method has no effect on activity toasts. Use `hideToastActivity` to 132 | hide activity toasts. 133 | */ 134 | - (void)hideToast; 135 | 136 | /** 137 | Hides an active toast. 138 | 139 | @param toast The active toast view to dismiss. Any toast that is currently being displayed 140 | on the screen is considered active. 141 | 142 | @warning this does not clear a toast view that is currently waiting in the queue. 143 | */ 144 | - (void)hideToast:(UIView *)toast; 145 | 146 | /** 147 | Hides all active toast views and clears the queue. 148 | */ 149 | - (void)hideAllToasts; 150 | 151 | /** 152 | Hides all active toast views, with options to hide activity and clear the queue. 153 | 154 | @param includeActivity If `true`, toast activity will also be hidden. Default is `false`. 155 | @param clearQueue If `true`, removes all toast views from the queue. Default is `true`. 156 | */ 157 | - (void)hideAllToasts:(BOOL)includeActivity clearQueue:(BOOL)clearQueue; 158 | 159 | /** 160 | Removes all toast views from the queue. This has no effect on toast views that are 161 | active. Use `hideAllToasts` to hide the active toasts views and clear the queue. 162 | */ 163 | - (void)clearToastQueue; 164 | 165 | /** 166 | Creates and displays a new toast activity indicator view at a specified position. 167 | 168 | @warning Only one toast activity indicator view can be presented per superview. Subsequent 169 | calls to `makeToastActivity:` will be ignored until hideToastActivity is called. 170 | 171 | @warning `makeToastActivity:` works independently of the showToast: methods. Toast activity 172 | views can be presented and dismissed while toast views are being displayed. `makeToastActivity:` 173 | has no effect on the queueing behavior of the showToast: methods. 174 | 175 | @param position The toast's center point. Can be one of the predefined CSToastPosition 176 | constants or a `CGPoint` wrapped in an `NSValue` object. 177 | */ 178 | - (void)makeToastActivity:(id)position; 179 | 180 | /** 181 | Dismisses the active toast activity indicator view. 182 | */ 183 | - (void)hideToastActivity; 184 | 185 | /** 186 | Displays any view as toast using the default duration and position. 187 | 188 | @param toast The view to be displayed as toast 189 | */ 190 | - (void)showToast:(UIView *)toast; 191 | 192 | /** 193 | Displays any view as toast at a provided position and duration. The completion block 194 | executes when the toast view completes. `didTap` will be `YES` if the toast view was 195 | dismissed from a tap. 196 | 197 | @param toast The view to be displayed as toast 198 | @param duration The notification duration 199 | @param position The toast's center point. Can be one of the predefined CSToastPosition 200 | constants or a `CGPoint` wrapped in an `NSValue` object. 201 | @param completion The completion block, executed after the toast view disappears. 202 | didTap will be `YES` if the toast view was dismissed from a tap. 203 | */ 204 | - (void)showToast:(UIView *)toast 205 | duration:(NSTimeInterval)duration 206 | position:(id)position 207 | completion:(void(^)(BOOL didTap))completion; 208 | 209 | @end 210 | 211 | /** 212 | `CSToastStyle` instances define the look and feel for toast views created via the 213 | `makeToast:` methods as well for toast views created directly with 214 | `toastViewForMessage:title:image:style:`. 215 | 216 | @warning `CSToastStyle` offers relatively simple styling options for the default 217 | toast view. If you require a toast view with more complex UI, it probably makes more 218 | sense to create your own custom UIView subclass and present it with the `showToast:` 219 | methods. 220 | */ 221 | @interface CSToastStyle : NSObject 222 | 223 | /** 224 | The background color. Default is `[UIColor blackColor]` at 80% opacity. 225 | */ 226 | @property (strong, nonatomic) UIColor *backgroundColor; 227 | 228 | /** 229 | The title color. Default is `[UIColor whiteColor]`. 230 | */ 231 | @property (strong, nonatomic) UIColor *titleColor; 232 | 233 | /** 234 | The message color. Default is `[UIColor whiteColor]`. 235 | */ 236 | @property (strong, nonatomic) UIColor *messageColor; 237 | 238 | /** 239 | A percentage value from 0.0 to 1.0, representing the maximum width of the toast 240 | view relative to it's superview. Default is 0.8 (80% of the superview's width). 241 | */ 242 | @property (assign, nonatomic) CGFloat maxWidthPercentage; 243 | 244 | /** 245 | A percentage value from 0.0 to 1.0, representing the maximum height of the toast 246 | view relative to it's superview. Default is 0.8 (80% of the superview's height). 247 | */ 248 | @property (assign, nonatomic) CGFloat maxHeightPercentage; 249 | 250 | /** 251 | The spacing from the horizontal edge of the toast view to the content. When an image 252 | is present, this is also used as the padding between the image and the text. 253 | Default is 10.0. 254 | */ 255 | @property (assign, nonatomic) CGFloat horizontalPadding; 256 | 257 | /** 258 | The spacing from the vertical edge of the toast view to the content. When a title 259 | is present, this is also used as the padding between the title and the message. 260 | Default is 10.0. 261 | */ 262 | @property (assign, nonatomic) CGFloat verticalPadding; 263 | 264 | /** 265 | The corner radius. Default is 10.0. 266 | */ 267 | @property (assign, nonatomic) CGFloat cornerRadius; 268 | 269 | /** 270 | The title font. Default is `[UIFont boldSystemFontOfSize:16.0]`. 271 | */ 272 | @property (strong, nonatomic) UIFont *titleFont; 273 | 274 | /** 275 | The message font. Default is `[UIFont systemFontOfSize:16.0]`. 276 | */ 277 | @property (strong, nonatomic) UIFont *messageFont; 278 | 279 | /** 280 | The title text alignment. Default is `NSTextAlignmentLeft`. 281 | */ 282 | @property (assign, nonatomic) NSTextAlignment titleAlignment; 283 | 284 | /** 285 | The message text alignment. Default is `NSTextAlignmentLeft`. 286 | */ 287 | @property (assign, nonatomic) NSTextAlignment messageAlignment; 288 | 289 | /** 290 | The maximum number of lines for the title. The default is 0 (no limit). 291 | */ 292 | @property (assign, nonatomic) NSInteger titleNumberOfLines; 293 | 294 | /** 295 | The maximum number of lines for the message. The default is 0 (no limit). 296 | */ 297 | @property (assign, nonatomic) NSInteger messageNumberOfLines; 298 | 299 | /** 300 | Enable or disable a shadow on the toast view. Default is `NO`. 301 | */ 302 | @property (assign, nonatomic) BOOL displayShadow; 303 | 304 | /** 305 | The shadow color. Default is `[UIColor blackColor]`. 306 | */ 307 | @property (strong, nonatomic) UIColor *shadowColor; 308 | 309 | /** 310 | A value from 0.0 to 1.0, representing the opacity of the shadow. 311 | Default is 0.8 (80% opacity). 312 | */ 313 | @property (assign, nonatomic) CGFloat shadowOpacity; 314 | 315 | /** 316 | The shadow radius. Default is 6.0. 317 | */ 318 | @property (assign, nonatomic) CGFloat shadowRadius; 319 | 320 | /** 321 | The shadow offset. The default is `CGSizeMake(4.0, 4.0)`. 322 | */ 323 | @property (assign, nonatomic) CGSize shadowOffset; 324 | 325 | /** 326 | The image size. The default is `CGSizeMake(80.0, 80.0)`. 327 | */ 328 | @property (assign, nonatomic) CGSize imageSize; 329 | 330 | /** 331 | The size of the toast activity view when `makeToastActivity:` is called. 332 | Default is `CGSizeMake(100.0, 100.0)`. 333 | */ 334 | @property (assign, nonatomic) CGSize activitySize; 335 | 336 | /** 337 | The fade in/out animation duration. Default is 0.2. 338 | */ 339 | @property (assign, nonatomic) NSTimeInterval fadeDuration; 340 | 341 | /** 342 | Creates a new instance of `CSToastStyle` with all the default values set. 343 | */ 344 | - (instancetype)initWithDefaultStyle NS_DESIGNATED_INITIALIZER; 345 | 346 | /** 347 | @warning Only the designated initializer should be used to create 348 | an instance of `CSToastStyle`. 349 | */ 350 | - (instancetype)init NS_UNAVAILABLE; 351 | 352 | @end 353 | 354 | /** 355 | `CSToastManager` provides general configuration options for all toast 356 | notifications. Backed by a singleton instance. 357 | */ 358 | @interface CSToastManager : NSObject 359 | 360 | /** 361 | Sets the shared style on the singleton. The shared style is used whenever 362 | a `makeToast:` method (or `toastViewForMessage:title:image:style:`) is called 363 | with with a nil style. By default, this is set to `CSToastStyle`'s default 364 | style. 365 | 366 | @param sharedStyle the shared style 367 | */ 368 | + (void)setSharedStyle:(CSToastStyle *)sharedStyle; 369 | 370 | /** 371 | Gets the shared style from the singlton. By default, this is 372 | `CSToastStyle`'s default style. 373 | 374 | @return the shared style 375 | */ 376 | + (CSToastStyle *)sharedStyle; 377 | 378 | /** 379 | Enables or disables tap to dismiss on toast views. Default is `YES`. 380 | 381 | @param tapToDismissEnabled YES or NO 382 | */ 383 | + (void)setTapToDismissEnabled:(BOOL)tapToDismissEnabled; 384 | 385 | /** 386 | Returns `YES` if tap to dismiss is enabled, otherwise `NO`. 387 | Default is `YES`. 388 | 389 | @return BOOL YES or NO 390 | */ 391 | + (BOOL)isTapToDismissEnabled; 392 | 393 | /** 394 | Enables or disables queueing behavior for toast views. When `YES`, 395 | toast views will appear one after the other. When `NO`, multiple Toast 396 | views will appear at the same time (potentially overlapping depending 397 | on their positions). This has no effect on the toast activity view, 398 | which operates independently of normal toast views. Default is `NO`. 399 | 400 | @param queueEnabled YES or NO 401 | */ 402 | + (void)setQueueEnabled:(BOOL)queueEnabled; 403 | 404 | /** 405 | Returns `YES` if the queue is enabled, otherwise `NO`. 406 | Default is `NO`. 407 | 408 | @return BOOL 409 | */ 410 | + (BOOL)isQueueEnabled; 411 | 412 | /** 413 | Sets the default duration. Used for the `makeToast:` and 414 | `showToast:` methods that don't require an explicit duration. 415 | Default is 3.0. 416 | 417 | @param duration The toast duration 418 | */ 419 | + (void)setDefaultDuration:(NSTimeInterval)duration; 420 | 421 | /** 422 | Returns the default duration. Default is 3.0. 423 | 424 | @return duration The toast duration 425 | */ 426 | + (NSTimeInterval)defaultDuration; 427 | 428 | /** 429 | Sets the default position. Used for the `makeToast:` and 430 | `showToast:` methods that don't require an explicit position. 431 | Default is `CSToastPositionBottom`. 432 | 433 | @param position The default center point. Can be one of the predefined 434 | CSToastPosition constants or a `CGPoint` wrapped in an `NSValue` object. 435 | */ 436 | + (void)setDefaultPosition:(id)position; 437 | 438 | /** 439 | Returns the default toast position. Default is `CSToastPositionBottom`. 440 | 441 | @return position The default center point. Will be one of the predefined 442 | CSToastPosition constants or a `CGPoint` wrapped in an `NSValue` object. 443 | */ 444 | + (id)defaultPosition; 445 | 446 | @end 447 | -------------------------------------------------------------------------------- /AppHostExample/UIView+Toast.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Toast.m 3 | // Toast 4 | // 5 | // Copyright (c) 2011-2017 Charles Scalesse. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a 8 | // copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included 16 | // in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | #import "UIView+Toast.h" 27 | #import 28 | #import 29 | 30 | // Positions 31 | NSString * CSToastPositionTop = @"CSToastPositionTop"; 32 | NSString * CSToastPositionCenter = @"CSToastPositionCenter"; 33 | NSString * CSToastPositionBottom = @"CSToastPositionBottom"; 34 | 35 | // Keys for values associated with toast views 36 | static const NSString * CSToastTimerKey = @"CSToastTimerKey"; 37 | static const NSString * CSToastDurationKey = @"CSToastDurationKey"; 38 | static const NSString * CSToastPositionKey = @"CSToastPositionKey"; 39 | static const NSString * CSToastCompletionKey = @"CSToastCompletionKey"; 40 | 41 | // Keys for values associated with self 42 | static const NSString * CSToastActiveKey = @"CSToastActiveKey"; 43 | static const NSString * CSToastActivityViewKey = @"CSToastActivityViewKey"; 44 | static const NSString * CSToastQueueKey = @"CSToastQueueKey"; 45 | 46 | @interface UIView (ToastPrivate) 47 | 48 | /** 49 | These private methods are being prefixed with "cs_" to reduce the likelihood of non-obvious 50 | naming conflicts with other UIView methods. 51 | 52 | @discussion Should the public API also use the cs_ prefix? Technically it should, but it 53 | results in code that is less legible. The current public method names seem unlikely to cause 54 | conflicts so I think we should favor the cleaner API for now. 55 | */ 56 | - (void)cs_showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)position; 57 | - (void)cs_hideToast:(UIView *)toast; 58 | - (void)cs_hideToast:(UIView *)toast fromTap:(BOOL)fromTap; 59 | - (void)cs_toastTimerDidFinish:(NSTimer *)timer; 60 | - (void)cs_handleToastTapped:(UITapGestureRecognizer *)recognizer; 61 | - (CGPoint)cs_centerPointForPosition:(id)position withToast:(UIView *)toast; 62 | - (NSMutableArray *)cs_toastQueue; 63 | 64 | @end 65 | 66 | @implementation UIView (Toast) 67 | 68 | #pragma mark - Make Toast Methods 69 | 70 | - (void)makeToast:(NSString *)message { 71 | [self makeToast:message duration:[CSToastManager defaultDuration] position:[CSToastManager defaultPosition] style:nil]; 72 | } 73 | 74 | - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position { 75 | [self makeToast:message duration:duration position:position style:nil]; 76 | } 77 | 78 | - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position style:(CSToastStyle *)style { 79 | UIView *toast = [self toastViewForMessage:message title:nil image:nil style:style]; 80 | [self showToast:toast duration:duration position:position completion:nil]; 81 | } 82 | 83 | - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position title:(NSString *)title image:(UIImage *)image style:(CSToastStyle *)style completion:(void(^)(BOOL didTap))completion { 84 | UIView *toast = [self toastViewForMessage:message title:title image:image style:style]; 85 | [self showToast:toast duration:duration position:position completion:completion]; 86 | } 87 | 88 | #pragma mark - Show Toast Methods 89 | 90 | - (void)showToast:(UIView *)toast { 91 | [self showToast:toast duration:[CSToastManager defaultDuration] position:[CSToastManager defaultPosition] completion:nil]; 92 | } 93 | 94 | - (void)showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)position completion:(void(^)(BOOL didTap))completion { 95 | // sanity 96 | if (toast == nil) return; 97 | 98 | // store the completion block on the toast view 99 | objc_setAssociatedObject(toast, &CSToastCompletionKey, completion, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 100 | 101 | if ([CSToastManager isQueueEnabled] && [self.cs_activeToasts count] > 0) { 102 | // we're about to queue this toast view so we need to store the duration and position as well 103 | objc_setAssociatedObject(toast, &CSToastDurationKey, @(duration), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 104 | objc_setAssociatedObject(toast, &CSToastPositionKey, position, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 105 | 106 | // enqueue 107 | [self.cs_toastQueue addObject:toast]; 108 | } else { 109 | // present 110 | [self cs_showToast:toast duration:duration position:position]; 111 | } 112 | } 113 | 114 | #pragma mark - Hide Toast Methods 115 | 116 | - (void)hideToast { 117 | [self hideToast:[[self cs_activeToasts] firstObject]]; 118 | } 119 | 120 | - (void)hideToast:(UIView *)toast { 121 | // sanity 122 | if (!toast || ![[self cs_activeToasts] containsObject:toast]) return; 123 | 124 | [self cs_hideToast:toast]; 125 | } 126 | 127 | - (void)hideAllToasts { 128 | [self hideAllToasts:NO clearQueue:YES]; 129 | } 130 | 131 | - (void)hideAllToasts:(BOOL)includeActivity clearQueue:(BOOL)clearQueue { 132 | if (clearQueue) { 133 | [self clearToastQueue]; 134 | } 135 | 136 | for (UIView *toast in [self cs_activeToasts]) { 137 | [self hideToast:toast]; 138 | } 139 | 140 | if (includeActivity) { 141 | [self hideToastActivity]; 142 | } 143 | } 144 | 145 | - (void)clearToastQueue { 146 | [[self cs_toastQueue] removeAllObjects]; 147 | } 148 | 149 | #pragma mark - Private Show/Hide Methods 150 | 151 | - (void)cs_showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)position { 152 | toast.center = [self cs_centerPointForPosition:position withToast:toast]; 153 | toast.alpha = 0.0; 154 | 155 | if ([CSToastManager isTapToDismissEnabled]) { 156 | UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cs_handleToastTapped:)]; 157 | [toast addGestureRecognizer:recognizer]; 158 | toast.userInteractionEnabled = YES; 159 | toast.exclusiveTouch = YES; 160 | } 161 | 162 | [[self cs_activeToasts] addObject:toast]; 163 | 164 | [self addSubview:toast]; 165 | 166 | [UIView animateWithDuration:[[CSToastManager sharedStyle] fadeDuration] 167 | delay:0.0 168 | options:(UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction) 169 | animations:^{ 170 | toast.alpha = 1.0; 171 | } completion:^(BOOL finished) { 172 | NSTimer *timer = [NSTimer timerWithTimeInterval:duration target:self selector:@selector(cs_toastTimerDidFinish:) userInfo:toast repeats:NO]; 173 | [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 174 | objc_setAssociatedObject(toast, &CSToastTimerKey, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 175 | }]; 176 | } 177 | 178 | - (void)cs_hideToast:(UIView *)toast { 179 | [self cs_hideToast:toast fromTap:NO]; 180 | } 181 | 182 | - (void)cs_hideToast:(UIView *)toast fromTap:(BOOL)fromTap { 183 | NSTimer *timer = (NSTimer *)objc_getAssociatedObject(toast, &CSToastTimerKey); 184 | [timer invalidate]; 185 | 186 | [UIView animateWithDuration:[[CSToastManager sharedStyle] fadeDuration] 187 | delay:0.0 188 | options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState) 189 | animations:^{ 190 | toast.alpha = 0.0; 191 | } completion:^(BOOL finished) { 192 | [toast removeFromSuperview]; 193 | 194 | // remove 195 | [[self cs_activeToasts] removeObject:toast]; 196 | 197 | // execute the completion block, if necessary 198 | void (^completion)(BOOL didTap) = objc_getAssociatedObject(toast, &CSToastCompletionKey); 199 | if (completion) { 200 | completion(fromTap); 201 | } 202 | 203 | if ([self.cs_toastQueue count] > 0) { 204 | // dequeue 205 | UIView *nextToast = [[self cs_toastQueue] firstObject]; 206 | [[self cs_toastQueue] removeObjectAtIndex:0]; 207 | 208 | // present the next toast 209 | NSTimeInterval duration = [objc_getAssociatedObject(nextToast, &CSToastDurationKey) doubleValue]; 210 | id position = objc_getAssociatedObject(nextToast, &CSToastPositionKey); 211 | [self cs_showToast:nextToast duration:duration position:position]; 212 | } 213 | }]; 214 | } 215 | 216 | #pragma mark - View Construction 217 | 218 | - (UIView *)toastViewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image style:(CSToastStyle *)style { 219 | // sanity 220 | if (message == nil && title == nil && image == nil) return nil; 221 | 222 | // default to the shared style 223 | if (style == nil) { 224 | style = [CSToastManager sharedStyle]; 225 | } 226 | 227 | // dynamically build a toast view with any combination of message, title, & image 228 | UILabel *messageLabel = nil; 229 | UILabel *titleLabel = nil; 230 | UIImageView *imageView = nil; 231 | 232 | UIView *wrapperView = [[UIView alloc] init]; 233 | wrapperView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin); 234 | wrapperView.layer.cornerRadius = style.cornerRadius; 235 | 236 | if (style.displayShadow) { 237 | wrapperView.layer.shadowColor = style.shadowColor.CGColor; 238 | wrapperView.layer.shadowOpacity = style.shadowOpacity; 239 | wrapperView.layer.shadowRadius = style.shadowRadius; 240 | wrapperView.layer.shadowOffset = style.shadowOffset; 241 | } 242 | 243 | wrapperView.backgroundColor = style.backgroundColor; 244 | 245 | if(image != nil) { 246 | imageView = [[UIImageView alloc] initWithImage:image]; 247 | imageView.contentMode = UIViewContentModeScaleAspectFit; 248 | imageView.frame = CGRectMake(style.horizontalPadding, style.verticalPadding, style.imageSize.width, style.imageSize.height); 249 | } 250 | 251 | CGRect imageRect = CGRectZero; 252 | 253 | if(imageView != nil) { 254 | imageRect.origin.x = style.horizontalPadding; 255 | imageRect.origin.y = style.verticalPadding; 256 | imageRect.size.width = imageView.bounds.size.width; 257 | imageRect.size.height = imageView.bounds.size.height; 258 | } 259 | 260 | if (title != nil) { 261 | titleLabel = [[UILabel alloc] init]; 262 | titleLabel.numberOfLines = style.titleNumberOfLines; 263 | titleLabel.font = style.titleFont; 264 | titleLabel.textAlignment = style.titleAlignment; 265 | titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; 266 | titleLabel.textColor = style.titleColor; 267 | titleLabel.backgroundColor = [UIColor clearColor]; 268 | titleLabel.alpha = 1.0; 269 | titleLabel.text = title; 270 | 271 | // size the title label according to the length of the text 272 | CGSize maxSizeTitle = CGSizeMake((self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, self.bounds.size.height * style.maxHeightPercentage); 273 | CGSize expectedSizeTitle = [titleLabel sizeThatFits:maxSizeTitle]; 274 | // UILabel can return a size larger than the max size when the number of lines is 1 275 | expectedSizeTitle = CGSizeMake(MIN(maxSizeTitle.width, expectedSizeTitle.width), MIN(maxSizeTitle.height, expectedSizeTitle.height)); 276 | titleLabel.frame = CGRectMake(0.0, 0.0, expectedSizeTitle.width, expectedSizeTitle.height); 277 | } 278 | 279 | if (message != nil) { 280 | messageLabel = [[UILabel alloc] init]; 281 | messageLabel.numberOfLines = style.messageNumberOfLines; 282 | messageLabel.font = style.messageFont; 283 | messageLabel.textAlignment = style.messageAlignment; 284 | messageLabel.lineBreakMode = NSLineBreakByTruncatingTail; 285 | messageLabel.textColor = style.messageColor; 286 | messageLabel.backgroundColor = [UIColor clearColor]; 287 | messageLabel.alpha = 1.0; 288 | messageLabel.text = message; 289 | 290 | CGSize maxSizeMessage = CGSizeMake((self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, self.bounds.size.height * style.maxHeightPercentage); 291 | CGSize expectedSizeMessage = [messageLabel sizeThatFits:maxSizeMessage]; 292 | // UILabel can return a size larger than the max size when the number of lines is 1 293 | expectedSizeMessage = CGSizeMake(MIN(maxSizeMessage.width, expectedSizeMessage.width), MIN(maxSizeMessage.height, expectedSizeMessage.height)); 294 | messageLabel.frame = CGRectMake(0.0, 0.0, expectedSizeMessage.width, expectedSizeMessage.height); 295 | } 296 | 297 | CGRect titleRect = CGRectZero; 298 | 299 | if(titleLabel != nil) { 300 | titleRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding; 301 | titleRect.origin.y = style.verticalPadding; 302 | titleRect.size.width = titleLabel.bounds.size.width; 303 | titleRect.size.height = titleLabel.bounds.size.height; 304 | } 305 | 306 | CGRect messageRect = CGRectZero; 307 | 308 | if(messageLabel != nil) { 309 | messageRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding; 310 | messageRect.origin.y = titleRect.origin.y + titleRect.size.height + style.verticalPadding; 311 | messageRect.size.width = messageLabel.bounds.size.width; 312 | messageRect.size.height = messageLabel.bounds.size.height; 313 | } 314 | 315 | CGFloat longerWidth = MAX(titleRect.size.width, messageRect.size.width); 316 | CGFloat longerX = MAX(titleRect.origin.x, messageRect.origin.x); 317 | 318 | // Wrapper width uses the longerWidth or the image width, whatever is larger. Same logic applies to the wrapper height. 319 | CGFloat wrapperWidth = MAX((imageRect.size.width + (style.horizontalPadding * 2.0)), (longerX + longerWidth + style.horizontalPadding)); 320 | CGFloat wrapperHeight = MAX((messageRect.origin.y + messageRect.size.height + style.verticalPadding), (imageRect.size.height + (style.verticalPadding * 2.0))); 321 | 322 | wrapperView.frame = CGRectMake(0.0, 0.0, wrapperWidth, wrapperHeight); 323 | 324 | if(titleLabel != nil) { 325 | titleLabel.frame = titleRect; 326 | [wrapperView addSubview:titleLabel]; 327 | } 328 | 329 | if(messageLabel != nil) { 330 | messageLabel.frame = messageRect; 331 | [wrapperView addSubview:messageLabel]; 332 | } 333 | 334 | if(imageView != nil) { 335 | [wrapperView addSubview:imageView]; 336 | } 337 | 338 | return wrapperView; 339 | } 340 | 341 | #pragma mark - Storage 342 | 343 | - (NSMutableArray *)cs_activeToasts { 344 | NSMutableArray *cs_activeToasts = objc_getAssociatedObject(self, &CSToastActiveKey); 345 | if (cs_activeToasts == nil) { 346 | cs_activeToasts = [[NSMutableArray alloc] init]; 347 | objc_setAssociatedObject(self, &CSToastActiveKey, cs_activeToasts, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 348 | } 349 | return cs_activeToasts; 350 | } 351 | 352 | - (NSMutableArray *)cs_toastQueue { 353 | NSMutableArray *cs_toastQueue = objc_getAssociatedObject(self, &CSToastQueueKey); 354 | if (cs_toastQueue == nil) { 355 | cs_toastQueue = [[NSMutableArray alloc] init]; 356 | objc_setAssociatedObject(self, &CSToastQueueKey, cs_toastQueue, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 357 | } 358 | return cs_toastQueue; 359 | } 360 | 361 | #pragma mark - Events 362 | 363 | - (void)cs_toastTimerDidFinish:(NSTimer *)timer { 364 | [self cs_hideToast:(UIView *)timer.userInfo]; 365 | } 366 | 367 | - (void)cs_handleToastTapped:(UITapGestureRecognizer *)recognizer { 368 | UIView *toast = recognizer.view; 369 | NSTimer *timer = (NSTimer *)objc_getAssociatedObject(toast, &CSToastTimerKey); 370 | [timer invalidate]; 371 | 372 | [self cs_hideToast:toast fromTap:YES]; 373 | } 374 | 375 | #pragma mark - Activity Methods 376 | 377 | - (void)makeToastActivity:(id)position { 378 | // sanity 379 | UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey); 380 | if (existingActivityView != nil) return; 381 | 382 | CSToastStyle *style = [CSToastManager sharedStyle]; 383 | 384 | UIView *activityView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, style.activitySize.width, style.activitySize.height)]; 385 | activityView.center = [self cs_centerPointForPosition:position withToast:activityView]; 386 | activityView.backgroundColor = style.backgroundColor; 387 | activityView.alpha = 0.0; 388 | activityView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin); 389 | activityView.layer.cornerRadius = style.cornerRadius; 390 | 391 | if (style.displayShadow) { 392 | activityView.layer.shadowColor = style.shadowColor.CGColor; 393 | activityView.layer.shadowOpacity = style.shadowOpacity; 394 | activityView.layer.shadowRadius = style.shadowRadius; 395 | activityView.layer.shadowOffset = style.shadowOffset; 396 | } 397 | 398 | UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; 399 | activityIndicatorView.center = CGPointMake(activityView.bounds.size.width / 2, activityView.bounds.size.height / 2); 400 | [activityView addSubview:activityIndicatorView]; 401 | [activityIndicatorView startAnimating]; 402 | 403 | // associate the activity view with self 404 | objc_setAssociatedObject (self, &CSToastActivityViewKey, activityView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 405 | 406 | [self addSubview:activityView]; 407 | 408 | [UIView animateWithDuration:style.fadeDuration 409 | delay:0.0 410 | options:UIViewAnimationOptionCurveEaseOut 411 | animations:^{ 412 | activityView.alpha = 1.0; 413 | } completion:nil]; 414 | } 415 | 416 | - (void)hideToastActivity { 417 | UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey); 418 | if (existingActivityView != nil) { 419 | [UIView animateWithDuration:[[CSToastManager sharedStyle] fadeDuration] 420 | delay:0.0 421 | options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState) 422 | animations:^{ 423 | existingActivityView.alpha = 0.0; 424 | } completion:^(BOOL finished) { 425 | [existingActivityView removeFromSuperview]; 426 | objc_setAssociatedObject (self, &CSToastActivityViewKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 427 | }]; 428 | } 429 | } 430 | 431 | #pragma mark - Helpers 432 | 433 | - (CGPoint)cs_centerPointForPosition:(id)point withToast:(UIView *)toast { 434 | CSToastStyle *style = [CSToastManager sharedStyle]; 435 | 436 | UIEdgeInsets safeInsets = UIEdgeInsetsZero; 437 | if (@available(iOS 11.0, *)) { 438 | safeInsets = self.safeAreaInsets; 439 | } 440 | 441 | CGFloat topPadding = style.verticalPadding + safeInsets.top; 442 | CGFloat bottomPadding = style.verticalPadding + safeInsets.bottom; 443 | 444 | if([point isKindOfClass:[NSString class]]) { 445 | if([point caseInsensitiveCompare:CSToastPositionTop] == NSOrderedSame) { 446 | return CGPointMake(self.bounds.size.width / 2.0, (toast.frame.size.height / 2.0) + topPadding); 447 | } else if([point caseInsensitiveCompare:CSToastPositionCenter] == NSOrderedSame) { 448 | return CGPointMake(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0); 449 | } 450 | } else if ([point isKindOfClass:[NSValue class]]) { 451 | return [point CGPointValue]; 452 | } 453 | 454 | // default to bottom 455 | return CGPointMake(self.bounds.size.width / 2.0, (self.bounds.size.height - (toast.frame.size.height / 2.0)) - bottomPadding); 456 | } 457 | 458 | @end 459 | 460 | @implementation CSToastStyle 461 | 462 | #pragma mark - Constructors 463 | 464 | - (instancetype)initWithDefaultStyle { 465 | self = [super init]; 466 | if (self) { 467 | self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.8]; 468 | self.titleColor = [UIColor whiteColor]; 469 | self.messageColor = [UIColor whiteColor]; 470 | self.maxWidthPercentage = 0.8; 471 | self.maxHeightPercentage = 0.8; 472 | self.horizontalPadding = 10.0; 473 | self.verticalPadding = 10.0; 474 | self.cornerRadius = 10.0; 475 | self.titleFont = [UIFont boldSystemFontOfSize:16.0]; 476 | self.messageFont = [UIFont systemFontOfSize:16.0]; 477 | self.titleAlignment = NSTextAlignmentLeft; 478 | self.messageAlignment = NSTextAlignmentLeft; 479 | self.titleNumberOfLines = 0; 480 | self.messageNumberOfLines = 0; 481 | self.displayShadow = NO; 482 | self.shadowOpacity = 0.8; 483 | self.shadowRadius = 6.0; 484 | self.shadowOffset = CGSizeMake(4.0, 4.0); 485 | self.imageSize = CGSizeMake(80.0, 80.0); 486 | self.activitySize = CGSizeMake(100.0, 100.0); 487 | self.fadeDuration = 0.2; 488 | } 489 | return self; 490 | } 491 | 492 | - (void)setMaxWidthPercentage:(CGFloat)maxWidthPercentage { 493 | _maxWidthPercentage = MAX(MIN(maxWidthPercentage, 1.0), 0.0); 494 | } 495 | 496 | - (void)setMaxHeightPercentage:(CGFloat)maxHeightPercentage { 497 | _maxHeightPercentage = MAX(MIN(maxHeightPercentage, 1.0), 0.0); 498 | } 499 | 500 | - (instancetype)init NS_UNAVAILABLE { 501 | return nil; 502 | } 503 | 504 | @end 505 | 506 | @interface CSToastManager () 507 | 508 | @property (strong, nonatomic) CSToastStyle *sharedStyle; 509 | @property (assign, nonatomic, getter=isTapToDismissEnabled) BOOL tapToDismissEnabled; 510 | @property (assign, nonatomic, getter=isQueueEnabled) BOOL queueEnabled; 511 | @property (assign, nonatomic) NSTimeInterval defaultDuration; 512 | @property (strong, nonatomic) id defaultPosition; 513 | 514 | @end 515 | 516 | @implementation CSToastManager 517 | 518 | #pragma mark - Constructors 519 | 520 | + (instancetype)sharedManager { 521 | static CSToastManager *_sharedManager = nil; 522 | static dispatch_once_t oncePredicate; 523 | dispatch_once(&oncePredicate, ^{ 524 | _sharedManager = [[self alloc] init]; 525 | }); 526 | 527 | return _sharedManager; 528 | } 529 | 530 | - (instancetype)init { 531 | self = [super init]; 532 | if (self) { 533 | self.sharedStyle = [[CSToastStyle alloc] initWithDefaultStyle]; 534 | self.tapToDismissEnabled = YES; 535 | self.queueEnabled = NO; 536 | self.defaultDuration = 3.0; 537 | self.defaultPosition = CSToastPositionBottom; 538 | } 539 | return self; 540 | } 541 | 542 | #pragma mark - Singleton Methods 543 | 544 | + (void)setSharedStyle:(CSToastStyle *)sharedStyle { 545 | [[self sharedManager] setSharedStyle:sharedStyle]; 546 | } 547 | 548 | + (CSToastStyle *)sharedStyle { 549 | return [[self sharedManager] sharedStyle]; 550 | } 551 | 552 | + (void)setTapToDismissEnabled:(BOOL)tapToDismissEnabled { 553 | [[self sharedManager] setTapToDismissEnabled:tapToDismissEnabled]; 554 | } 555 | 556 | + (BOOL)isTapToDismissEnabled { 557 | return [[self sharedManager] isTapToDismissEnabled]; 558 | } 559 | 560 | + (void)setQueueEnabled:(BOOL)queueEnabled { 561 | [[self sharedManager] setQueueEnabled:queueEnabled]; 562 | } 563 | 564 | + (BOOL)isQueueEnabled { 565 | return [[self sharedManager] isQueueEnabled]; 566 | } 567 | 568 | + (void)setDefaultDuration:(NSTimeInterval)duration { 569 | [[self sharedManager] setDefaultDuration:duration]; 570 | } 571 | 572 | + (NSTimeInterval)defaultDuration { 573 | return [[self sharedManager] defaultDuration]; 574 | } 575 | 576 | + (void)setDefaultPosition:(id)position { 577 | if ([position isKindOfClass:[NSString class]] || [position isKindOfClass:[NSValue class]]) { 578 | [[self sharedManager] setDefaultPosition:position]; 579 | } 580 | } 581 | 582 | + (id)defaultPosition { 583 | return [[self sharedManager] defaultPosition]; 584 | } 585 | 586 | @end 587 | -------------------------------------------------------------------------------- /AppHostExample/UIViewPreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewPreview.swift 3 | // AppHostExample 4 | // 5 | // Created by liang on 2020/1/2. 6 | // Copyright © 2020 liang. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | #if canImport(SwiftUI) 11 | 12 | import SwiftUI 13 | 14 | struct UIViewPreview: UIViewRepresentable { 15 | typealias UIViewType = UIView 16 | 17 | let view: View 18 | 19 | init(_ builder: @escaping () -> View) { 20 | view = builder() 21 | } 22 | // MARK: - UIViewRepresentable 23 | func makeUIView(context: Context) -> UIView { 24 | return view 25 | } 26 | 27 | func updateUIView(_ view: UIView, context: Context) { 28 | view.setContentHuggingPriority(.defaultHigh, for: .horizontal) 29 | view.setContentHuggingPriority(.defaultHigh, for: .vertical) 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /AppHostExample/ViewController_Preview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController_Preview.swift 3 | // AppHostExample 4 | // 5 | // Created by liang on 2020/1/2. 6 | // Copyright © 2020 liang. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | #if canImport(SwiftUI) 13 | 14 | @available(iOS 13.0, *) 15 | struct ViewController_Preview: PreviewProvider, UIViewControllerRepresentable { 16 | 17 | typealias UIViewControllerType = UIViewController 18 | 19 | static var previews: some View { 20 | ViewController_Preview() 21 | } 22 | 23 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewController { 24 | let vc = MasterViewController() 25 | // vc.title = "标题" 26 | // let nav = UINavigationController(rootViewController: vc) 27 | return vc 28 | } 29 | 30 | func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext) { 31 | 32 | } 33 | } 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /AppHostExample/View_Preview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View_Preview.swift 3 | // AppHostExample 4 | // 5 | // Created by liang on 2020/1/2. 6 | // Copyright © 2020 liang. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if canImport(SwiftUI) 12 | import SwiftUI 13 | 14 | @available(iOS 13.0, *) 15 | struct View_Preview: PreviewProvider { 16 | static var previews: some View { 17 | UIViewPreview { 18 | let name = DescTableViewCell(style: .subtitle, reuseIdentifier: "cell2") 19 | name.configure(withTitle: "有关埋点", desc: "数据希望规范内容体系") 20 | return name 21 | }.padding() 22 | } 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /AppHostExample/WebViewViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewViewController.h 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface WebViewViewController : AppHostViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /AppHostExample/WebViewViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewViewController.m 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | #import "WebViewViewController.h" 10 | #import 11 | 12 | @interface WebViewViewController () 13 | 14 | @end 15 | 16 | @implementation WebViewViewController 17 | 18 | - (void)viewDidLoad { 19 | [super viewDidLoad]; 20 | // Do any additional setup after loading the view. 21 | } 22 | 23 | - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 24 | { 25 | NSURLRequest *request = navigationAction.request; 26 | //此url解析规则自己定义 27 | NSString *rurl = [[request URL] absoluteString]; 28 | NSString *kProtocol = @"openapp.jdmobile://virtual?params="; 29 | if ([rurl hasPrefix:kProtocol]) { 30 | NSString *param = [rurl stringByReplacingOccurrencesOfString:kProtocol withString:@""]; 31 | 32 | NSDictionary *contentJSON = nil; 33 | NSError *contentParseError; 34 | if (param) { 35 | param = [self stringDecodeURIComponent:param]; 36 | contentJSON = [NSJSONSerialization JSONObjectWithData:[param dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&contentParseError]; 37 | } 38 | 39 | [self callNative:@"toast" parameter:@{ 40 | @"text":[contentJSON description] 41 | }]; 42 | decisionHandler(WKNavigationActionPolicyCancel); 43 | } else { 44 | [super webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler]; 45 | } 46 | } 47 | 48 | - (NSString *)stringDecodeURIComponent:(NSString *)encoded{ 49 | NSString *decoded = (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (CFStringRef)encoded, CFSTR(""), kCFStringEncodingUTF8); 50 | // NSLog(@"decodedString %@", decoded); 51 | return decoded; 52 | } 53 | @end 54 | -------------------------------------------------------------------------------- /AppHostExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // AppHostExample 4 | // 5 | // Created by liang on 2019/4/16. 6 | // Copyright © 2019 liang. 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 | -------------------------------------------------------------------------------- /AppHostExample/resources/TestCase/index.css: -------------------------------------------------------------------------------- 1 | 2 | fieldset { 3 | border-width: 1px; 4 | border-color: #7D5C5C; 5 | border-style: dashed; 6 | } 7 | 8 | legend { 9 | text-align: left; 10 | color: #290B0B; 11 | } 12 | 13 | a { 14 | width: 100%; 15 | height: 25px; 16 | text-decoration: none; 17 | text-align: left; 18 | } 19 | 20 | ol { 21 | margin: 0; 22 | padding-left: 24px; 23 | } 24 | 25 | ol li { 26 | text-align: left; 27 | } 28 | 29 | ol span { 30 | font-size: 12px; 31 | text-align: left; 32 | display: block; 33 | } 34 | 35 | ol span:before { 36 | content: "("; 37 | } 38 | 39 | ol span:after { 40 | content: ")"; 41 | } 42 | 43 | ol .propertyValue { 44 | display: block; 45 | width: 100%; 46 | } 47 | 48 | .err { 49 | color: red; 50 | } 51 | -------------------------------------------------------------------------------- /AppHostExample/resources/TestCase/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 发现 10 | 11 | 12 | 13 |
14 | 发现频道接口测试 15 |
16 |

测试 style 内联本地图片

17 | 18 |
19 | 测试webview浏览器 20 |
    21 |
  1. 22 | 23 |
    24 | 25 | 使用webview页面,加载以上链接 26 |
  2. 27 |
  3. 28 | 打开网易严选网站 29 | iOS 专有,打开和APP不在同一主站的url,如其它公司的页面、做展示的,不需要h5接口的url等。 30 |
  4. 31 |
  5. 32 | 打开cnbeta网站 33 | iOS 专有,打开和APP不在同一主站的url,如其它公司的页面、做展示的,不需要h5接口的url等。 34 |
  6. 35 |
36 |
37 |
38 | 导航栏相关 39 |
    40 |
  1. 41 | 只设置中间的标题 42 | 期望为,只有标题变化为 古墓丽影,其他左右按钮不变化 43 |
  2. 44 |
  3. 45 | 只设置右侧的按钮 46 | 期望为,只有右侧多一个按钮,其他返回按钮、标题、左按钮不变化 47 |
  4. 48 |
49 |
50 |
51 | Navigation相关 52 |
    53 |
  1. 54 | 用指定链接打开浏览器 55 | 使用浏览器打开网址.http://www.163.com/ 56 |
  2. 57 | 58 |
  3. 59 | 打开新页面 60 | 打开新页面,进入严选官网,右上角有个发布的按钮 61 |
  4. 62 |
  5. 63 | backPageParameter参数 64 | backPageParameter的参数,目的是为了实现A页面带有参数backPageParameter,进入C页面(有钱官网),当C页面点击返回B页面(邮箱大师页面),返回到backPageParameter指定的参数的功能。本测试用例里,是返回B页面 65 |
  6. 66 |
67 |
68 |
69 | 提示、跳转 70 |
    71 |
  1. 72 | toast 73 | toast就是在屏幕中间显示的提示,持续0.5s后自动消失,web页面模态,但可以回退或者点击右上角按钮 74 |
  2. 75 |
  3. 76 | 显示loading 77 | 位于屏幕中间的一个菊花转圈的动画,非模态,不主动隐藏是不会消失的 78 |
  4. 79 |
  5. 80 | 隐藏loading 81 | 隐藏loading动画 82 |
  6. 83 |
84 |
85 |
86 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /AppHostExample/resources/TestCase/index.js: -------------------------------------------------------------------------------- 1 | console.log('load index.js') 2 | -------------------------------------------------------------------------------- /AppHostExample/resources/TestCase/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hite/AppHostExample/9f691bd3974f5237a6754734609c6e781c1fcfd9/AppHostExample/resources/TestCase/index.png -------------------------------------------------------------------------------- /AppHostExampleUITests/AppHostExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppHostExampleUITests.swift 3 | // AppHostExampleUITests 4 | // 5 | // Created by liang on 2019/4/23. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class AppHostExampleUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 20 | XCUIApplication().launch() 21 | 22 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testExample() { 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | func testJSBridge(){ 34 | let app = XCUIApplication() 35 | 36 | // TODO 通过某些步骤到达 testcase 页面,开始执行。 37 | // 注意 testcase 页面的“关闭自动化测试的 checkbox 需要去掉勾选” 38 | let check = app.navigationBars.buttons["测试下一步接口"]; 39 | check.tap() 40 | sleep(4); 41 | var next = app.alerts.element.exists 42 | 43 | var testCount = 1; 44 | while next { 45 | let isTrue = app.alerts.staticTexts["true"].exists; 46 | let isFalse = app.alerts.staticTexts["false"].exists; 47 | if (isTrue || isFalse){ 48 | app.alerts.buttons.element.tap(); 49 | check.tap() 50 | next = app.alerts.element.exists 51 | } else { 52 | let isFinish = app.alerts.staticTexts["finish"].exists 53 | if (isFinish){ 54 | app.alerts.buttons.element.tap(); 55 | next = false 56 | } else { 57 | let isSwipeDown = app.alerts.staticTexts["swipeDown"].exists; 58 | if (isSwipeDown) { 59 | app.alerts.buttons.element.tap(); 60 | app.swipeDown() 61 | app.swipeDown() 62 | 63 | sleep(4) 64 | next = app.alerts.element.exists 65 | } else { 66 | // unknown 67 | next = false 68 | } 69 | } 70 | } 71 | 72 | testCount += 1 73 | if(testCount > 3){ 74 | app.swipeUp() 75 | } 76 | } 77 | 78 | sleep(18); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /AppHostExampleUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppHostExample 2 | 3 | 本示例工程一共有 5 个实例,分别展示 AppHost 不同方面的能力,用户可自选下载后,运行后选择不同用例体验。 4 | 5 | ## 如何打开远程调试功能 6 | 工程代码运行之后,按照 XCode 日志里的提示(或者点击 App 里右上角一个 AH 样的图标,展开后的日志了有 url,长按复制或者在浏览器输入),用电脑浏览器打开调试页面,展现的就是调试 Console。 7 | 8 | ### 1. 加载京东页面,拦截京东 JSBridge 协议 9 | **本用例展示**:AppHost 不仅提供了内置的 JSBridge 协议,还可以和原有的协议共存。 10 | 通过继承 AppHostViewController,重载了 decidePolicy 来实现这一点。保持内聚的同时,也具备一定的灵活性。 11 | 另外,可以看到 AppHostRespone 作为业务逻辑实现类的角色,不仅可以被 h5 调用,AppHost 也可以让 native 主动调用此能力。将前后端能力统一。 12 | >操作步骤; 13 | 点击顶部的立即下载,此时弹出一个 toast,内容是京东 JSBridge 接口参数。 另外这里的 toast 是由 AppHostRespone 的扩展类 HUDResponse 来实现具体功能的,展示灵活的业务扩展能力。 14 | 15 | PS: 通过 AppHost 提供的 debugger,可以看到线上的代码里还有 console.log 日志 16 | ### 2. 加载严选移动端首页,观察其性能参数 17 | **本用例展示**:AppHost 的定制能力和 timing 工具。这次加载的进度条是金色的,颜色是可配置的,当遇到某个请求 302 时,进度条也可以正常显示。 18 | >查看 timing 步骤,首先用电脑浏览器打开调试页面(步骤见前面描述),按照提示或者点击左侧快捷菜单、或直接输入 :timing 接口查看 19 | ### 3. 加载严选酒水专题页面,使用 weinre 调试样式 20 | **本用例展示**: AppHost 的扩展能力,可以接入现有的工具,增强调试能力。 21 | > 首先用电脑浏览器打开调试页面(步骤见前面描述),在命令行里输入,本地启动的 weinre 目标调试脚本,如`:weinre http://10.12.0.0:8888/target/target-script-min.js#anonymous`,不需要修改目标调试页面即可实现对此页面的调试,而且对后续的所有页面都有效。 22 | 23 | 注意:因为浏览器 CSP 的限制 https 的页面无法加载 http 的资源。如果需要调试 https,你可能需要安装 ngrok,输入命令,`:weinre https://3c2c9d94.ngrok.io/target/target-script-min.js#anonymous` 24 | ### 4. 加载本地文件夹,测试接口参数 25 | **本用例展示**: AppHost 加载本地文件夹资源的能力。可以加载 html,以及 html 里引用的相同目录下的 图片资源\\javascript\\css 文件 3 类资源(不支持字体等)。 26 | 如果要加载的主域是 http 的,可以使用嵌套性的资源引用,如 css 文件里引用了一个相对路径的图标。 27 | ### 5. 加载本地页面,向美团服务器发 ajax 请求 28 | **本用例展示**: 本地文件向美团的服务器发送请求。因为 Cookie 不对,所以返回错误数据,但也演示了,是可以正确的发送和接受数据的 29 | 30 | 31 | ## 详细 AppHost 使用方法见 32 | AppHost 项目,https://github.com/hite/AppHost 33 | --------------------------------------------------------------------------------