├── KakiWebView.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── KakiWebView.xcscheme └── project.pbxproj ├── KakiWebViewExample ├── ViewController.h ├── AppDelegate.h ├── main.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── ViewController.m ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── AppDelegate.m ├── KakiWebView ├── Classes │ ├── Plugins │ │ ├── KakiPopGesturePlugin.h │ │ ├── KakiTitleObserverPlugin.h │ │ ├── KakiProgressPlugin.h │ │ ├── KakiJavascriptCorePlugin.h │ │ ├── KakiTitleObserverPlugin.m │ │ ├── KakiJavascriptCorePlugin.m │ │ ├── KakiProgressPlugin.m │ │ └── KakiPopGesturePlugin.m │ ├── Utils │ │ ├── KakiWebViewPluginContainer.h │ │ ├── KakiWebViewPatcher.h │ │ ├── KakiWebViewPluginContainer.m │ │ └── KakiWebViewPatcher.m │ ├── UIWebView+Kaki.h │ ├── KakiWebViewPlugin.h │ └── UIWebView+Kaki.m ├── KakiWebView.h └── Info.plist ├── .gitignore ├── KakiWebView.podspec ├── README.md └── LICENSE /KakiWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /KakiWebViewExample/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // KakiWebViewExample 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /KakiWebViewExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // KakiWebViewExample 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. 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 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Plugins/KakiPopGesturePlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // KakiPopGesturePlugin.h 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface KakiPopGesturePlugin : NSObject 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /KakiWebViewExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // KakiWebViewExample 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # git worktree 2 | worktree/ 3 | 4 | # OS X 5 | .DS_Store 6 | 7 | # Xcode 8 | build/ 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | *.xccheckout 19 | profile 20 | *.moved-aside 21 | DerivedData 22 | *.hmap 23 | *.ipa 24 | 25 | # AppCode 26 | .idea/ 27 | 28 | xcodebuild_output 29 | 30 | Pods/ 31 | 32 | # Vim 33 | .swn 34 | 35 | # unit test 36 | *.testfile 37 | -------------------------------------------------------------------------------- /KakiWebView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "KakiWebView" 3 | s.version = "0.0.2" 4 | s.summary = "Simple && Scalable UIWebview Framework" 5 | 6 | s.homepage = "http://blog.makeex.com/2017/04/06/thinking-in-fe-how-to-enhance-the-uiwebview/" 7 | s.license = "MIT" 8 | s.author = { "makee" => "wengyang56@163.com" } 9 | s.source = { :git => "https://github.com/prinsun/KakiWebView.git", :tag => "#{s.version}" } 10 | 11 | s.platform = :ios, "7.0" 12 | s.source_files = "KakiWebView/Classes/**/*.{h,m,mm,c,cpp}" 13 | 14 | s.frameworks = "UIKit", "JavaScriptCore" 15 | end 16 | -------------------------------------------------------------------------------- /KakiWebView/KakiWebView.h: -------------------------------------------------------------------------------- 1 | // 2 | // KakiWebView.h 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/3/27. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for KakiWebView. 12 | FOUNDATION_EXPORT double KakiWebViewVersionNumber; 13 | 14 | //! Project version string for KakiWebView. 15 | FOUNDATION_EXPORT const unsigned char KakiWebViewVersionString[]; 16 | 17 | 18 | #import 19 | #import 20 | #import 21 | #import 22 | #import 23 | #import 24 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Utils/KakiWebViewPluginContainer.h: -------------------------------------------------------------------------------- 1 | // 2 | // KakiWebViewPluginContainer.h 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/4. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface KakiWebViewPluginContainer : NSObject 14 | 15 | /** 16 | * 往插件容器中添加一个插件 17 | * 18 | * @param plugin 要添加的插件 19 | */ 20 | - (void)addPlugin:(id)plugin; 21 | 22 | /** 23 | * 从插件容器中移除一个插件 24 | * 25 | * @param plugin 要移除的插件 26 | */ 27 | - (void)removePlugin:(id)plugin; 28 | 29 | /** 30 | * 移除当前容器中的所有插件 31 | */ 32 | - (void)removeAllPlugins; 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /KakiWebViewExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /KakiWebView/Classes/Plugins/KakiTitleObserverPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // KakiTitleObserverPlugin.h 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | typedef void(^KakiTitleChangedBlock)(NSString *title); 14 | 15 | 16 | @interface KakiTitleObserverPlugin : NSObject 17 | 18 | /** 19 | * 获取或设置 Title 变更时监听 block 20 | */ 21 | @property (nullable, nonatomic, copy) KakiTitleChangedBlock onTitleChanged; 22 | 23 | @end 24 | 25 | 26 | @interface UIWebView (KakiTitleObserverPlugin) 27 | 28 | /** 29 | * 获取 Title Observer 插件实例,如果没有安装则返回 nil 30 | */ 31 | @property (nullable, readonly) KakiTitleObserverPlugin *titleObserverPlugin; 32 | 33 | @end 34 | 35 | NS_ASSUME_NONNULL_END 36 | -------------------------------------------------------------------------------- /KakiWebView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Utils/KakiWebViewPatcher.h: -------------------------------------------------------------------------------- 1 | // 2 | // KakiWebViewPatcher.h 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/4. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import "KakiWebViewPluginContainer.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface KakiWebViewPatcher : NSObject 14 | 15 | /** 16 | * 应用开启插件补丁 17 | * 18 | * @param webView 要应用开启插件补丁的 WebView 19 | */ 20 | + (void)applyPatchForWebView:(UIWebView *)webView; 21 | 22 | /** 23 | * 移除开启插件补丁 24 | * 25 | * @param webView 要移除开启插件补丁的 WebView 26 | */ 27 | + (void)removePatchForWebView:(UIWebView *)webView; 28 | 29 | @end 30 | 31 | 32 | @interface UIWebView (KakiPluginContainer) 33 | 34 | /** 35 | * 获取插件容器,注意:只有在应用了 Patch 之后才能获取到,否则为 nil 36 | * 37 | * @return 插件容器 或 nil 38 | */ 39 | - (KakiWebViewPluginContainer *)kakiPluginContainer; 40 | 41 | @end 42 | 43 | NS_ASSUME_NONNULL_END 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KakiWebView 2 | 3 | ## 描述 4 | 5 | KakiWebView,应用于`UIWebView`,提供一些通用的扩展功能,以下是该库的设计目标: 6 | 7 | * 对于现有的`UIWebView`无侵入性的使用 8 | * 可扩展性强,可实现自定义扩展 9 | * 简单易用,学习成本低 10 | 11 | 详见[《Thinking in FE 更好用的 UIWebView》](http://blog.makeex.com/2017/04/06/thinking-in-fe-how-to-enhance-the-uiwebview/)。 12 | 13 | ## 安装 14 | 15 | ### Cocoapods 16 | 17 | pod KakiWebView 18 | 19 | ### Carthage 20 | 21 | github prinsun/KakiWebView 22 | 23 | 24 | ## 使用 25 | 26 | ```objc 27 | // 启用 Kaki 28 | [self.webView setEnableKakiPlugins:YES]; 29 | 30 | // 安装 Kaki 插件 31 | [self.webView installKakiPlugin:[KakiProgressPlugin.alloc init]]; 32 | [self.webView installKakiPlugin:[KakiPopGesturePlugin.alloc init]]; 33 | [self.webView installKakiPlugin:[KakiTitleObserverPlugin.alloc init]]; 34 | 35 | // 配置插件 36 | __weak __typeof(self) wself = self; 37 | [self.webView.titleObserverPlugin setOnTitleChanged:^(NSString *title) { 38 | wself.titleLabel.text = title; 39 | }]; 40 | self.webView.progressPlugin.progressColor = [UIColor redColor]; 41 | ``` -------------------------------------------------------------------------------- /KakiWebView/Classes/UIWebView+Kaki.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIWebView+Kaki.h 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/4. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface UIWebView (Kaki) 14 | 15 | /** 16 | * 设置是否启用 Kaki 插件系统 17 | * 18 | * @param enable 是否启用插件系统 19 | */ 20 | - (void)setEnableKakiPlugins:(BOOL)enable; 21 | 22 | /** 23 | * 安装一个插件 24 | * 25 | * @param plugin 要安装的插件 26 | */ 27 | - (void)installKakiPlugin:(id)plugin; 28 | 29 | /** 30 | * 卸载一个插件 31 | * 32 | * @param pluginClass 要卸载的插件类型 33 | */ 34 | - (void)uninstallKakiPluginForClass:(Class)pluginClass; 35 | 36 | /** 37 | * 卸载所有已安装的插件 38 | */ 39 | - (void)uninstallAllKakiPlugins; 40 | 41 | /** 42 | * 获取已安装的特定插件 43 | * 44 | * @param pluginClass 要获取的插件类型 45 | * 46 | * @return 返回 nil 或者匹配的插件实例 47 | */ 48 | - (__kindof id _Nullable)kakiPluginForClass:(Class)pluginClass; 49 | 50 | @end 51 | 52 | NS_ASSUME_NONNULL_END 53 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Plugins/KakiProgressPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // KakiProgressPlugin.h 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface KakiProgressPlugin : NSObject 14 | 15 | /** 16 | * 获取或设置进度条颜色 17 | */ 18 | @property (nonatomic, strong) UIColor *progressColor; 19 | 20 | /** 21 | * 获取或设定当前进度, 0.0..1.0 22 | */ 23 | @property (nonatomic, assign) float progress; 24 | 25 | /** 26 | * 获取是否已加载完成 27 | */ 28 | @property (nonatomic, assign, readonly) BOOL isFinishLoad; 29 | 30 | /** 31 | * 重置进度条 32 | */ 33 | - (void)reset; 34 | 35 | /** 36 | * 在进度条视图下,添加一个视图 37 | * 38 | * @param view 要添加的视图 39 | */ 40 | - (void)addViewBelowProgressView:(UIView *)view; 41 | 42 | @end 43 | 44 | 45 | @interface UIWebView (KakiProgressPlugin) 46 | 47 | /** 48 | * 获取 Progress 插件实例,如果没有安装则返回 nil 49 | */ 50 | @property (nullable, readonly) KakiProgressPlugin *progressPlugin; 51 | 52 | @end 53 | 54 | NS_ASSUME_NONNULL_END 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 KakiWebView 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /KakiWebView/Classes/KakiWebViewPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // KakiWebViewPlugin.h 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/4. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @protocol KakiWebViewPlugin 14 | @optional 15 | 16 | /** 17 | * 插件被安装到 WebView 时触发该方法 18 | * 19 | * @param webView 被安装到的 WebView 20 | */ 21 | - (void)didInstallToWebView:(UIWebView *)webView; 22 | 23 | /** 24 | * 插件从 WebView 中卸载时,触发该方法 25 | */ 26 | - (void)didUninstall; 27 | 28 | /** 29 | * Web 调用了返回 30 | * 31 | * @param webView 调用了返回的 WebView 32 | */ 33 | - (void)webViewDidGoback:(UIWebView *)webView; 34 | 35 | /** 36 | * WebView 的 Superview 变更检测 37 | * 38 | * @param webView 相关的 WebView 39 | */ 40 | - (void)webViewDidMoveToSuperview:(UIWebView *)webView; 41 | 42 | /** 43 | * WebView 的 Window 变更检测 44 | * 45 | * @param webView 相关的 WebView 46 | */ 47 | - (void)webViewDidMoveToWindow:(UIWebView *)webView; 48 | 49 | /** 50 | * WebView 布局子视图触发该方法 51 | * 52 | * @param webView 布局子视图的 WebView 53 | */ 54 | - (void)webViewLayoutSubviews:(UIWebView *)webView; 55 | 56 | @end 57 | 58 | NS_ASSUME_NONNULL_END 59 | -------------------------------------------------------------------------------- /KakiWebViewExample/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // KakiWebViewExample 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "ViewController.h" 12 | 13 | @interface ViewController () 14 | 15 | @property (strong, nonatomic) IBOutlet UILabel *titleLabel; 16 | @property (strong, nonatomic) IBOutlet UIWebView *webView; 17 | 18 | @end 19 | 20 | @implementation ViewController 21 | 22 | - (void)viewDidLoad { 23 | [super viewDidLoad]; 24 | 25 | // config plugins 26 | [self.webView setEnableKakiPlugins:YES]; 27 | [self.webView installKakiPlugin:[KakiProgressPlugin.alloc init]]; 28 | [self.webView installKakiPlugin:[KakiPopGesturePlugin.alloc init]]; 29 | [self.webView installKakiPlugin:[KakiTitleObserverPlugin.alloc init]]; 30 | 31 | __weak __typeof(self) wself = self; 32 | [self.webView.titleObserverPlugin setOnTitleChanged:^(NSString *title) { 33 | wself.titleLabel.text = title; 34 | }]; 35 | self.webView.progressPlugin.progressColor = [UIColor redColor]; 36 | } 37 | 38 | - (void)viewDidAppear:(BOOL)animated { 39 | [super viewDidAppear:animated]; 40 | 41 | [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://blog.makeex.com"]]]; 42 | } 43 | 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /KakiWebViewExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | NSAppTransportSecurity 38 | 39 | NSAllowsArbitraryLoadsInWebContent 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Plugins/KakiJavascriptCorePlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // KakiJavascriptCorePlugin.h 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface KakiJavascriptCorePlugin : NSObject 15 | 16 | /** 17 | * 注入一个 Objective-C 对象到 Javascript 环境中,该对象必须直接实现了一个 **实现JSExport协议的协议** 18 | * 19 | * @param object 要注入的对象 20 | * @param name 在 JS 中调用的名称 21 | */ 22 | - (void)setJSObject:(NSObject *)object forName:(NSString *)name; 23 | 24 | /** 25 | * 注入一个 Objective-C 对象到 Javascript 环境中,该对象必须直接实现了一个 **实现JSExport协议的协议** 26 | * 27 | * @param object 要注入的对象 28 | * @param protocol 导出到Javascript环境中的协议,必须直接继承至 JSExport 29 | * @param name 在 JS 中调用的名称 30 | */ 31 | - (void)setJSObject:(NSObject *)object withExportProtocol:(Protocol *)protocol forName:(NSString *)name; 32 | 33 | /** 34 | * 移除注入到 Javascript 环境中的对象 35 | * 36 | * @param name 要移除的名称 37 | */ 38 | - (void)removeJSObjectForName:(NSString *)name; 39 | 40 | /** 41 | * 移除所有注入到 Javascript 环境中的对象 42 | */ 43 | - (void)removeAllJSObjects; 44 | 45 | /** 46 | * 获取当前的 JSContext 47 | */ 48 | @property (nonatomic, strong, readonly) JSContext *jsContext; 49 | 50 | @end 51 | 52 | 53 | @interface UIWebView (KakiJavascriptCorePlugin) 54 | 55 | /** 56 | * 获取 JavascriptCore 插件实例,如果没有安装则返回 nil 57 | */ 58 | @property (nullable, readonly) KakiJavascriptCorePlugin *javascriptCorePlugin; 59 | 60 | @end 61 | 62 | NS_ASSUME_NONNULL_END 63 | -------------------------------------------------------------------------------- /KakiWebViewExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /KakiWebViewExample/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // KakiWebViewExample 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // 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. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // 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. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // 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. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // 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. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /KakiWebView/Classes/UIWebView+Kaki.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIWebView+Kaki.m 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/4. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "UIWebView+Kaki.h" 12 | #import "KakiWebViewPatcher.h" 13 | 14 | @implementation UIWebView (Kaki) 15 | 16 | - (void)setEnableKakiPlugins:(BOOL)enable { 17 | if (enable) { 18 | [KakiWebViewPatcher applyPatchForWebView:self]; 19 | } else { 20 | [KakiWebViewPatcher removePatchForWebView:self]; 21 | } 22 | } 23 | 24 | - (void)installKakiPlugin:(id)plugin { 25 | NSAssert(plugin != nil, @"plugin can not be nil"); 26 | 27 | NSMutableDictionary *pluginMap = [self __pluginMap]; 28 | pluginMap[NSStringFromClass([plugin class])] = plugin; 29 | 30 | [self.kakiPluginContainer removePlugin:plugin]; 31 | [self.kakiPluginContainer addPlugin:plugin]; 32 | 33 | if ([plugin respondsToSelector:@selector(didInstallToWebView:)]) { 34 | [plugin didInstallToWebView:self]; 35 | } 36 | } 37 | 38 | - (void)uninstallKakiPluginForClass:(Class)pluginClass { 39 | NSAssert(pluginClass != NULL, @"plugin class can not be nil"); 40 | 41 | NSMutableDictionary *pluginMap = [self __pluginMap]; 42 | id plugin = [pluginMap objectForKey:NSStringFromClass(pluginClass)]; 43 | if (plugin != nil) { 44 | [pluginMap removeObjectForKey:NSStringFromClass(pluginClass)]; 45 | [self.kakiPluginContainer removePlugin:plugin]; 46 | 47 | if ([plugin respondsToSelector:@selector(didUninstall)]) { 48 | [plugin didUninstall]; 49 | } 50 | } 51 | } 52 | 53 | - (void)uninstallAllKakiPlugins { 54 | for (id plugin in self.__pluginMap.allValues) { 55 | [self.kakiPluginContainer removePlugin:plugin]; 56 | 57 | if ([plugin respondsToSelector:@selector(didUninstall)]) { 58 | [plugin didUninstall]; 59 | } 60 | } 61 | 62 | [self.__pluginMap removeAllObjects]; 63 | } 64 | 65 | - (__kindof id _Nullable)kakiPluginForClass:(Class)pluginClass { 66 | NSMutableDictionary *pluginMap = [self __pluginMap]; 67 | return [pluginMap objectForKey:NSStringFromClass(pluginClass)]; 68 | } 69 | 70 | - (NSMutableDictionary *)__pluginMap { 71 | NSAssert(self.kakiPluginContainer != nil, @"must enable kaki plugins first"); 72 | 73 | NSMutableDictionary *result = objc_getAssociatedObject(self.kakiPluginContainer, _cmd); 74 | if (result == nil) { 75 | result = [[NSMutableDictionary alloc] init]; 76 | objc_setAssociatedObject(self.kakiPluginContainer, _cmd, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 77 | } 78 | return result; 79 | } 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /KakiWebView.xcodeproj/xcshareddata/xcschemes/KakiWebView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Plugins/KakiTitleObserverPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // KakiTitleObserverPlugin.m 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import "KakiTitleObserverPlugin.h" 10 | #import "UIWebView+Kaki.h" 11 | 12 | static NSString *KakiTitleChangedRPCURLPath = @"/kakititleobserverplugin/title_changed"; 13 | 14 | @interface KakiTitleObserverPlugin () 15 | 16 | @property (nonatomic, weak, readonly) UIWebView *webView; 17 | @property (nonatomic, copy) NSString *title; 18 | 19 | @end 20 | 21 | @implementation KakiTitleObserverPlugin 22 | 23 | - (BOOL)isTitleMonitorRequest:(NSURLRequest *)request { 24 | return [request.URL.path isEqualToString:KakiTitleChangedRPCURLPath]; 25 | } 26 | 27 | 28 | ////////////////////////////////////////////////////////////////////////////////////// 29 | #pragma mark - KakiWebViewPlugin 30 | ////////////////////////////////////////////////////////////////////////////////////// 31 | 32 | - (void)didInstallToWebView:(UIWebView *)webView { 33 | _webView = webView; 34 | [self __updateTitle]; 35 | } 36 | 37 | - (void)didUninstall { 38 | _webView = nil; 39 | } 40 | 41 | - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 42 | if ([self isTitleMonitorRequest:request]) { 43 | [self __updateTitle]; 44 | return NO; 45 | } 46 | return YES; 47 | } 48 | 49 | - (void)webViewDidFinishLoad:(UIWebView *)webView { 50 | [self __updateTitle]; 51 | 52 | NSString *observerTitleJS = [NSString stringWithFormat: 53 | @"(function() {" 54 | @" if(window.kakiTitleChangeObserverd) return;" 55 | @" var target = document.querySelector('head > title');" 56 | @" var observer = new window.MutationObserver(function(mutations) {" 57 | @" mutations.forEach(function(mutation) {" 58 | @" var ifr = document.createElement('iframe');" 59 | @" ifr.style.display = 'none';" 60 | @" ifr.src = '%@://%@%@';" 61 | @" document.body.appendChild(ifr);" 62 | @" setTimeout(function() {" 63 | @" ifr.parentNode.removeChild(ifr);" 64 | @" }, 0);" 65 | @" });" 66 | @" });" 67 | @" observer.observe(target, {" 68 | @" subtree: true," 69 | @" characterData: true," 70 | @" childList: true" 71 | @" });" 72 | @" window.kakiTitleChangeObserverd = true;" 73 | @"})();", 74 | webView.request.mainDocumentURL.scheme, 75 | webView.request.mainDocumentURL.host, 76 | KakiTitleChangedRPCURLPath]; 77 | [webView stringByEvaluatingJavaScriptFromString:observerTitleJS]; 78 | } 79 | 80 | - (void)__updateTitle { 81 | NSString *documentTitle = [self.webView stringByEvaluatingJavaScriptFromString:@"document.title"]; 82 | if (self.title && documentTitle && [documentTitle isEqualToString:self.title]) 83 | return; 84 | 85 | self.title = documentTitle; 86 | 87 | if (self.onTitleChanged) { 88 | self.onTitleChanged(documentTitle); 89 | } 90 | } 91 | 92 | @end 93 | 94 | 95 | @implementation UIWebView (KakiTitleObserverPlugin) 96 | 97 | - (KakiTitleObserverPlugin *)titleObserverPlugin { 98 | return [self kakiPluginForClass:KakiTitleObserverPlugin.class]; 99 | } 100 | 101 | @end 102 | -------------------------------------------------------------------------------- /KakiWebViewExample/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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Utils/KakiWebViewPluginContainer.m: -------------------------------------------------------------------------------- 1 | // 2 | // KakiWebViewPluginContainer.m 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/4. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import "KakiWebViewPluginContainer.h" 10 | 11 | @interface KakiWebViewPluginContainer () 12 | 13 | @property (nonatomic, strong, readonly) NSPointerArray *plugins; 14 | 15 | @end 16 | 17 | @implementation KakiWebViewPluginContainer 18 | 19 | - (instancetype)init { 20 | if (self = [super init]) { 21 | _plugins = [NSPointerArray weakObjectsPointerArray]; 22 | } 23 | return self; 24 | } 25 | 26 | - (void)dealloc { 27 | [self didUninstall]; 28 | } 29 | 30 | - (void)addPlugin:(id)plugin { 31 | NSAssert(plugin != nil, @"plugin can not be nil"); 32 | 33 | [self.plugins addPointer:(__bridge void *)(plugin)]; 34 | } 35 | 36 | - (void)removePlugin:(id)plugin { 37 | if (plugin == nil) return; 38 | 39 | @autoreleasepool { 40 | for (int i = 0; i < self.plugins.count; i++) { 41 | void *pointer = [self.plugins pointerAtIndex:i]; 42 | if (pointer == NULL) continue; 43 | 44 | if (plugin == (__bridge id)pointer) { 45 | [self.plugins removePointerAtIndex:i]; 46 | break; 47 | } 48 | } 49 | } 50 | } 51 | 52 | - (void)removeAllPlugins { 53 | [self.plugins setCount:0]; 54 | } 55 | 56 | 57 | ////////////////////////////////////////////////////////////////////////////////////// 58 | #pragma mark - KakiWebViewPlugin 59 | ////////////////////////////////////////////////////////////////////////////////////// 60 | 61 | - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 62 | 63 | BOOL result = YES; 64 | 65 | for (id delegate in self.plugins) { 66 | if ([delegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) { 67 | BOOL delegateResult = [delegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; 68 | result = result && delegateResult; 69 | } 70 | } 71 | 72 | return result; 73 | } 74 | 75 | - (void)webViewDidStartLoad:(UIWebView *)webView { 76 | for (id delegate in self.plugins) { 77 | if ([delegate respondsToSelector:@selector(webViewDidStartLoad:)]) { 78 | [delegate webViewDidStartLoad:webView]; 79 | } 80 | } 81 | } 82 | 83 | - (void)webViewDidFinishLoad:(UIWebView *)webView { 84 | for (id delegate in self.plugins) { 85 | if ([delegate respondsToSelector:@selector(webViewDidFinishLoad:)]) { 86 | [delegate webViewDidFinishLoad:webView]; 87 | } 88 | } 89 | } 90 | 91 | - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { 92 | for (id delegate in self.plugins) { 93 | if ([delegate respondsToSelector:@selector(webView:didFailLoadWithError:)]) { 94 | [delegate webView:webView didFailLoadWithError:error]; 95 | } 96 | } 97 | } 98 | 99 | - (void)webViewDidGoback:(UIWebView *)webView { 100 | for (id plugin in self.plugins) { 101 | if ([plugin respondsToSelector:@selector(webViewDidGoback:)]) { 102 | [plugin webViewDidGoback:webView]; 103 | } 104 | } 105 | } 106 | 107 | - (void)webViewDidMoveToSuperview:(UIWebView *)webView { 108 | for (id plugin in self.plugins) { 109 | if ([plugin respondsToSelector:@selector(webViewDidMoveToSuperview:)]) { 110 | [plugin webViewDidMoveToSuperview:webView]; 111 | } 112 | } 113 | } 114 | 115 | - (void)webViewDidMoveToWindow:(UIWebView *)webView { 116 | for (id plugin in self.plugins) { 117 | if ([plugin respondsToSelector:@selector(webViewDidMoveToWindow:)]) { 118 | [plugin webViewDidMoveToWindow:webView]; 119 | } 120 | } 121 | } 122 | 123 | - (void)didInstallToWebView:(UIWebView *)webView { 124 | for (id plugin in self.plugins) { 125 | if ([plugin respondsToSelector:@selector(didInstallToWebView:)]) { 126 | [plugin webViewDidStartLoad:webView]; 127 | } 128 | } 129 | } 130 | 131 | - (void)didUninstall { 132 | for (id plugin in self.plugins) { 133 | if ([plugin respondsToSelector:@selector(didUninstall)]) { 134 | [plugin didUninstall]; 135 | } 136 | } 137 | } 138 | 139 | - (void)webViewLayoutSubviews:(UIWebView *)webView { 140 | for (id plugin in self.plugins) { 141 | if ([plugin respondsToSelector:@selector(webViewLayoutSubviews:)]) { 142 | [plugin webViewLayoutSubviews:webView]; 143 | } 144 | } 145 | } 146 | 147 | @end 148 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Plugins/KakiJavascriptCorePlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // KakiJavascriptCorePlugin.m 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "KakiJavascriptCorePlugin.h" 12 | #import "UIWebView+Kaki.h" 13 | 14 | static NSString *const KakiJSContextDidCreateNotification = @"com.makee.kaki.notify.jscontext"; 15 | 16 | @interface KakiJavascriptCorePlugin () 17 | 18 | @property (nonatomic, weak, readonly) UIWebView *webView; 19 | @property (nonatomic, strong, readonly) NSMutableDictionary *jsObjects; 20 | 21 | @end 22 | 23 | @implementation KakiJavascriptCorePlugin 24 | 25 | - (instancetype)init { 26 | if (self = [super init]) { 27 | _jsObjects = [[NSMutableDictionary alloc] init]; 28 | } 29 | return self; 30 | } 31 | 32 | - (void)dealloc { 33 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 34 | } 35 | 36 | - (void)setJSObject:(NSObject *)object forName:(NSString *)name { 37 | NSAssert(object != nil, @"object cannot be nil!"); 38 | NSAssert(name != nil, @"name cannot be nil!"); 39 | 40 | self.jsObjects[name] = object; 41 | 42 | if (self.jsContext != nil) { 43 | self.jsContext[name] = nil; 44 | self.jsContext[name] = object; 45 | } 46 | } 47 | 48 | - (void)setJSObject:(NSObject *)object withExportProtocol:(Protocol *)protocol forName:(NSString *)name { 49 | NSAssert(object != nil, @"object cannot be nil!"); 50 | NSAssert(name != nil, @"name cannot be nil!"); 51 | 52 | #if DEBUG 53 | { 54 | unsigned protocolCount = 0; 55 | Protocol * __unsafe_unretained * inheritProtocols = protocol_copyProtocolList(protocol, &protocolCount); 56 | 57 | BOOL findJSExport = NO; 58 | for (unsigned i = 0; i < protocolCount; i++) { 59 | findJSExport = protocol_isEqual(inheritProtocols[i], @protocol(JSExport)); 60 | if (findJSExport) break; 61 | } 62 | 63 | if (inheritProtocols != NULL) free(inheritProtocols); 64 | 65 | NSAssert(findJSExport == YES, @"protocol must inherit of JSExport"); 66 | } 67 | #endif 68 | 69 | if (![object conformsToProtocol:protocol]) { 70 | class_addProtocol(object.class, protocol); 71 | } 72 | 73 | self.jsObjects[name] = object; 74 | 75 | if (self.jsContext != nil) { 76 | self.jsContext[name] = nil; 77 | self.jsContext[name] = object; 78 | } 79 | } 80 | 81 | - (void)removeJSObjectForName:(NSString *)name { 82 | NSAssert(name != nil, @"name cannot be nil!"); 83 | 84 | [self.jsObjects removeObjectForKey:name]; 85 | 86 | if (self.jsContext != nil) { self.jsContext[name] = nil; } 87 | } 88 | 89 | - (void)removeAllJSObjects { 90 | if (self.jsContext != nil) { 91 | [self.jsObjects.allKeys enumerateObjectsUsingBlock:^(NSString *name, NSUInteger idx, BOOL *stop) { 92 | self.jsContext[name] = nil; 93 | }]; 94 | } 95 | 96 | [self.jsObjects removeAllObjects]; 97 | } 98 | 99 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 100 | #pragma mark - KakiWebViewPlugin 101 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 102 | 103 | - (void)didInstallToWebView:(UIWebView *)webView { 104 | _webView = webView; 105 | [[NSNotificationCenter defaultCenter] addObserver:self 106 | selector:@selector(__didCreateJSContext:) 107 | name:KakiJSContextDidCreateNotification 108 | object:nil]; 109 | 110 | } 111 | 112 | - (void)__didCreateJSContext:(NSNotification *)notification { 113 | NSString *cookie = [NSString stringWithFormat:@"KakiTest_%lud", (unsigned long)self.webView.hash]; 114 | NSString *cookieTestJS = [NSString stringWithFormat:@"var %@ = '%@'", cookie, cookie]; 115 | [self.webView stringByEvaluatingJavaScriptFromString:cookieTestJS]; 116 | 117 | JSContext *ctx = notification.object; 118 | 119 | if (![ctx[cookie].toString isEqualToString:cookie]) return; 120 | 121 | _jsContext = ctx; 122 | 123 | [self.jsObjects enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSObject *obj, BOOL *stop) { 124 | self.jsContext[name] = nil; 125 | self.jsContext[name]= obj; 126 | }]; 127 | 128 | } 129 | 130 | - (void)didUninstall { 131 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 132 | 133 | if (self.jsContext == nil) return; 134 | 135 | [self.jsObjects.allKeys enumerateObjectsUsingBlock:^(NSString *name, NSUInteger idx, BOOL *stop) { 136 | self.jsContext[name] = nil; 137 | }]; 138 | } 139 | 140 | @end 141 | 142 | 143 | @implementation NSObject (JSContextCreation) 144 | 145 | - (void)webView:(id)unuse didCreateJavaScriptContext:(JSContext *)ctx forFrame:(id)frame { 146 | [[NSNotificationCenter defaultCenter] postNotificationName:KakiJSContextDidCreateNotification 147 | object:ctx]; 148 | } 149 | 150 | @end 151 | 152 | 153 | @implementation UIWebView (KakiJavascriptCorePlugin) 154 | 155 | - (KakiJavascriptCorePlugin *)javascriptCorePlugin { 156 | return [self kakiPluginForClass:KakiJavascriptCorePlugin.class]; 157 | } 158 | 159 | @end 160 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Utils/KakiWebViewPatcher.m: -------------------------------------------------------------------------------- 1 | // 2 | // KakiWebViewPatcher.m 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/4. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "KakiWebViewPatcher.h" 12 | 13 | 14 | static char *const kKakiOriginDelegateKey = "com.makee.kaki.delegate"; 15 | static char *const kKakiPluginContainerKey = "com.makee.kaki.plugin_conatienr"; 16 | static NSString *const kKakiWebViewClassPrefix = @"KakiDynamic_"; 17 | 18 | @implementation KakiWebViewPatcher 19 | 20 | + (void)applyPatchForWebView:(UIWebView *)webView { 21 | if (webView.delegate != nil && [webView.delegate isKindOfClass:[KakiWebViewPluginContainer class]]) { 22 | return; 23 | } 24 | 25 | KakiWebViewPluginContainer *pluginContainer = [KakiWebViewPluginContainer new]; 26 | objc_setAssociatedObject(webView, kKakiPluginContainerKey, pluginContainer, OBJC_ASSOCIATION_RETAIN); 27 | 28 | if (webView.delegate != nil) { 29 | NSString *delegateClsName = NSStringFromClass(object_getClass(webView.delegate)); 30 | if (![delegateClsName hasPrefix:@"NBSLensWebView"] && 31 | ![delegateClsName hasPrefix:@"A2Dynamic"]) { 32 | objc_setAssociatedObject(webView, kKakiOriginDelegateKey, webView.delegate, OBJC_ASSOCIATION_ASSIGN); 33 | [pluginContainer addPlugin:(id)webView.delegate]; 34 | } 35 | } 36 | 37 | [webView setDelegate:pluginContainer]; 38 | [self __dynamicInheritWebView:webView]; 39 | } 40 | 41 | + (void)removePatchForWebView:(UIWebView *)webView { 42 | if (webView.delegate == nil || ![webView.delegate isKindOfClass:[KakiWebViewPluginContainer class]]) { 43 | return; 44 | } 45 | 46 | [self __restoreDynamicInherittedWebView:webView]; 47 | 48 | [webView setDelegate:objc_getAssociatedObject(webView, kKakiOriginDelegateKey)]; 49 | 50 | objc_setAssociatedObject(webView, kKakiOriginDelegateKey, nil, OBJC_ASSOCIATION_ASSIGN); 51 | objc_setAssociatedObject(webView, kKakiPluginContainerKey, nil, OBJC_ASSOCIATION_RETAIN); 52 | } 53 | 54 | + (void)__dynamicInheritWebView:(UIWebView *)webView { 55 | NSString *className = NSStringFromClass(webView.class); 56 | 57 | if ([className hasPrefix:kKakiWebViewClassPrefix]) return; 58 | 59 | NSString *dynamicClassName = [NSString stringWithFormat:@"%@%@", kKakiWebViewClassPrefix, className]; 60 | Class dynamicClass = NSClassFromString(dynamicClassName); 61 | 62 | if (dynamicClass == NULL) { 63 | dynamicClass = objc_allocateClassPair(webView.class, dynamicClassName.UTF8String, 0); 64 | IMP dynamicIMP; 65 | dynamicIMP = class_getMethodImplementation(self, @selector(setDelegate:)); 66 | if (dynamicIMP) { 67 | class_addMethod(dynamicClass, @selector(setDelegate:), dynamicIMP, "v@:@"); 68 | } 69 | dynamicIMP = class_getMethodImplementation(self, @selector(didMoveToSuperview)); 70 | if (dynamicIMP) { 71 | class_addMethod(dynamicClass, @selector(didMoveToSuperview), dynamicIMP, "v@:"); 72 | } 73 | dynamicIMP = class_getMethodImplementation(self, @selector(didMoveToWindow)); 74 | if (dynamicIMP) { 75 | class_addMethod(dynamicClass, @selector(didMoveToWindow), dynamicIMP, "v@:"); 76 | } 77 | dynamicIMP = class_getMethodImplementation(self, @selector(layoutSubviews)); 78 | if (dynamicIMP) { 79 | class_addMethod(dynamicClass, @selector(layoutSubviews), dynamicIMP, "v@:"); 80 | } 81 | dynamicIMP = class_getMethodImplementation(self, @selector(goBack)); 82 | if (dynamicIMP) { 83 | class_addMethod(dynamicClass, @selector(goBack), dynamicIMP, "v@:"); 84 | } 85 | 86 | objc_registerClassPair(dynamicClass); 87 | } 88 | 89 | if (dynamicClass != NULL) { 90 | object_setClass(webView, dynamicClass); 91 | } 92 | } 93 | 94 | + (void)__restoreDynamicInherittedWebView:(UIWebView *)webView { 95 | NSString *className = NSStringFromClass(webView.class); 96 | 97 | if (![className hasPrefix:kKakiWebViewClassPrefix]) return; 98 | 99 | NSString *originClassName = [className substringFromIndex:kKakiWebViewClassPrefix.length]; 100 | Class originClass = NSClassFromString(originClassName); 101 | 102 | NSAssert(originClass != NULL, @"unknow exception, could not found origin class!"); 103 | 104 | object_setClass(webView, originClass); 105 | } 106 | 107 | ////////////////////////////////////////////////////////////////////////////////////// 108 | #pragma mark - Dynamic IMP 109 | ////////////////////////////////////////////////////////////////////////////////////// 110 | 111 | - (void)setDelegate:(id)delegate { 112 | KakiWebViewPluginContainer *pluginContainer = objc_getAssociatedObject(self, kKakiOriginDelegateKey); 113 | id originDelegate = objc_getAssociatedObject(self, kKakiOriginDelegateKey); 114 | 115 | NSAssert(pluginContainer != nil, @"plugin container can not be nil"); 116 | 117 | if (delegate == nil && originDelegate != nil) { 118 | [pluginContainer removePlugin:(id)originDelegate]; 119 | 120 | objc_setAssociatedObject(self, kKakiOriginDelegateKey, nil, OBJC_ASSOCIATION_ASSIGN); 121 | } else if (delegate != nil && originDelegate == nil) { 122 | [pluginContainer addPlugin:(id)delegate]; 123 | } else if (delegate != nil && originDelegate != nil) { 124 | [pluginContainer removePlugin:(id)originDelegate]; 125 | [pluginContainer addPlugin:(id)delegate]; 126 | 127 | objc_setAssociatedObject(self, kKakiOriginDelegateKey, nil, OBJC_ASSOCIATION_ASSIGN); 128 | objc_setAssociatedObject(self, kKakiOriginDelegateKey, delegate, OBJC_ASSOCIATION_ASSIGN); 129 | } 130 | } 131 | 132 | - (void)didMoveToSuperview { 133 | __weak UIWebView *realSelf = (UIWebView *)self; 134 | 135 | Class superClass = [realSelf superclass]; 136 | while (superClass) { 137 | IMP superIMP = class_getMethodImplementation(superClass, @selector(didMoveToSuperview)); 138 | if (superIMP == NULL) { 139 | superClass = class_getSuperclass(superClass); 140 | } else { 141 | typedef void(*SuperMethodPointer)(id, SEL); 142 | SuperMethodPointer pMethod = (SuperMethodPointer)superIMP; 143 | pMethod(self, @selector(didMoveToSuperview)); 144 | break; 145 | } 146 | } 147 | 148 | if ([realSelf.delegate respondsToSelector:@selector(webViewDidMoveToSuperview:)]) { 149 | [(id)realSelf.delegate webViewDidMoveToSuperview:realSelf]; 150 | } 151 | } 152 | 153 | - (void)didMoveToWindow { 154 | __weak UIWebView *realSelf = (UIWebView *)self; 155 | 156 | Class superClass = [realSelf superclass]; 157 | while (superClass) { 158 | IMP superIMP = class_getMethodImplementation(superClass, @selector(didMoveToWindow)); 159 | if (superIMP == NULL) { 160 | superClass = class_getSuperclass(superClass); 161 | } else { 162 | typedef void(*SuperMethodPointer)(id, SEL); 163 | SuperMethodPointer pMethod = (SuperMethodPointer)superIMP; 164 | pMethod(self, @selector(didMoveToWindow)); 165 | break; 166 | } 167 | } 168 | 169 | if ([realSelf.delegate respondsToSelector:@selector(webViewDidMoveToWindow:)]) { 170 | [(id)realSelf.delegate webViewDidMoveToWindow:realSelf]; 171 | } 172 | } 173 | 174 | - (void)layoutSubviews { 175 | __weak UIWebView *realSelf = (UIWebView *)self; 176 | 177 | Class superClass = [realSelf superclass]; 178 | while (superClass) { 179 | IMP superIMP = class_getMethodImplementation(superClass, @selector(layoutSubviews)); 180 | if (superIMP == NULL) { 181 | superClass = class_getSuperclass(superClass); 182 | } else { 183 | typedef void(*SuperMethodPointer)(id, SEL); 184 | SuperMethodPointer pMethod = (SuperMethodPointer)superIMP; 185 | pMethod(self, @selector(layoutSubviews)); 186 | break; 187 | } 188 | } 189 | 190 | if ([realSelf.delegate respondsToSelector:@selector(webViewLayoutSubviews:)]) { 191 | [(id)realSelf.delegate webViewLayoutSubviews:realSelf]; 192 | } 193 | } 194 | 195 | - (void)goBack { 196 | __weak UIWebView *realSelf = (UIWebView *)self; 197 | 198 | Class superClass = [realSelf superclass]; 199 | while (superClass) { 200 | IMP superIMP = class_getMethodImplementation(superClass, @selector(goBack)); 201 | if (superIMP == NULL) { 202 | superClass = class_getSuperclass(superClass); 203 | } else { 204 | typedef void(*SuperMethodPointer)(id, SEL); 205 | SuperMethodPointer pMethod = (SuperMethodPointer)superIMP; 206 | pMethod(self, @selector(goBack)); 207 | break; 208 | } 209 | } 210 | 211 | if ([realSelf.delegate respondsToSelector:@selector(webViewDidGoback:)]) { 212 | [(id)realSelf.delegate webViewDidGoback:realSelf]; 213 | } 214 | } 215 | 216 | @end 217 | 218 | 219 | @implementation UIWebView (KakiPluginContainer) 220 | 221 | - (KakiWebViewPluginContainer *)kakiPluginContainer { 222 | return objc_getAssociatedObject(self, kKakiPluginContainerKey); 223 | } 224 | 225 | @end 226 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Plugins/KakiProgressPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // KakiProgressPlugin.m 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | // Note: This file was modify from **NJKWebViewProgress** 9 | // https://github.com/ninjinkun/NJKWebViewProgress 10 | 11 | #import 12 | 13 | #import "KakiProgressPlugin.h" 14 | #import "UIWebView+Kaki.h" 15 | 16 | static NSString *const KakiProgressCompletedRPCURLPath = @"/kakiprogressplugin/completed"; 17 | 18 | static inline NSString * KakiURLTrimFragment(NSURL *url) { 19 | if (url.fragment) { 20 | return [url.absoluteString stringByReplacingOccurrencesOfString:[@"#" stringByAppendingString:url.fragment] withString:@""]; 21 | } 22 | return url.absoluteString; 23 | } 24 | 25 | @interface KakiWebViewProgressView : UIView 26 | 27 | @property (nonatomic) float progress; 28 | @property (nonatomic) UIView *progressBarView; 29 | @property (nonatomic) NSTimeInterval barAnimationDuration; 30 | @property (nonatomic) NSTimeInterval fadeAnimationDuration; 31 | @property (nonatomic) NSTimeInterval fadeOutDelay; 32 | @property (nonatomic) UIColor *progressColor; 33 | 34 | - (void)setProgress:(float)progress animated:(BOOL)animated; 35 | 36 | @end 37 | 38 | @interface KakiProgressPlugin () { 39 | NSUInteger _loadingCount; 40 | NSUInteger _maxLoadCount; 41 | NSURL *_currentURL; 42 | BOOL _interactive; 43 | } 44 | 45 | @property (nonatomic, weak, readonly) UIWebView *webView; 46 | @property (nonatomic, strong) KakiWebViewProgressView *progressView; 47 | 48 | @end 49 | 50 | 51 | @implementation KakiProgressPlugin 52 | 53 | - (instancetype)init { 54 | if (self = [super init]) { 55 | _maxLoadCount = _loadingCount = 0; 56 | _progress = 0.0; 57 | _interactive = NO; 58 | _progressView = [[KakiWebViewProgressView alloc] initWithFrame:CGRectZero]; 59 | _progressView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; 60 | _progressView.progressColor = [UIColor colorWithRed:0x44/255.f green:0xb3/255.f blue:0x36/255.f alpha:1]; 61 | } 62 | return self; 63 | } 64 | 65 | - (void)dealloc { 66 | [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; 67 | } 68 | 69 | - (void)setProgressColor:(UIColor *)progressColor { 70 | self.progressView.progressColor = progressColor; 71 | } 72 | 73 | - (UIColor *)progressColor { 74 | return self.progressView.progressColor; 75 | } 76 | 77 | - (void)__startProgress { 78 | if (_progress < 0.1f) { 79 | [self setProgress:0.1f]; 80 | } 81 | } 82 | 83 | - (void)__incrementProgress { 84 | float progress = self.progress; 85 | float maxProgress = _interactive ? 0.9f : 0.5f; 86 | float remainPercent = (float)_loadingCount / (float)_maxLoadCount; 87 | float increment = (maxProgress - progress) * remainPercent; 88 | progress += increment; 89 | progress = fmin(progress, maxProgress); 90 | [self setProgress:progress]; 91 | } 92 | 93 | - (void)__completeProgress { 94 | [self setProgress:1.0]; 95 | _isFinishLoad = YES; 96 | [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; 97 | } 98 | 99 | - (void)setProgress:(float)progress { 100 | if (progress > _progress || progress == 0) { 101 | _progress = progress; 102 | [self.progressView setProgress:progress]; 103 | } 104 | } 105 | 106 | - (void)reset { 107 | _maxLoadCount = _loadingCount = 0; 108 | _interactive = _isFinishLoad = NO; 109 | [self setProgress:0.0]; 110 | } 111 | 112 | - (BOOL)isProgressMonitorRequest:(NSURLRequest *)request { 113 | return [request.URL.path isEqualToString:KakiProgressCompletedRPCURLPath]; 114 | } 115 | 116 | - (void)addViewBelowProgressView:(UIView *)view { 117 | [self.webView insertSubview:view belowSubview:self.progressView]; 118 | } 119 | 120 | ////////////////////////////////////////////////////////////////////////////////////// 121 | #pragma mark - KakiWebViewPlugin 122 | ////////////////////////////////////////////////////////////////////////////////////// 123 | 124 | - (void)didInstallToWebView:(UIWebView *)webView { 125 | _webView = webView; 126 | self.progressView.frame = CGRectMake(0, 0, webView.frame.size.width, 3.0); 127 | [self reset]; 128 | [webView addSubview:self.progressView]; 129 | } 130 | 131 | - (void)didUninstall { 132 | _webView = nil; 133 | [self reset]; 134 | [self.progressView removeFromSuperview]; 135 | [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; 136 | } 137 | 138 | - (void)webViewLayoutSubviews:(UIWebView *)webView { 139 | CGRect frame = self.progressView.frame; 140 | frame.origin.y = webView.scrollView.contentInset.top; 141 | if (self.progressView.frame.origin.y != frame.origin.y) { 142 | self.progressView.frame = frame; 143 | } 144 | } 145 | 146 | - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 147 | if ([self isProgressMonitorRequest:request]) { 148 | [self __completeProgress]; 149 | return NO; 150 | } 151 | 152 | BOOL ret = YES; 153 | 154 | BOOL isFragmentJump = [KakiURLTrimFragment(request.URL) isEqualToString:webView.request.URL.absoluteString]; 155 | BOOL isTopLevelNavigation = [request.mainDocumentURL isEqual:request.URL]; 156 | BOOL isHTTPOrLocalFile = [request.URL.scheme isEqualToString:@"http"] || [request.URL.scheme isEqualToString:@"https"] || [request.URL.scheme isEqualToString:@"file"]; 157 | 158 | if (ret && !isFragmentJump && isHTTPOrLocalFile && isTopLevelNavigation) { 159 | _currentURL = request.URL; 160 | [self reset]; 161 | } 162 | 163 | _isFinishLoad = NO; 164 | return ret; 165 | } 166 | 167 | - (void)webViewDidStartLoad:(UIWebView *)webView { 168 | _loadingCount++; 169 | _maxLoadCount = fmax(_maxLoadCount, _loadingCount); 170 | 171 | [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; 172 | 173 | [self __startProgress]; 174 | } 175 | 176 | - (void)webViewDidFinishLoad:(UIWebView *)webView { 177 | _loadingCount--; 178 | [self __incrementProgress]; 179 | 180 | NSString *readyState = [webView stringByEvaluatingJavaScriptFromString:@"document.readyState"]; 181 | 182 | BOOL interactive = [readyState isEqualToString:@"interactive"]; 183 | if (interactive) { 184 | _interactive = YES; 185 | [self __createWaitCompleteJSForWebView:webView]; 186 | } 187 | 188 | BOOL isNotRedirect = _currentURL && [KakiURLTrimFragment(_currentURL) isEqualToString:KakiURLTrimFragment(webView.request.mainDocumentURL)]; 189 | BOOL complete = [readyState isEqualToString:@"complete"]; 190 | if (complete && isNotRedirect) { 191 | [self __completeProgress]; 192 | } 193 | } 194 | 195 | - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { 196 | _loadingCount--; 197 | [self __incrementProgress]; 198 | 199 | NSString *readyState = [webView stringByEvaluatingJavaScriptFromString:@"document.readyState"]; 200 | 201 | BOOL interactive = [readyState isEqualToString:@"interactive"]; 202 | if (interactive) { 203 | _interactive = YES; 204 | [self __createWaitCompleteJSForWebView:webView]; 205 | } 206 | 207 | BOOL isNotRedirect = _currentURL && [KakiURLTrimFragment(_currentURL) isEqualToString:KakiURLTrimFragment(webView.request.mainDocumentURL)]; 208 | BOOL complete = [readyState isEqualToString:@"complete"]; 209 | if ((complete && isNotRedirect) || error) { 210 | [self __completeProgress]; 211 | } 212 | } 213 | 214 | - (void)__createWaitCompleteJSForWebView:(UIWebView *)webView { 215 | NSString *javascript = [NSString stringWithFormat: 216 | @"(function() {" 217 | @" var isSentMockRequest = false;" 218 | @" if (window.kakiProgressFinishObservered) return;" 219 | @" var sendMockRequest = function() {" 220 | @" var ifr = document.createElement('iframe');" 221 | @" ifr.style.display = 'none';" 222 | @" ifr.src = '%@://%@%@';" 223 | @" document.body.appendChild(ifr);" 224 | @" setTimeout(function() {" 225 | @" ifr.parentNode.removeChild(ifr);" 226 | @" }, 0);" 227 | @" isSentMockRequest = true; " 228 | @" };" 229 | @" " 230 | @" var timeoutID = setTimeout(sendMockRequest, 3000);" 231 | @" " 232 | @" window.addEventListener('load', function() {" 233 | @" clearTimeout(timeoutID);" 234 | @" if (isSentMockRequest) return; " 235 | @" sendMockRequest();" 236 | @" }, false);" 237 | @" " 238 | @" window.kakiProgressFinishObservered = true;" 239 | @"})();", 240 | webView.request.mainDocumentURL.scheme, 241 | webView.request.mainDocumentURL.host, 242 | KakiProgressCompletedRPCURLPath]; 243 | [webView stringByEvaluatingJavaScriptFromString:javascript]; 244 | } 245 | 246 | @end 247 | 248 | 249 | @implementation KakiWebViewProgressView 250 | 251 | - (id)initWithFrame:(CGRect)frame { 252 | if (self = [super initWithFrame:frame]) { 253 | [self configureViews]; 254 | } 255 | return self; 256 | } 257 | 258 | - (void)configureViews { 259 | self.userInteractionEnabled = NO; 260 | self.autoresizingMask = UIViewAutoresizingFlexibleWidth; 261 | _progressBarView = [[UIView alloc] initWithFrame:self.bounds]; 262 | _progressBarView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 263 | [self addSubview:_progressBarView]; 264 | 265 | _barAnimationDuration = 0.27f; 266 | _fadeAnimationDuration = 0.27f; 267 | _fadeOutDelay = 0.1f; 268 | } 269 | 270 | - (void)setProgressColor:(UIColor *)progressColor { 271 | _progressBarView.backgroundColor = progressColor; 272 | } 273 | 274 | - (UIColor *)progressColor { 275 | return _progressBarView.backgroundColor; 276 | } 277 | 278 | - (void)setProgress:(float)progress { 279 | [self setProgress:progress animated:NO]; 280 | } 281 | 282 | - (void)setProgress:(float)progress animated:(BOOL)animated { 283 | BOOL isGrowing = progress > 0.0; 284 | [UIView animateWithDuration:(isGrowing && animated) ? _barAnimationDuration : 0.0 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 285 | CGRect frame = _progressBarView.frame; 286 | frame.size.width = progress * self.bounds.size.width; 287 | _progressBarView.frame = frame; 288 | } completion:nil]; 289 | 290 | if (progress >= 1.0) { 291 | [UIView animateWithDuration:animated ? _fadeAnimationDuration : 0.0 delay:_fadeOutDelay options:UIViewAnimationOptionCurveEaseInOut animations:^{ 292 | _progressBarView.alpha = 0.0; 293 | } completion:^(BOOL completed){ 294 | CGRect frame = _progressBarView.frame; 295 | frame.size.width = 0; 296 | _progressBarView.frame = frame; 297 | }]; 298 | } else { 299 | [UIView animateWithDuration:animated ? _fadeAnimationDuration : 0.0 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 300 | _progressBarView.alpha = 1.0; 301 | } completion:nil]; 302 | } 303 | } 304 | 305 | @end 306 | 307 | 308 | @implementation UIWebView (KakiProgressPlugin) 309 | 310 | - (KakiProgressPlugin *)progressPlugin { 311 | return [self kakiPluginForClass:KakiProgressPlugin.class]; 312 | } 313 | 314 | @end 315 | -------------------------------------------------------------------------------- /KakiWebView/Classes/Plugins/KakiPopGesturePlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // KakiPopGesturePlugin.m 3 | // KakiWebView 4 | // 5 | // Created by MK on 2017/4/5. 6 | // Copyright © 2017年 makee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "KakiPopGesturePlugin.h" 12 | 13 | static NSString *const KakiLocationChangedRPCURLPath = @"/kakipopgestureplugin/location_changed"; 14 | 15 | typedef NS_ENUM(NSInteger, KakiSnapshotViewType) { 16 | KakiSnapshotViewTypeShadow = 2, 17 | KakiSnapshotViewTypeBlackAlpha = 3, 18 | }; 19 | 20 | @interface KakiPopGesturePlugin () 21 | 22 | // {'href': string, 'snapshot': image } 23 | @property (nonatomic, strong, readonly) NSMutableArray *historySnapshots; 24 | @property (nonatomic, assign) NSUInteger historyCursor; 25 | @property (nonatomic, weak, readonly) UIWebView *webView; 26 | @property (nonatomic, assign, readonly) CGRect webViewOriginalFrame; 27 | @property (nonatomic, strong, readonly) UIPanGestureRecognizer *panGesture; 28 | @property (nonatomic, strong) UIView *snapshotView; 29 | 30 | @property (nonatomic, weak, readonly) UINavigationController *navigationController; 31 | 32 | @end 33 | 34 | @implementation KakiPopGesturePlugin 35 | 36 | + (UIImage *)snapshotForView:(UIView *)view { 37 | UIGraphicsBeginImageContextWithOptions(view.frame.size, YES, 0.0); 38 | 39 | if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) { 40 | [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO]; 41 | } else { 42 | [view.layer renderInContext:UIGraphicsGetCurrentContext()]; 43 | } 44 | 45 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 46 | UIGraphicsEndImageContext(); 47 | return image; 48 | } 49 | 50 | @synthesize navigationController = _navigationController; 51 | - (UINavigationController *)navigationController { 52 | if (_navigationController == nil) { 53 | id nextResponder = [self.webView nextResponder]; 54 | while (nextResponder && ![nextResponder isKindOfClass:[UIViewController class]]) { 55 | nextResponder = [nextResponder nextResponder]; 56 | } 57 | 58 | if (nextResponder && [nextResponder isKindOfClass:[UINavigationController class]]) { 59 | _navigationController = nextResponder; 60 | } else if (nextResponder && [nextResponder isKindOfClass:[UIViewController class]]) { 61 | _navigationController = [nextResponder navigationController]; 62 | } 63 | } 64 | return _navigationController; 65 | } 66 | 67 | - (void)__createOrUpdateSnapshots { 68 | __strong UIWebView *strongWebView = self.webView; 69 | if (strongWebView == nil) return; 70 | 71 | NSUInteger historyCount = [strongWebView stringByEvaluatingJavaScriptFromString:@"window.history.length"].integerValue; 72 | NSString *href = [strongWebView stringByEvaluatingJavaScriptFromString:@"window.location.href"]; 73 | UIImage *snapshot = [self.class snapshotForView:strongWebView]; 74 | if (snapshot == nil) snapshot = [UIImage new]; 75 | id snapshotObj = @{@"href": href, @"snapshot": snapshot}; 76 | 77 | if (historyCount > self.historySnapshots.count) { 78 | [self.historySnapshots addObject:snapshotObj]; 79 | self.historyCursor = self.historySnapshots.count - 1; 80 | } else if (historyCount == self.historySnapshots.count) { 81 | if (self.historySnapshots.count > self.historyCursor + 1) { 82 | NSString *matchHref = self.historySnapshots[self.historyCursor + 1][@"href"]; 83 | if ([matchHref isEqualToString:href]) { 84 | self.historySnapshots[self.historyCursor + 1] = snapshotObj; 85 | self.historyCursor += 1; 86 | return; 87 | } 88 | } 89 | 90 | for (NSInteger index = self.historyCursor; index >= 0; index--) { 91 | NSString *matchHref = self.historySnapshots[index][@"href"]; 92 | if ([matchHref isEqualToString:href]) { 93 | self.historySnapshots[index] = snapshotObj; 94 | self.historyCursor = index; 95 | return; 96 | } 97 | } 98 | 99 | self.historySnapshots[historyCount - 1] = snapshotObj; 100 | self.historyCursor = historyCount - 1; 101 | } else { 102 | NSRange removeRange = NSMakeRange(historyCount, self.historySnapshots.count - historyCount); 103 | [self.historySnapshots removeObjectsInRange:removeRange]; 104 | self.historySnapshots[historyCount - 1] = snapshotObj; 105 | self.historyCursor = historyCount - 1; 106 | } 107 | } 108 | 109 | - (void)__createSnapshotView { 110 | if (_snapshotView != nil) { 111 | [_snapshotView removeFromSuperview]; 112 | _snapshotView = nil; 113 | } 114 | 115 | __strong UIWebView *strongWebView = self.webView; 116 | if (strongWebView == nil) return; 117 | 118 | CGRect rect = strongWebView.frame; 119 | _snapshotView = [[UIView alloc] initWithFrame:rect]; 120 | CGRect bounds = _snapshotView.bounds; 121 | 122 | UIImageView *leftView = [[UIImageView alloc] initWithFrame:CGRectOffset(bounds, -44, 0)]; 123 | leftView.contentMode = UIViewContentModeScaleAspectFit; 124 | if (self.historySnapshots.count > 0) { 125 | leftView.image = self.historySnapshots[self.historyCursor - 1][@"snapshot"]; 126 | } 127 | leftView.tag = KakiSnapshotViewTypeShadow; 128 | [_snapshotView addSubview:leftView]; 129 | 130 | UIView *blackView = [[UIView alloc] initWithFrame:bounds]; 131 | blackView.alpha = 0.8; 132 | blackView.backgroundColor = [UIColor blackColor]; 133 | blackView.tag = KakiSnapshotViewTypeBlackAlpha; 134 | [_snapshotView addSubview:blackView]; 135 | 136 | _snapshotView.layer.masksToBounds = YES; 137 | [strongWebView.superview insertSubview:_snapshotView belowSubview:strongWebView]; 138 | } 139 | 140 | - (void)__updateSnapshotViewWithX:(CGFloat)x { 141 | __strong UIWebView *strongWebView = self.webView; 142 | if (strongWebView == nil) return; 143 | 144 | if (x >= 0) { 145 | CGRect bounds = self.snapshotView.bounds; 146 | CGRect left = CGRectOffset(bounds, -44, 0); 147 | UIView *leftView = [self.snapshotView viewWithTag:KakiSnapshotViewTypeShadow]; 148 | leftView.frame = CGRectOffset(left, (x / bounds.size.width) * 44, 0); 149 | 150 | strongWebView.frame = CGRectOffset(self.webViewOriginalFrame, x, 0); 151 | 152 | UIView *blackView = [self.snapshotView viewWithTag:KakiSnapshotViewTypeBlackAlpha]; 153 | blackView.alpha = 0.8 * (1 - x / leftView.frame.size.width); 154 | } 155 | } 156 | 157 | - (void)__handlePanGesture:(UIPanGestureRecognizer *)pan { 158 | if (!self.webView.canGoBack || self.historySnapshots.count == 0 || self.historyCursor < 1) 159 | return; 160 | 161 | CGPoint offset = [pan translationInView:self.webView.superview]; 162 | 163 | if (pan.state == UIGestureRecognizerStateBegan ) { 164 | [self __createSnapshotView]; 165 | self.snapshotView.hidden = NO; 166 | } else if (pan.state == UIGestureRecognizerStateChanged && offset.x > 0) { 167 | [self __updateSnapshotViewWithX:offset.x]; 168 | } else if (pan.state == UIGestureRecognizerStateEnded) { 169 | if (offset.x > 44) { 170 | [self.webView goBack]; 171 | [UIView animateWithDuration:0.2 animations:^{ 172 | [self __updateSnapshotViewWithX:self.webView.frame.size.width]; 173 | } completion:^(BOOL finished) { 174 | self.webView.frame = self.webViewOriginalFrame; 175 | }]; 176 | } else { 177 | [UIView animateWithDuration:0.2 animations:^{ 178 | [self __updateSnapshotViewWithX:0]; 179 | } completion:^(BOOL finished) { 180 | self.snapshotView.hidden = YES; 181 | }]; 182 | } 183 | } 184 | } 185 | 186 | - (BOOL)isSnapshotMonitorRequest:(NSURLRequest *)request { 187 | return [request.URL.path isEqualToString:KakiLocationChangedRPCURLPath]; 188 | } 189 | 190 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { 191 | if ([touch locationInView:self.webView].x > 40) { 192 | return NO; 193 | } 194 | 195 | return [self gestureRecognizerShouldBegin:gestureRecognizer]; 196 | } 197 | 198 | - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { 199 | self.navigationController.interactivePopGestureRecognizer.enabled = YES; 200 | 201 | if ([UIApplication sharedApplication].statusBarOrientation != UIInterfaceOrientationPortrait && 202 | [UIApplication sharedApplication].statusBarOrientation != UIInterfaceOrientationPortraitUpsideDown) { 203 | return NO; 204 | } 205 | 206 | [self __createOrUpdateSnapshots]; 207 | 208 | if (self.historySnapshots.count == 0 || self.historyCursor < 1) { 209 | return NO; 210 | } 211 | 212 | if ([self.webView canGoBack]) { 213 | self.navigationController.interactivePopGestureRecognizer.enabled = NO; 214 | _webViewOriginalFrame = self.webView.frame; 215 | return YES; 216 | } 217 | 218 | return NO; 219 | } 220 | 221 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { 222 | [otherGestureRecognizer requireGestureRecognizerToFail:gestureRecognizer]; 223 | return YES; 224 | } 225 | 226 | ////////////////////////////////////////////////////////////////////////////////////// 227 | #pragma mark - KakiWebViewPlugin 228 | ////////////////////////////////////////////////////////////////////////////////////// 229 | 230 | - (void)didInstallToWebView:(UIWebView *)webView { 231 | _webView = webView; 232 | _webViewOriginalFrame = _webView.frame; 233 | _historySnapshots = [NSMutableArray new]; 234 | _historyCursor = 0; 235 | _panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(__handlePanGesture:)]; 236 | _panGesture.delegate = self; 237 | 238 | [_webView addGestureRecognizer:_panGesture]; 239 | } 240 | 241 | - (void)didUninstall { 242 | [_webView removeGestureRecognizer:_panGesture]; 243 | _webView = nil; 244 | _historySnapshots = nil; 245 | _historyCursor = 0; 246 | _panGesture.delegate = nil; 247 | _panGesture = nil; 248 | 249 | if (_snapshotView != nil) { 250 | [_snapshotView removeFromSuperview]; 251 | _snapshotView = nil; 252 | } 253 | } 254 | 255 | - (void)webViewDidMoveToSuperview:(UIWebView *)webView { 256 | _webViewOriginalFrame = webView.frame; 257 | } 258 | 259 | - (void)webViewDidMoveToWindow:(UIWebView *)webView { 260 | _webViewOriginalFrame = webView.frame; 261 | } 262 | 263 | - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 264 | if ([self isSnapshotMonitorRequest:request]) { 265 | self.snapshotView.hidden = YES; 266 | [self __createOrUpdateSnapshots]; 267 | return NO; 268 | } 269 | 270 | BOOL isTopLevelNavigation = [request.mainDocumentURL isEqual:request.URL]; 271 | if (!isTopLevelNavigation) { 272 | self.snapshotView.hidden = YES; 273 | } 274 | 275 | [self __createOrUpdateSnapshots]; 276 | 277 | return YES; 278 | } 279 | 280 | - (void)webViewDidFinishLoad:(UIWebView *)webView { 281 | NSString *javascript = [NSString stringWithFormat: 282 | @"(function() {" 283 | @" if (window.kakiSnapshotLocationObservered) return;" 284 | @" var sendMockRequest = function() {" 285 | @" var ifr = document.createElement('iframe');" 286 | @" ifr.style.display = 'none';" 287 | @" ifr.src = '%@://%@%@';" 288 | @" document.body.appendChild(ifr);" 289 | @" setTimeout(function() {" 290 | @" ifr.parentNode.removeChild(ifr);" 291 | @" }, 0);" 292 | @" };" 293 | @" " 294 | @" var pushState = window.history.pushState; " 295 | @" window.history.pushState = function(state) { " 296 | @" sendMockRequest();" 297 | @" return pushState.apply(window.history, arguments); " 298 | @" };" 299 | @" " 300 | @" window.addEventListener('hashchange', function() {" 301 | @" sendMockRequest();" 302 | @" }, false);" 303 | @" " 304 | @" window.addEventListener('popstate', function() {" 305 | @" sendMockRequest();" 306 | @" }, false);" 307 | @" " 308 | @" window.kakiSnapshotLocationObservered = true;" 309 | @"})();", 310 | webView.request.mainDocumentURL.scheme, 311 | webView.request.mainDocumentURL.host, 312 | KakiLocationChangedRPCURLPath]; 313 | [webView stringByEvaluatingJavaScriptFromString:javascript]; 314 | 315 | self.snapshotView.hidden = YES; 316 | [self __createOrUpdateSnapshots]; 317 | } 318 | 319 | @end 320 | -------------------------------------------------------------------------------- /KakiWebView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C11699DF1E951F870028E657 /* KakiJavascriptCorePlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = C11699DD1E951F870028E657 /* KakiJavascriptCorePlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | C11699E01E951F870028E657 /* KakiJavascriptCorePlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = C11699DE1E951F870028E657 /* KakiJavascriptCorePlugin.m */; }; 12 | C11699E31E9526050028E657 /* KakiProgressPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = C11699E11E9526050028E657 /* KakiProgressPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | C11699E41E9526050028E657 /* KakiProgressPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = C11699E21E9526050028E657 /* KakiProgressPlugin.m */; }; 14 | C11699E71E952B460028E657 /* KakiPopGesturePlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = C11699E51E952B460028E657 /* KakiPopGesturePlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; 15 | C11699E81E952B460028E657 /* KakiPopGesturePlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = C11699E61E952B460028E657 /* KakiPopGesturePlugin.m */; }; 16 | C11699EB1E952E5B0028E657 /* KakiTitleObserverPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = C11699E91E952E5B0028E657 /* KakiTitleObserverPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | C11699EC1E952E5B0028E657 /* KakiTitleObserverPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = C11699EA1E952E5B0028E657 /* KakiTitleObserverPlugin.m */; }; 18 | C11699F51E9532C90028E657 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C11699F41E9532C90028E657 /* main.m */; }; 19 | C11699F81E9532C90028E657 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = C11699F71E9532C90028E657 /* AppDelegate.m */; }; 20 | C11699FB1E9532C90028E657 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C11699FA1E9532C90028E657 /* ViewController.m */; }; 21 | C11699FE1E9532C90028E657 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C11699FC1E9532C90028E657 /* Main.storyboard */; }; 22 | C1169A001E9532C90028E657 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C11699FF1E9532C90028E657 /* Assets.xcassets */; }; 23 | C1169A031E9532C90028E657 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C1169A011E9532C90028E657 /* LaunchScreen.storyboard */; }; 24 | C1169A081E9532E20028E657 /* KakiWebView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C16EC28D1E88DA8200B96B38 /* KakiWebView.framework */; }; 25 | C1169A091E9532E20028E657 /* KakiWebView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C16EC28D1E88DA8200B96B38 /* KakiWebView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 26 | C15460541E9362DB00641418 /* UIWebView+Kaki.h in Headers */ = {isa = PBXBuildFile; fileRef = C15460521E9362DB00641418 /* UIWebView+Kaki.h */; settings = {ATTRIBUTES = (Public, ); }; }; 27 | C15460551E9362DB00641418 /* UIWebView+Kaki.m in Sources */ = {isa = PBXBuildFile; fileRef = C15460531E9362DB00641418 /* UIWebView+Kaki.m */; }; 28 | C15460571E93645600641418 /* KakiWebViewPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = C15460561E93645600641418 /* KakiWebViewPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; 29 | C154605A1E9368BF00641418 /* KakiWebViewPatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = C15460581E9368BF00641418 /* KakiWebViewPatcher.h */; }; 30 | C154605B1E9368BF00641418 /* KakiWebViewPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = C15460591E9368BF00641418 /* KakiWebViewPatcher.m */; }; 31 | C154605E1E93690B00641418 /* KakiWebViewPluginContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = C154605C1E93690B00641418 /* KakiWebViewPluginContainer.h */; }; 32 | C154605F1E93690B00641418 /* KakiWebViewPluginContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = C154605D1E93690B00641418 /* KakiWebViewPluginContainer.m */; }; 33 | C16EC2921E88DA8200B96B38 /* KakiWebView.h in Headers */ = {isa = PBXBuildFile; fileRef = C16EC2901E88DA8200B96B38 /* KakiWebView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34 | /* End PBXBuildFile section */ 35 | 36 | /* Begin PBXContainerItemProxy section */ 37 | C1169A0A1E9532E20028E657 /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = C16EC2841E88DA8200B96B38 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = C16EC28C1E88DA8200B96B38; 42 | remoteInfo = KakiWebView; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXCopyFilesBuildPhase section */ 47 | C1169A0C1E9532E30028E657 /* Embed Frameworks */ = { 48 | isa = PBXCopyFilesBuildPhase; 49 | buildActionMask = 2147483647; 50 | dstPath = ""; 51 | dstSubfolderSpec = 10; 52 | files = ( 53 | C1169A091E9532E20028E657 /* KakiWebView.framework in Embed Frameworks */, 54 | ); 55 | name = "Embed Frameworks"; 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXCopyFilesBuildPhase section */ 59 | 60 | /* Begin PBXFileReference section */ 61 | C11699DD1E951F870028E657 /* KakiJavascriptCorePlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KakiJavascriptCorePlugin.h; sourceTree = ""; }; 62 | C11699DE1E951F870028E657 /* KakiJavascriptCorePlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KakiJavascriptCorePlugin.m; sourceTree = ""; }; 63 | C11699E11E9526050028E657 /* KakiProgressPlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KakiProgressPlugin.h; sourceTree = ""; }; 64 | C11699E21E9526050028E657 /* KakiProgressPlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KakiProgressPlugin.m; sourceTree = ""; }; 65 | C11699E51E952B460028E657 /* KakiPopGesturePlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KakiPopGesturePlugin.h; sourceTree = ""; }; 66 | C11699E61E952B460028E657 /* KakiPopGesturePlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KakiPopGesturePlugin.m; sourceTree = ""; }; 67 | C11699E91E952E5B0028E657 /* KakiTitleObserverPlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KakiTitleObserverPlugin.h; sourceTree = ""; }; 68 | C11699EA1E952E5B0028E657 /* KakiTitleObserverPlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KakiTitleObserverPlugin.m; sourceTree = ""; }; 69 | C11699F11E9532C90028E657 /* KakiWebViewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KakiWebViewExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | C11699F41E9532C90028E657 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 71 | C11699F61E9532C90028E657 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 72 | C11699F71E9532C90028E657 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 73 | C11699F91E9532C90028E657 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 74 | C11699FA1E9532C90028E657 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 75 | C11699FD1E9532C90028E657 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 76 | C11699FF1E9532C90028E657 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 77 | C1169A021E9532C90028E657 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 78 | C1169A041E9532C90028E657 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 79 | C15460521E9362DB00641418 /* UIWebView+Kaki.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIWebView+Kaki.h"; sourceTree = ""; }; 80 | C15460531E9362DB00641418 /* UIWebView+Kaki.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIWebView+Kaki.m"; sourceTree = ""; }; 81 | C15460561E93645600641418 /* KakiWebViewPlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KakiWebViewPlugin.h; sourceTree = ""; }; 82 | C15460581E9368BF00641418 /* KakiWebViewPatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KakiWebViewPatcher.h; sourceTree = ""; }; 83 | C15460591E9368BF00641418 /* KakiWebViewPatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KakiWebViewPatcher.m; sourceTree = ""; }; 84 | C154605C1E93690B00641418 /* KakiWebViewPluginContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KakiWebViewPluginContainer.h; sourceTree = ""; }; 85 | C154605D1E93690B00641418 /* KakiWebViewPluginContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KakiWebViewPluginContainer.m; sourceTree = ""; }; 86 | C16EC28D1E88DA8200B96B38 /* KakiWebView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KakiWebView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 87 | C16EC2901E88DA8200B96B38 /* KakiWebView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KakiWebView.h; sourceTree = ""; }; 88 | C16EC2911E88DA8200B96B38 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 89 | /* End PBXFileReference section */ 90 | 91 | /* Begin PBXFrameworksBuildPhase section */ 92 | C11699EE1E9532C90028E657 /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | C1169A081E9532E20028E657 /* KakiWebView.framework in Frameworks */, 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | C16EC2891E88DA8200B96B38 /* Frameworks */ = { 101 | isa = PBXFrameworksBuildPhase; 102 | buildActionMask = 2147483647; 103 | files = ( 104 | ); 105 | runOnlyForDeploymentPostprocessing = 0; 106 | }; 107 | /* End PBXFrameworksBuildPhase section */ 108 | 109 | /* Begin PBXGroup section */ 110 | C11699F21E9532C90028E657 /* KakiWebViewExample */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | C11699F61E9532C90028E657 /* AppDelegate.h */, 114 | C11699F71E9532C90028E657 /* AppDelegate.m */, 115 | C11699F91E9532C90028E657 /* ViewController.h */, 116 | C11699FA1E9532C90028E657 /* ViewController.m */, 117 | C11699FC1E9532C90028E657 /* Main.storyboard */, 118 | C11699FF1E9532C90028E657 /* Assets.xcassets */, 119 | C1169A011E9532C90028E657 /* LaunchScreen.storyboard */, 120 | C1169A041E9532C90028E657 /* Info.plist */, 121 | C11699F31E9532C90028E657 /* Supporting Files */, 122 | ); 123 | path = KakiWebViewExample; 124 | sourceTree = ""; 125 | }; 126 | C11699F31E9532C90028E657 /* Supporting Files */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | C11699F41E9532C90028E657 /* main.m */, 130 | ); 131 | name = "Supporting Files"; 132 | sourceTree = ""; 133 | }; 134 | C154604F1E93629F00641418 /* Classes */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | C15460511E93629F00641418 /* Utils */, 138 | C15460501E93629F00641418 /* Plugins */, 139 | C15460521E9362DB00641418 /* UIWebView+Kaki.h */, 140 | C15460531E9362DB00641418 /* UIWebView+Kaki.m */, 141 | C15460561E93645600641418 /* KakiWebViewPlugin.h */, 142 | ); 143 | path = Classes; 144 | sourceTree = ""; 145 | }; 146 | C15460501E93629F00641418 /* Plugins */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | C11699DD1E951F870028E657 /* KakiJavascriptCorePlugin.h */, 150 | C11699DE1E951F870028E657 /* KakiJavascriptCorePlugin.m */, 151 | C11699E11E9526050028E657 /* KakiProgressPlugin.h */, 152 | C11699E21E9526050028E657 /* KakiProgressPlugin.m */, 153 | C11699E51E952B460028E657 /* KakiPopGesturePlugin.h */, 154 | C11699E61E952B460028E657 /* KakiPopGesturePlugin.m */, 155 | C11699E91E952E5B0028E657 /* KakiTitleObserverPlugin.h */, 156 | C11699EA1E952E5B0028E657 /* KakiTitleObserverPlugin.m */, 157 | ); 158 | path = Plugins; 159 | sourceTree = ""; 160 | }; 161 | C15460511E93629F00641418 /* Utils */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | C15460581E9368BF00641418 /* KakiWebViewPatcher.h */, 165 | C15460591E9368BF00641418 /* KakiWebViewPatcher.m */, 166 | C154605C1E93690B00641418 /* KakiWebViewPluginContainer.h */, 167 | C154605D1E93690B00641418 /* KakiWebViewPluginContainer.m */, 168 | ); 169 | path = Utils; 170 | sourceTree = ""; 171 | }; 172 | C16EC2831E88DA8200B96B38 = { 173 | isa = PBXGroup; 174 | children = ( 175 | C16EC28F1E88DA8200B96B38 /* KakiWebView */, 176 | C11699F21E9532C90028E657 /* KakiWebViewExample */, 177 | C16EC28E1E88DA8200B96B38 /* Products */, 178 | ); 179 | sourceTree = ""; 180 | }; 181 | C16EC28E1E88DA8200B96B38 /* Products */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | C16EC28D1E88DA8200B96B38 /* KakiWebView.framework */, 185 | C11699F11E9532C90028E657 /* KakiWebViewExample.app */, 186 | ); 187 | name = Products; 188 | sourceTree = ""; 189 | }; 190 | C16EC28F1E88DA8200B96B38 /* KakiWebView */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | C154604F1E93629F00641418 /* Classes */, 194 | C16EC2901E88DA8200B96B38 /* KakiWebView.h */, 195 | C16EC2911E88DA8200B96B38 /* Info.plist */, 196 | ); 197 | path = KakiWebView; 198 | sourceTree = ""; 199 | }; 200 | /* End PBXGroup section */ 201 | 202 | /* Begin PBXHeadersBuildPhase section */ 203 | C16EC28A1E88DA8200B96B38 /* Headers */ = { 204 | isa = PBXHeadersBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | C154605E1E93690B00641418 /* KakiWebViewPluginContainer.h in Headers */, 208 | C11699E71E952B460028E657 /* KakiPopGesturePlugin.h in Headers */, 209 | C11699DF1E951F870028E657 /* KakiJavascriptCorePlugin.h in Headers */, 210 | C15460571E93645600641418 /* KakiWebViewPlugin.h in Headers */, 211 | C15460541E9362DB00641418 /* UIWebView+Kaki.h in Headers */, 212 | C154605A1E9368BF00641418 /* KakiWebViewPatcher.h in Headers */, 213 | C11699EB1E952E5B0028E657 /* KakiTitleObserverPlugin.h in Headers */, 214 | C11699E31E9526050028E657 /* KakiProgressPlugin.h in Headers */, 215 | C16EC2921E88DA8200B96B38 /* KakiWebView.h in Headers */, 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | }; 219 | /* End PBXHeadersBuildPhase section */ 220 | 221 | /* Begin PBXNativeTarget section */ 222 | C11699F01E9532C90028E657 /* KakiWebViewExample */ = { 223 | isa = PBXNativeTarget; 224 | buildConfigurationList = C1169A051E9532C90028E657 /* Build configuration list for PBXNativeTarget "KakiWebViewExample" */; 225 | buildPhases = ( 226 | C11699ED1E9532C90028E657 /* Sources */, 227 | C11699EE1E9532C90028E657 /* Frameworks */, 228 | C11699EF1E9532C90028E657 /* Resources */, 229 | C1169A0C1E9532E30028E657 /* Embed Frameworks */, 230 | ); 231 | buildRules = ( 232 | ); 233 | dependencies = ( 234 | C1169A0B1E9532E20028E657 /* PBXTargetDependency */, 235 | ); 236 | name = KakiWebViewExample; 237 | productName = KakiWebViewExample; 238 | productReference = C11699F11E9532C90028E657 /* KakiWebViewExample.app */; 239 | productType = "com.apple.product-type.application"; 240 | }; 241 | C16EC28C1E88DA8200B96B38 /* KakiWebView */ = { 242 | isa = PBXNativeTarget; 243 | buildConfigurationList = C16EC2951E88DA8200B96B38 /* Build configuration list for PBXNativeTarget "KakiWebView" */; 244 | buildPhases = ( 245 | C16EC2881E88DA8200B96B38 /* Sources */, 246 | C16EC2891E88DA8200B96B38 /* Frameworks */, 247 | C16EC28A1E88DA8200B96B38 /* Headers */, 248 | C16EC28B1E88DA8200B96B38 /* Resources */, 249 | ); 250 | buildRules = ( 251 | ); 252 | dependencies = ( 253 | ); 254 | name = KakiWebView; 255 | productName = KakiWebView; 256 | productReference = C16EC28D1E88DA8200B96B38 /* KakiWebView.framework */; 257 | productType = "com.apple.product-type.framework"; 258 | }; 259 | /* End PBXNativeTarget section */ 260 | 261 | /* Begin PBXProject section */ 262 | C16EC2841E88DA8200B96B38 /* Project object */ = { 263 | isa = PBXProject; 264 | attributes = { 265 | LastUpgradeCheck = 0820; 266 | ORGANIZATIONNAME = makee; 267 | TargetAttributes = { 268 | C11699F01E9532C90028E657 = { 269 | CreatedOnToolsVersion = 8.3; 270 | DevelopmentTeam = 38RFS996H7; 271 | ProvisioningStyle = Automatic; 272 | }; 273 | C16EC28C1E88DA8200B96B38 = { 274 | CreatedOnToolsVersion = 8.2.1; 275 | DevelopmentTeam = 38RFS996H7; 276 | ProvisioningStyle = Automatic; 277 | }; 278 | }; 279 | }; 280 | buildConfigurationList = C16EC2871E88DA8200B96B38 /* Build configuration list for PBXProject "KakiWebView" */; 281 | compatibilityVersion = "Xcode 3.2"; 282 | developmentRegion = English; 283 | hasScannedForEncodings = 0; 284 | knownRegions = ( 285 | en, 286 | Base, 287 | ); 288 | mainGroup = C16EC2831E88DA8200B96B38; 289 | productRefGroup = C16EC28E1E88DA8200B96B38 /* Products */; 290 | projectDirPath = ""; 291 | projectRoot = ""; 292 | targets = ( 293 | C16EC28C1E88DA8200B96B38 /* KakiWebView */, 294 | C11699F01E9532C90028E657 /* KakiWebViewExample */, 295 | ); 296 | }; 297 | /* End PBXProject section */ 298 | 299 | /* Begin PBXResourcesBuildPhase section */ 300 | C11699EF1E9532C90028E657 /* Resources */ = { 301 | isa = PBXResourcesBuildPhase; 302 | buildActionMask = 2147483647; 303 | files = ( 304 | C1169A031E9532C90028E657 /* LaunchScreen.storyboard in Resources */, 305 | C1169A001E9532C90028E657 /* Assets.xcassets in Resources */, 306 | C11699FE1E9532C90028E657 /* Main.storyboard in Resources */, 307 | ); 308 | runOnlyForDeploymentPostprocessing = 0; 309 | }; 310 | C16EC28B1E88DA8200B96B38 /* Resources */ = { 311 | isa = PBXResourcesBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | ); 315 | runOnlyForDeploymentPostprocessing = 0; 316 | }; 317 | /* End PBXResourcesBuildPhase section */ 318 | 319 | /* Begin PBXSourcesBuildPhase section */ 320 | C11699ED1E9532C90028E657 /* Sources */ = { 321 | isa = PBXSourcesBuildPhase; 322 | buildActionMask = 2147483647; 323 | files = ( 324 | C11699FB1E9532C90028E657 /* ViewController.m in Sources */, 325 | C11699F81E9532C90028E657 /* AppDelegate.m in Sources */, 326 | C11699F51E9532C90028E657 /* main.m in Sources */, 327 | ); 328 | runOnlyForDeploymentPostprocessing = 0; 329 | }; 330 | C16EC2881E88DA8200B96B38 /* Sources */ = { 331 | isa = PBXSourcesBuildPhase; 332 | buildActionMask = 2147483647; 333 | files = ( 334 | C11699E01E951F870028E657 /* KakiJavascriptCorePlugin.m in Sources */, 335 | C11699EC1E952E5B0028E657 /* KakiTitleObserverPlugin.m in Sources */, 336 | C11699E41E9526050028E657 /* KakiProgressPlugin.m in Sources */, 337 | C15460551E9362DB00641418 /* UIWebView+Kaki.m in Sources */, 338 | C154605B1E9368BF00641418 /* KakiWebViewPatcher.m in Sources */, 339 | C11699E81E952B460028E657 /* KakiPopGesturePlugin.m in Sources */, 340 | C154605F1E93690B00641418 /* KakiWebViewPluginContainer.m in Sources */, 341 | ); 342 | runOnlyForDeploymentPostprocessing = 0; 343 | }; 344 | /* End PBXSourcesBuildPhase section */ 345 | 346 | /* Begin PBXTargetDependency section */ 347 | C1169A0B1E9532E20028E657 /* PBXTargetDependency */ = { 348 | isa = PBXTargetDependency; 349 | target = C16EC28C1E88DA8200B96B38 /* KakiWebView */; 350 | targetProxy = C1169A0A1E9532E20028E657 /* PBXContainerItemProxy */; 351 | }; 352 | /* End PBXTargetDependency section */ 353 | 354 | /* Begin PBXVariantGroup section */ 355 | C11699FC1E9532C90028E657 /* Main.storyboard */ = { 356 | isa = PBXVariantGroup; 357 | children = ( 358 | C11699FD1E9532C90028E657 /* Base */, 359 | ); 360 | name = Main.storyboard; 361 | sourceTree = ""; 362 | }; 363 | C1169A011E9532C90028E657 /* LaunchScreen.storyboard */ = { 364 | isa = PBXVariantGroup; 365 | children = ( 366 | C1169A021E9532C90028E657 /* Base */, 367 | ); 368 | name = LaunchScreen.storyboard; 369 | sourceTree = ""; 370 | }; 371 | /* End PBXVariantGroup section */ 372 | 373 | /* Begin XCBuildConfiguration section */ 374 | C1169A061E9532C90028E657 /* Debug */ = { 375 | isa = XCBuildConfiguration; 376 | buildSettings = { 377 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 378 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 379 | DEVELOPMENT_TEAM = 38RFS996H7; 380 | INFOPLIST_FILE = KakiWebViewExample/Info.plist; 381 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 382 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 383 | PRODUCT_BUNDLE_IDENTIFIER = com.makee.KakiWebViewExample; 384 | PRODUCT_NAME = "$(TARGET_NAME)"; 385 | }; 386 | name = Debug; 387 | }; 388 | C1169A071E9532C90028E657 /* Release */ = { 389 | isa = XCBuildConfiguration; 390 | buildSettings = { 391 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 392 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 393 | DEVELOPMENT_TEAM = 38RFS996H7; 394 | INFOPLIST_FILE = KakiWebViewExample/Info.plist; 395 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 396 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 397 | PRODUCT_BUNDLE_IDENTIFIER = com.makee.KakiWebViewExample; 398 | PRODUCT_NAME = "$(TARGET_NAME)"; 399 | }; 400 | name = Release; 401 | }; 402 | C16EC2931E88DA8200B96B38 /* Debug */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | ALWAYS_SEARCH_USER_PATHS = NO; 406 | CLANG_ANALYZER_NONNULL = YES; 407 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 408 | CLANG_CXX_LIBRARY = "libc++"; 409 | CLANG_ENABLE_MODULES = YES; 410 | CLANG_ENABLE_OBJC_ARC = YES; 411 | CLANG_WARN_BOOL_CONVERSION = YES; 412 | CLANG_WARN_CONSTANT_CONVERSION = YES; 413 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 414 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 415 | CLANG_WARN_EMPTY_BODY = YES; 416 | CLANG_WARN_ENUM_CONVERSION = YES; 417 | CLANG_WARN_INFINITE_RECURSION = YES; 418 | CLANG_WARN_INT_CONVERSION = YES; 419 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 420 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 421 | CLANG_WARN_UNREACHABLE_CODE = YES; 422 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 423 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 424 | COPY_PHASE_STRIP = NO; 425 | CURRENT_PROJECT_VERSION = 1; 426 | DEBUG_INFORMATION_FORMAT = dwarf; 427 | ENABLE_STRICT_OBJC_MSGSEND = YES; 428 | ENABLE_TESTABILITY = YES; 429 | GCC_C_LANGUAGE_STANDARD = gnu99; 430 | GCC_DYNAMIC_NO_PIC = NO; 431 | GCC_NO_COMMON_BLOCKS = YES; 432 | GCC_OPTIMIZATION_LEVEL = 0; 433 | GCC_PREPROCESSOR_DEFINITIONS = ( 434 | "DEBUG=1", 435 | "$(inherited)", 436 | ); 437 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 438 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 439 | GCC_WARN_UNDECLARED_SELECTOR = YES; 440 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 441 | GCC_WARN_UNUSED_FUNCTION = YES; 442 | GCC_WARN_UNUSED_VARIABLE = YES; 443 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 444 | MTL_ENABLE_DEBUG_INFO = YES; 445 | ONLY_ACTIVE_ARCH = YES; 446 | SDKROOT = iphoneos; 447 | TARGETED_DEVICE_FAMILY = "1,2"; 448 | VERSIONING_SYSTEM = "apple-generic"; 449 | VERSION_INFO_PREFIX = ""; 450 | }; 451 | name = Debug; 452 | }; 453 | C16EC2941E88DA8200B96B38 /* Release */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | ALWAYS_SEARCH_USER_PATHS = NO; 457 | CLANG_ANALYZER_NONNULL = YES; 458 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 459 | CLANG_CXX_LIBRARY = "libc++"; 460 | CLANG_ENABLE_MODULES = YES; 461 | CLANG_ENABLE_OBJC_ARC = YES; 462 | CLANG_WARN_BOOL_CONVERSION = YES; 463 | CLANG_WARN_CONSTANT_CONVERSION = YES; 464 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 465 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 466 | CLANG_WARN_EMPTY_BODY = YES; 467 | CLANG_WARN_ENUM_CONVERSION = YES; 468 | CLANG_WARN_INFINITE_RECURSION = YES; 469 | CLANG_WARN_INT_CONVERSION = YES; 470 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 471 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 472 | CLANG_WARN_UNREACHABLE_CODE = YES; 473 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 474 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 475 | COPY_PHASE_STRIP = NO; 476 | CURRENT_PROJECT_VERSION = 1; 477 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 478 | ENABLE_NS_ASSERTIONS = NO; 479 | ENABLE_STRICT_OBJC_MSGSEND = YES; 480 | GCC_C_LANGUAGE_STANDARD = gnu99; 481 | GCC_NO_COMMON_BLOCKS = YES; 482 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 483 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 484 | GCC_WARN_UNDECLARED_SELECTOR = YES; 485 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 486 | GCC_WARN_UNUSED_FUNCTION = YES; 487 | GCC_WARN_UNUSED_VARIABLE = YES; 488 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 489 | MTL_ENABLE_DEBUG_INFO = NO; 490 | SDKROOT = iphoneos; 491 | TARGETED_DEVICE_FAMILY = "1,2"; 492 | VALIDATE_PRODUCT = YES; 493 | VERSIONING_SYSTEM = "apple-generic"; 494 | VERSION_INFO_PREFIX = ""; 495 | }; 496 | name = Release; 497 | }; 498 | C16EC2961E88DA8200B96B38 /* Debug */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | CODE_SIGN_IDENTITY = ""; 502 | DEFINES_MODULE = YES; 503 | DEVELOPMENT_TEAM = 38RFS996H7; 504 | DYLIB_COMPATIBILITY_VERSION = 1; 505 | DYLIB_CURRENT_VERSION = 1; 506 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 507 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 508 | INFOPLIST_FILE = KakiWebView/Info.plist; 509 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 510 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 511 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 512 | PRODUCT_BUNDLE_IDENTIFIER = com.makee.KakiWebView; 513 | PRODUCT_NAME = "$(TARGET_NAME)"; 514 | SKIP_INSTALL = YES; 515 | }; 516 | name = Debug; 517 | }; 518 | C16EC2971E88DA8200B96B38 /* Release */ = { 519 | isa = XCBuildConfiguration; 520 | buildSettings = { 521 | CODE_SIGN_IDENTITY = ""; 522 | DEFINES_MODULE = YES; 523 | DEVELOPMENT_TEAM = 38RFS996H7; 524 | DYLIB_COMPATIBILITY_VERSION = 1; 525 | DYLIB_CURRENT_VERSION = 1; 526 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 527 | INFOPLIST_FILE = KakiWebView/Info.plist; 528 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 529 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 530 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 531 | PRODUCT_BUNDLE_IDENTIFIER = com.makee.KakiWebView; 532 | PRODUCT_NAME = "$(TARGET_NAME)"; 533 | SKIP_INSTALL = YES; 534 | }; 535 | name = Release; 536 | }; 537 | /* End XCBuildConfiguration section */ 538 | 539 | /* Begin XCConfigurationList section */ 540 | C1169A051E9532C90028E657 /* Build configuration list for PBXNativeTarget "KakiWebViewExample" */ = { 541 | isa = XCConfigurationList; 542 | buildConfigurations = ( 543 | C1169A061E9532C90028E657 /* Debug */, 544 | C1169A071E9532C90028E657 /* Release */, 545 | ); 546 | defaultConfigurationIsVisible = 0; 547 | }; 548 | C16EC2871E88DA8200B96B38 /* Build configuration list for PBXProject "KakiWebView" */ = { 549 | isa = XCConfigurationList; 550 | buildConfigurations = ( 551 | C16EC2931E88DA8200B96B38 /* Debug */, 552 | C16EC2941E88DA8200B96B38 /* Release */, 553 | ); 554 | defaultConfigurationIsVisible = 0; 555 | defaultConfigurationName = Release; 556 | }; 557 | C16EC2951E88DA8200B96B38 /* Build configuration list for PBXNativeTarget "KakiWebView" */ = { 558 | isa = XCConfigurationList; 559 | buildConfigurations = ( 560 | C16EC2961E88DA8200B96B38 /* Debug */, 561 | C16EC2971E88DA8200B96B38 /* Release */, 562 | ); 563 | defaultConfigurationIsVisible = 0; 564 | defaultConfigurationName = Release; 565 | }; 566 | /* End XCConfigurationList section */ 567 | }; 568 | rootObject = C16EC2841E88DA8200B96B38 /* Project object */; 569 | } 570 | --------------------------------------------------------------------------------