├── 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 |
--------------------------------------------------------------------------------