diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2a8c58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +#global +*.orig + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +#ios +.DS_Store +*~ +project.xcworkspace/ +*.xccheckout +xcuserdata/ +.svn +.idea +.ropeproject +.venv + +# CocoaPods +Pods + +#CrashMonkey +instrumentscli*.trace/ +crash_monkey_result/ + +# Docs +docs/_build +reports diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..943e1a2 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +**Copyright (c) 2016, Douban. Inc.** +**All rights reserved.** + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +INCLUDINGMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8975e0f --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# Rexxar iOS + +[![Build status](http://shields.dapps.douban.com/badge/qa-ci/peteris-rexxar-ios-inHouse)](http://qa-ci.intra.douban.com/job/peteris-rexxar-ios-inHouse) +[![Language](https://img.shields.io/badge/language-ObjC-blue.svg)](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html) +[![iOS](https://img.shields.io/badge/iOS-7.0-green.svg)]() + +**Rexxar** 是一个针对移动端的混合开发框架。现在支持 Android 和 iOS 平台。`Rexxar-iOS` 是 Rexxar 在 iOS 系统上的客户端实现。 + +通过 Rexxar,你可以使用包括 javascript,css,html 在内的传统前端技术开发移动应用。Rexxar 的客户端实现 Rexxar Container 对于 Web 端使用何种技术并无要求。我们现在的 Rexxar 的前端实现 Rexxar-Web,以及 Rexxar Container 在两个平台的实现 Rexxar-iOS 和 Rexxar-Android 项目中所带的 Demo 都使用了 [React](https://facebook.github.io/react/)。但你完全可以选择自己的前端框架在 Rexxar-Container 中进行开发。 + +Rexxar-iOS 现在支持 iOS 7.0 及以上版本。 + + +## 安装 + +### 安装 Cocoapods + +[CocoaPods](http://cocoapods.org) 是一个 Objective-c 和 Swift 的依赖管理工具。你可以通过以下命令安装 CocoaPods: + +```bash +$ gem install cocoapods +``` + +### Podfile + +```ruby +target 'TargetName' do + pod 'Rexxar', '~> 1.2.0' +end +``` + +然后,运行以下命令: + +```bash +$ pod install +``` + +## 使用 + +你可以查看 RexxarDemo 中的例子。了解如何使用 Rexxar。RexxarDemo 给出了完善的实例。 + +### 启动本地服务器 + +启动本地服务器,提供路由文件 api 和资源文件的访问服务。启动命令如下: + +```bash +$ python routes.py +``` + +在浏览器中输入以下 url [http://localhost:5000](http://localhost:5000)。你应该能看到如下类似的 json 格式的输出: + +```json +{ + "items": [{ + "remote_file": "https://img1.doubanio.com/dae/rexxar/files/rexxar/demo-ffb8a4a9fa.html", + "deploy_time": "Thu, 04 Aug 2016 07:43:47 GMT", + "uri": "douban://douban.com/rexxar_demo[/]?.*" + }], + "partial_items": [{ + "remote_file": "https://img1.doubanio.com/dae/rexxar/files/rexxar/demo-ffb8a4a9fa.html", + "deploy_time": "Thu, 04 Aug 2016 07:43:47 GMT", + "uri": "douban://partial.douban.com/rexxar_demo/_.*" + }], + "deploy_time": "Thu, 04 Aug 2016 07:43:47 GMT", +} +``` + +这个 Demo 中,我们使用 [Flask](http://flask.pocoo.org/) 启动了一个简单的本地服务器。在你的线上服务中,当然需要一个真正的生产环境的服务器,以应付更大规模的对路由文件 api,以及 javascript,css,html 这些资源文件的访问。你可以使用任何服务端框架。Rexxar 对服务端框架并不要求。`RXRConfig` 提供了对路由文件 api 地址的配置接口。下一节描述了配置方法。 + +### 配置 RXRConfig + +配置路由文件 api,缓存路径: + +```Swift + RXRConfig.setRoutesMapURL(NSURL(string:"http://rexxar.douban.com/api/routes?edition=pre")!) + RXRConfig.setRoutesCachePath("com.douban.RexxarDemo.rexxar") + RXRConfig.setRoutesResourcePath("rexxar") +``` + +注意,如果自己配置 RoutesResourcePath,即意味着在打包好的包内预置一份资源文件。这样所有页面,即使在没有网络的情况下,也都可以访问。这个文件夹需要是 folder references 类型,即在 Xcode 中呈现为蓝色文件夹图标。创建方法是将文件夹拖入 Xcode 项目,选择 Create folder references 选项。 + +### 使用 RXRViewController + +你可以直接使用 `RXRViewController` 作为你的混合开发客户端容器。或者你也可以继承 `RXRViewController`,在 `RXRViewController` 基础上以实现你自己客户端容器。在 RexxarDemo 中,我们直接使用了 `RXRViewController`。 + +为了初始化 RXRViewController,你需要只一个 url。在路由文件 api 提供的路由表中可以找到这个 url。这个 url 标识了该页面所需使用的资源文件的位置。Rexxar Container 会通过 url 在路由表中寻找对应的 javascript,css,html 资源文件。 + +```Swift + let controller = RXRViewController(URI: uri) + let titleWidget = RXRNavTitleWidget() + let alertDialogWidget = RXRAlertDialogWidget() + controller.activities = [titleWidget, alertDialogWidget] + navigationController?.pushViewController(controller, animated: true) +``` + + +## 定制你自己的 Rexxar Container + +首先,可以继承 `RXRViewController`,在 `RXRViewController` 基础上以实现你自己客户端容器。 + +另外,我们暴露了三类接口。供开发者更方便地扩展属于自己的特定功能实现。 + +### 定制 RXRWidget + +Rexxar Container 提供了一些原生 UI 组件供 Rexxar-Web 使用。RXRWidget 协议是对这类原生 UI 组件的抽象。如果,你需要实现某些原生 UI 组件,例如,弹出一个 Toast,或者添加原生效果的下拉刷新,你就可以实现一个符合 RXRWidget 协议的对象,并实现以下三个方法:`canPerformWithURL:`,`prepareWithURL:`,`performWithController:`。 + +你可以在 RexxarDemo 中找到一个 RXRNavTitleWidget 的例子,通过它可以设置导航栏的标题文字。 + +```Objective-C +@interface RXRNavTitleWidget () + +@property (nonatomic, copy) NSString *title; + +@end + + +@implementation RXRNavTitleWidget + +- (BOOL)canPerformWithURL:(NSURL *)URL +{ + NSString *path = URL.path; + if (path && [path isEqualToString:@"/widget/nav_title"]) { + return true; + } + return false; +} + +- (void)prepareWithURL:(NSURL *)URL +{ + self.title = [[URL rxr_queryDictionary] rxr_itemForKey:@"title"]; +} + +- (void)performWithController:(RXRViewController *)controller +{ + if (controller) { + controller.title = self.title; + } +} + +@end +``` + +### 定制 RXRContainerAPI + +Rexxar Container 和 Rexxar-Web 做数据上的交互。比如 Rexxar Container 可以为 Rexxar-Web 提供一些计算结果。RXRContainerAPI 协议是对这类 Rexxar Container 和 Rexxar-Web 之间的数据交互的抽象。如果你需要提供一些由原生代码计算的数据给 Rexxar-Web 使用,你可以实现 RXRContainerAPI 协议,并实现以下三个方法:`shouldInterceptRequest:`, `responseWithRequest:`, `responseData`。 + +你可以在 RexxarDemo 中找到一个例子:`RXRLocContainerAPI`。这个例子中,`RXRLocContainerAPI` 返回了设备所在城市信息。当然,这个 Container API 仅仅是一个实例,它提供的是一个假数据,数据永远不会变化。你当然可以遵守 `RXRContainerAPI` 协议,实现一个类似的功能。 + +### 定制 RXRDecorator + +如果你需要修改运行在 Rexxar-Container 中的 Rexxar-Web 所发出的请求。例如,在 http 头中添加登录信息,你可以实现 `RXRDecorator` 协议,并实现这两个方法:`shouldInterceptRequest:`, `prepareWithRequest:`。 + +你可以在 RexxarDemo 中找到一个例子 `RXRAuthDecorator`。这个例子,为 Rexxar-Web 发出的请求添加了登录信息。 + + +## Rexxar 的公开接口 + +* Rexxar Container + - `RXRConfig` + - `RXRViewController` + +* Widget + - `RXRWidget` + - `RXRNavTitleWidget` + - `RXRAlertDialogWidget` + +* ContainerAPI + - `RXRNSURLProtocol` + - `RXRContainerIntercepter` + - `RXRContainerAPI` + +* Decorator + - `RXRRequestIntercepter` + - `RXRDecorator` + - `RXRRequestDecorator` + +* Util + - `NSURL+Rexxar` + - `NSDictionary+RXRMultipleItem` + + +## Unit Test + +在项目的 RexxarTests 文件夹下可以找到一系列单元测试。这些单元测试可以很容易地在 Xcode 中运行:cmd+u。单元测试在验证代码的正确性之外,还提供了如何使用这些代码的实例。可以查看这些单元测试,以了解如何使用 Rexxar。 + + +## License + +Rexxar is released under the MIT license. See LICENSE for details. diff --git a/Rexxar.podspec b/Rexxar.podspec new file mode 100644 index 0000000..d50506f --- /dev/null +++ b/Rexxar.podspec @@ -0,0 +1,35 @@ +Pod::Spec.new do |s| + + s.name = "Rexxar" + s.version = "1.3.2" + s.license = { :type => 'MIT', :text => 'LICENSE.md' } + + s.summary = "Rexxar Hybrid Framework" + s.description = "Rexxar is Douban Hybrid Framework. By Rexxar, You can develop UI interface with Web tech." + s.homepage = "http://github.intra.douban.com/rexxar/rexxar-ios" + s.author = { "iOS Dev" => "ios-dev@douban.com" } + s.platform = :ios, "7.0" + s.source = { :git => "http://github.intra.douban.com/rexxar/rexxar-ios.git", + :tag => "v#{s.version}" } + s.requires_arc = true + s.source_files = "Rexxar/**/*.{h,m}" + s.public_header_files = 'Rexxar/Rexxar.h' + + s.framework = "UIKit" + + s.subspec 'Core' do |core| + core.source_files = 'Rexxar/Core/**/*.{h,m}', 'Rexxar/ContainerAPI/**/*.{h,m}', 'Rexxar/Decorator/**/*.{h,m}' + core.frameworks = 'UIKit' + core.requires_arc = true + end + + s.subspec 'Widget' do |widget| + widget.source_files = 'Rexxar/Widget/**/*.{h,m}' + widget.requires_arc = true + widget.xcconfig = {"GCC_PREPROCESSOR_DEFINITIONS" => 'DSK_WIDGET=1'} + widget.dependency 'Rexxar/Core' + end + + s.default_subspec = 'Widget' + +end diff --git a/Rexxar.xcodeproj/project.pbxproj b/Rexxar.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ef6abcd --- /dev/null +++ b/Rexxar.xcodeproj/project.pbxproj @@ -0,0 +1,944 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 987FA5221BFC78310096ED5C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98B47B141BF1E0F20087735C /* AppDelegate.swift */; }; + 98B47B071BF1E0E00087735C /* Rexxar.h in Headers */ = {isa = PBXBuildFile; fileRef = 98B47B061BF1E0E00087735C /* Rexxar.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 98B47B1F1BF1E0F20087735C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 98B47B1D1BF1E0F20087735C /* LaunchScreen.storyboard */; }; + A3249A0C1D66EF27006CCCCB /* FRDToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3249A081D66EF27006CCCCB /* FRDToast.swift */; }; + A3249A0D1D66EF27006CCCCB /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3249A091D66EF27006CCCCB /* LoadingView.swift */; }; + A3249A0E1D66EF27006CCCCB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3249A0A1D66EF27006CCCCB /* ToastView.swift */; }; + A3249A0F1D66EF27006CCCCB /* UIColor+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3249A0B1D66EF27006CCCCB /* UIColor+helper.swift */; }; + A3249A151D66EF98006CCCCB /* RXRToastWidget.m in Sources */ = {isa = PBXBuildFile; fileRef = A3249A141D66EF98006CCCCB /* RXRToastWidget.m */; }; + A3249A1A1D66F941006CCCCB /* DemoRXRViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3249A191D66F941006CCCCB /* DemoRXRViewController.swift */; }; + A35925261CE487870032B1E6 /* RXRRouteFileCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A35925251CE487870032B1E6 /* RXRRouteFileCacheTests.m */; }; + A35D15A21D66AE2B009B24DC /* RXRAlertDialogWidget.h in Headers */ = {isa = PBXBuildFile; fileRef = A35D15A01D66AE2B009B24DC /* RXRAlertDialogWidget.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35D15A31D66AE2B009B24DC /* RXRAlertDialogWidget.m in Sources */ = {isa = PBXBuildFile; fileRef = A35D15A11D66AE2B009B24DC /* RXRAlertDialogWidget.m */; }; + A35E71DF1D7FC86000D38BC0 /* RXRPullRefreshWidget.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E71DD1D7FC86000D38BC0 /* RXRPullRefreshWidget.m */; }; + A35E71E01D7FC86000D38BC0 /* RXRPullRefreshWidget.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71DE1D7FC86000D38BC0 /* RXRPullRefreshWidget.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E71F31D7FCA0200D38BC0 /* RXRCacheFileIntercepter.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71E21D7FCA0200D38BC0 /* RXRCacheFileIntercepter.h */; }; + A35E71F41D7FCA0200D38BC0 /* RXRCacheFileIntercepter.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E71E31D7FCA0200D38BC0 /* RXRCacheFileIntercepter.m */; }; + A35E71F51D7FCA0200D38BC0 /* RXRConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71E41D7FCA0200D38BC0 /* RXRConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E71F61D7FCA0200D38BC0 /* RXRConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E71E51D7FCA0200D38BC0 /* RXRConfig.m */; }; + A35E71F71D7FCA0200D38BC0 /* RXRLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71E61D7FCA0200D38BC0 /* RXRLogging.h */; }; + A35E71F81D7FCA0200D38BC0 /* RXRNSURLProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71E71D7FCA0200D38BC0 /* RXRNSURLProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E71F91D7FCA0200D38BC0 /* RXRNSURLProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E71E81D7FCA0200D38BC0 /* RXRNSURLProtocol.m */; }; + A35E71FA1D7FCA0200D38BC0 /* RXRRoute.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71E91D7FCA0200D38BC0 /* RXRRoute.h */; }; + A35E71FB1D7FCA0200D38BC0 /* RXRRoute.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E71EA1D7FCA0200D38BC0 /* RXRRoute.m */; }; + A35E71FC1D7FCA0200D38BC0 /* RXRRouteFileCache.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71EB1D7FCA0200D38BC0 /* RXRRouteFileCache.h */; }; + A35E71FD1D7FCA0200D38BC0 /* RXRRouteFileCache.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E71EC1D7FCA0200D38BC0 /* RXRRouteFileCache.m */; }; + A35E71FE1D7FCA0200D38BC0 /* RXRRouteManager.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71ED1D7FCA0200D38BC0 /* RXRRouteManager.h */; }; + A35E71FF1D7FCA0200D38BC0 /* RXRRouteManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E71EE1D7FCA0200D38BC0 /* RXRRouteManager.m */; }; + A35E72001D7FCA0200D38BC0 /* RXRViewController+Router.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E71EF1D7FCA0200D38BC0 /* RXRViewController+Router.m */; }; + A35E72011D7FCA0200D38BC0 /* RXRViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71F01D7FCA0200D38BC0 /* RXRViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E72021D7FCA0200D38BC0 /* RXRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E71F11D7FCA0200D38BC0 /* RXRViewController.m */; }; + A35E72031D7FCA0200D38BC0 /* RXRWidget.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E71F21D7FCA0200D38BC0 /* RXRWidget.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E72221D7FCB0B00D38BC0 /* NSData+RXRDigest.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E720B1D7FCB0B00D38BC0 /* NSData+RXRDigest.h */; }; + A35E72231D7FCB0B00D38BC0 /* NSData+RXRDigest.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E720C1D7FCB0B00D38BC0 /* NSData+RXRDigest.m */; }; + A35E72241D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E720D1D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E72251D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E720E1D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.m */; }; + A35E72261D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E720F1D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.h */; }; + A35E72271D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E72101D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.m */; }; + A35E72281D7FCB0B00D38BC0 /* NSString+RXRURLEscape.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E72111D7FCB0B00D38BC0 /* NSString+RXRURLEscape.h */; }; + A35E72291D7FCB0B00D38BC0 /* NSString+RXRURLEscape.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E72121D7FCB0B00D38BC0 /* NSString+RXRURLEscape.m */; }; + A35E722A1D7FCB0B00D38BC0 /* NSURL+Rexxar.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E72131D7FCB0B00D38BC0 /* NSURL+Rexxar.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E722B1D7FCB0B00D38BC0 /* NSURL+Rexxar.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E72141D7FCB0B00D38BC0 /* NSURL+Rexxar.m */; }; + A35E722C1D7FCB0B00D38BC0 /* UIColor+Rexxar.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E72151D7FCB0B00D38BC0 /* UIColor+Rexxar.h */; }; + A35E722D1D7FCB0B00D38BC0 /* UIColor+Rexxar.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E72161D7FCB0B00D38BC0 /* UIColor+Rexxar.m */; }; + A35E72351D7FCBD300D38BC0 /* RXRLocContainerAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E72341D7FCBD300D38BC0 /* RXRLocContainerAPI.m */; }; + A35E72401D7FCBF900D38BC0 /* RXRDecorator.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E72371D7FCBF900D38BC0 /* RXRDecorator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E72411D7FCBF900D38BC0 /* RXRRequestDecorator.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E72381D7FCBF900D38BC0 /* RXRRequestDecorator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E72421D7FCBF900D38BC0 /* RXRRequestDecorator.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E72391D7FCBF900D38BC0 /* RXRRequestDecorator.m */; }; + A35E72431D7FCBF900D38BC0 /* RXRRequestIntercepter.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E723A1D7FCBF900D38BC0 /* RXRRequestIntercepter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E72441D7FCBF900D38BC0 /* RXRRequestIntercepter.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E723B1D7FCBF900D38BC0 /* RXRRequestIntercepter.m */; }; + A35E72451D7FCBF900D38BC0 /* RXRContainerAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E723D1D7FCBF900D38BC0 /* RXRContainerAPI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E72461D7FCBF900D38BC0 /* RXRContainerIntercepter.h in Headers */ = {isa = PBXBuildFile; fileRef = A35E723E1D7FCBF900D38BC0 /* RXRContainerIntercepter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A35E72471D7FCBF900D38BC0 /* RXRContainerIntercepter.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E723F1D7FCBF900D38BC0 /* RXRContainerIntercepter.m */; }; + A35E72521D8004D400D38BC0 /* RXRMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E724C1D8004D400D38BC0 /* RXRMenuItem.m */; }; + A35E72551D8004D400D38BC0 /* RXRNavMenuWidget.m in Sources */ = {isa = PBXBuildFile; fileRef = A35E72511D8004D400D38BC0 /* RXRNavMenuWidget.m */; }; + A37E59431D8FDC38007E73C4 /* hybrid in Resources */ = {isa = PBXBuildFile; fileRef = A37E59421D8FDC38007E73C4 /* hybrid */; }; + A398DC2D1D221BA4006D4817 /* RXRNavTitleWidget.h in Headers */ = {isa = PBXBuildFile; fileRef = A398DC2B1D221BA4006D4817 /* RXRNavTitleWidget.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A398DC2E1D221BA4006D4817 /* RXRNavTitleWidget.m in Sources */ = {isa = PBXBuildFile; fileRef = A398DC2C1D221BA4006D4817 /* RXRNavTitleWidget.m */; }; + A398DC321D222CC1006D4817 /* RXRModel.h in Headers */ = {isa = PBXBuildFile; fileRef = A398DC301D222CC1006D4817 /* RXRModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A398DC331D222CC1006D4817 /* RXRModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A398DC311D222CC1006D4817 /* RXRModel.m */; }; + A398DC361D222D8F006D4817 /* RXRAlertDialogData.h in Headers */ = {isa = PBXBuildFile; fileRef = A398DC341D222D8F006D4817 /* RXRAlertDialogData.h */; }; + A398DC371D222D8F006D4817 /* RXRAlertDialogData.m in Sources */ = {isa = PBXBuildFile; fileRef = A398DC351D222D8F006D4817 /* RXRAlertDialogData.m */; }; + A39994651CEDB0280084DCD6 /* PartialRexxarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A39994641CEDB0280084DCD6 /* PartialRexxarViewController.swift */; }; + B90EBEC11BFEAA830055D477 /* Rexxar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98B47B031BF1E0E00087735C /* Rexxar.framework */; }; + B9E27ED01C049A9900C12DAF /* RXRRouteManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B9E27ECF1C049A9900C12DAF /* RXRRouteManagerTests.m */; }; + B9E27ED41C054A7A00C12DAF /* RoutesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9E27ED31C054A7A00C12DAF /* RoutesViewController.swift */; }; + B9E31F381C4F36C800DD0067 /* Rexxar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98B47B031BF1E0E00087735C /* Rexxar.framework */; }; + B9EF27D21C03315B00C7E2E9 /* URITests.m in Sources */ = {isa = PBXBuildFile; fileRef = B9EF27D11C03315B00C7E2E9 /* URITests.m */; }; + B9EF27D81C033C5300C7E2E9 /* www in Resources */ = {isa = PBXBuildFile; fileRef = B9EF27D71C033C5300C7E2E9 /* www */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B90EBEC21BFEAA830055D477 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 98B47AFA1BF1E0E00087735C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 98B47B021BF1E0E00087735C; + remoteInfo = Rexxar; + }; + B9E31F391C4F36CE00DD0067 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 98B47AFA1BF1E0E00087735C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 98B47B021BF1E0E00087735C; + remoteInfo = Rexxar; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2A439B659D80C1F6A81CEB22 /* Pods-RexxarDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RexxarDemo.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-RexxarDemo/Pods-RexxarDemo.debug.xcconfig"; sourceTree = ""; }; + 6143A5FB8F8F1D086AC883C1 /* libPods-RexxarDemo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RexxarDemo.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 77BB1328573CD94C05F4665D /* libPods-Rexxar.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Rexxar.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 83BCA0BDFDFD248FED598EEA /* Pods-Rexxar.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Rexxar.release.xcconfig"; path = "../Pods/Target Support Files/Pods-Rexxar/Pods-Rexxar.release.xcconfig"; sourceTree = ""; }; + 98B47B031BF1E0E00087735C /* Rexxar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Rexxar.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 98B47B061BF1E0E00087735C /* Rexxar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Rexxar.h; sourceTree = ""; }; + 98B47B081BF1E0E00087735C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 98B47B121BF1E0F20087735C /* RexxarDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RexxarDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 98B47B141BF1E0F20087735C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 98B47B1E1BF1E0F20087735C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 98B47B201BF1E0F20087735C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 98B47B391BF1E2BD0087735C /* Bridge-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bridge-Header.h"; sourceTree = ""; }; + 9AD2C999156315D603AC712D /* Pods-RexxarDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RexxarDemo.release.xcconfig"; path = "../Pods/Target Support Files/Pods-RexxarDemo/Pods-RexxarDemo.release.xcconfig"; sourceTree = ""; }; + A3249A081D66EF27006CCCCB /* FRDToast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FRDToast.swift; sourceTree = ""; }; + A3249A091D66EF27006CCCCB /* LoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + A3249A0A1D66EF27006CCCCB /* ToastView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; + A3249A0B1D66EF27006CCCCB /* UIColor+helper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+helper.swift"; sourceTree = ""; }; + A3249A131D66EF98006CCCCB /* RXRToastWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RXRToastWidget.h; path = RexxarDemo/Widget/RXRToastWidget.h; sourceTree = SOURCE_ROOT; }; + A3249A141D66EF98006CCCCB /* RXRToastWidget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RXRToastWidget.m; path = RexxarDemo/Widget/RXRToastWidget.m; sourceTree = SOURCE_ROOT; }; + A3249A191D66F941006CCCCB /* DemoRXRViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoRXRViewController.swift; sourceTree = ""; }; + A35925251CE487870032B1E6 /* RXRRouteFileCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRRouteFileCacheTests.m; sourceTree = ""; }; + A35D15A01D66AE2B009B24DC /* RXRAlertDialogWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRAlertDialogWidget.h; sourceTree = ""; }; + A35D15A11D66AE2B009B24DC /* RXRAlertDialogWidget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRAlertDialogWidget.m; sourceTree = ""; }; + A35E71DD1D7FC86000D38BC0 /* RXRPullRefreshWidget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRPullRefreshWidget.m; sourceTree = ""; }; + A35E71DE1D7FC86000D38BC0 /* RXRPullRefreshWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRPullRefreshWidget.h; sourceTree = ""; }; + A35E71E21D7FCA0200D38BC0 /* RXRCacheFileIntercepter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRCacheFileIntercepter.h; sourceTree = ""; }; + A35E71E31D7FCA0200D38BC0 /* RXRCacheFileIntercepter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRCacheFileIntercepter.m; sourceTree = ""; }; + A35E71E41D7FCA0200D38BC0 /* RXRConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRConfig.h; sourceTree = ""; }; + A35E71E51D7FCA0200D38BC0 /* RXRConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRConfig.m; sourceTree = ""; }; + A35E71E61D7FCA0200D38BC0 /* RXRLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRLogging.h; sourceTree = ""; }; + A35E71E71D7FCA0200D38BC0 /* RXRNSURLProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRNSURLProtocol.h; sourceTree = ""; }; + A35E71E81D7FCA0200D38BC0 /* RXRNSURLProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRNSURLProtocol.m; sourceTree = ""; }; + A35E71E91D7FCA0200D38BC0 /* RXRRoute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRRoute.h; sourceTree = ""; }; + A35E71EA1D7FCA0200D38BC0 /* RXRRoute.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRRoute.m; sourceTree = ""; }; + A35E71EB1D7FCA0200D38BC0 /* RXRRouteFileCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRRouteFileCache.h; sourceTree = ""; }; + A35E71EC1D7FCA0200D38BC0 /* RXRRouteFileCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRRouteFileCache.m; sourceTree = ""; }; + A35E71ED1D7FCA0200D38BC0 /* RXRRouteManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRRouteManager.h; sourceTree = ""; }; + A35E71EE1D7FCA0200D38BC0 /* RXRRouteManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRRouteManager.m; sourceTree = ""; }; + A35E71EF1D7FCA0200D38BC0 /* RXRViewController+Router.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RXRViewController+Router.m"; sourceTree = ""; }; + A35E71F01D7FCA0200D38BC0 /* RXRViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRViewController.h; sourceTree = ""; }; + A35E71F11D7FCA0200D38BC0 /* RXRViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRViewController.m; sourceTree = ""; }; + A35E71F21D7FCA0200D38BC0 /* RXRWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRWidget.h; sourceTree = ""; }; + A35E720B1D7FCB0B00D38BC0 /* NSData+RXRDigest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+RXRDigest.h"; sourceTree = ""; }; + A35E720C1D7FCB0B00D38BC0 /* NSData+RXRDigest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+RXRDigest.m"; sourceTree = ""; }; + A35E720D1D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+RXRMultipleItems.h"; sourceTree = ""; }; + A35E720E1D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+RXRMultipleItems.m"; sourceTree = ""; }; + A35E720F1D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableDictionary+RXRMultipleItems.h"; sourceTree = ""; }; + A35E72101D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableDictionary+RXRMultipleItems.m"; sourceTree = ""; }; + A35E72111D7FCB0B00D38BC0 /* NSString+RXRURLEscape.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+RXRURLEscape.h"; sourceTree = ""; }; + A35E72121D7FCB0B00D38BC0 /* NSString+RXRURLEscape.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+RXRURLEscape.m"; sourceTree = ""; }; + A35E72131D7FCB0B00D38BC0 /* NSURL+Rexxar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURL+Rexxar.h"; sourceTree = ""; }; + A35E72141D7FCB0B00D38BC0 /* NSURL+Rexxar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURL+Rexxar.m"; sourceTree = ""; }; + A35E72151D7FCB0B00D38BC0 /* UIColor+Rexxar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+Rexxar.h"; sourceTree = ""; }; + A35E72161D7FCB0B00D38BC0 /* UIColor+Rexxar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+Rexxar.m"; sourceTree = ""; }; + A35E72331D7FCBD300D38BC0 /* RXRLocContainerAPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RXRLocContainerAPI.h; path = RexxarDemo/ContainerAPI/RXRLocContainerAPI.h; sourceTree = SOURCE_ROOT; }; + A35E72341D7FCBD300D38BC0 /* RXRLocContainerAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RXRLocContainerAPI.m; path = RexxarDemo/ContainerAPI/RXRLocContainerAPI.m; sourceTree = SOURCE_ROOT; }; + A35E72371D7FCBF900D38BC0 /* RXRDecorator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRDecorator.h; sourceTree = ""; }; + A35E72381D7FCBF900D38BC0 /* RXRRequestDecorator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRRequestDecorator.h; sourceTree = ""; }; + A35E72391D7FCBF900D38BC0 /* RXRRequestDecorator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRRequestDecorator.m; sourceTree = ""; }; + A35E723A1D7FCBF900D38BC0 /* RXRRequestIntercepter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRRequestIntercepter.h; sourceTree = ""; }; + A35E723B1D7FCBF900D38BC0 /* RXRRequestIntercepter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRRequestIntercepter.m; sourceTree = ""; }; + A35E723D1D7FCBF900D38BC0 /* RXRContainerAPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRContainerAPI.h; sourceTree = ""; }; + A35E723E1D7FCBF900D38BC0 /* RXRContainerIntercepter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRContainerIntercepter.h; sourceTree = ""; }; + A35E723F1D7FCBF900D38BC0 /* RXRContainerIntercepter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRContainerIntercepter.m; sourceTree = ""; }; + A35E72481D8004D400D38BC0 /* RXRNavMenuWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RXRNavMenuWidget.h; path = RexxarDemo/Widget/RXRNavMenuWidget.h; sourceTree = SOURCE_ROOT; }; + A35E724B1D8004D400D38BC0 /* RXRMenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRMenuItem.h; sourceTree = ""; }; + A35E724C1D8004D400D38BC0 /* RXRMenuItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRMenuItem.m; sourceTree = ""; }; + A35E72511D8004D400D38BC0 /* RXRNavMenuWidget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RXRNavMenuWidget.m; path = RexxarDemo/Widget/RXRNavMenuWidget.m; sourceTree = SOURCE_ROOT; }; + A37E59421D8FDC38007E73C4 /* hybrid */ = {isa = PBXFileReference; lastKnownFileType = folder; path = hybrid; sourceTree = ""; }; + A398DC2B1D221BA4006D4817 /* RXRNavTitleWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRNavTitleWidget.h; sourceTree = ""; }; + A398DC2C1D221BA4006D4817 /* RXRNavTitleWidget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRNavTitleWidget.m; sourceTree = ""; }; + A398DC301D222CC1006D4817 /* RXRModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRModel.h; sourceTree = ""; }; + A398DC311D222CC1006D4817 /* RXRModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRModel.m; sourceTree = ""; }; + A398DC341D222D8F006D4817 /* RXRAlertDialogData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RXRAlertDialogData.h; sourceTree = ""; }; + A398DC351D222D8F006D4817 /* RXRAlertDialogData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRAlertDialogData.m; sourceTree = ""; }; + A39994641CEDB0280084DCD6 /* PartialRexxarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PartialRexxarViewController.swift; sourceTree = ""; }; + B90EBEBC1BFEAA830055D477 /* RexxarTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RexxarTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B90EBEC01BFEAA830055D477 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B9E27ECF1C049A9900C12DAF /* RXRRouteManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RXRRouteManagerTests.m; sourceTree = ""; }; + B9E27ED31C054A7A00C12DAF /* RoutesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoutesViewController.swift; sourceTree = ""; }; + B9EF27D11C03315B00C7E2E9 /* URITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = URITests.m; sourceTree = ""; }; + B9EF27D71C033C5300C7E2E9 /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; path = www; sourceTree = ""; }; + E6E1E32ECDF1A49AD080CDE8 /* Pods-Rexxar.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Rexxar.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-Rexxar/Pods-Rexxar.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 98B47AFF1BF1E0E00087735C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 98B47B0F1BF1E0F20087735C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B9E31F381C4F36C800DD0067 /* Rexxar.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B90EBEB91BFEAA830055D477 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B90EBEC11BFEAA830055D477 /* Rexxar.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0C14B124D07B4AD6475C82AD /* Frameworks */ = { + isa = PBXGroup; + children = ( + 77BB1328573CD94C05F4665D /* libPods-Rexxar.a */, + 6143A5FB8F8F1D086AC883C1 /* libPods-RexxarDemo.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5D8E48CEBE685332DC186A49 /* Pods */ = { + isa = PBXGroup; + children = ( + E6E1E32ECDF1A49AD080CDE8 /* Pods-Rexxar.debug.xcconfig */, + 83BCA0BDFDFD248FED598EEA /* Pods-Rexxar.release.xcconfig */, + 2A439B659D80C1F6A81CEB22 /* Pods-RexxarDemo.debug.xcconfig */, + 9AD2C999156315D603AC712D /* Pods-RexxarDemo.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 98B47AF91BF1E0E00087735C = { + isa = PBXGroup; + children = ( + 98B47B051BF1E0E00087735C /* Rexxar */, + 98B47B131BF1E0F20087735C /* RexxarDemo */, + B90EBEBD1BFEAA830055D477 /* RexxarTests */, + 98B47B041BF1E0E00087735C /* Products */, + 5D8E48CEBE685332DC186A49 /* Pods */, + 0C14B124D07B4AD6475C82AD /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 4; + }; + 98B47B041BF1E0E00087735C /* Products */ = { + isa = PBXGroup; + children = ( + 98B47B031BF1E0E00087735C /* Rexxar.framework */, + 98B47B121BF1E0F20087735C /* RexxarDemo.app */, + B90EBEBC1BFEAA830055D477 /* RexxarTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 98B47B051BF1E0E00087735C /* Rexxar */ = { + isa = PBXGroup; + children = ( + A35E71E11D7FCA0200D38BC0 /* Core */, + A35E72361D7FCBF900D38BC0 /* Decorator */, + A35E723C1D7FCBF900D38BC0 /* ContainerAPI */, + A398DC261D221B6A006D4817 /* Widget */, + 98B47B061BF1E0E00087735C /* Rexxar.h */, + B9101BCF1C2446B5002D5DF2 /* Supporting Files */, + ); + path = Rexxar; + sourceTree = ""; + }; + 98B47B131BF1E0F20087735C /* RexxarDemo */ = { + isa = PBXGroup; + children = ( + A3249A061D66EF27006CCCCB /* Library */, + A39964791D549DB700813A60 /* Widget */, + A399647A1D549DB700813A60 /* ContainerAPI */, + A399647B1D549DB700813A60 /* Decorator */, + 98B47B141BF1E0F20087735C /* AppDelegate.swift */, + B9E27ED31C054A7A00C12DAF /* RoutesViewController.swift */, + A39994641CEDB0280084DCD6 /* PartialRexxarViewController.swift */, + A3249A191D66F941006CCCCB /* DemoRXRViewController.swift */, + 98B47B1D1BF1E0F20087735C /* LaunchScreen.storyboard */, + 98B47B201BF1E0F20087735C /* Info.plist */, + 98B47B391BF1E2BD0087735C /* Bridge-Header.h */, + 98B47B8B1BF1ED7D0087735C /* Resource */, + ); + path = RexxarDemo; + sourceTree = ""; + }; + 98B47B8B1BF1ED7D0087735C /* Resource */ = { + isa = PBXGroup; + children = ( + A37E59421D8FDC38007E73C4 /* hybrid */, + ); + path = Resource; + sourceTree = ""; + }; + A3249A061D66EF27006CCCCB /* Library */ = { + isa = PBXGroup; + children = ( + A3249A071D66EF27006CCCCB /* FRDToast */, + ); + path = Library; + sourceTree = ""; + }; + A3249A071D66EF27006CCCCB /* FRDToast */ = { + isa = PBXGroup; + children = ( + A3249A081D66EF27006CCCCB /* FRDToast.swift */, + A3249A091D66EF27006CCCCB /* LoadingView.swift */, + A3249A0A1D66EF27006CCCCB /* ToastView.swift */, + A3249A0B1D66EF27006CCCCB /* UIColor+helper.swift */, + ); + path = FRDToast; + sourceTree = ""; + }; + A35E71E11D7FCA0200D38BC0 /* Core */ = { + isa = PBXGroup; + children = ( + A35E720A1D7FCB0B00D38BC0 /* Extension */, + A35E71E21D7FCA0200D38BC0 /* RXRCacheFileIntercepter.h */, + A35E71E31D7FCA0200D38BC0 /* RXRCacheFileIntercepter.m */, + A35E71E41D7FCA0200D38BC0 /* RXRConfig.h */, + A35E71E51D7FCA0200D38BC0 /* RXRConfig.m */, + A35E71E61D7FCA0200D38BC0 /* RXRLogging.h */, + A35E71E71D7FCA0200D38BC0 /* RXRNSURLProtocol.h */, + A35E71E81D7FCA0200D38BC0 /* RXRNSURLProtocol.m */, + A35E71E91D7FCA0200D38BC0 /* RXRRoute.h */, + A35E71EA1D7FCA0200D38BC0 /* RXRRoute.m */, + A35E71EB1D7FCA0200D38BC0 /* RXRRouteFileCache.h */, + A35E71EC1D7FCA0200D38BC0 /* RXRRouteFileCache.m */, + A35E71ED1D7FCA0200D38BC0 /* RXRRouteManager.h */, + A35E71EE1D7FCA0200D38BC0 /* RXRRouteManager.m */, + A35E71EF1D7FCA0200D38BC0 /* RXRViewController+Router.m */, + A35E71F01D7FCA0200D38BC0 /* RXRViewController.h */, + A35E71F11D7FCA0200D38BC0 /* RXRViewController.m */, + A35E71F21D7FCA0200D38BC0 /* RXRWidget.h */, + ); + path = Core; + sourceTree = ""; + }; + A35E720A1D7FCB0B00D38BC0 /* Extension */ = { + isa = PBXGroup; + children = ( + A35E720B1D7FCB0B00D38BC0 /* NSData+RXRDigest.h */, + A35E720C1D7FCB0B00D38BC0 /* NSData+RXRDigest.m */, + A35E720D1D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.h */, + A35E720E1D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.m */, + A35E720F1D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.h */, + A35E72101D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.m */, + A35E72111D7FCB0B00D38BC0 /* NSString+RXRURLEscape.h */, + A35E72121D7FCB0B00D38BC0 /* NSString+RXRURLEscape.m */, + A35E72131D7FCB0B00D38BC0 /* NSURL+Rexxar.h */, + A35E72141D7FCB0B00D38BC0 /* NSURL+Rexxar.m */, + A35E72151D7FCB0B00D38BC0 /* UIColor+Rexxar.h */, + A35E72161D7FCB0B00D38BC0 /* UIColor+Rexxar.m */, + ); + path = Extension; + sourceTree = ""; + }; + A35E72361D7FCBF900D38BC0 /* Decorator */ = { + isa = PBXGroup; + children = ( + A35E72371D7FCBF900D38BC0 /* RXRDecorator.h */, + A35E72381D7FCBF900D38BC0 /* RXRRequestDecorator.h */, + A35E72391D7FCBF900D38BC0 /* RXRRequestDecorator.m */, + A35E723A1D7FCBF900D38BC0 /* RXRRequestIntercepter.h */, + A35E723B1D7FCBF900D38BC0 /* RXRRequestIntercepter.m */, + ); + path = Decorator; + sourceTree = ""; + }; + A35E723C1D7FCBF900D38BC0 /* ContainerAPI */ = { + isa = PBXGroup; + children = ( + A35E723D1D7FCBF900D38BC0 /* RXRContainerAPI.h */, + A35E723E1D7FCBF900D38BC0 /* RXRContainerIntercepter.h */, + A35E723F1D7FCBF900D38BC0 /* RXRContainerIntercepter.m */, + ); + path = ContainerAPI; + sourceTree = ""; + }; + A35E72491D8004D400D38BC0 /* Model */ = { + isa = PBXGroup; + children = ( + A35E724A1D8004D400D38BC0 /* Menu */, + ); + name = Model; + path = RexxarDemo/Widget/Model; + sourceTree = SOURCE_ROOT; + }; + A35E724A1D8004D400D38BC0 /* Menu */ = { + isa = PBXGroup; + children = ( + A35E724B1D8004D400D38BC0 /* RXRMenuItem.h */, + A35E724C1D8004D400D38BC0 /* RXRMenuItem.m */, + ); + path = Menu; + sourceTree = ""; + }; + A398DC261D221B6A006D4817 /* Widget */ = { + isa = PBXGroup; + children = ( + A398DC2F1D222CA8006D4817 /* Model */, + A398DC2B1D221BA4006D4817 /* RXRNavTitleWidget.h */, + A398DC2C1D221BA4006D4817 /* RXRNavTitleWidget.m */, + A35D15A01D66AE2B009B24DC /* RXRAlertDialogWidget.h */, + A35D15A11D66AE2B009B24DC /* RXRAlertDialogWidget.m */, + A35E71DE1D7FC86000D38BC0 /* RXRPullRefreshWidget.h */, + A35E71DD1D7FC86000D38BC0 /* RXRPullRefreshWidget.m */, + ); + path = Widget; + sourceTree = ""; + }; + A398DC2F1D222CA8006D4817 /* Model */ = { + isa = PBXGroup; + children = ( + A398DC301D222CC1006D4817 /* RXRModel.h */, + A398DC311D222CC1006D4817 /* RXRModel.m */, + A398DC341D222D8F006D4817 /* RXRAlertDialogData.h */, + A398DC351D222D8F006D4817 /* RXRAlertDialogData.m */, + ); + path = Model; + sourceTree = ""; + }; + A39964791D549DB700813A60 /* Widget */ = { + isa = PBXGroup; + children = ( + A35E72491D8004D400D38BC0 /* Model */, + A35E72481D8004D400D38BC0 /* RXRNavMenuWidget.h */, + A35E72511D8004D400D38BC0 /* RXRNavMenuWidget.m */, + A3249A131D66EF98006CCCCB /* RXRToastWidget.h */, + A3249A141D66EF98006CCCCB /* RXRToastWidget.m */, + ); + name = Widget; + path = ../Rexxar/Widget; + sourceTree = ""; + }; + A399647A1D549DB700813A60 /* ContainerAPI */ = { + isa = PBXGroup; + children = ( + A35E72331D7FCBD300D38BC0 /* RXRLocContainerAPI.h */, + A35E72341D7FCBD300D38BC0 /* RXRLocContainerAPI.m */, + ); + name = ContainerAPI; + path = ../Rexxar/ContainerAPI; + sourceTree = ""; + }; + A399647B1D549DB700813A60 /* Decorator */ = { + isa = PBXGroup; + children = ( + ); + path = Decorator; + sourceTree = ""; + }; + B90EBEBD1BFEAA830055D477 /* RexxarTests */ = { + isa = PBXGroup; + children = ( + B9E27ECF1C049A9900C12DAF /* RXRRouteManagerTests.m */, + A35925251CE487870032B1E6 /* RXRRouteFileCacheTests.m */, + B9EF27D11C03315B00C7E2E9 /* URITests.m */, + B90EBEC01BFEAA830055D477 /* Info.plist */, + B9EF27D71C033C5300C7E2E9 /* www */, + ); + path = RexxarTests; + sourceTree = ""; + }; + B9101BCF1C2446B5002D5DF2 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 98B47B081BF1E0E00087735C /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 98B47B001BF1E0E00087735C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A35E72011D7FCA0200D38BC0 /* RXRViewController.h in Headers */, + A35E72241D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.h in Headers */, + A35E722A1D7FCB0B00D38BC0 /* NSURL+Rexxar.h in Headers */, + A35E72431D7FCBF900D38BC0 /* RXRRequestIntercepter.h in Headers */, + A35E72411D7FCBF900D38BC0 /* RXRRequestDecorator.h in Headers */, + A35E71F81D7FCA0200D38BC0 /* RXRNSURLProtocol.h in Headers */, + A35E72461D7FCBF900D38BC0 /* RXRContainerIntercepter.h in Headers */, + A35E72031D7FCA0200D38BC0 /* RXRWidget.h in Headers */, + A35E71F51D7FCA0200D38BC0 /* RXRConfig.h in Headers */, + A35E72401D7FCBF900D38BC0 /* RXRDecorator.h in Headers */, + A35E72451D7FCBF900D38BC0 /* RXRContainerAPI.h in Headers */, + A35E71E01D7FC86000D38BC0 /* RXRPullRefreshWidget.h in Headers */, + A35D15A21D66AE2B009B24DC /* RXRAlertDialogWidget.h in Headers */, + A398DC321D222CC1006D4817 /* RXRModel.h in Headers */, + A35E72281D7FCB0B00D38BC0 /* NSString+RXRURLEscape.h in Headers */, + A35E71FE1D7FCA0200D38BC0 /* RXRRouteManager.h in Headers */, + A35E72261D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.h in Headers */, + A35E71F71D7FCA0200D38BC0 /* RXRLogging.h in Headers */, + A35E72221D7FCB0B00D38BC0 /* NSData+RXRDigest.h in Headers */, + A398DC2D1D221BA4006D4817 /* RXRNavTitleWidget.h in Headers */, + A35E722C1D7FCB0B00D38BC0 /* UIColor+Rexxar.h in Headers */, + A398DC361D222D8F006D4817 /* RXRAlertDialogData.h in Headers */, + A35E71F31D7FCA0200D38BC0 /* RXRCacheFileIntercepter.h in Headers */, + A35E71FA1D7FCA0200D38BC0 /* RXRRoute.h in Headers */, + 98B47B071BF1E0E00087735C /* Rexxar.h in Headers */, + A35E71FC1D7FCA0200D38BC0 /* RXRRouteFileCache.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 98B47B021BF1E0E00087735C /* Rexxar */ = { + isa = PBXNativeTarget; + buildConfigurationList = 98B47B0B1BF1E0E00087735C /* Build configuration list for PBXNativeTarget "Rexxar" */; + buildPhases = ( + 98B47AFE1BF1E0E00087735C /* Sources */, + 98B47AFF1BF1E0E00087735C /* Frameworks */, + 98B47B001BF1E0E00087735C /* Headers */, + 98B47B011BF1E0E00087735C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Rexxar; + productName = Rexxar; + productReference = 98B47B031BF1E0E00087735C /* Rexxar.framework */; + productType = "com.apple.product-type.framework"; + }; + 98B47B111BF1E0F20087735C /* RexxarDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 98B47B211BF1E0F20087735C /* Build configuration list for PBXNativeTarget "RexxarDemo" */; + buildPhases = ( + 98B47B0E1BF1E0F20087735C /* Sources */, + 98B47B0F1BF1E0F20087735C /* Frameworks */, + 98B47B101BF1E0F20087735C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B9E31F3A1C4F36CE00DD0067 /* PBXTargetDependency */, + ); + name = RexxarDemo; + productName = RexxarDemo; + productReference = 98B47B121BF1E0F20087735C /* RexxarDemo.app */; + productType = "com.apple.product-type.application"; + }; + B90EBEBB1BFEAA830055D477 /* RexxarTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B90EBEC61BFEAA830055D477 /* Build configuration list for PBXNativeTarget "RexxarTests" */; + buildPhases = ( + B90EBEB81BFEAA830055D477 /* Sources */, + B90EBEB91BFEAA830055D477 /* Frameworks */, + B90EBEBA1BFEAA830055D477 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B90EBEC31BFEAA830055D477 /* PBXTargetDependency */, + ); + name = RexxarTests; + productName = RexxarTests; + productReference = B90EBEBC1BFEAA830055D477 /* RexxarTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 98B47AFA1BF1E0E00087735C /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0710; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Douban.Inc; + TargetAttributes = { + 98B47B021BF1E0E00087735C = { + CreatedOnToolsVersion = 7.1; + }; + 98B47B111BF1E0F20087735C = { + CreatedOnToolsVersion = 7.1; + LastSwiftMigration = 0800; + }; + B90EBEBB1BFEAA830055D477 = { + CreatedOnToolsVersion = 7.1.1; + }; + }; + }; + buildConfigurationList = 98B47AFD1BF1E0E00087735C /* Build configuration list for PBXProject "Rexxar" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 98B47AF91BF1E0E00087735C; + productRefGroup = 98B47B041BF1E0E00087735C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 98B47B021BF1E0E00087735C /* Rexxar */, + 98B47B111BF1E0F20087735C /* RexxarDemo */, + B90EBEBB1BFEAA830055D477 /* RexxarTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 98B47B011BF1E0E00087735C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 98B47B101BF1E0F20087735C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A37E59431D8FDC38007E73C4 /* hybrid in Resources */, + 98B47B1F1BF1E0F20087735C /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B90EBEBA1BFEAA830055D477 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B9EF27D81C033C5300C7E2E9 /* www in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 98B47AFE1BF1E0E00087735C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A35E71FD1D7FCA0200D38BC0 /* RXRRouteFileCache.m in Sources */, + A35E71FF1D7FCA0200D38BC0 /* RXRRouteManager.m in Sources */, + A35E722B1D7FCB0B00D38BC0 /* NSURL+Rexxar.m in Sources */, + A398DC331D222CC1006D4817 /* RXRModel.m in Sources */, + A35E72421D7FCBF900D38BC0 /* RXRRequestDecorator.m in Sources */, + A35E72441D7FCBF900D38BC0 /* RXRRequestIntercepter.m in Sources */, + A35D15A31D66AE2B009B24DC /* RXRAlertDialogWidget.m in Sources */, + A35E72251D7FCB0B00D38BC0 /* NSDictionary+RXRMultipleItems.m in Sources */, + A35E71F91D7FCA0200D38BC0 /* RXRNSURLProtocol.m in Sources */, + A398DC371D222D8F006D4817 /* RXRAlertDialogData.m in Sources */, + A35E71FB1D7FCA0200D38BC0 /* RXRRoute.m in Sources */, + A35E71F61D7FCA0200D38BC0 /* RXRConfig.m in Sources */, + A35E72021D7FCA0200D38BC0 /* RXRViewController.m in Sources */, + A35E71F41D7FCA0200D38BC0 /* RXRCacheFileIntercepter.m in Sources */, + A35E72471D7FCBF900D38BC0 /* RXRContainerIntercepter.m in Sources */, + A35E72271D7FCB0B00D38BC0 /* NSMutableDictionary+RXRMultipleItems.m in Sources */, + A35E72001D7FCA0200D38BC0 /* RXRViewController+Router.m in Sources */, + A35E71DF1D7FC86000D38BC0 /* RXRPullRefreshWidget.m in Sources */, + A398DC2E1D221BA4006D4817 /* RXRNavTitleWidget.m in Sources */, + A35E72291D7FCB0B00D38BC0 /* NSString+RXRURLEscape.m in Sources */, + A35E72231D7FCB0B00D38BC0 /* NSData+RXRDigest.m in Sources */, + A35E722D1D7FCB0B00D38BC0 /* UIColor+Rexxar.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 98B47B0E1BF1E0F20087735C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B9E27ED41C054A7A00C12DAF /* RoutesViewController.swift in Sources */, + A3249A0E1D66EF27006CCCCB /* ToastView.swift in Sources */, + A3249A0C1D66EF27006CCCCB /* FRDToast.swift in Sources */, + 987FA5221BFC78310096ED5C /* AppDelegate.swift in Sources */, + A3249A151D66EF98006CCCCB /* RXRToastWidget.m in Sources */, + A39994651CEDB0280084DCD6 /* PartialRexxarViewController.swift in Sources */, + A35E72351D7FCBD300D38BC0 /* RXRLocContainerAPI.m in Sources */, + A3249A0D1D66EF27006CCCCB /* LoadingView.swift in Sources */, + A35E72551D8004D400D38BC0 /* RXRNavMenuWidget.m in Sources */, + A3249A1A1D66F941006CCCCB /* DemoRXRViewController.swift in Sources */, + A35E72521D8004D400D38BC0 /* RXRMenuItem.m in Sources */, + A3249A0F1D66EF27006CCCCB /* UIColor+helper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B90EBEB81BFEAA830055D477 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B9EF27D21C03315B00C7E2E9 /* URITests.m in Sources */, + A35925261CE487870032B1E6 /* RXRRouteFileCacheTests.m in Sources */, + B9E27ED01C049A9900C12DAF /* RXRRouteManagerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B90EBEC31BFEAA830055D477 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 98B47B021BF1E0E00087735C /* Rexxar */; + targetProxy = B90EBEC21BFEAA830055D477 /* PBXContainerItemProxy */; + }; + B9E31F3A1C4F36CE00DD0067 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 98B47B021BF1E0E00087735C /* Rexxar */; + targetProxy = B9E31F391C4F36CE00DD0067 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 98B47B1D1BF1E0F20087735C /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 98B47B1E1BF1E0F20087735C /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 98B47B091BF1E0E00087735C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 98B47B0A1BF1E0E00087735C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 98B47B0C1BF1E0E00087735C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Rexxar/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.douban.Rexxar; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 98B47B0D1BF1E0E00087735C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Rexxar/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.douban.Rexxar; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 98B47B221BF1E0F20087735C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "Brand Assets"; + INFOPLIST_FILE = RexxarDemo/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.douban.RexxarDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "RexxarDemo/Bridge-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 98B47B231BF1E0F20087735C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "Brand Assets"; + INFOPLIST_FILE = RexxarDemo/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.douban.RexxarDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "RexxarDemo/Bridge-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + B90EBEC41BFEAA830055D477 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = RexxarTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.douban.RexxarTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + B90EBEC51BFEAA830055D477 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = RexxarTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.douban.RexxarTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 98B47AFD1BF1E0E00087735C /* Build configuration list for PBXProject "Rexxar" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 98B47B091BF1E0E00087735C /* Debug */, + 98B47B0A1BF1E0E00087735C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 98B47B0B1BF1E0E00087735C /* Build configuration list for PBXNativeTarget "Rexxar" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 98B47B0C1BF1E0E00087735C /* Debug */, + 98B47B0D1BF1E0E00087735C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 98B47B211BF1E0F20087735C /* Build configuration list for PBXNativeTarget "RexxarDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 98B47B221BF1E0F20087735C /* Debug */, + 98B47B231BF1E0F20087735C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B90EBEC61BFEAA830055D477 /* Build configuration list for PBXNativeTarget "RexxarTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B90EBEC41BFEAA830055D477 /* Debug */, + B90EBEC51BFEAA830055D477 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 98B47AFA1BF1E0E00087735C /* Project object */; +} diff --git a/Rexxar.xcodeproj/xcshareddata/xcschemes/Rexxar.xcscheme b/Rexxar.xcodeproj/xcshareddata/xcschemes/Rexxar.xcscheme new file mode 100644 index 0000000..2752256 --- /dev/null +++ b/Rexxar.xcodeproj/xcshareddata/xcschemes/Rexxar.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Rexxar.xcodeproj/xcshareddata/xcschemes/RexxarTests.xcscheme b/Rexxar.xcodeproj/xcshareddata/xcschemes/RexxarTests.xcscheme new file mode 100644 index 0000000..6d2e83d --- /dev/null +++ b/Rexxar.xcodeproj/xcshareddata/xcschemes/RexxarTests.xcscheme @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Rexxar/ContainerAPI/RXRContainerAPI.h b/Rexxar/ContainerAPI/RXRContainerAPI.h new file mode 100644 index 0000000..ff62b8b --- /dev/null +++ b/Rexxar/ContainerAPI/RXRContainerAPI.h @@ -0,0 +1,53 @@ +// +// RXRContainerAPI.h +// Rexxar +// +// Created by GUO Lin on 5/17/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRContainerAPI` 是一个请求模拟器协议。请求模拟器代表了一个可用于模拟 http 请求的类的协议。 + * 符合该协议的类可以用于模拟 Rexxar-Container 内发出的 Http 请求。 + */ +@protocol RXRContainerAPI + +/** + * 判断是否应该截获该请求,对该请求做模拟操作。 + */ +- (BOOL)shouldInterceptRequest:(NSURLRequest *)request; + +/** + * 模拟请求的返回,返回 NSURLResponse 对象。 + */ +- (NSURLResponse *)responseWithRequest:(NSURLRequest *)request; + +/** + * 模拟请求返回的内容,返回二进制数据。 + */ +- (nullable NSData *)responseData; + +@optional + +/** + * 准备对请求的模拟。 + * + * @param request 对应的请求 + */ +- (void)prepareWithRequest:(NSURLRequest *)request; + +/** + * 执行对请求的模拟。 + * + * @param request 对应的请求 + */ +- (void)performWithRequest:(NSURLRequest *)request; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Rexxar/ContainerAPI/RXRContainerIntercepter.h b/Rexxar/ContainerAPI/RXRContainerIntercepter.h new file mode 100644 index 0000000..271603e --- /dev/null +++ b/Rexxar/ContainerAPI/RXRContainerIntercepter.h @@ -0,0 +1,39 @@ +// +// RXRContainerIntercepter.h +// Rexxar +// +// Created by GUO Lin on 5/17/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +@import Foundation; + +#import "RXRNSURLProtocol.h" + +@protocol RXRContainerAPI; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRContainerIntercepter` 是一个 Rexxar-Container 的请求侦听器。 + * 这个侦听器用于模拟网络请求。这些网络请求并不会发送出去,而是由 Native 处理。 + * 比如向 Web 提供当前位置信息。 + * + */ +@interface RXRContainerIntercepter : RXRNSURLProtocol + +/** + * 设置这个侦听器所有的请求模仿器数组,该数组成员是符合 `RXRContainerAPI` 协议的对象,即一组请求模仿器。 + * + * @param mockers 模仿器数组 + */ ++ (void)setContainerAPIs:(NSArray> *)containerAPIs; + +/** + * 这个侦听器所有的请求模仿器,该数组成员是符合 `RXRContainerAPI` 协议的对象,即一组请求模仿器。 + */ ++ (nullable NSArray> *)containerAPIs; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Rexxar/ContainerAPI/RXRContainerIntercepter.m b/Rexxar/ContainerAPI/RXRContainerIntercepter.m new file mode 100644 index 0000000..1c5dd0d --- /dev/null +++ b/Rexxar/ContainerAPI/RXRContainerIntercepter.m @@ -0,0 +1,65 @@ +// +// RXRContainerIntercepter.m +// Rexxar +// +// Created by GUO Lin on 5/17/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +#import "RXRContainerIntercepter.h" +#import "RXRContainerAPI.h" + +static NSArray> *sContainerAPIs; + +@implementation RXRContainerIntercepter + ++ (void)setContainerAPIs:(NSArray> *)mockers +{ + sContainerAPIs = mockers; +} + ++ (NSArray> *)containerAPIs +{ + return sContainerAPIs; +} + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + // 请求不是来自浏览器,不处理 + if (![request.allHTTPHeaderFields[@"User-Agent"] hasPrefix:@"Mozilla"]) { + return NO; + } + + for (id mocker in sContainerAPIs) { + if ([mocker shouldInterceptRequest:request]) { + return YES; + } + } + + return NO; +} + +- (void)startLoading +{ + for (id containerAPI in sContainerAPIs) { + if ([containerAPI shouldInterceptRequest:self.request]) { + + if ([containerAPI respondsToSelector:@selector(prepareWithRequest:)]) { + [containerAPI prepareWithRequest:self.request]; + } + + if ([containerAPI respondsToSelector:@selector(performWithRequest:)]) { + [containerAPI performWithRequest:self.request]; + } + + NSData *data = [containerAPI responseData]; + NSURLResponse *response = [containerAPI responseWithRequest:self.request]; + [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + [self.client URLProtocol:self didLoadData:data]; + [self.client URLProtocolDidFinishLoading:self]; + break; + } + } +} + +@end diff --git a/Rexxar/Core/Extension/NSData+RXRDigest.h b/Rexxar/Core/Extension/NSData+RXRDigest.h new file mode 100644 index 0000000..be10884 --- /dev/null +++ b/Rexxar/Core/Extension/NSData+RXRDigest.h @@ -0,0 +1,18 @@ +// +// NSData+RXRDigest.h +// Rexxar +// +// Created by GUO Lin on 11/10/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +@import Foundation; + +@interface NSData (RXRDigest) + +- (NSString *)md5; +- (NSString *)sha1; +- (NSString *)sha256; +- (NSString *)sha512; + +@end diff --git a/Rexxar/Core/Extension/NSData+RXRDigest.m b/Rexxar/Core/Extension/NSData+RXRDigest.m new file mode 100644 index 0000000..fe6776f --- /dev/null +++ b/Rexxar/Core/Extension/NSData+RXRDigest.m @@ -0,0 +1,50 @@ +// +// NSData+DOUDigest.m +// Rexxar +// +// Created by GUO Lin on 10/04/13. +// Copyright (c) 2013 Douban Inc. All rights reserved. +// + +#import "NSData+RXRDigest.h" +#include + +#define DOU_DIGEST_PERFORM(_LENGTH, _FUNCTION) \ + NSMutableString *result; \ + do { \ + size_t i; \ + unsigned char md[(_LENGTH)]; \ + \ + bzero(md, sizeof(md)); \ + (_FUNCTION)([self bytes], (CC_LONG)[self length], md); \ + \ + result = [NSMutableString stringWithCapacity:(_LENGTH) * 2]; \ + for (i = 0; i < (_LENGTH); ++i) { \ + [result appendFormat:@"%02x", md[i]]; \ + } \ + } while (0); \ + return [result copy] + +@implementation NSData (RXRDigest) + +- (NSString *)md5 +{ + DOU_DIGEST_PERFORM(CC_MD5_DIGEST_LENGTH, CC_MD5); +} + +- (NSString *)sha1 +{ + DOU_DIGEST_PERFORM(CC_SHA1_DIGEST_LENGTH, CC_SHA1); +} + +- (NSString *)sha256 +{ + DOU_DIGEST_PERFORM(CC_SHA256_DIGEST_LENGTH, CC_SHA256); +} + +- (NSString *)sha512 +{ + DOU_DIGEST_PERFORM(CC_SHA512_DIGEST_LENGTH, CC_SHA512); +} + +@end diff --git a/Rexxar/Core/Extension/NSDictionary+RXRMultipleItems.h b/Rexxar/Core/Extension/NSDictionary+RXRMultipleItems.h new file mode 100644 index 0000000..d194ce7 --- /dev/null +++ b/Rexxar/Core/Extension/NSDictionary+RXRMultipleItems.h @@ -0,0 +1,33 @@ +// +// NSDictionary+RXRMultipleItems.h +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import Foundation; + +@interface NSDictionary (RXRMultipleItems) + +/** + * 字典对应关键字的元素,该元素如果是数组,返回数组的首个元素。 + * + * Return the first item of array for the specificed key. + * -[NSDictionary objectForKey:] will return an object or an array depending on how the NSDictionary is created. + * + * @param key 关键字 + */ +- (id)rxr_itemForKey:(id)key; + +/** + * 字典对应该关键字的元素,该元素如果是数组,返回该数组。 + * + * Return a NSArray object which contains all the items for specificed key. + * -[NSDictionary objectForKey:] will return an object or an array depending on how the NSDictionary is created. + * + * @param key 关键字 + */ +- (NSArray *)rxr_allItemsForKey:(id)key; + +@end diff --git a/Rexxar/Core/Extension/NSDictionary+RXRMultipleItems.m b/Rexxar/Core/Extension/NSDictionary+RXRMultipleItems.m new file mode 100644 index 0000000..f6a30e5 --- /dev/null +++ b/Rexxar/Core/Extension/NSDictionary+RXRMultipleItems.m @@ -0,0 +1,27 @@ +// +// NSDictionary+RXRMultipleItems.m +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "NSDictionary+RXRMultipleItems.h" + +@implementation NSDictionary (RXRMultipleItems) + +- (id)rxr_itemForKey:(id)key { + id obj = [self objectForKey:key]; + if ([obj isKindOfClass:[NSArray class]]) { + return [obj count] > 0 ? [obj objectAtIndex:0] : nil; + } else { + return obj; + } +} + +- (NSArray *)rxr_allItemsForKey:(id)key { + id obj = [self objectForKey:key]; + return [obj isKindOfClass:[NSArray class]] ? obj : (obj ? [NSArray arrayWithObject:obj] : nil); +} + +@end diff --git a/Rexxar/Core/Extension/NSMutableDictionary+RXRMultipleItems.h b/Rexxar/Core/Extension/NSMutableDictionary+RXRMultipleItems.h new file mode 100644 index 0000000..f5b2315 --- /dev/null +++ b/Rexxar/Core/Extension/NSMutableDictionary+RXRMultipleItems.h @@ -0,0 +1,21 @@ +// +// NSMutableDictionary+RXRMultipleItems.h +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import Foundation; + +@interface NSMutableDictionary (RXRMultipleItems) + +/** + * 在字典以关键字添加一个元素。 + * + * @param item 待添加的元素 + * @param aKey 关键字 + */ +- (void)rxr_addItem:(id)item forKey:(id)key; + +@end diff --git a/Rexxar/Core/Extension/NSMutableDictionary+RXRMultipleItems.m b/Rexxar/Core/Extension/NSMutableDictionary+RXRMultipleItems.m new file mode 100644 index 0000000..205cccc --- /dev/null +++ b/Rexxar/Core/Extension/NSMutableDictionary+RXRMultipleItems.m @@ -0,0 +1,28 @@ +// +// NSMutableDictionary+RXRMultipleItems.m +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "NSMutableDictionary+RXRMultipleItems.h" + +@implementation NSMutableDictionary (RXRMultipleItems) + +- (void)rxr_addItem:(id)item forKey:(id)aKey { + if (item == nil) { + return; + } + id obj = [self objectForKey:aKey]; + NSMutableArray *array = nil; + if ([obj isKindOfClass:[NSArray class]]) { + array = [NSMutableArray arrayWithArray:obj]; + } else { + array = obj ? [NSMutableArray arrayWithObject:obj] : [NSMutableArray array]; + } + [array addObject:item]; + [self setObject:[array copy] forKey:aKey]; +} + +@end diff --git a/Rexxar/Core/Extension/NSString+RXRURLEscape.h b/Rexxar/Core/Extension/NSString+RXRURLEscape.h new file mode 100644 index 0000000..738cd8e --- /dev/null +++ b/Rexxar/Core/Extension/NSString+RXRURLEscape.h @@ -0,0 +1,23 @@ +// +// NSString+RXRURLEscape.h +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import Foundation; + +@interface NSString (RXRURLEscape) + +/** + * url 字符串编码 + */ +- (NSString *)rxr_encodingStringUsingURLEscape; + +/** + * url 字符串解码 + */ +- (NSString *)rxr_decodingStringUsingURLEscape; + +@end diff --git a/Rexxar/Core/Extension/NSString+RXRURLEscape.m b/Rexxar/Core/Extension/NSString+RXRURLEscape.m new file mode 100644 index 0000000..11c2c16 --- /dev/null +++ b/Rexxar/Core/Extension/NSString+RXRURLEscape.m @@ -0,0 +1,38 @@ +// +// NSString+RXRURLEscape.m +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "NSString+RXRURLEscape.h" + +@implementation NSString (RXRURLEscape) + +- (NSString *)rxr_encodingStringUsingURLEscape +{ + CFStringRef originStringRef = (__bridge_retained CFStringRef)self; + CFStringRef escapedStringRef = CFURLCreateStringByAddingPercentEscapes(NULL, + originStringRef, + NULL, + (CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ", + kCFStringEncodingUTF8); + NSString *escapedString = (__bridge_transfer NSString *)escapedStringRef; + CFRelease(originStringRef); + return escapedString; +} + +- (NSString *)rxr_decodingStringUsingURLEscape +{ + CFStringRef originStringRef = (__bridge_retained CFStringRef)self; + CFStringRef escapedStringRef = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, + originStringRef, + CFSTR(""), + kCFStringEncodingUTF8); + NSString *escapedString = (__bridge_transfer NSString *)escapedStringRef; + CFRelease(originStringRef); + return escapedString; +} + +@end diff --git a/Rexxar/Core/Extension/NSURL+Rexxar.h b/Rexxar/Core/Extension/NSURL+Rexxar.h new file mode 100644 index 0000000..b447465 --- /dev/null +++ b/Rexxar/Core/Extension/NSURL+Rexxar.h @@ -0,0 +1,30 @@ +// +// NSURL+Rexxar.h +// Rexxar +// +// Created by GUO Lin on 1/18/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import Foundation; + +@interface NSURL (Rexxar) + +/** + * 将一个字典内容转换成 url 的 query 的形式。 + * + * @param dict 需要转换成的 query 的 dictionary。 + */ ++ (NSString *)rxr_queryFromDictionary:(NSDictionary *)dict; + +/** + * 该 url 的 scheme 是否是 http 或 https? + */ +- (BOOL)rxr_isHttpOrHttps; + +/** + * 将该 url 的 query 以字典形式返回。 + */ +- (NSDictionary *)rxr_queryDictionary; + +@end diff --git a/Rexxar/Core/Extension/NSURL+Rexxar.m b/Rexxar/Core/Extension/NSURL+Rexxar.m new file mode 100644 index 0000000..aa86345 --- /dev/null +++ b/Rexxar/Core/Extension/NSURL+Rexxar.m @@ -0,0 +1,65 @@ +// +// NSURL+Rexxar.m +// Rexxar +// +// Created by GUO Lin on 1/18/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "NSURL+Rexxar.h" +#import "NSString+RXRURLEscape.h" +#import "NSMutableDictionary+RXRMultipleItems.h" + +@implementation NSURL (Rexxar) + ++ (NSString *)rxr_queryFromDictionary:(NSDictionary *)dict +{ + NSMutableArray *pairs = [NSMutableArray array]; + [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { + [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, obj]]; + }]; + + NSString *query = nil; + if (pairs.count > 0) { + query = [pairs componentsJoinedByString:@"&"]; + } + return query; +} + +- (BOOL)rxr_isHttpOrHttps +{ + if ([self.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || + [self.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { + return YES; + } + return NO; +} + +- (NSDictionary *)rxr_queryDictionary { + NSString *query = [self query]; + if ([query length] == 0) { + return nil; + } + + // Replace '+' with space + query = [query stringByReplacingOccurrencesOfString:@"+" withString:@"%20"]; + + NSCharacterSet *delimiterSet = [NSCharacterSet characterSetWithCharactersInString:@"&;"]; + NSMutableDictionary *pairs = [NSMutableDictionary dictionary]; + + NSScanner *scanner = [[NSScanner alloc] initWithString:query]; + while (![scanner isAtEnd]) { + NSString *pairString = nil; + [scanner scanUpToCharactersFromSet:delimiterSet intoString:&pairString]; + [scanner scanCharactersFromSet:delimiterSet intoString:NULL]; + NSArray *kvPair = [pairString componentsSeparatedByString:@"="]; + if (kvPair.count == 2) { + [pairs rxr_addItem:[[kvPair objectAtIndex:1] rxr_decodingStringUsingURLEscape] + forKey:[[kvPair objectAtIndex:0] rxr_decodingStringUsingURLEscape]]; + } + } + + return [pairs copy]; +} + +@end diff --git a/Rexxar/Core/Extension/UIColor+Rexxar.h b/Rexxar/Core/Extension/UIColor+Rexxar.h new file mode 100644 index 0000000..64de807 --- /dev/null +++ b/Rexxar/Core/Extension/UIColor+Rexxar.h @@ -0,0 +1,20 @@ +// +// UIColor+Rexxar.h +// Rexxar +// +// Created by Tony Li on 12/9/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +@import UIKit; + +@interface UIColor (Rexxar) + +/** + * 字符串形式创建的 UIColor。 + * + * @param colorComponents 颜色的字符串,颜色格式:rgba(0,0,0,0)。 + */ ++ (instancetype)rxr_colorWithComponent:(NSString *)colorComponents; + +@end diff --git a/Rexxar/Core/Extension/UIColor+Rexxar.m b/Rexxar/Core/Extension/UIColor+Rexxar.m new file mode 100644 index 0000000..6fdadb9 --- /dev/null +++ b/Rexxar/Core/Extension/UIColor+Rexxar.m @@ -0,0 +1,52 @@ +// +// UIColor+Rexxar.m +// Rexxar +// +// Created by Tony Li on 12/9/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +#import "UIColor+Rexxar.h" + +@implementation UIColor (Rexxar) + ++ (instancetype)rxr_colorWithComponent:(NSString *)colorComponents +{ + UIColor *color = nil; + + NSScanner *scanner = [NSScanner scannerWithString:colorComponents]; + scanner.charactersToBeSkipped = [NSCharacterSet whitespaceCharacterSet]; + + NSString *colorType = nil; + if ([scanner scanUpToString:@"(" intoString:&colorType] && colorType // 解析颜色值类型 + && scanner.scanLocation < (scanner.string.length - 1) && ++scanner.scanLocation && !scanner.atEnd // 跳过类型后的 `(` + ) { + NSUInteger length = colorType.length; + if (length <= 4) { + // RGB / HSL 三部分 + alpha + NSInteger components[4] = {-1, -1, -1, 255}; + for (NSUInteger index = 0; index < length; ++index) { + if (index > 0) { + [scanner scanString:@"," intoString:nil]; + } + [scanner scanInteger:&components[index]]; + } + + if (components[0] >= 0 && components[1] >= 0 && components[2] >= 0 && components[3] >= 0 + && [colorType hasPrefix:@"rgb"]) { + color = [UIColor colorWithRed:(components[0] / 255.f) + green:(components[1] / 255.f) + blue:(components[2] / 255.f) + alpha:(components[3] / 255.f)]; + } + } + } + + if (color == nil) { + NSLog(@"Unkown color: %@", colorComponents); + } + + return color; +} + +@end diff --git a/Rexxar/Core/RXRCacheFileIntercepter.h b/Rexxar/Core/RXRCacheFileIntercepter.h new file mode 100644 index 0000000..b71d1b9 --- /dev/null +++ b/Rexxar/Core/RXRCacheFileIntercepter.h @@ -0,0 +1,17 @@ +// +// RXRCacheFileIntercepter.h +// Rexxar +// +// Created by Tony Li on 11/4/15. +// Copyright © 2015 Douban Inc. All rights reserved. +// + +@import Foundation; + +/** + * `RXRCacheFileIntercepter` 用于拦截进入 Rexxar Container 的请求,并可对请求做所需的变化。 + * 目前完成: 1 本地文件映射,如请求服务器上的 html 资源,先检查本地,如存在则使用本地html文件(包括本地缓存,和应用内置资源)显示。 + */ +@interface RXRCacheFileIntercepter : NSURLProtocol + +@end diff --git a/Rexxar/Core/RXRCacheFileIntercepter.m b/Rexxar/Core/RXRCacheFileIntercepter.m new file mode 100644 index 0000000..6964369 --- /dev/null +++ b/Rexxar/Core/RXRCacheFileIntercepter.m @@ -0,0 +1,184 @@ +// +// RXRCacheFileIntercepter.m +// Rexxar +// +// Created by Tony Li on 11/4/15. +// Copyright © 2015 Douban Inc. All rights reserved. +// + +#import "RXRCacheFileIntercepter.h" + +#import "RXRRouteFileCache.h" + +#import "RXRLogging.h" +#import "NSURL+Rexxar.h" + +static NSString * const RXRCacheFileIntercepterHandledKey = @"RXRCacheFileIntercepterHandledKey"; + +@interface RXRCacheFileIntercepter () + +@property (nonatomic, strong) NSURLConnection *connection; +@property (nonatomic, strong) NSFileHandle *fileHandle; +@property (nonatomic, strong) NSString *responseDataFilePath; + +@end + + +@implementation RXRCacheFileIntercepter + +#pragma mark - NSURLProtocol's methods + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + // 不是 HTTP 请求,不处理 + if (![request.URL rxr_isHttpOrHttps]) { + return NO; + } + // 请求被忽略(被标记为忽略或者已经请求过),不处理 + if ([self isRequestIgnored:request]) { + return NO; + } + // 请求不是来自浏览器,不处理 + if (![request.allHTTPHeaderFields[@"User-Agent"] hasPrefix:@"Mozilla"]) { + return NO; + } + + // 如果请求不需要被拦截,不处理 + if (![self shouldInterceptRequest:request]) { + return NO; + } + + return YES; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request +{ + return request; +} + +- (void)startLoading +{ + NSParameterAssert(self.connection == nil); + NSParameterAssert([[self class] canInitWithRequest:self.request]); + + RXRDebugLog(@"Intercept <%@> within <%@>", self.request.URL, self.request.mainDocumentURL); + + __block NSMutableURLRequest *request = nil; + if ([self.request isKindOfClass:[NSMutableURLRequest class]]) { + request = (NSMutableURLRequest *)self.request; + } else { + request = [self.request mutableCopy]; + } + + NSURL *localURL = [self _rxr_localFileURL:request.URL]; + if (localURL) { + request.URL = localURL; + } + + [[self class] markRequestAsIgnored:request]; + self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]; +} + +- (void)stopLoading +{ + [self.connection cancel]; +} + +#pragma mark - NSURLConnectionDataDelegate' methods + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response +{ + NSURLRequest *request = connection.currentRequest; + + if (![request.URL isFileURL] && + [[self class] shouldInterceptRequest:request] && + [[self class] _rxr_isCacheableResponse:response]) { + + self.responseDataFilePath = [self _rxr_temporaryFilePath]; + [[NSFileManager defaultManager] createFileAtPath:self.responseDataFilePath contents:nil attributes:nil]; + self.fileHandle = nil; + self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.responseDataFilePath]; + } + + [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data +{ + if ([[self class] shouldInterceptRequest:connection.currentRequest] && self.fileHandle) { + [self.fileHandle writeData:data]; + } + [self.client URLProtocol:self didLoadData:data]; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection +{ + if ([[self class] shouldInterceptRequest:connection.currentRequest] && self.fileHandle) { + [self.fileHandle closeFile]; + self.fileHandle = nil; + NSData *data = [NSData dataWithContentsOfFile:self.responseDataFilePath]; + [[RXRRouteFileCache sharedInstance] saveRouteFileData:data withRemoteURL:connection.currentRequest.URL]; + } + [self.client URLProtocolDidFinishLoading:self]; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error +{ + if ([[self class] shouldInterceptRequest:connection.currentRequest] && self.fileHandle) { + [self.fileHandle closeFile]; + self.fileHandle = nil; + [[NSFileManager defaultManager] removeItemAtPath:self.responseDataFilePath error:nil]; + } + [self.client URLProtocol:self didFailWithError:error]; +} + +#pragma mark - Public methods + ++ (BOOL)shouldInterceptRequest:(NSURLRequest *)request +{ + NSString *extension = request.URL.pathExtension; + if ([extension isEqualToString:@"js"] || + [extension isEqualToString:@"css"]) { + return YES; + } + return NO; +} + ++ (void)markRequestAsIgnored:(NSMutableURLRequest *)request +{ + [NSURLProtocol setProperty:@YES forKey:RXRCacheFileIntercepterHandledKey inRequest:request]; +} + ++ (BOOL)isRequestIgnored:(NSURLRequest *)request +{ + if ([NSURLProtocol propertyForKey:RXRCacheFileIntercepterHandledKey inRequest:request]) { + return YES; + } + return NO; +} + +#pragma mark - Private methods + +- (NSURL *)_rxr_localFileURL:(NSURL *)remoteURL +{ + NSURL *URL = [[NSURL alloc] initWithScheme:[remoteURL scheme] + host:[remoteURL host] + path:[remoteURL path]]; + NSURL *localURL = [[RXRRouteFileCache sharedInstance] routeFileURLForRemoteURL:URL]; + return localURL; +} + ++ (BOOL)_rxr_isCacheableResponse:(NSURLResponse *)response +{ + NSSet *cachableTypes = [NSSet setWithObjects:@"application/javascript", @"application/x-javascript", + @"text/javascript", @"text/css", nil]; + return [cachableTypes containsObject:response.MIMEType]; +} + +- (NSString *)_rxr_temporaryFilePath +{ + NSString *fileName = [[NSUUID UUID] UUIDString]; + return [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; +} + +@end diff --git a/Rexxar/Core/RXRConfig.h b/Rexxar/Core/RXRConfig.h new file mode 100644 index 0000000..c5611c6 --- /dev/null +++ b/Rexxar/Core/RXRConfig.h @@ -0,0 +1,108 @@ +// +// RXRConfig.h +// Rexxar +// +// Created by GUO Lin on 5/30/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRConfig` 提供对 Rexxar 的全局配置接口。 + */ +@interface RXRConfig : NSObject + +/** + * 设置 rxrProtocolScheme。 + * + * @discussion Rexxar-Container 实现了实现了一些供 Web 调用的功能。Web 调用这些功能的方式是发出一个特定的请求。 + * `rxrProtocolHost` 是对这些特定请求的 scheme 的商定。如不设置,缺省为 douban。 + */ ++ (void)setRXRProtocolScheme:(NSString *)scheme; + +/** + * 设置 rxrProtocolScheme。 + * + * @discussion Rexxar-Container 实现了实现了一些供 Web 调用的功能。Web 调用这些功能的方式是发出一个特定的请求。 + * `rxrProtocolHost` 是对这些特定请求的 scheme 的商定。如不设置,缺省为 douban。 + */ ++ (NSString *)rxrProtocolScheme; + +/** + * 设置 rxrProtocolHost。 + * + * @discussion Rexxar-Container 实现了实现了一些供 Web 调用的功能。Web 调用这些功能的方式是发出一个特定的请求。 + * `rxrProtocolHost` 是对这些特定请求的 host 的商定。如不设置,缺省为 rexxar-container。 + */ ++ (void)setRXRProtocolHost:(NSString *)host; + +/** + * 读取 rxrProtocolHost。 + * + * @discussion Rexxar-Container 实现了实现了一些供 Web 调用的功能。Web 调用这些功能的方式是发出一个特定的请求。 + * `rxrProtocolHost` 是对这些特定请求的 host 的商定。如不设置,缺省为 rexxar-container。 + */ ++ (NSString *)rxrProtocolHost; + +/** + * 设置 Routes Map URL。 + */ ++ (void)setRoutesMapURL:(NSURL *)routesMapURL; + +/** + * 读取 Routes Map URL。 + */ ++ (nullable NSURL *)routesMapURL; + +/** + * 设置 Route Files 的 Cache URL。 + */ ++ (void)setRoutesCachePath:(nullable NSString *)routesCachePath; + +/** + * 读取 Route Files 的 Cache URL。 + */ ++ (nullable NSString *)routesCachePath; + +/** + * 设置 Route Files 的 Resource Path。 + */ ++ (void)setRoutesResourcePath:(nullable NSString *)routesResourcePath; + +/** + * 读取 Route Files 的 Resource Path。 + */ ++ (nullable NSString *)routesResourcePath; + +/** + * 设置 Rexxar 接收的外部 User-Agent。Rexxar 会将这个 UserAgent 加到其所发出的所有的请求的 Headers 中。 + */ ++ (void)setExternalUserAgent:(NSString *)userAgent; + +/** + * 读取 Rexxar 接收的外部 User-Agent。 + */ ++ (NSString *)externalUserAgent; + +/** + * 更新全局配置。 + */ ++ (void)updateConfig; + +/** + * 全局设置 Rexxar Container 是否使用路由文件的本地 Cache。 + * 如果使用,优先读取本地缓存的 html 文件;如果不使用,则每次都读取服务器的 html 文件。 + */ ++ (void)setCacheEnable:(BOOL)isCacheEnable; + +/** + * 读取 Rexxar Container 是否使用缓存的全局配置。该缺省是打开的。Rexxar Container 会使用缓存保存 html 文件。 + */ ++ (BOOL)isCacheEnable; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Rexxar/Core/RXRConfig.m b/Rexxar/Core/RXRConfig.m new file mode 100644 index 0000000..ac25db3 --- /dev/null +++ b/Rexxar/Core/RXRConfig.m @@ -0,0 +1,147 @@ +// +// RXRConfig.m +// Rexxar +// +// Created by GUO Lin on 5/30/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import UIKit; + +#import "RXRConfig.h" +#import "RXRRouteManager.h" + +@implementation RXRConfig + +static NSString *sRXRProtocolScheme; +static NSString *sRXRProtocolHost; +static NSString *sRXRUserAgent; +static NSURL *sRoutesMapURL; +static NSString *sRoutesCachePath; +static NSString *sRoutesResourcePath; +static BOOL sIsCacheEnable = YES; + +static NSString * const DefaultRXRScheme = @"douban"; +static NSString * const DefaultRXRHost = @"rexxar-container"; + + ++ (void)setRXRProtocolScheme:(NSString *)scheme +{ + @synchronized (self) { + sRXRProtocolScheme = scheme; + } +} + ++ (NSString *)rxrProtocolScheme +{ + if (sRXRProtocolScheme) { + return sRXRProtocolScheme; + } + return DefaultRXRScheme; +} + ++ (void)setRXRProtocolHost:(NSString *)host +{ + @synchronized (self) { + sRXRProtocolHost = host; + } +} + ++ (NSString *)rxrProtocolHost +{ + if (sRXRProtocolHost) { + return sRXRProtocolHost; + } + return DefaultRXRHost; +} + ++ (void)setRoutesMapURL:(NSURL *)routesMapURL +{ + @synchronized (self) { + sRoutesMapURL = routesMapURL; + } +} + ++ (NSURL *)routesMapURL +{ + return sRoutesMapURL; +} + ++ (void)setRoutesCachePath:(NSString *)routesCachePath +{ + @synchronized (self) { + sRoutesCachePath = routesCachePath; + } +} + ++ (NSString *)routesCachePath +{ + return sRoutesCachePath; +} + ++ (void)setRoutesResourcePath:(NSString *)routesResourcePath +{ + @synchronized (self) { + sRoutesResourcePath = routesResourcePath; + } +} + ++ (NSString *)routesResourcePath +{ + return sRoutesResourcePath; +} + ++ (void)setExternalUserAgent:(NSString *)externalUserAgent +{ + if ([sRXRUserAgent isEqualToString:externalUserAgent]) { + return; + } + + @synchronized (self) { + sRXRUserAgent = externalUserAgent; + + NSArray *externalUserAgents = [externalUserAgent componentsSeparatedByString:@" "]; + + NSMutableString *newUserAgent = [NSMutableString string]; + NSString *oldUserAgent = [[UIWebView new] stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"]; + if (oldUserAgent) { + [newUserAgent appendString:oldUserAgent]; + } + + for (NSString *item in externalUserAgents) { + if (![newUserAgent containsString:item]) { + [newUserAgent appendFormat:@" %@", item]; + } + } + + [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent": newUserAgent}]; + + } +} + ++ (NSString *)externalUserAgent +{ + return sRXRUserAgent; +} + ++ (void)updateConfig +{ + RXRRouteManager *routeManager = [RXRRouteManager sharedInstance]; + routeManager.routesMapURL = sRoutesMapURL; + [routeManager setCachePath:sRoutesCachePath]; + [routeManager setResoucePath:sRoutesResourcePath]; +} + ++ (void)setCacheEnable:(BOOL)isCacheEnable +{ + @synchronized (self) { + sIsCacheEnable = isCacheEnable; + } +} + ++(BOOL)isCacheEnable +{ + return sIsCacheEnable; +} + +@end diff --git a/Rexxar/Core/RXRLogging.h b/Rexxar/Core/RXRLogging.h new file mode 100644 index 0000000..98e232c --- /dev/null +++ b/Rexxar/Core/RXRLogging.h @@ -0,0 +1,17 @@ +// +// RXRLogging.h +// Rexxar +// +// Created by Tony Li on 12/18/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +#ifdef DEBUG +#define RXRLog(...) NSLog(@"[Rexxar] " __VA_ARGS__) +#else /* DEBUG */ +#define RXRLog(...) +#endif /* DEBUG */ + +#define RXRDebugLog(...) RXRLog(@"[DEBUG] " __VA_ARGS__) +#define RXRWarnLog(...) RXRLog(@"[WARN] " __VA_ARGS__) +#define RXRErrorLog(...) RXRLog(@"[ERROR] " __VA_ARGS__) diff --git a/Rexxar/Core/RXRNSURLProtocol.h b/Rexxar/Core/RXRNSURLProtocol.h new file mode 100644 index 0000000..b7052fe --- /dev/null +++ b/Rexxar/Core/RXRNSURLProtocol.h @@ -0,0 +1,29 @@ +// +// RXRNSURLProtocol.h +// Rexxar +// +// Created by GUO Lin on 5/17/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +@import Foundation; + +@interface RXRNSURLProtocol : NSURLProtocol + +@property (nonatomic, strong) NSURLConnection *connection; + +/** + * 将该请求标记为可以忽略 + * + * @param request + */ ++ (void)markRequestAsIgnored:(NSMutableURLRequest *)request; + +/** + * 判断该请求是否是被忽略的 + * + * @param request + */ ++ (BOOL)isRequestIgnored:(NSURLRequest *)request; + +@end diff --git a/Rexxar/Core/RXRNSURLProtocol.m b/Rexxar/Core/RXRNSURLProtocol.m new file mode 100644 index 0000000..6a7d775 --- /dev/null +++ b/Rexxar/Core/RXRNSURLProtocol.m @@ -0,0 +1,59 @@ +// +// RXRNSURLProtocol.m +// Rexxar +// +// Created by GUO Lin on 5/17/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +#import "RXRNSURLProtocol.h" + +@implementation RXRNSURLProtocol + +- (void)stopLoading +{ + [self.connection cancel]; + self.connection = nil; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request +{ + return request; +} + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response +{ + [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data +{ + [self.client URLProtocol:self didLoadData:data]; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection +{ + [self.client URLProtocolDidFinishLoading:self]; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error +{ + [self.client URLProtocol:self didFailWithError:error]; +} + ++ (void)markRequestAsIgnored:(NSMutableURLRequest *)request +{ + NSString *key = NSStringFromClass([self class]); + [NSURLProtocol setProperty:@YES forKey:key inRequest:request]; +} + ++ (BOOL)isRequestIgnored:(NSURLRequest *)request +{ + NSString *key = NSStringFromClass([self class]); + if ([NSURLProtocol propertyForKey:key inRequest:request]) { + return YES; + } + return NO; +} + +@end diff --git a/Rexxar/Core/RXRRoute.h b/Rexxar/Core/RXRRoute.h new file mode 100644 index 0000000..b0d2447 --- /dev/null +++ b/Rexxar/Core/RXRRoute.h @@ -0,0 +1,35 @@ +// +// RXRRoute.h +// Rexxar +// +// Created by Tony Li on 11/20/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRRoute` 路由信息对象。 + */ +@interface RXRRoute : NSObject + +/** + * 以一个字典初始化路由信息对象。 + */ +- (instancetype)initWithDictionary:(NSDictionary *)dict; + +/** + * 匹配该路由的 URI 正则表达式。 + */ +@property (nonatomic, readonly) NSRegularExpression *URIRegex; + +/** + * 该路由对于的 html 文件地址。 + */ +@property (nonatomic, readonly) NSURL *remoteHTML; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Rexxar/Core/RXRRoute.m b/Rexxar/Core/RXRRoute.m new file mode 100644 index 0000000..793facb --- /dev/null +++ b/Rexxar/Core/RXRRoute.m @@ -0,0 +1,22 @@ +// +// RXRRoute.m +// Rexxar +// +// Created by Tony Li on 11/20/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +#import "RXRRoute.h" + +@implementation RXRRoute + +- (instancetype)initWithDictionary:(NSDictionary *)dict +{ + if ( (self = [super init]) ) { + _remoteHTML = [NSURL URLWithString:dict[@"remote_file"]]; + _URIRegex = [NSRegularExpression regularExpressionWithPattern:dict[@"uri"] options:0 error:nil]; + } + return self; +} + +@end diff --git a/Rexxar/Core/RXRRouteFileCache.h b/Rexxar/Core/RXRRouteFileCache.h new file mode 100644 index 0000000..4d62b38 --- /dev/null +++ b/Rexxar/Core/RXRRouteFileCache.h @@ -0,0 +1,93 @@ +// +// RXRRouteFileCache.h +// Rexxar +// +// Created by GUO Lin on 5/11/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRRouteCache` 提供对 Route files 的读取。 + * Route files 包括用于渲染 rexxar 页面的静态文件,例如 html, css, js, image。 + * 为何我们会自己实现一个缓存,而不使用 NSURLCache? + * 因为获取 Route 信息有两个来源,要么从本地缓存(上线后发布,下载的资源会有本地缓存),要么资源文件夹(上线时打入的)。这和 NSURLCache 缓存机制不同。 + * 1. 本地缓存; + * 2. 资源:应用打包的资源文件中有一份, 这部分资源不会改变。 + * + * `RXRRouteCache` offer the access method of Route files. + * Route files include rexxar page 's static file like html, css, js, image. + * Why we write this cache instead of using NSURLCache? + * It's because that there are two sources of Route files,local cache (create and save the downloaded resources in cache after app release) or resource file (in the release ipa): + * 1. local cache: disk cache; + * 2. resource file: a copy in ipa's resource bundle, this resource will not change. + */ +@interface RXRRouteFileCache : NSObject + +/** + * cachePath, 如果是相对路径的话,则认为其是相对于应用缓存路径。 + */ +@property (nonatomic, copy) NSString *cachePath; + +/** + * Rexxar 资源地址, 会在打包应用时,打包进入 ipa。如果是相对路径的话,则认为其是相对于 main bundle 路径。 + */ +@property (nonatomic, copy) NSString *resourcePath; + +/** + * 单例方法,获取一个 RXRRouteFileCache 实例。 + * + * Get RXRRouteFileCache Singleton instance. + */ ++ (RXRRouteFileCache *)sharedInstance; + +/** + * 存储 Route Map File,文件名为 `routes.json`。 + * + * Save routes map file with file name : `routes.json`. + */ +- (void)saveRoutesMapFile:(NSData *)data; + +/** + * 读取 Route Map File。 + * + * Read routes map file. + */ +- (nullable NSData *)routesMapFile; + + +/** + * 将 `url` 下载下来的资源数据,存入缓存。 + * + * Save the route file with url. + */ +- (void)saveRouteFileData:(NSData *)data withRemoteURL:(NSURL *)url; + +/** + * 从缓存中读取出 `url` 下载的资源。 + * + * Read the route file according url. + */ +- (nullable NSData *)routeFileDataForRemoteURL:(NSURL *)url; + +/** + * 获取远程 url 对于的本地 url。先在缓存文件夹中寻找,再在资源文件夹中寻找。如果在缓存文件和资源文件中都找不到对应的本地文件,返回 nil。 + * + * Get the local url for remote url. Search the local file first from cache file, then from resource file. + * If it dose not exist in cache file and resource file, return nil. + */ +- (nullable NSURL *)routeFileURLForRemoteURL:(NSURL *)url; + +/** + * 清理缓存。 + * + * Clean Cache。 + */ +- (void)cleanCache; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Rexxar/Core/RXRRouteFileCache.m b/Rexxar/Core/RXRRouteFileCache.m new file mode 100644 index 0000000..81ee9dc --- /dev/null +++ b/Rexxar/Core/RXRRouteFileCache.m @@ -0,0 +1,182 @@ +// +// RXRRouteFileCache.m +// Rexxar +// +// Created by GUO Lin on 5/11/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "RXRRouteFileCache.h" +#import "RXRConfig.h" + +#import "RXRLogging.h" +#import "NSData+RXRDigest.h" + +static NSString * const RoutesMapFile = @"routes.json"; + +@implementation RXRRouteFileCache + ++ (RXRRouteFileCache *)sharedInstance +{ + static RXRRouteFileCache *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[RXRRouteFileCache alloc] init]; + instance.cachePath = [RXRConfig routesCachePath]; + instance.resourcePath = [RXRConfig routesResourcePath]; + }); + return instance; +} + +- (instancetype)initWithCachePath:(NSString *)cachePath + resourcePath:(NSString *)resourcePath +{ + self = [super init]; + if (self) { + } + return self; +} + +#pragma mark - Save & Read methods + +- (void)setCachePath:(NSString *)cachePath +{ + // cache dir + if (!cachePath) { + // 默认缓存路径:/.rexxar + cachePath = [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:@".rexxar"]; + } + + if (![cachePath isAbsolutePath]) { + cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) + firstObject] stringByAppendingPathComponent:cachePath]; + } + + _cachePath = [cachePath copy]; + + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtPath:_cachePath + withIntermediateDirectories:YES + attributes:@{} + error:&error]; + if (error) { + RXRDebugLog(@"Failed to create directory: %@", _cachePath); + } +} + +- (void)setResourcePath:(NSString *)resourcePath +{ + // resource dir + if (!resourcePath && [resourcePath length] > 0) { + // 默认资源路径:/rexxar + resourcePath = [[NSBundle mainBundle] pathForResource:@"rexxar" ofType:nil]; + } + + if (![resourcePath isAbsolutePath]) { + resourcePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:resourcePath]; + } + _resourcePath = [resourcePath copy]; +} + +- (void)cleanCache +{ + NSFileManager *manager = [NSFileManager defaultManager]; + [manager removeItemAtPath:self.cachePath error:nil]; + [manager createDirectoryAtPath:self.cachePath + withIntermediateDirectories:YES + attributes:@{} + error:NULL]; +} + +- (void)saveRoutesMapFile:(NSData *)data +{ + NSString *filePath = [self.cachePath stringByAppendingPathComponent:RoutesMapFile]; + if (data == nil) { + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + } else { + [data writeToFile:filePath atomically:YES]; + } +} + +- (NSData *)routesMapFile +{ + NSString *filePath = [self.cachePath stringByAppendingPathComponent:RoutesMapFile]; + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + return [NSData dataWithContentsOfFile:filePath]; + } + + filePath = [self.resourcePath stringByAppendingPathComponent:RoutesMapFile]; + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + return [NSData dataWithContentsOfFile:filePath]; + } + + return nil; +} + +- (void)saveRouteFileData:(NSData *)data withRemoteURL:(NSURL *)url +{ + NSString *filePath = [self _rxr_cachedRouteFilePathForRemoteURL:url]; + if (data == nil) { + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + } else { + [data writeToFile:filePath atomically:YES]; + } +} + +- (NSData *)routeFileDataForRemoteURL:(NSURL *)url +{ + NSString *filePath = [self routeFilePathForRemoteURL:url]; + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + return [NSData dataWithContentsOfFile:filePath]; + } + + return nil; +} + +- (NSString *)routeFilePathForRemoteURL:(NSURL *)url +{ + NSString *filePath = [self _rxr_cachedRouteFilePathForRemoteURL:url]; + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + return filePath; + } + + filePath = [self _rxr_resourceRouteFilePathForRemoteURL:url]; + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + return filePath; + } + + return nil; +} + +- (NSURL *)routeFileURLForRemoteURL:(NSURL *)url +{ + if (url == nil) { + return nil; + } + + NSString *filePath = [self routeFilePathForRemoteURL:url]; + return [[NSFileManager defaultManager] fileExistsAtPath:filePath] ? [NSURL fileURLWithPath:filePath] : nil; +} + +#pragma mark - Private methods + +- (NSString *)_rxr_cachedRouteFilePathForRemoteURL:(NSURL *)url +{ + NSString *md5 = [[url.absoluteString dataUsingEncoding:NSUTF8StringEncoding] md5]; + NSString *filename = [self.cachePath stringByAppendingPathComponent:md5]; + return [filename stringByAppendingPathExtension:url.pathExtension]; +} + +- (NSString *)_rxr_resourceRouteFilePathForRemoteURL:(NSURL *)url +{ + NSString *filename = nil; + NSArray *pathComps = url.pathComponents; + if (pathComps.count > 2) { // 取后两位作为文件路径 + filename = [[pathComps subarrayWithRange:NSMakeRange(pathComps.count - 2, 2)] componentsJoinedByString:@"/"]; + } else { + filename = url.path; + } + return [self.resourcePath stringByAppendingPathComponent:filename]; +} + +@end diff --git a/Rexxar/Core/RXRRouteManager.h b/Rexxar/Core/RXRRouteManager.h new file mode 100644 index 0000000..5129827 --- /dev/null +++ b/Rexxar/Core/RXRRouteManager.h @@ -0,0 +1,73 @@ +// +// RXRRouteManager.h +// Rexxar +// +// Created by GUO Lin on 5/11/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import Foundation; + +@class RXRRoute; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRRouteManager` 提供了对路由信息的管理和使用接口。 + */ +@interface RXRRouteManager : NSObject + +/** + * uri 和 html 对应关系的路由表。 + * + * 路由表读取路径优先级: + * - 缓存路由表。 + * - 配置中地址的根目录下的路由表。 + * + * 路由表更新策略: + * - 对象创建后。 + * - 当通过 `htmlForURI:` 没有找到目标 html 时。 + */ +@property (readonly, nullable) NSArray *routes; + +/** + * 读取 Routes Map 信息的 URL 地址。路由表应该由服务器提供。 + */ +@property (nonatomic, strong) NSURL *routesMapURL; + +/** + * 单例方法,获取一个 RXRRouteManager 实例。 + */ ++ (RXRRouteManager *)sharedInstance; + +/** + * 设置缓存地址。如果是相对路径的话,则认为其是相对于应用缓存路径 + */ +- (void)setCachePath:(NSString *)cachePath; + +/** + * 设置 rexxar 资源地址。如果是相对路径的话,则认为其是相对于 main bundle 路径。 + */ +- (void)setResoucePath:(NSString *)resourcePath; + +/** + * 查找 uri 对应的本地 html 文件 URL。先查 Cache,再查 Resource + */ +- (nullable NSURL *)localHtmlURLForURI:(NSURL *)uri; + +/** + * 查找 uri 对应的服务器上 html 文件。 + */ +- (nullable NSURL *)remoteHtmlURLForURI:(NSURL *)uri; + +/** + * 立即同步路由表。 + * + * @param completion 同步完成后的回调,可以为 nil + */ +- (void)updateRoutesWithCompletion:(nullable void (^)(BOOL success))completion; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Rexxar/Core/RXRRouteManager.m b/Rexxar/Core/RXRRouteManager.m new file mode 100644 index 0000000..eb06e66 --- /dev/null +++ b/Rexxar/Core/RXRRouteManager.m @@ -0,0 +1,255 @@ +// +// RXRRouteManager.m +// Rexxar +// +// Created by GUO Lin on 5/11/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import + +#import "RXRRouteManager.h" +#import "RXRRouteFileCache.h" +#import "RXRConfig.h" +#import "RXRRoute.h" +#import "RXRLogging.h" + +@interface RXRRouteManager () + +@property (nonatomic, strong) NSURLSession *session; + +@property (nonatomic, strong) NSArray *routes; +@property (nonatomic, assign) BOOL updatingRoutes; +@property (nonatomic, strong) NSMutableArray *updateRoutesCompletions; + +@end + + +@implementation RXRRouteManager + ++ (RXRRouteManager *)sharedInstance +{ + static RXRRouteManager *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[RXRRouteManager alloc] init]; + instance.routesMapURL = [RXRConfig routesMapURL]; + }); + return instance; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + NSURLSessionConfiguration *sessionCfg = [NSURLSessionConfiguration defaultSessionConfiguration]; + _session = [NSURLSession sessionWithConfiguration:sessionCfg + delegate:nil + delegateQueue:[[NSOperationQueue alloc] init]]; + + _updateRoutesCompletions = [NSMutableArray array]; + } + return self; +} + +- (void)setRoutesMapURL:(NSURL *)routesMapURL +{ + if (_routesMapURL != routesMapURL) { + _routesMapURL = routesMapURL; + self.routes = [self _rxr_routesWithData:[[RXRRouteFileCache sharedInstance] routesMapFile]]; + } +} + +- (void)setCachePath:(NSString *)cachePath +{ + RXRRouteFileCache *routeFileCache = [RXRRouteFileCache sharedInstance]; + routeFileCache.cachePath = cachePath; + self.routes = [self _rxr_routesWithData:[routeFileCache routesMapFile]]; +} + +- (void)setResoucePath:(NSString *)resourcePath +{ + RXRRouteFileCache *routeFileCache = [RXRRouteFileCache sharedInstance]; + routeFileCache.resourcePath = resourcePath; + self.routes = [self _rxr_routesWithData:[routeFileCache routesMapFile]]; +} + +- (void)updateRoutesWithCompletion:(void (^)(BOOL success))completion +{ + NSParameterAssert([NSThread isMainThread]); + + if (self.routesMapURL == nil) { + RXRDebugLog(@"[Warning] `routesRemoteURL` not set."); + return; + } + + if (completion) { + [self.updateRoutesCompletions addObject:completion]; + } + + if (self.updatingRoutes) { + return; + } + + self.updatingRoutes = YES; + + void (^APICompletion)(BOOL) = ^(BOOL success){ + dispatch_async(dispatch_get_main_queue(), ^{ + for (void (^item)(BOOL) in self.updateRoutesCompletions) { + item(success); + } + [self.updateRoutesCompletions removeAllObjects]; + self.updatingRoutes = NO; + }); + }; + + // 请求路由表 API + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.routesMapURL + cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData + timeoutInterval:60]; + // 更新 Http UserAgent Header + NSString *externalUserAgent = [RXRConfig externalUserAgent]; + if (externalUserAgent) { + NSString *userAgent = [request.allHTTPHeaderFields objectForKey:@"User-Agent"]; + NSString *newUserAgent = externalUserAgent; + if (userAgent) { + newUserAgent = [@[userAgent, externalUserAgent] componentsJoinedByString:@" "]; + } + [request setValue:newUserAgent forHTTPHeaderField:@"User-Agent"]; + } + + [[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + RXRDebugLog(@"Download %@", response.URL); + RXRDebugLog(@"Response: %@", response); + + if (((NSHTTPURLResponse *)response).statusCode != 200) { + APICompletion(NO); + return; + } + + // 下载最新 routes 中的资源文件,只有成功后,才更新 `routes.json` 及内存中的 `routes`。 + NSArray *routes = [self _rxr_routesWithData:data]; + [self _rxr_downloadFilesWithinRoutes:routes completion:^(BOOL success) { + if (success) { + self.routes = routes; + RXRRouteFileCache *routeFileCache = [RXRRouteFileCache sharedInstance]; + [routeFileCache saveRoutesMapFile:data]; + } + + APICompletion(success); + }]; + }] resume]; +} + +- (NSURL *)localHtmlURLForURI:(NSURL *)uri +{ + NSURL *remoteHtmlURL = [self remoteHtmlURLForURI:uri]; + RXRRouteFileCache *routeFileCache = [RXRRouteFileCache sharedInstance]; + return [routeFileCache routeFileURLForRemoteURL:remoteHtmlURL]; +} + +- (NSURL *)remoteHtmlURLForURI:(NSURL *)uri +{ + RXRRoute *route = [self _rxr_routeForURI:uri]; + if (route) { + return route.remoteHTML; + } + return nil; +} + +#pragma mark - Private Methods + +- (RXRRoute *)_rxr_routeForURI:(NSURL *)uri +{ + NSString *uriString = uri.absoluteString; + if (uriString.length == 0) { + return nil; + } + + // 从路由表中找到符合 URI 的 Route。 + for (RXRRoute *route in self.routes) { + if ([route.URIRegex numberOfMatchesInString:uriString options:0 range:NSMakeRange(0, uriString.length)] > 0) { + return route; + } + } + return nil; +} + +- (NSArray *)_rxr_routesWithData:(NSData *)data +{ + if (data == nil) { + return nil; + } + + NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + if (JSON == nil) { + return nil; + } + + NSMutableArray *items = [[NSMutableArray alloc] init]; + // 页面级别的 route + for (NSDictionary *item in JSON[@"items"]) { + [items addObject:[[RXRRoute alloc] initWithDictionary:item]]; + } + + // 局部页面的 route + for (NSDictionary *item in JSON[@"partial_items"]) { + [items addObject:[[RXRRoute alloc] initWithDictionary:item]]; + } + + return items; +} + +/** + * 下载 `routes` 中的资源文件。 + */ +- (void)_rxr_downloadFilesWithinRoutes:(NSArray *)routes completion:(void (^)(BOOL success))completion +{ + dispatch_group_t downloadGroup = nil; + if (completion) { + downloadGroup = dispatch_group_create(); + } + + BOOL __block success = YES; + + for (RXRRoute *route in routes) { + + // 如果文件在本地文件存在(要么在缓存,要么在资源文件夹),什么都不需要做 + if ([[RXRRouteFileCache sharedInstance] routeFileURLForRemoteURL:route.remoteHTML]) { + continue; + } + + if (downloadGroup) { dispatch_group_enter(downloadGroup); } + + // 文件不存在,下载下来。 + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:route.remoteHTML + cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData + timeoutInterval:60]; + [[self.session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + + RXRDebugLog(@"Download %@", response.URL); + RXRDebugLog(@"Response: %@", response); + + if (error || ((NSHTTPURLResponse *)response).statusCode != 200) { + success = NO; + if (downloadGroup) { dispatch_group_leave(downloadGroup); } + + RXRDebugLog(@"Fail to move download remote html: %@", error); + return; + } + + NSData *data = [NSData dataWithContentsOfURL:location]; + [[RXRRouteFileCache sharedInstance] saveRouteFileData:data withRemoteURL:response.URL]; + + if (downloadGroup) { dispatch_group_leave(downloadGroup); } + }] resume]; + } + + if (downloadGroup) { + dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ + completion(success); + }); + } +} + +@end diff --git a/Rexxar/Core/RXRViewController+Router.m b/Rexxar/Core/RXRViewController+Router.m new file mode 100644 index 0000000..bd5f7ab --- /dev/null +++ b/Rexxar/Core/RXRViewController+Router.m @@ -0,0 +1,42 @@ +// +// RXRViewController+Router.m +// Rexxar +// +// Created by GUO Lin on 5/26/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "RXRViewController.h" +#import "RXRRouteManager.h" + +@implementation RXRViewController (Router) + +#pragma mark - Route File Interface + ++ (void)updateRouteFilesWithCompletion:(void (^)(BOOL success))completion +{ + RXRRouteManager *routeManager = [RXRRouteManager sharedInstance]; + [routeManager updateRoutesWithCompletion:completion]; +} + ++ (BOOL)isRouteExistForURI:(NSURL *)uri +{ + RXRRouteManager *routeManager = [RXRRouteManager sharedInstance]; + NSURL *remoteHtml = [routeManager remoteHtmlURLForURI:uri]; + if (remoteHtml) { + return YES; + } + return NO; +} + ++ (BOOL)isLocalRouteFileExistForURI:(NSURL *)uri +{ + RXRRouteManager *routeManager = [RXRRouteManager sharedInstance]; + NSURL *localHtml = [routeManager localHtmlURLForURI:uri]; + if (localHtml) { + return YES; + } + return NO; +} + +@end diff --git a/Rexxar/Core/RXRViewController.h b/Rexxar/Core/RXRViewController.h new file mode 100644 index 0000000..a30ad42 --- /dev/null +++ b/Rexxar/Core/RXRViewController.h @@ -0,0 +1,105 @@ +// +// RXRViewController.h +// Rexxar +// +// Created by Tony Li on 11/4/15. +// Copyright © 2015 Douban Inc. All rights reserved. +// + +@import UIKit; + +@protocol RXRWidget; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRViewController` 是一个 Rexxar Container。 + * 它提供了一个使用 web 技术 html, css, javascript 开发 UI 界面的容器。 + */ +@interface RXRViewController : UIViewController + +/** + * 对应的 uri。 + */ +@property (nonatomic, strong, readonly) NSURL *uri; + +/** + * 内置的 WebView。 + */ +@property (nonatomic, strong, readonly) UIWebView *webView; + +/** + * activities 代表该 Rexxar Container 可以响应的协议。 + */ +@property (nonatomic, strong) NSArray> *widgets; + +/** + * 初始化一个RXRViewController。 + * + * @param uri 该页面对应的 uri。 + * + * @discussion 会根据 uri 从 Route Map File 中选择对应本地 html 文件加载。如果无本地 html 文件,则从服务器加载 html 资源。 + * 在 UIWebView 中,远程 URL 需要注意跨域问题。 + */ +- (instancetype)initWithURI:(NSURL *)uri; + +/** + * 初始化一个RXRViewController。 + * + * @param uri 该页面对应的 uri。 + * @param htmlFileURL 该页面对应的 html file url。 + * + * @discussion 会根据 uri 从 Route Map File 中选择对应本地 html 文件加载。如果无本地 html 文件,则从服务器加载 html 资源。 + * 在 UIWebView 中,远程 URL 需要注意跨域问题。 + */ +- (instancetype)initWithURI:(NSURL *)uri htmlFileURL:(NSURL *)htmlFileURL; + +/** + * 重新加载 WebView。 + */ +- (void)reloadWebView; + +/** + * 通知 WebView 页面显示,缺省会在 viewWillAppear 里调用。本方法可以由业务层自主定制向 WebView 通知 onPageVisible 的时机。 + */ +- (void)onPageVisible; + +/** + * 通知 WebView 页面消失,缺省会在 viewDidDisappear 里调用。本方法可以由业务层自主定制向 WebView 通知 onPageInvisible 的时机。 + */ +- (void)onPageInvisible; + +@end + + +#pragma mark - Public Route Methods + +/** + * 暴露出 Route 相关的接口。 + */ +@interface RXRViewController (Router) + +/** + * 更新 Route Files。 + * + * @param completion 更新完成后将执行这个 block。 + */ ++ (void)updateRouteFilesWithCompletion:(nullable void (^)(BOOL success))completion; + +/** + * 判断存在对应于 uri 的 route 信息 + * + * @param uri 待判断的 uri + */ ++ (BOOL)isRouteExistForURI:(NSURL *)uri; + +/** + * 判断存在对应于 uri 的 route 信息 + * + * @param uri 待判断的 uri + */ ++ (BOOL)isLocalRouteFileExistForURI:(NSURL *)uri; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Rexxar/Core/RXRViewController.m b/Rexxar/Core/RXRViewController.m new file mode 100644 index 0000000..ce7931e --- /dev/null +++ b/Rexxar/Core/RXRViewController.m @@ -0,0 +1,237 @@ +// +// RXRViewController.m +// Rexxar +// +// Created by Tony Li on 11/4/15. +// Copyright © 2015 Douban Inc. All rights reserved. +// + +#import + +#import "RXRViewController.h" +#import "RXRCacheFileIntercepter.h" +#import "RXRRouteManager.h" +#import "RXRLogging.h" +#import "RXRConfig.h" +#import "RXRWidget.h" + +#import "UIColor+Rexxar.h" +#import "NSURL+Rexxar.h" + + +@interface RXRViewController () + +@property (nonatomic, strong) NSURL *requestURL; + +@property (nonatomic, strong) NSURL *htmlFileURL; + +@end + + +@implementation RXRViewController + +#pragma mark - LifeCycle + +- (instancetype)initWithURI:(NSURL *)uri htmlFileURL:(NSURL *)htmlFileURL +{ + self = [super initWithNibName:nil bundle:nil]; + if (self) { + _uri = uri; + _htmlFileURL = htmlFileURL; + } + return self; +} + + + +- (instancetype)initWithURI:(NSURL *)uri +{ + self = [super initWithNibName:nil bundle:nil]; + if (self) { + _uri = uri; + } + return self; +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + NSAssert(NO, @"Should use initWithURI: instead."); + return nil; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _webView = [[UIWebView alloc] initWithFrame:self.view.bounds]; + _webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal; + _webView.dataDetectorTypes = UIDataDetectorTypeLink; + _webView.scalesPageToFit = YES; + _webView.delegate = self; + [self.view addSubview:_webView]; + + [self reloadWebView]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + [NSURLProtocol registerClass:RXRCacheFileIntercepter.class]; + [self onPageVisible]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [NSURLProtocol unregisterClass:RXRCacheFileIntercepter.class]; + [self onPageInvisible]; +} + +#pragma mark - Public methods + +- (void)reloadWebView +{ + if (!self.requestURL) { + _requestURL = [self _rxr_htmlURLWithUri:self.uri htmlFileURL:self.htmlFileURL]; + } + + if (self.requestURL) { + [_webView loadRequest:[NSURLRequest requestWithURL:self.requestURL]]; + } +} + +- (void)onPageVisible +{ + // Call the WebView's visiblity change hook for javascript. + RXRDebugLog(@"window.Rexxar.Lifecycle.onPageVisible: %@", + [_webView stringByEvaluatingJavaScriptFromString:@"window.Rexxar.Lifecycle.onPageVisible()"]); +} + +- (void)onPageInvisible +{ + // Call the WebView's visiblity change hook for javascript. + RXRDebugLog(@"window.Rexxar.Lifecycle.onPageInvisible: %@", + [_webView stringByEvaluatingJavaScriptFromString:@"window.Rexxar.Lifecycle.onPageInvisible()"]); +} + +#pragma mark - UIWebViewDelegate's method + +- (BOOL)webView:(UIWebView *)webView + shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType +{ + NSURL *reqURL = request.URL; + + if ([reqURL isEqual:self.requestURL]) { + return YES; + } + + // http:// or https:// 开头,则打开网页 + if ([reqURL rxr_isHttpOrHttps]) { + return ![self _rxr_openWebPage:reqURL]; + } + + NSString *scheme = [RXRConfig rxrProtocolScheme]; + NSString *host = [RXRConfig rxrProtocolHost]; + + if ([request.URL.scheme isEqualToString:scheme] + && [request.URL.host isEqualToString:host] ) { + + NSURL *URL = request.URL; + + for (id widget in self.widgets) { + if ([widget canPerformWithURL:URL]) { + [widget prepareWithURL:URL]; + [widget performWithController:self]; + RXRDebugLog(@"Rexxar callback handle: %@", URL); + return NO; + } + } + + RXRDebugLog(@"Rexxar callback can not handle: %@", URL); + } + + return YES; +} + +- (void)webViewDidStartLoad:(UIWebView *)webView +{ + [self _rxr_resetControllerAppearance]; +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView +{ + [self _rxr_resetControllerAppearance]; +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error +{ + [self _rxr_resetControllerAppearance]; +} + +#pragma mark - Private Methods + +- (NSURL *)_rxr_htmlURLWithUri:(NSURL *)uri htmlFileURL:(NSURL *)htmlFileURL +{ + if (!htmlFileURL) { + // 没有设置 htmlFileURL,则使用本地 html 文件或者服务器读取 html 文件。 + + htmlFileURL = [[RXRRouteManager sharedInstance] remoteHtmlURLForURI:self.uri]; + + if ([RXRConfig isCacheEnable]) { + // 如果缓存启用,尝试读取本地文件。如果没有本地文件(本地文件包括缓存,和资源文件夹),则从服务器读取。 + NSURL *localHtmlURL = [[RXRRouteManager sharedInstance] localHtmlURLForURI:self.uri]; + if (localHtmlURL) { + htmlFileURL = localHtmlURL; + } + } + + } + + + if (htmlFileURL.query.length != 0 && htmlFileURL.fragment.length != 0) { + // 为了方便 escape 正确的 uri,做了下面的假设。之后放弃 iOS 7 后可以改用 `queryItem` 来实现。 + // 做个合理假设:html URL 中不应该有 query string 和 fragment。 + RXRWarnLog(@"local html 's format is not right! Url has query and fragment."); + } + + // `absoluteString` 返回的是已经 escape 过的文本,这里先转换为原始文本。 + NSString *uriText = uri.absoluteString.stringByRemovingPercentEncoding; + // 把 uri 的原始文本所有内容全部 escape。 + NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@""]; + uriText = [uriText stringByAddingPercentEncodingWithAllowedCharacters:set]; + + return [NSURL URLWithString:[NSString stringWithFormat:@"%@?uri=%@", htmlFileURL.absoluteString, uriText]]; +} + +- (void)_rxr_resetControllerAppearance +{ + self.title = [self.webView stringByEvaluatingJavaScriptFromString:@"document.title"]; + + NSString *bgColor = [self.webView stringByEvaluatingJavaScriptFromString: + @"window.getComputedStyle(document.getElementsByTagName('body')[0]).backgroundColor"]; + self.webView.backgroundColor = [UIColor rxr_colorWithComponent:bgColor] ?: [UIColor whiteColor]; +} + +- (BOOL)_rxr_openWebPage:(NSURL *)url +{ + // 让 App 打开网页,通常 `UIApplicationDelegate` 都会实现 open url 相关的 delegate 方法。 + id delegate = [[UIApplication sharedApplication] delegate]; + if ([delegate respondsToSelector:@selector(application:openURL:options:)]) { + [delegate application:[UIApplication sharedApplication] + openURL:url + options:@{}]; + } else if ([delegate respondsToSelector:@selector(application:openURL:sourceApplication:annotation:)]) { + [delegate application:[UIApplication sharedApplication] + openURL:url + sourceApplication:nil + annotation:@""]; + } else if ([delegate respondsToSelector:@selector(application:handleOpenURL:)]) { + [delegate application:[UIApplication sharedApplication] handleOpenURL:url]; + } + + return YES; +} + +@end diff --git a/Rexxar/Core/RXRWidget.h b/Rexxar/Core/RXRWidget.h new file mode 100644 index 0000000..043ab53 --- /dev/null +++ b/Rexxar/Core/RXRWidget.h @@ -0,0 +1,44 @@ +// +// RXRWidget.h +// Frodo +// +// Created by GUO Lin on 5/5/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +@import Foundation; + + +@class RXRViewController; + +NS_ASSUME_NONNULL_BEGIN +/** + * `RXRWidget` 是一个 Widget 协议。 + * 实现 RXRWidget 协议的类将完成一个 Web 对 Native 的功能调用。 + */ +@protocol RXRWidget + +/** + * 判断该 Widget 是否要对该 URL 做出反应。 + * + * @param URL 对应的 URL。 + */ +- (BOOL)canPerformWithURL:(NSURL *)URL; + +/** + * 对该 URL,执行 Widget 的各项准备工作。 + * + * @param URL 对应的 URL。 + */ +- (void)prepareWithURL:(NSURL *)URL; + +/** + * 执行 Widget 的操作。 + * + * @param controller 执行该 Widget 的 Controller。 + */ +- (void)performWithController:(RXRViewController *)controller; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Rexxar/Decorator/RXRDecorator.h b/Rexxar/Decorator/RXRDecorator.h new file mode 100644 index 0000000..cc5b846 --- /dev/null +++ b/Rexxar/Decorator/RXRDecorator.h @@ -0,0 +1,45 @@ +// +// RXRDecorator.h +// Rexxar +// +// Created by GUO Lin on 5/17/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRDecorator` 是一个请求装修器协议。请求装修器代表了一个可用于修改 http 请求的类的协议。 + * 符合该协议的类可以用于修改 Rexxar-Container 内发出的 Http 请求。 + */ +@protocol RXRDecorator + +/** + * 判断是否应该拦截侦听该请求 + * + * @param request 对应请求 + */ +- (BOOL)shouldInterceptRequest:(NSURLRequest *)request; + +/** + * 对该请求的修改动作 + * + * @param request 对应请求 + */ +- (void)decorateRequest:(NSMutableURLRequest *)request; + + +@optional + +/** + * 准备执行对该请求的修改动作 + * + * @param request 对应请求 + */ +- (void)prepareWithRequest:(NSURLRequest *)request; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Rexxar/Decorator/RXRRequestDecorator.h b/Rexxar/Decorator/RXRRequestDecorator.h new file mode 100644 index 0000000..d1504d1 --- /dev/null +++ b/Rexxar/Decorator/RXRRequestDecorator.h @@ -0,0 +1,42 @@ +// +// RXRRequestDecorator.h +// Rexxar +// +// Created by GUO Lin on 7/1/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import Foundation; + +#import "RXRDecorator.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRRequestDecorator` 是一个具体的请求装修器。 + * 通过该装修器对 Rexxar-Conntainer 中发出的请求作修改。增加其 url 参数,以及增添自定义 header。 + */ +@interface RXRRequestDecorator : NSObject + +/** + * 需要为请求增添的自定义 header。 + */ +@property (nonatomic, strong) NSDictionary *headers; + +/** + * 需要为请求增添的 url 参数。 + */ +@property (nonatomic, strong) NSDictionary *parameters; + +/** + * 初始化一个请求装修器。 + * + * @param headers 需要为请求增添的自定义 header + * @param parameters 需要为请求增添的 url 参数 + */ +- (instancetype)initWithHeaders:(NSDictionary *)headers + parameters:(NSDictionary *)parameters; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Rexxar/Decorator/RXRRequestDecorator.m b/Rexxar/Decorator/RXRRequestDecorator.m new file mode 100644 index 0000000..ec5be07 --- /dev/null +++ b/Rexxar/Decorator/RXRRequestDecorator.m @@ -0,0 +1,67 @@ +// +// RXRRequestDecorator.m +// Rexxar +// +// Created by GUO Lin on 7/1/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "RXRRequestDecorator.h" + +#import "NSURL+Rexxar.h" + +@implementation RXRRequestDecorator + +- (instancetype)initWithHeaders:(NSDictionary *)headers + parameters:(NSDictionary *)parameters +{ + self = [super init]; + if (self) { + _headers = headers; + _parameters = parameters; + } + return self; +} + +- (BOOL)shouldInterceptRequest:(NSURLRequest *)request +{ + if ([request.URL rxr_isHttpOrHttps]) { + return YES; + } + + return NO; +} + +- (void)decorateRequest:(NSMutableURLRequest *)request +{ + // Request headers + [self.headers enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if ([key isKindOfClass:[NSString class]] && [obj isKindOfClass:[NSString class]]){ + [request setValue:obj forHTTPHeaderField:key]; + } + }]; + + // Request url parameters + NSMutableDictionary *parametersEncoded = [NSMutableDictionary dictionaryWithDictionary:self.parameters]; + for (NSString *pair in [request.URL.query componentsSeparatedByString:@"&"]) { + + NSArray *keyValuePair = [pair componentsSeparatedByString:@"="]; + if (keyValuePair.count != 2) { + continue; + } + + NSString *key = [keyValuePair[0] stringByRemovingPercentEncoding]; + if (parametersEncoded[key] == nil) { + parametersEncoded[key] = [keyValuePair[1] stringByRemovingPercentEncoding]; + } + } + + NSString *query = [NSURL rxr_queryFromDictionary:parametersEncoded]; + if (query) { + NSURLComponents *urlComps = [NSURLComponents componentsWithURL:request.URL resolvingAgainstBaseURL:YES]; + urlComps.query = query; + request.URL = urlComps.URL; + } +} + +@end diff --git a/Rexxar/Decorator/RXRRequestIntercepter.h b/Rexxar/Decorator/RXRRequestIntercepter.h new file mode 100644 index 0000000..31fbf8f --- /dev/null +++ b/Rexxar/Decorator/RXRRequestIntercepter.h @@ -0,0 +1,38 @@ +// +// RXRRequestIntercepter.h +// Rexxar +// +// Created by GUO Lin on 5/17/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +@import Foundation; + +#import "RXRNSURLProtocol.h" + +@protocol RXRDecorator; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRRequestIntercepter` 是一个 Rexxar-Container 的请求侦听器。 + * 这个侦听器用于修改请求,比如增添请求的 url 参数,添加自定义的 http header。 + * + */ +@interface RXRRequestIntercepter : RXRNSURLProtocol + +/** + * 设置这个侦听器所有的请求装修器数组,该数组成员是符合 `RXRDecorator` 协议的对象,即一组请求装修器。 + * + * @param decorators 装修器数组 + */ ++ (void)setDecorators:(NSArray> *)decorators; + +/** + * 获得对应的请求装修器数组,该数组成员是符合 `RXRDecorator` 协议的对象,即一组请求装修器。 + */ ++ (nullable NSArray> *)decorators; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Rexxar/Decorator/RXRRequestIntercepter.m b/Rexxar/Decorator/RXRRequestIntercepter.m new file mode 100644 index 0000000..449ffdd --- /dev/null +++ b/Rexxar/Decorator/RXRRequestIntercepter.m @@ -0,0 +1,74 @@ +// +// RXRRequestIntercepter.m +// Rexxar +// +// Created by GUO Lin on 5/17/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +#import "RXRRequestIntercepter.h" + +#import "RXRDecorator.h" + +static NSArray> *sDecorators; + +@implementation RXRRequestIntercepter + ++ (void)setDecorators:(NSArray> *)decorators +{ + sDecorators = decorators; +} + ++ (NSArray> *)decorators +{ + return sDecorators; +} + +#pragma mark - Implement NSURLProtocol methods + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + // 请求被忽略(被标记为忽略或者已经请求过),不处理 + if ([self isRequestIgnored:request]) { + return NO; + } + // 请求不是来自浏览器,不处理 + if (![request.allHTTPHeaderFields[@"User-Agent"] hasPrefix:@"Mozilla"]) { + return NO; + } + + for (id decorator in sDecorators) { + if ([decorator shouldInterceptRequest:request]){ + return YES; + } + } + + return NO; +} + +- (void)startLoading +{ + NSParameterAssert(self.connection == nil); + NSParameterAssert([[self class] canInitWithRequest:self.request]); + + __block NSMutableURLRequest *request = nil; + if ([self.request isKindOfClass:[NSMutableURLRequest class]]) { + request = (NSMutableURLRequest *)self.request; + } else { + request = [self.request mutableCopy]; + } + + for (id decorator in sDecorators) { + if ([decorator shouldInterceptRequest:request]) { + if ([decorator respondsToSelector:@selector(prepareWithRequest:)]) { + [decorator prepareWithRequest:request]; + } + [decorator decorateRequest:request]; + } + } + + [[self class] markRequestAsIgnored:request]; + self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]; +} + +@end diff --git a/Rexxar/Info.plist b/Rexxar/Info.plist new file mode 100644 index 0000000..4ac239f --- /dev/null +++ b/Rexxar/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/Rexxar/Rexxar.h b/Rexxar/Rexxar.h new file mode 100644 index 0000000..9c54d31 --- /dev/null +++ b/Rexxar/Rexxar.h @@ -0,0 +1,36 @@ +// +// Rexxar.h +// Rexxar +// +// Created by XueMing on 11/10/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +#ifndef _REXXAR_ + #define _REXXAR_ + +#import "RXRConfig.h" +#import "RXRViewController.h" + +#import "RXRWidget.h" + +#import "RXRNSURLProtocol.h" + +#import "RXRContainerIntercepter.h" +#import "RXRContainerAPI.h" + +#import "RXRRequestIntercepter.h" +#import "RXRDecorator.h" +#import "RXRRequestDecorator.h" + +#import "NSURL+Rexxar.h" +#import "NSDictionary+RXRMultipleItems.h" + +#if DSK_WIDGET +#import "RXRModel.h" +#import "RXRNavTitleWidget.h" +#import "RXRAlertDialogWidget.h" +#import "RXRPullRefreshWidget.h" +#endif + +#endif /* _REXXAR_ */ diff --git a/Rexxar/Widget/Model/RXRAlertDialogData.h b/Rexxar/Widget/Model/RXRAlertDialogData.h new file mode 100644 index 0000000..be0f30d --- /dev/null +++ b/Rexxar/Widget/Model/RXRAlertDialogData.h @@ -0,0 +1,48 @@ +// +// RXRAlertDialogData.h +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "RXRModel.h" + +/** + * `RXRAlertDialogButton` 对话框上按钮的数据对象。 + */ +@interface RXRAlertDialogButton : RXRModel + +/** + * 按钮的标题文字。 + */ +@property (nonatomic, copy, readonly) NSString *text; + +/** + * 按按钮后将执行的动作。 + */ +@property (nonatomic, copy, readonly) NSString *action; + +@end + +/** + * `RXRAlertDialogData` 对话框的数据对象。 + */ +@interface RXRAlertDialogData : RXRModel + +/** + * 对话框的标题。 + */ +@property (nonatomic, copy, readonly) NSString *title; + +/** + * 对话框的消息。 + */ +@property (nonatomic, copy, readonly) NSString *message; + +/** + * 对话框的按钮。 + */ +@property (nonatomic, readonly) NSArray *buttons; + +@end diff --git a/Rexxar/Widget/Model/RXRAlertDialogData.m b/Rexxar/Widget/Model/RXRAlertDialogData.m new file mode 100644 index 0000000..19e2645 --- /dev/null +++ b/Rexxar/Widget/Model/RXRAlertDialogData.m @@ -0,0 +1,53 @@ +// +// RXRAlertDialogData.m +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "RXRAlertDialogData.h" + +@implementation RXRAlertDialogButton + +- (NSString *)text +{ + return [self.dictionary objectForKey:@"text"]; +} + +- (NSString *)action +{ + return [self.dictionary objectForKey:@"action"]; +} + +@end + + +@implementation RXRAlertDialogData + +- (NSString *)title +{ + return [self.dictionary objectForKey:@"title"]; +} + +- (NSString *)message +{ + return [self.dictionary objectForKey:@"message"]; +} + +- (NSArray *)buttons +{ + NSMutableArray *result = [NSMutableArray array]; + NSArray *array = [self.dictionary objectForKey:@"buttons"]; + for (id dic in array) { + if ([dic isKindOfClass:[NSDictionary class]]) { + RXRAlertDialogButton *button = [[RXRAlertDialogButton alloc] initWithDictionary:dic]; + if (button) { + [result addObject:button]; + } + } + } + return result; +} + +@end diff --git a/Rexxar/Widget/Model/RXRModel.h b/Rexxar/Widget/Model/RXRModel.h new file mode 100644 index 0000000..5b000a3 --- /dev/null +++ b/Rexxar/Widget/Model/RXRModel.h @@ -0,0 +1,46 @@ +// +// RXRModel.h +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `RXRModel` 数据对象。 + * Web 对 Native 调用时可能会出发一些结构化的数据。 + * RXRModel 提供了对这些数据的更简便的访问方法。 + */ +@interface RXRModel : NSObject + +/** + * 数据对象的 json 字符串形式。 + */ +@property (nonatomic, readonly, copy) NSString *string; + +/** + * 数据对象的字典形式。 + */ +@property (nonatomic, strong) NSMutableDictionary *dictionary; + +/** + * 以 json 字符串初始化数据对象。 + * + * @param theJsonStr 字符串 + */ +- (id)initWithString:(NSString *)theJsonStr; + +/** + * 以字典初始化数据对象。 + * + * @param theDictionary 字典 + */ +- (id)initWithDictionary:(NSDictionary *)theDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Rexxar/Widget/Model/RXRModel.m b/Rexxar/Widget/Model/RXRModel.m new file mode 100644 index 0000000..e464247 --- /dev/null +++ b/Rexxar/Widget/Model/RXRModel.m @@ -0,0 +1,90 @@ +// +// RXRModel.m +// Rexxar +// +// Created by GUO Lin on 6/28/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "RXRModel.h" + +@implementation RXRModel + +- (id)init +{ + self = [super init]; + if (self) { + self.dictionary = [NSMutableDictionary dictionary]; + } + return self; +} + +- (id)initWithDictionary:(NSDictionary *)theDictionary +{ + self = [self init]; + if (self) { + if (![theDictionary isKindOfClass:[NSDictionary class]]) { + theDictionary = nil; + } + self.dictionary = [[NSMutableDictionary alloc] initWithDictionary:theDictionary]; + } + return self; +} + +- (id)initWithString:(NSString *)theJsonStr +{ + if (!theJsonStr || [theJsonStr length] <= 0) { + return nil; + } + + NSData *jsonStrData = [theJsonStr dataUsingEncoding:NSUTF8StringEncoding]; + if (!jsonStrData) { + return nil; + } + + NSError *error = nil; + id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonStrData + options:kNilOptions + error:&error]; + if (error) { + return nil; + } + + NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:jsonObject]; + if (!dic) { + return nil; + } + + self = [self initWithDictionary:dic]; + + return self; +} + +- (NSString *)string +{ + if (self.dictionary) { + NSData *data = [NSJSONSerialization dataWithJSONObject:self.dictionary options:kNilOptions error:nil]; + NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + return result; + } + return nil; +} + +- (void)setString:(NSString *)theJsonStr +{ + NSError *error = nil; + id jsonObject = [NSJSONSerialization JSONObjectWithData:[theJsonStr dataUsingEncoding:NSUTF8StringEncoding] + options:kNilOptions + error:&error]; + if (error) { + return; + } + + NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:jsonObject]; + if (!dic) { + return; + } + self.dictionary = dic; +} + +@end diff --git a/Rexxar/Widget/RXRAlertDialogWidget.h b/Rexxar/Widget/RXRAlertDialogWidget.h new file mode 100644 index 0000000..a43eefe --- /dev/null +++ b/Rexxar/Widget/RXRAlertDialogWidget.h @@ -0,0 +1,17 @@ +// +// RXRAlertDialogWidget.h +// Frodo +// +// Created by GUO Lin on 5/6/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +#import "RXRWidget.h" + +/** + * `RXRAlertDialogWidget` 实现弹出一个对话框的功能。 + */ +@interface RXRAlertDialogWidget : NSObject + +@end + diff --git a/Rexxar/Widget/RXRAlertDialogWidget.m b/Rexxar/Widget/RXRAlertDialogWidget.m new file mode 100644 index 0000000..3ea07f5 --- /dev/null +++ b/Rexxar/Widget/RXRAlertDialogWidget.m @@ -0,0 +1,110 @@ +// +// RXRAlertDialogWidget.m +// Frodo +// +// Created by GUO Lin on 5/6/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +#import "RXRAlertDialogWidget.h" +#import "RXRViewController.h" +#import "RXRAlertDialogData.h" +#import "NSDictionary+RXRMultipleItems.h" +#import "NSURL+Rexxar.h" + +@interface RXRAlertDialogWidget () + +@property (nonatomic, weak) RXRViewController *rexxarViewController; +@property (nonatomic, strong) RXRAlertDialogData *alertDialogData; + +@end + + +@implementation RXRAlertDialogWidget + +- (BOOL)canPerformWithURL:(NSURL *)URL +{ + NSString *path = URL.path; + if (path && [path isEqualToString:@"/widget/alert_dialog"]) { + return YES; + } + return NO; +} + +- (void)prepareWithURL:(NSURL *)URL +{ + NSString *string = [[URL rxr_queryDictionary] rxr_itemForKey:@"data"]; + self.alertDialogData = [[RXRAlertDialogData alloc] initWithString:string]; +} + +- (void)performWithController:(RXRViewController *)controller +{ + + self.rexxarViewController = controller; + + if (!self.alertDialogData) { + return; + } + + NSString *title = self.alertDialogData.title; + NSString *message = self.alertDialogData.message; + NSArray *buttons = self.alertDialogData.buttons; + + if (NSClassFromString(@"UIAlertController")) { + [self _rxr_alertWithTitle:title message:message buttons:buttons]; + } else { + [self _rxr_ios7_alertWithTitle:title message:message buttons:buttons]; + } +} + +#pragma mark - Private methods + +- (void)_rxr_alertWithTitle:(NSString *)title + message:(NSString *)message + buttons:(NSArray *)buttons +{ + UIAlertController *alertView = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + for (RXRAlertDialogButton *button in buttons) { + UIAlertAction *action = [UIAlertAction actionWithTitle:button.text + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *alertAction) + { + [self.rexxarViewController.webView stringByEvaluatingJavaScriptFromString:button.action]; + }]; + + [alertView addAction:action]; + } + + [self.rexxarViewController presentViewController:alertView animated:YES completion:nil]; +} + +- (void)_rxr_ios7_alertWithTitle:(NSString *)title + message:(NSString *)message + buttons:(NSArray *)buttons +{ + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title + message:message + delegate:self + cancelButtonTitle:nil + otherButtonTitles:nil, nil]; + + for (RXRAlertDialogButton *button in buttons) { + [alertView addButtonWithTitle:button.text]; + } + + [alertView show]; +} + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex +{ + NSArray *buttons = self.alertDialogData.buttons; + if (buttons.count < buttonIndex) { + RXRAlertDialogButton *button = buttons[buttonIndex]; + [self.rexxarViewController.webView stringByEvaluatingJavaScriptFromString:button.action]; + } +} + +@end diff --git a/Rexxar/Widget/RXRNavTitleWidget.h b/Rexxar/Widget/RXRNavTitleWidget.h new file mode 100644 index 0000000..3b6a2e0 --- /dev/null +++ b/Rexxar/Widget/RXRNavTitleWidget.h @@ -0,0 +1,16 @@ +// +// RXRNavTitleWidget.h +// Frodo +// +// Created by GUO Lin on 5/5/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +#import "RXRWidget.h" + +/** + * `RXRNavTitleWidget` 实现对导航的标题进行设置。 + */ +@interface RXRNavTitleWidget : NSObject + +@end diff --git a/Rexxar/Widget/RXRNavTitleWidget.m b/Rexxar/Widget/RXRNavTitleWidget.m new file mode 100644 index 0000000..17b00ea --- /dev/null +++ b/Rexxar/Widget/RXRNavTitleWidget.m @@ -0,0 +1,44 @@ +// +// RXRNavTitleWidget.m +// Frodo +// +// Created by GUO Lin on 5/5/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +#import "RXRNavTitleWidget.h" +#import "RXRViewController.h" +#import "NSDictionary+RXRMultipleItems.h" +#import "NSURL+Rexxar.h" + +@interface RXRNavTitleWidget () + +@property (nonatomic, copy) NSString *title; + +@end + + +@implementation RXRNavTitleWidget + +- (BOOL)canPerformWithURL:(NSURL *)URL +{ + NSString *path = URL.path; + if (path && [path isEqualToString:@"/widget/nav_title"]) { + return YES; + } + return NO; +} + +- (void)prepareWithURL:(NSURL *)URL +{ + self.title = [[URL rxr_queryDictionary] rxr_itemForKey:@"title"]; +} + +- (void)performWithController:(RXRViewController *)controller +{ + if (controller) { + controller.title = self.title; + } +} + +@end diff --git a/Rexxar/Widget/RXRPullRefreshWidget.h b/Rexxar/Widget/RXRPullRefreshWidget.h new file mode 100644 index 0000000..cc2fc20 --- /dev/null +++ b/Rexxar/Widget/RXRPullRefreshWidget.h @@ -0,0 +1,16 @@ +// +// RXRPullRefreshWidget.h +// Rexxar +// +// Created by GUO Lin on 8/5/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import "RXRWidget.h" + +/** + * `RXRPullRefreshWidget` 实现下拉刷新。 + */ +@interface RXRPullRefreshWidget : NSObject + +@end diff --git a/Rexxar/Widget/RXRPullRefreshWidget.m b/Rexxar/Widget/RXRPullRefreshWidget.m new file mode 100644 index 0000000..80e6e5c --- /dev/null +++ b/Rexxar/Widget/RXRPullRefreshWidget.m @@ -0,0 +1,79 @@ +// +// RXRPullRefreshWidget.m +// Rexxar +// +// Created by GUO Lin on 8/5/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +@import UIKit; + +#import "RXRPullRefreshWidget.h" +#import "RXRViewController.h" +#import "NSURL+Rexxar.h" +#import "NSDictionary+RXRMultipleItems.h" + +@interface RXRPullRefreshWidget () + +@property (nonatomic, strong) UIRefreshControl *refreshControl; +@property (nonatomic, copy) NSString *action; +@property (nonatomic, assign) BOOL onRefreshStart; + +@end + + +@implementation RXRPullRefreshWidget + +- (BOOL)canPerformWithURL:(NSURL *)URL +{ + NSString *path = URL.path; + if (path && [path isEqualToString:@"/widget/pull_to_refresh"]) { + return YES; + } + return NO; +} + +- (void)prepareWithURL:(NSURL *)URL +{ + NSDictionary *queryItems = [URL rxr_queryDictionary]; + self.action = [queryItems rxr_itemForKey:@"action"]; +} + +- (void)performWithController:(RXRViewController *)controller +{ + + if ([self.action isEqualToString:@"enable"] && !self.refreshControl.isRefreshing) { + // Web 通知该页面有下拉组件 + if (!self.refreshControl) { + self.refreshControl = [self _rxr_refreshControllerWithScrollView:controller.webView]; + } + + } else if ([self.action isEqualToString:@"complete"]) { + // Web 通知下拉动作完成 + [self.refreshControl endRefreshing]; + self.onRefreshStart = NO; + } +} + +#pragma mark - Private + +- (UIRefreshControl *)_rxr_refreshControllerWithScrollView:(UIWebView *)webView +{ + UIScrollView *scrollView = webView.scrollView; + UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; + [scrollView addSubview:refreshControl]; + [refreshControl addTarget:self action:@selector(_rxr_refresh:) forControlEvents:UIControlEventValueChanged]; + return refreshControl; +} + +- (void)_rxr_refresh:(UIRefreshControl *)refreshControl +{ + UIView *view = [[refreshControl superview] superview]; + if ([view isKindOfClass:[UIWebView class]] && !self.onRefreshStart) { + self.onRefreshStart = YES; + UIWebView *webView = (UIWebView *)view; + [webView stringByEvaluatingJavaScriptFromString:@"window.Rexxar.Widget.PullToRefresh.onRefreshStart()"]; + } +} + +@end diff --git a/RexxarDemo/AppDelegate.swift b/RexxarDemo/AppDelegate.swift new file mode 100644 index 0000000..4abf135 --- /dev/null +++ b/RexxarDemo/AppDelegate.swift @@ -0,0 +1,61 @@ +// +// AppDelegate.swift +// RexxarDemo +// +// Created by XueMing on 11/10/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = UINavigationController(rootViewController: RoutesViewController()) + window?.makeKeyAndVisible() + + // Config Rexxar + let routesMapURL = "https://rexxar.douban.com/api/routes" + RXRConfig.setRoutesMapURL(URL(string: routesMapURL)!) + RXRConfig.setRoutesCachePath("com.douban.RexxarDemo") + RXRConfig.setRoutesResourcePath("hybrid") + + // Update UserAgent need by RexxarDemo javascript code. + let appSuffix = "com.douban.frodo/4.4.0" + let rexxarVersion = "Rexxar/1.2.5" + let userAgent = appSuffix.appendingFormat(" %@", rexxarVersion) + RXRConfig.setExternalUserAgent(userAgent) + + RXRViewController.updateRouteFiles(completion: nil) + + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // 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. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // 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. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // 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. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + +} diff --git a/RexxarDemo/Base.lproj/LaunchScreen.storyboard b/RexxarDemo/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..0b34d93 --- /dev/null +++ b/RexxarDemo/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RexxarDemo/Bridge-Header.h b/RexxarDemo/Bridge-Header.h new file mode 100644 index 0000000..20635b0 --- /dev/null +++ b/RexxarDemo/Bridge-Header.h @@ -0,0 +1,18 @@ +// +// Briding-Header.h +// Example +// +// Created by Tony Li on 11/4/15. +// Copyright © 2015 Douban Inc. All rights reserved. +// + +#import +#import "Rexxar/RXRNavTitleWidget.h" +#import "Rexxar/RXRAlertDialogWidget.h" +#import "Rexxar/RXRPullRefreshWidget.h" + + +#import "RXRToastWidget.h" +#import "RXRNavMenuWidget.h" +#import "RXRLocContainerAPI.h" + diff --git a/RexxarDemo/ContainerAPI/RXRLocContainerAPI.h b/RexxarDemo/ContainerAPI/RXRLocContainerAPI.h new file mode 100644 index 0000000..79320c5 --- /dev/null +++ b/RexxarDemo/ContainerAPI/RXRLocContainerAPI.h @@ -0,0 +1,13 @@ +// +// RXRLocContainerAPI.h +// Rexxar +// +// Created by GUO Lin on 8/19/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import + +@interface RXRLocContainerAPI : NSObject + +@end diff --git a/RexxarDemo/ContainerAPI/RXRLocContainerAPI.m b/RexxarDemo/ContainerAPI/RXRLocContainerAPI.m new file mode 100644 index 0000000..b8b4435 --- /dev/null +++ b/RexxarDemo/ContainerAPI/RXRLocContainerAPI.m @@ -0,0 +1,51 @@ +// +// RXRLocContainerAPI.m +// Rexxar +// +// Created by GUO Lin on 8/19/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// +#import + +#import "RXRLocContainerAPI.h" + +@implementation RXRLocContainerAPI + +- (BOOL)shouldInterceptRequest:(NSURLRequest *)request +{ + // http://rexxar-container/api/event_location + if ([request.URL.scheme isEqualToString:@"http"] && + [request.URL.host isEqualToString:@"rexxar-container"] && + [request.URL.path hasPrefix:@"/api/event_location"]) { + + return YES; + } + return NO; +} + +- (NSURLResponse *)responseWithRequest:(NSURLRequest *)request +{ + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + return response; +} + + +- (NSData *)responseData +{ + // It's just a demo here. + // You can implement your own loc service to get the current city data. + NSDictionary *dictionary = @{@"name": @"北京", + @"letter": @"beijing", + @"longitude": @(116.41667), + @"latitude": @(39.91667)}; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary + options:NSJSONWritingPrettyPrinted + error:&error]; + return jsonData; +} + +@end diff --git a/RexxarDemo/DemoRXRViewController.swift b/RexxarDemo/DemoRXRViewController.swift new file mode 100644 index 0000000..53f9d11 --- /dev/null +++ b/RexxarDemo/DemoRXRViewController.swift @@ -0,0 +1,41 @@ +// +// DemoRXRViewController.swift +// Rexxar +// +// Created by GUO Lin on 8/19/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +import UIKit + +class DemoRXRViewController: RXRViewController { + + + override func viewDidLoad() { + super.viewDidLoad() + + let pullRefreshWidget = RXRPullRefreshWidget() + let titleWidget = RXRNavTitleWidget() + let alertDialogWidget = RXRAlertDialogWidget() + let toastWidget = RXRToastWidget() + let navMenuWidget = RXRNavMenuWidget() + + widgets = [titleWidget, alertDialogWidget, pullRefreshWidget, toastWidget, navMenuWidget] + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + let locContainerAPI = RXRLocContainerAPI() + + RXRContainerIntercepter.setContainerAPIs([locContainerAPI]) + URLProtocol.registerClass(RXRContainerIntercepter.self) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + URLProtocol.unregisterClass(RXRContainerIntercepter.self) + } + +} diff --git a/RexxarDemo/Info.plist b/RexxarDemo/Info.plist new file mode 100644 index 0000000..a195fc7 --- /dev/null +++ b/RexxarDemo/Info.plist @@ -0,0 +1,61 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + None + CFBundleURLSchemes + + douban + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/RexxarDemo/Library/FRDToast/FRDToast.swift b/RexxarDemo/Library/FRDToast/FRDToast.swift new file mode 100644 index 0000000..54f9051 --- /dev/null +++ b/RexxarDemo/Library/FRDToast/FRDToast.swift @@ -0,0 +1,306 @@ +// +// FRDToast.swift +// FRDToast +// +// Created by 李俊 on 15/11/11. +// Copyright © 2015 Douban Inc. All rights reserved. +// + +import UIKit + +private let toastStartY: CGFloat = 50 +private let toastFinalY: CGFloat = 80 +private let miniToastShowTime: TimeInterval = 1.5 +private let horizonalMargin: CGFloat = 25 + + +@objc public enum FRDToastMaskType: Int { + case `default` // allow user interactions while Toast is displayed + case clear // don't allow user interactions +} + + +open class FRDToast: NSObject { + + /** + 设置文本字体,如果不设置该属性,缺省为 HelveticaNeue-Medium 字体。 + */ + open static var titleFont = UIFont(name:"HelveticaNeue-Medium", size:15) { + didSet { + sharedToast.toastView.titleFont = titleFont + } + } + + fileprivate static let sharedToast = FRDToast() + + fileprivate lazy var toastView: ToastView = { + let view = ToastView() + view.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin] + return view + }() + + fileprivate lazy var overlayView: UIControl = { + + let application = UIApplication.shared + let window = application.delegate?.window ?? nil + + var windowBounds = CGRect.zero + if let bounds = window?.bounds { + windowBounds = bounds + } + + let view = UIControl(frame: windowBounds) + view.backgroundColor = UIColor.clear + view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.isUserInteractionEnabled = false + return view + }() + + fileprivate var fadeOutTimer: Timer? + fileprivate var toastShowTime = miniToastShowTime + fileprivate var isFadeIn = false + fileprivate var isFadeOut = false + + fileprivate func showToast(_ title: String, color: UIColor, maskType: FRDToastMaskType, image: UIImage?, loadingAnimateOrNot: Bool) { + + let application = UIApplication.shared + let window = application.delegate?.window ?? nil + + var windowBounds = CGRect.zero + if let bounds = window?.bounds { + windowBounds = bounds + } + + overlayView.frame = windowBounds + if overlayView.superview == nil { + for window in UIApplication.shared.windows { + let windowOnMainScreen = window.screen == UIScreen.main + let windowIsVisible = !window.isHidden && window.alpha > 0 + let windowLevelNormal = window.windowLevel == UIWindowLevelNormal + + if windowOnMainScreen && windowIsVisible && windowLevelNormal { + window.addSubview(overlayView) + break + } + } + } else { + overlayView.superview?.bringSubview(toFront: overlayView) + } + + switch maskType { + case .default: + overlayView.isUserInteractionEnabled = false + case .clear: + overlayView.isUserInteractionEnabled = true + } + + toastShowTime = displayDurationForTitle(title) + if fadeOutTimer != nil { + fadeOutTimer?.invalidate() + } + + if toastView.superview != nil && !isFadeIn { + let newToastView = ToastView() + newToastView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin] + newToastView.updateContent(title, color: color, image: image, loadingAnimateOrNot: loadingAnimateOrNot) + switchToastViewWithAnimation(newToastView) + } else { + toastView.updateContent(title, color: color, image: image, loadingAnimateOrNot: loadingAnimateOrNot) + addToastViewToOverLayView(toastView, center: nil) + showToastWithAnimation() + } + } + + fileprivate func addToastViewToOverLayView(_ toastView: ToastView, center: CGPoint?) { + let toastViewSize = toastView.sizeThatFits(CGSize(width: (overlayView.bounds.width - 2 * horizonalMargin), height: 0)) + toastView.bounds = CGRect(x: 0, y: 0, width: toastViewSize.width, height: toastViewSize.height) + + if toastView.superview == nil { + overlayView.addSubview(toastView) + toastView.alpha = 0 + let centerX = overlayView.bounds.width / 2 + toastView.center = (center != nil ? center! : CGPoint(x: centerX, y: toastStartY + toastView.bounds.height/2)) + } + } + + fileprivate func showToastWithAnimation() { + if isFadeIn { + return + } + isFadeIn = true + isFadeOut = false + UIView.animate(withDuration: TimeInterval(0.5 * (1 - toastView.alpha)), delay: 0, options: [.curveEaseIn, .allowUserInteraction], animations: { () -> Void in + self.toastView.center.y = toastFinalY + self.toastView.bounds.height/2 + self.toastView.alpha = 1 + }, completion: { (_) -> Void in + if self.toastView.alpha == 1 { + self.isFadeIn = false + if self.toastView.loadingAnimateOrNot { + self.toastView.startLoadingAnimation() + return + } + + self.fadeOutTimer = Timer(timeInterval: self.toastShowTime, target: self, selector: #selector(self.dismiss), userInfo: nil, repeats: false) + RunLoop.main.add(self.fadeOutTimer!, forMode: RunLoopMode.commonModes) + } + }) + } + + fileprivate func switchToastViewWithAnimation(_ newToastView: ToastView) { + let oldToastView = toastView + if oldToastView.loadingAnimateOrNot { + oldToastView.stopLoadingAnimation() + } + + toastView = newToastView + newToastView.alpha = 0 + addToastViewToOverLayView(newToastView, center: oldToastView.center) + + if isFadeOut || isFadeIn { + oldToastView.removeFromSuperview() + newToastView.alpha = oldToastView.alpha + showToastWithAnimation() + return + } + + UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn, .allowUserInteraction], animations: { () -> Void in + oldToastView.alpha = 0 + }, completion: { (_) -> Void in + if self.toastView.alpha == 0 { + oldToastView.removeFromSuperview() + } + }) + + UIView.animate(withDuration: TimeInterval(0.5 * (1 - newToastView.alpha)), delay: 0, options: [.curveEaseIn, .allowUserInteraction], animations: { () -> Void in + newToastView.alpha = 1 + }, completion: nil) + + showToastWithAnimation() + } + + @objc fileprivate func dismiss() { + if isFadeOut || toastView.alpha == 0 { + return + } + isFadeIn = false + isFadeOut = true + if toastView.loadingAnimateOrNot { + toastView.stopLoadingAnimation() + } + + UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseIn, .allowUserInteraction], animations: { () -> Void in + self.toastView.center.y = toastStartY + self.toastView.alpha = 0 + }, completion: { (_) -> Void in + if self.toastView.alpha == 0 { + self.isFadeOut = false + self.toastView.removeFromSuperview() + self.overlayView.removeFromSuperview() + self.fadeOutTimer = nil + } + }) + } + + fileprivate func showStaticToast(_ status: String, color: UIColor, image: UIImage?) { + showToast(status, color: color, maskType: .default, image: image, loadingAnimateOrNot: false) + } + + fileprivate func displayDurationForTitle(_ title: String) -> TimeInterval { + let nsTitle = title as NSString + let time = max(TimeInterval(nsTitle.length)*0.06 + 0.5, miniToastShowTime) + return min(time, 5.0) + } +} + +// MARK: public function for show + +public extension FRDToast { + + /** + 灰色,用于展示信息的提示。 + + - Parameter status: 文本信息 + */ + class func showInfo(_ status: String) { + showInfo(status, image: nil) + } + + /** + 灰色,用于展示带图片的信息提示。 + + - Parameter status: 文本信息 + - Parameter image: 图片 + */ + class func showInfo(_ status: String, image: UIImage?) { + let color = UIColor(hex: 0x494949, alpha: 0.96) + FRDToast.sharedToast.showStaticToast(status, color: color, image: image) + } + + /** + 绿色,用于成功的提示。 + + - Parameter status: 文本信息 + */ + class func showSuccess(_ status: String) { + showSuccess(status, image: nil) + } + + /** + 绿色,用于带图片的成功的提示。 + + - Parameter status: 文本信息 + - Parameter image: 图片 + */ + class func showSuccess(_ status: String, image: UIImage?) { + let color = UIColor(hex: 0x42bd56, alpha: 0.96) + FRDToast.sharedToast.showStaticToast(status, color: color, image: image) + } + + /** + 红色,用于失败、警告信息,比如某项操作失败,密码错误等。 + + - Parameter status: 展示的文本信息 + */ + class func showError(_ status: String) { + showError(status, image: nil) + } + + /** + 红色,用于带图片的失败、警告信息,比如某项操作失败,密码错误等。 + + - Parameter status: 展示的文本信息 + - Parameter image: 图片 + + */ + class func showError(_ status: String, image: UIImage?) { + let color = UIColor(hex: 0xff4055, alpha: 0.96) + FRDToast.sharedToast.showStaticToast(status, color: color, image: image) + } + + /** + 显示一个自己定制的 Toast。 + + - Parameter status: 展示的文本信息 + - Parameter image: 图片 + - Parameter backgroundColor: 背景色 + - Parameter maskType: 交互类型 + */ + class func show(_ status: String, backgroundColor: UIColor, image: UIImage?, maskType: FRDToastMaskType) { + FRDToast.sharedToast.showStaticToast(status, color: backgroundColor, image: image) + } + + /** + 使 Toast 消失。 + */ + class func dismiss() { + FRDToast.sharedToast.dismiss() + } + + /** + 检查 Toast 的可见性。 + */ + class func isVisible() -> Bool { + let toastView = FRDToast.sharedToast.toastView + return toastView.superview != nil && toastView.alpha == 1.0 + } +} diff --git a/RexxarDemo/Library/FRDToast/LoadingView.swift b/RexxarDemo/Library/FRDToast/LoadingView.swift new file mode 100644 index 0000000..c8cfbe3 --- /dev/null +++ b/RexxarDemo/Library/FRDToast/LoadingView.swift @@ -0,0 +1,183 @@ +// +// LoadingView.swift +// Frodo +// +// Created by 李俊 on 15/12/7. +// Copyright © 2015年 Douban Inc. All rights reserved. +// + +import UIKit + +private let animationDuration: TimeInterval = 2.4 + +@objc class LoadingView: UIView { + + var lineWidth: CGFloat = 5 { + didSet { + setNeedsLayout() + } + } + + var strokeColor = UIColor(hex: 0x42BD56) { + didSet { + ringLayer.strokeColor = strokeColor.cgColor + rightPointLayer.strokeColor = strokeColor.cgColor + leftPointLayer.strokeColor = strokeColor.cgColor + } + } + + fileprivate let ringLayer = CAShapeLayer() + fileprivate let pointSuperLayer = CALayer() + fileprivate let rightPointLayer = CAShapeLayer() + fileprivate let leftPointLayer = CAShapeLayer() + fileprivate var isAnimating = false + + init(frame: CGRect, color: UIColor?) { + super.init(frame: frame) + strokeColor = color ?? strokeColor + + ringLayer.contentsScale = UIScreen.main.scale + ringLayer.strokeColor = strokeColor.cgColor + ringLayer.fillColor = UIColor.clear.cgColor + ringLayer.lineCap = kCALineCapRound + ringLayer.lineJoin = kCALineJoinBevel + + layer.addSublayer(ringLayer) + layer.addSublayer(pointSuperLayer) + + rightPointLayer.strokeColor = strokeColor.cgColor + rightPointLayer.lineCap = kCALineCapRound + pointSuperLayer.addSublayer(rightPointLayer) + + leftPointLayer.strokeColor = strokeColor.cgColor + leftPointLayer.lineCap = kCALineCapRound + pointSuperLayer.addSublayer(leftPointLayer) + + NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + let centerPoint = CGPoint(x: bounds.width/2, y: bounds.height/2) + let radius = bounds.width/2 - lineWidth + let path = UIBezierPath(arcCenter: centerPoint, radius: radius, startAngle:CGFloat(-M_PI), endAngle: CGFloat(M_PI * 0.6), clockwise: true) + + ringLayer.lineWidth = lineWidth + ringLayer.path = path.cgPath + ringLayer.frame = bounds + + let x = bounds.width/2 - CGFloat(sin(M_PI * 50.0/180.0)) * radius + let y = bounds.height/2 - CGFloat(sin(M_PI * 40.0/180.0)) * radius + let rightPoint = CGPoint(x: bounds.width - x, y: y) + let leftPoint = CGPoint(x: x, y: y) + + let rightPointPath = UIBezierPath() + rightPointPath.move(to: rightPoint) + rightPointPath.addLine(to: rightPoint) + rightPointLayer.path = rightPointPath.cgPath + rightPointLayer.lineWidth = lineWidth + + let leftPointPath = UIBezierPath() + leftPointPath.move(to: leftPoint) + leftPointPath.addLine(to: leftPoint) + leftPointLayer.path = leftPointPath.cgPath + leftPointLayer.lineWidth = lineWidth + + pointSuperLayer.frame = bounds + } + + func startAnimation() { + + if isAnimating { return } + pointSuperLayer.isHidden = false + + let keyTimes = [NSNumber(value: 0 as Double), NSNumber(value: 0.216 as Double), NSNumber(value: 0.396 as Double), NSNumber(value: 0.8 as Double), NSNumber(value: 1 as Int32)] + + // pointSuperLayer animation + + let pointKeyAnimation = CAKeyframeAnimation(keyPath: "transform.rotation.z") + pointKeyAnimation.duration = animationDuration + pointKeyAnimation.repeatCount = Float.infinity + pointKeyAnimation.values = [0, (2 * M_PI * 0.375 + 2 * M_PI), (4 * M_PI), (4 * M_PI), (4 * M_PI + 0.3 * M_PI)] + pointKeyAnimation.keyTimes = keyTimes + pointSuperLayer.add(pointKeyAnimation, forKey: nil) + + // ringLayer animation + + let ringAnimationGroup = CAAnimationGroup() + + let ringKeyRotationAnimation = CAKeyframeAnimation(keyPath: "transform.rotation.z") + ringKeyRotationAnimation.values = [0, (2 * M_PI), (M_PI/2 + 2 * M_PI), (M_PI/2 + 2 * M_PI), (4 * M_PI)] + ringKeyRotationAnimation.keyTimes = keyTimes + ringAnimationGroup.animations = [ringKeyRotationAnimation] + + let ringKeyStartAnimation = CAKeyframeAnimation(keyPath: "strokeStart") + ringKeyStartAnimation.values = [0, 0.25, 0.35, 0.35, 0] + ringKeyStartAnimation.keyTimes = keyTimes + ringAnimationGroup.animations?.append(ringKeyStartAnimation) + + let ringKeyEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd") + ringKeyEndAnimation.values = [1, 1, 0.9, 0.9, 1] + ringKeyEndAnimation.keyTimes = keyTimes + ringAnimationGroup.animations?.append(ringKeyEndAnimation) + + ringAnimationGroup.duration = animationDuration + ringAnimationGroup.repeatCount = Float.infinity + ringLayer.add(ringAnimationGroup, forKey: nil) + + // pointAnimation + + let rightPointKeyAnimation = CAKeyframeAnimation(keyPath: "lineWidth") + rightPointKeyAnimation.values = [lineWidth, lineWidth, lineWidth * 1.4, lineWidth * 1.4, lineWidth] + rightPointKeyAnimation.keyTimes = [NSNumber(value: 0 as Double), NSNumber(value: 0.21 as Double), NSNumber(value: 0.29 as Double), NSNumber(value: 0.88 as Double), NSNumber(value: 0.96 as Double)] + rightPointKeyAnimation.duration = animationDuration + rightPointKeyAnimation.repeatCount = Float.infinity + rightPointLayer.add(rightPointKeyAnimation, forKey: nil) + + let leftPointKeyAnimation = CAKeyframeAnimation(keyPath: "lineWidth") + leftPointKeyAnimation.values = [lineWidth, lineWidth, lineWidth * 1.4, lineWidth * 1.4, lineWidth] + leftPointKeyAnimation.keyTimes = [NSNumber(value: 0 as Double), NSNumber(value: 0.31 as Double), NSNumber(value: 0.39 as Double), NSNumber(value: 0.8 as Double), NSNumber(value: 0.88 as Double)] + leftPointKeyAnimation.duration = animationDuration + leftPointKeyAnimation.repeatCount = Float.infinity + leftPointLayer.add(leftPointKeyAnimation, forKey: nil) + + isAnimating = true + } + + func stopAnimation() { + pointSuperLayer.removeAllAnimations() + ringLayer.removeAllAnimations() + rightPointLayer.removeAllAnimations() + leftPointLayer.removeAllAnimations() + + isAnimating = false + } + + func setPercentage(_ percent: CGFloat) { + pointSuperLayer.isHidden = true + ringLayer.strokeEnd = percent + } + + @objc fileprivate func appWillEnterForeground() { + if isAnimating { + isAnimating = false + startAnimation() + } + } + + override func willMove(toWindow newWindow: UIWindow?) { + if newWindow != nil && isAnimating { + isAnimating = false + startAnimation() + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + } +} diff --git a/RexxarDemo/Library/FRDToast/ToastView.swift b/RexxarDemo/Library/FRDToast/ToastView.swift new file mode 100644 index 0000000..3333d3d --- /dev/null +++ b/RexxarDemo/Library/FRDToast/ToastView.swift @@ -0,0 +1,163 @@ +// +// ToastView.swift +// FRDToast +// +// Created by 李俊 on 15/11/11. +// Copyright © 2015年 Douban Inc. All rights reserved. +// + +import UIKit + +private let horizonalMargin: CGFloat = 25 +private let imageTitleMargin: CGFloat = 5 +private let verticalMargin: CGFloat = 10 + +class ToastView: UIView { + + var titleFont = UIFont(name:"HelveticaNeue-Medium", size:15) { + didSet { + label.font = titleFont + } + } + + fileprivate var image: UIImage? { + didSet { + guard let image = image else { + imageView?.removeFromSuperview() + imageView = nil + return + } + + if imageView == nil { + imageView = UIImageView() + addSubview(imageView!) + } + imageView?.image = image + } + } + + fileprivate var loadingView: LoadingView? + fileprivate var imageView: UIImageView? + fileprivate let label: UILabel + + var loadingAnimateOrNot: Bool = false { + didSet { + if !loadingAnimateOrNot { + loadingView?.removeFromSuperview() + loadingView = nil + return + } + + if loadingView == nil { + loadingView = LoadingView(frame: .zero, color: UIColor.white) + loadingView?.lineWidth = 2 + addSubview(loadingView!) + } + } + } + + override init(frame: CGRect) { + label = UILabel(frame: frame) + super.init(frame: frame) + + label.font = titleFont + label.textColor = UIColor.white + label.textAlignment = .center + label.numberOfLines = 3 + addSubview(label) + + layer.shadowColor = UIColor(hex: 0x000000).cgColor + layer.shadowOpacity = 0.3 + layer.shadowOffset = CGSize(width: 0, height: 0) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + var labelMaxWidth = size.width - horizonalMargin * 2 + var width: CGFloat = horizonalMargin * 2 + if image != nil || loadingAnimateOrNot { + let lineHeight = label.font.lineHeight + var imageViewSize = CGSize.zero + if let image = image { + imageViewSize = computeLableSideViewSize(lineHeight, width: size.width, imageContentSize: image.size) + } + + if loadingAnimateOrNot { + imageViewSize = computeLableSideViewSize(lineHeight, width: size.width, imageContentSize: CGSize(width: lineHeight, height: lineHeight)) + } + + let imageViewWidth = imageViewSize.width + width += (imageViewWidth + imageTitleMargin) + labelMaxWidth -= (imageViewWidth + imageTitleMargin) + } + + let maxSize = CGSize(width: labelMaxWidth, height: size.height) + let labelSize = label.sizeThatFits(maxSize) + width += labelSize.width + return CGSize(width: width, height: labelSize.height + 2 * verticalMargin) + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = bounds.height / 2 + + var labelMaxWidth = bounds.width - horizonalMargin * 2 + var x = horizonalMargin + if image != nil || loadingAnimateOrNot { + let lineHeight = label.font.lineHeight + var sideViewSize = CGSize.zero + if let image = image { + sideViewSize = computeLableSideViewSize(lineHeight, width: bounds.width, imageContentSize: image.size) + imageView?.frame = CGRect(x: x, y: (bounds.height - sideViewSize.height) / 2, width: sideViewSize.width, height: sideViewSize.height) + } + + if loadingAnimateOrNot { + sideViewSize = computeLableSideViewSize(lineHeight, width: bounds.width, imageContentSize: CGSize(width: lineHeight, height: lineHeight)) + loadingView?.frame = CGRect(x: x, y: (bounds.height - sideViewSize.height) / 2, width: sideViewSize.width, height: sideViewSize.height) + } + + x += (sideViewSize.width + imageTitleMargin) + labelMaxWidth -= (sideViewSize.width + imageTitleMargin) + } + + let maxSize = CGSize(width: labelMaxWidth, height: 0) + let labelSize = label.sizeThatFits(maxSize) + label.frame = CGRect(x: x, y: (bounds.height - labelSize.height) / 2, width: labelSize.width, height: labelSize.height) + } + + fileprivate func computeLableSideViewSize(_ height: CGFloat, width: CGFloat, imageContentSize: CGSize) -> CGSize { + let width = imageContentSize.width / imageContentSize.height * height + let labelSize = label.sizeThatFits(CGSize(width: width - horizonalMargin * 2 - width - imageTitleMargin, height: 0)) + if labelSize.height > height { + return computeLableSideViewSize(labelSize.height, width: width, imageContentSize: imageContentSize) + } + + return CGSize(width: width, height: height) + } + +} + +// MARK: internal method + +extension ToastView { + + func updateContent(_ title: String, color: UIColor, image toastImage: UIImage?, loadingAnimateOrNot bool: Bool) { + label.text = title + backgroundColor = color + image = toastImage + loadingAnimateOrNot = bool + setNeedsLayout() + } + + func startLoadingAnimation() { + loadingView?.startAnimation() + } + + func stopLoadingAnimation() { + loadingView?.stopAnimation() + } + +} diff --git a/RexxarDemo/Library/FRDToast/UIColor+helper.swift b/RexxarDemo/Library/FRDToast/UIColor+helper.swift new file mode 100644 index 0000000..c71c63c --- /dev/null +++ b/RexxarDemo/Library/FRDToast/UIColor+helper.swift @@ -0,0 +1,24 @@ +// +// UIColor+helper.swift +// FRDToast +// +// Created by GUO Lin on 7/13/16. +// Copyright © 2015年 Douban Inc. All rights reserved. +// + +import UIKit + +extension UIColor { + + convenience init(hex rgbHexValue: UInt, alpha: CGFloat) { + self.init(red: ((CGFloat)((rgbHexValue & 0xFF0000) >> 16))/255.0, + green: ((CGFloat)((rgbHexValue & 0xFF00) >> 8))/255.0, + blue: ((CGFloat)(rgbHexValue & 0xFF))/255.0, + alpha: alpha) + } + + convenience init(hex rgbHexValue: UInt) { + self.init(hex: rgbHexValue, alpha: 1.0) + } + +} \ No newline at end of file diff --git a/RexxarDemo/PartialRexxarViewController.swift b/RexxarDemo/PartialRexxarViewController.swift new file mode 100644 index 0000000..68c7b3e --- /dev/null +++ b/RexxarDemo/PartialRexxarViewController.swift @@ -0,0 +1,70 @@ +// +// PartialRexxarViewController.swift +// Rexxar +// +// Created by GUO Lin on 5/19/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +import UIKit + +class PartialRexxarViewController: UIViewController { + + var rexxarURI: URL + var childRexxarViewController: RXRViewController + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + init(URI: URL) { + rexxarURI = URI + childRexxarViewController = RXRViewController(uri: rexxarURI) + + super.init(nibName: nil, bundle: nil) + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.lightGray + childRexxarViewController.view.backgroundColor = UIColor.white + + addChildViewController(childRexxarViewController) + childRexxarViewController.view.frame = CGRect(x: 0, + y: 100, + width: view.frame.size.width, + height: 500) + view.addSubview(childRexxarViewController.view) + childRexxarViewController.didMove(toParentViewController: self) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + childRexxarViewController.beginAppearanceTransition(true, animated: animated) + + let headers = ["Customer-Authorization": "Bearer token"] + let parameters = ["apikey": "apikey value"] + let requestDecorator = RXRRequestDecorator(headers: headers, parameters: parameters) + RXRRequestIntercepter.setDecorators([requestDecorator]) + + URLProtocol.registerClass(RXRRequestIntercepter.self) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + childRexxarViewController.endAppearanceTransition() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + childRexxarViewController.beginAppearanceTransition(false, animated: animated) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + childRexxarViewController.endAppearanceTransition() + + URLProtocol.unregisterClass(RXRRequestIntercepter.self) + } + +} diff --git a/RexxarDemo/Resource/hybrid/rexxar/demo-dd19d987ef.html b/RexxarDemo/Resource/hybrid/rexxar/demo-dd19d987ef.html new file mode 100644 index 0000000..94293f2 --- /dev/null +++ b/RexxarDemo/Resource/hybrid/rexxar/demo-dd19d987ef.html @@ -0,0 +1,90 @@ + + + + + + + Rexxar Demo + + +
+
+
记录全局错误信息
+
----errorMsg----
+
----url----
+
----lineNumber----
+
----column----
+
----errorObj----
+
+ + + + + + + + + \ No newline at end of file diff --git a/RexxarDemo/Resource/hybrid/routes.json b/RexxarDemo/Resource/hybrid/routes.json new file mode 100644 index 0000000..7fc2b95 --- /dev/null +++ b/RexxarDemo/Resource/hybrid/routes.json @@ -0,0 +1,12 @@ +{ + "items": [{ + "remote_file": "https://img3.doubanio.com/dae/rexxar/pre-files/rexxar/demo-dd19d987ef.html", + "deploy_time": "Tue, 06 Sep 2016 08:42:09 GMT", + "uri": "douban://douban.com/rexxar_demo[/]?.*" + }], + "partial_items": [{ + "remote_file": "https://img3.doubanio.com/dae/rexxar/pre-files/rexxar/demo-dd19d987ef.html", + "deploy_time": "Tue, 06 Sep 2016 08:42:09 GMT", + "uri": "douban://partial.douban.com/rexxar_demo/_.*" + }] +} \ No newline at end of file diff --git a/RexxarDemo/RoutesViewController.swift b/RexxarDemo/RoutesViewController.swift new file mode 100644 index 0000000..6629792 --- /dev/null +++ b/RexxarDemo/RoutesViewController.swift @@ -0,0 +1,46 @@ +// +// RoutesViewController.swift +// Rexxar +// +// Created by Tony Li on 11/25/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +import UIKit + +class RoutesViewController: UITableViewController { + + fileprivate let URIs = [URL(string: "douban://douban.com/rexxar_demo")!, + URL(string: "douban://partial.douban.com/rexxar_demo/_.s")!] + + override func viewDidLoad() { + super.viewDidLoad() + navigationController?.navigationBar.isTranslucent = false; + + title = "URIs" + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return URIs.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + cell.textLabel?.text = URIs[(indexPath as NSIndexPath).row].absoluteString + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let uri = URIs[(indexPath as NSIndexPath).row] + if (indexPath as NSIndexPath).row == 0 { + + let controller = DemoRXRViewController(uri: uri) + navigationController?.pushViewController(controller, animated: true) + controller.view.backgroundColor = UIColor.white + } else if (indexPath as NSIndexPath).row == 1 { + navigationController?.pushViewController(PartialRexxarViewController(URI: uri), animated: true) + } + } + +} diff --git a/RexxarDemo/Widget/Model/Menu/RXRMenuItem.h b/RexxarDemo/Widget/Model/Menu/RXRMenuItem.h new file mode 100644 index 0000000..24d2e3f --- /dev/null +++ b/RexxarDemo/Widget/Model/Menu/RXRMenuItem.h @@ -0,0 +1,20 @@ +// +// RXRMenuItem.h +// Frodo +// +// Created by Tony Li on 11/25/15. +// Copyright © 2015 Douban Inc. All rights reserved. +// + +@import UIKit; + +#import + +@interface RXRMenuItem : RXRModel + +@property (nonatomic, copy, readonly) NSString *type; +@property (nonatomic, copy, readonly) NSString *title; +@property (nonatomic, copy, readonly) UIColor *color; +@property (nonatomic, copy, readonly) NSURL *uri; + +@end diff --git a/RexxarDemo/Widget/Model/Menu/RXRMenuItem.m b/RexxarDemo/Widget/Model/Menu/RXRMenuItem.m new file mode 100644 index 0000000..4030226 --- /dev/null +++ b/RexxarDemo/Widget/Model/Menu/RXRMenuItem.m @@ -0,0 +1,34 @@ +// +// RXRMenuItem.m +// Frodo +// +// Created by Tony Li on 11/25/15. +// Copyright © 2015 Douban Inc. All rights reserved. +// + + +#import "RXRMenuItem.h" + +@implementation RXRMenuItem + +- (NSString *)type +{ + return [self.dictionary objectForKey:@"type"]; +} + +- (NSString *)title +{ + return [self.dictionary objectForKey:@"title"]; +} + +- (NSString *)color +{ + return [self.dictionary objectForKey:@"color"]; +} + +- (NSURL *)uri +{ + return [NSURL URLWithString:[self.dictionary objectForKey:@"uri"]]; +} + +@end diff --git a/RexxarDemo/Widget/RXRNavMenuWidget.h b/RexxarDemo/Widget/RXRNavMenuWidget.h new file mode 100644 index 0000000..4757939 --- /dev/null +++ b/RexxarDemo/Widget/RXRNavMenuWidget.h @@ -0,0 +1,13 @@ +// +// RXRNavMenuWidget.h +// RexxarDemo +// +// Created by GUO Lin on 5/5/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +#import + +@interface RXRNavMenuWidget : NSObject + +@end diff --git a/RexxarDemo/Widget/RXRNavMenuWidget.m b/RexxarDemo/Widget/RXRNavMenuWidget.m new file mode 100644 index 0000000..9b636d8 --- /dev/null +++ b/RexxarDemo/Widget/RXRNavMenuWidget.m @@ -0,0 +1,84 @@ +// +// RXRNavMenuWidget.m +// RexxarDemo +// +// Created by GUO Lin on 5/5/16. +// Copyright © 2016 Douban Inc. All rights reserved. +// + +#import +#import +#import + +#import "RXRNavMenuWidget.h" +#import "RXRMenuItem.h" + + +@interface RXRNavMenuWidget () + +@property (nonatomic, copy) NSArray *menuItems; + +@end + + +@implementation RXRNavMenuWidget + +- (BOOL)canPerformWithURL:(NSURL *)URL +{ + NSString *path = URL.path; + if (path && [path isEqualToString:@"/widget/nav_menu"]) { + return true; + } + return false; +} + +- (void)prepareWithURL:(NSURL *)URL +{ + NSString *string = [[URL rxr_queryDictionary] rxr_itemForKey:@"data"]; + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + NSArray *itemJSONs = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; + if ([itemJSONs isKindOfClass:[NSArray class]] && itemJSONs.count > 0) { + NSMutableArray *menuItems = [NSMutableArray array]; + for (id JSON in itemJSONs) { + if ([JSON isKindOfClass:[NSDictionary class]]) { + [menuItems addObject:[[RXRMenuItem alloc] initWithDictionary:JSON]]; + } + } + self.menuItems = [menuItems copy]; + } +} + +- (void)performWithController:(RXRViewController *)controller +{ + if (!self.menuItems || self.menuItems.count == 0) { + return; + } + + NSMutableArray *items = [NSMutableArray array]; + [self.menuItems enumerateObjectsUsingBlock:^(RXRMenuItem *menu, NSUInteger idx, BOOL *stop) { + UIBarButtonItem *item = [self _frd_buildMenuItem:menu]; + item.tag = idx; + [items addObject:item]; + }]; + controller.navigationItem.rightBarButtonItems = items; +} + + +#pragma mark - Private methods + +- (void)_frd_buttonItemAction:(UIBarButtonItem *)item +{ + RXRMenuItem *menu = self.menuItems[item.tag]; + NSLog(@"Action go to uri: %@", menu.uri); +} + +- (UIBarButtonItem *)_frd_buildMenuItem:(RXRMenuItem *)menu +{ + UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithTitle:menu.title + style:UIBarButtonItemStylePlain + target:self + action:@selector(_frd_buttonItemAction:)]; + return item; +} + +@end diff --git a/RexxarDemo/Widget/RXRToastWidget.h b/RexxarDemo/Widget/RXRToastWidget.h new file mode 100644 index 0000000..c1202b8 --- /dev/null +++ b/RexxarDemo/Widget/RXRToastWidget.h @@ -0,0 +1,13 @@ +// +// RXRToastWidget.h +// Rexxar +// +// Created by GUO Lin on 8/19/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import + +@interface RXRToastWidget : NSObject + +@end diff --git a/RexxarDemo/Widget/RXRToastWidget.m b/RexxarDemo/Widget/RXRToastWidget.m new file mode 100644 index 0000000..bff7875 --- /dev/null +++ b/RexxarDemo/Widget/RXRToastWidget.m @@ -0,0 +1,54 @@ +// +// RXRToastWidget.m +// Rexxar +// +// Created by GUO Lin on 8/19/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import +#import + +#import "RXRToastWidget.h" + +#import "RexxarDemo-Swift.h" + +@interface RXRToastWidget () + +@property (nonatomic, copy) NSString *level; +@property (nonatomic, copy) NSString *message; + +@end + + +@implementation RXRToastWidget + +- (BOOL)canPerformWithURL:(NSURL *)URL +{ + NSString *path = URL.path; + if (path && [path isEqualToString:@"/widget/toast"]) { + return true; + } + return false; +} + + +- (void)prepareWithURL:(NSURL *)URL +{ + NSDictionary *queryItems = [URL rxr_queryDictionary]; + self.level = [queryItems rxr_itemForKey:@"level"]; + self.message = [queryItems rxr_itemForKey:@"message"]; +} + +- (void)performWithController:(RXRViewController *)controller +{ + if ([self.level isEqualToString:@"info"]) { + [FRDToast showSuccess:self.message]; + } else if ([self.level isEqualToString:@"error"]) { + [FRDToast showInfo:self.message]; + } else if ([self.level isEqualToString:@"fatal"]) { + [FRDToast showError:self.message]; + } +} + +@end diff --git a/RexxarTests/Info.plist b/RexxarTests/Info.plist new file mode 100644 index 0000000..ba72822 --- /dev/null +++ b/RexxarTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/RexxarTests/RXRRouteFileCacheTests.m b/RexxarTests/RXRRouteFileCacheTests.m new file mode 100644 index 0000000..af2a1ed --- /dev/null +++ b/RexxarTests/RXRRouteFileCacheTests.m @@ -0,0 +1,94 @@ +// +// RXRRouteFileCacheTests.m +// Rexxar +// +// Created by GUO Lin on 5/12/16. +// Copyright © 2016 Douban.Inc. All rights reserved. +// + +#import + +#import "RXRCacheFileIntercepter.h" +#import "RXRConfig.h" + +#import "RXRRouteFileCache.h" + +@interface RXRRouteFileCacheTests : XCTestCase + + +@end + +@implementation RXRRouteFileCacheTests + +- (void)setUp +{ + NSString *resourcePath = [[NSBundle bundleForClass:self.class] pathForResource:@"www" ofType:nil]; + [RXRConfig setRoutesResourcePath:resourcePath]; + + [RXRConfig setRoutesCachePath:[[NSUUID UUID] UUIDString]]; + [NSURLProtocol registerClass:[RXRCacheFileIntercepter class]]; +} + ++ (void)tearDown +{ + [NSURLProtocol unregisterClass:[RXRCacheFileIntercepter class]]; +} + + +- (void)testCacheJS +{ + NSURL *resourceURL = [NSURL URLWithString:@"http://img3.doubanio.com/f/shire/3d5cb5d1155d18c20ab9bd966387432a8a9f2008/js/core/_init_.js"]; + + XCTestExpectation *expect = [self expectationWithDescription:@"Resource cached"]; + [[[NSURLSession sharedSession] dataTaskWithRequest:[self webResourceRequest:resourceURL] + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + if (data && [[RXRRouteFileCache sharedInstance] routeFileURLForRemoteURL:resourceURL]) { + [expect fulfill]; + } + }] resume]; + + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)testCacheCss +{ + NSURL *resourceURL = [NSURL URLWithString:@"https://img3.doubanio.com/misc/mixed_static/6f59bfb52430ee85.css"]; + + XCTestExpectation *expect = [self expectationWithDescription:@"Resource cached"]; + [[[NSURLSession sharedSession] dataTaskWithRequest:[self webResourceRequest:resourceURL] + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + if (data && [[RXRRouteFileCache sharedInstance] routeFileURLForRemoteURL:resourceURL]) { + [expect fulfill]; + } + }] resume]; + + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)testNoCacheResource +{ + NSURL *resourceURL = [NSURL URLWithString:@"http://cdn.staticfile.org/jquery/2.1.1-rc2/jquery.js"]; + + XCTestExpectation *expect = [self expectationWithDescription:@"Resource should not be cached"]; + [[[NSURLSession sharedSession] dataTaskWithRequest:[self webResourceRequest:resourceURL] + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + if (data && [[RXRRouteFileCache sharedInstance] routeFileURLForRemoteURL:resourceURL]) { + [expect fulfill]; + } + }] resume]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + + +- (NSMutableURLRequest *)webResourceRequest:(NSURL *)url +{ + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request addValue:@"Mozilla" forHTTPHeaderField:@"User-Agent"]; + return request; +} + +@end diff --git a/RexxarTests/RXRRouteManagerTests.m b/RexxarTests/RXRRouteManagerTests.m new file mode 100644 index 0000000..1e5dbbd --- /dev/null +++ b/RexxarTests/RXRRouteManagerTests.m @@ -0,0 +1,70 @@ +// +// RouteTests.m +// Rexxar +// +// Created by Tony Li on 11/24/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// +#import + +#import "RXRConfig.h" +#import "RXRRouteManager.h" +#import "RXRViewController.h" +#import "RXRRoute.h" + +@interface RXRRouteManagerTests : XCTestCase + + +@end + + +@implementation RXRRouteManagerTests + +- (void)setUp +{ + [RXRConfig setRoutesCachePath:[[NSUUID UUID] UUIDString]]; + NSString *resourcePath = [[NSBundle bundleForClass:self.class] pathForResource:@"www" ofType:nil]; + [RXRConfig setRoutesResourcePath:resourcePath]; + + NSURL *routesMapURL = [NSURL URLWithString:@"https://rexxar.douban.com/api/routes"];; + [RXRConfig setRoutesMapURL:routesMapURL]; +} + +- (void)testRoutes +{ + NSURL *uri = [NSURL URLWithString:@"douban://douban.com/subject_collection/123"]; + + [RXRViewController updateRouteFilesWithCompletion:NULL]; + + [self expectationForPredicate:[self predicateForURI:uri routable:YES] + evaluatedWithObject:[NSObject new] + handler:nil]; + + [self expectationForPredicate:[self predicateForURI:[NSURL URLWithString:@"douban://douban.com/foo"] routable:NO] + evaluatedWithObject:[NSObject new] + handler:nil]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testLocalRoutes +{ + NSString *uri = @"douban://douban.com/subject_collection/123"; + BOOL found = NO; + + NSURL *remoteHtmlURL = [[RXRRouteManager sharedInstance] remoteHtmlURLForURI:[NSURL URLWithString:uri]]; + if (remoteHtmlURL) { + found = YES; + } + XCTAssertTrue(found); +} + +- (NSPredicate *)predicateForURI:(NSURL *)uri routable:(BOOL)routable +{ + return [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary * bindings) { + id instance = [[RXRRouteManager sharedInstance] remoteHtmlURLForURI:uri]; + return routable ? instance != nil : instance == nil; + }]; +} + +@end diff --git a/RexxarTests/RexxarTests.m b/RexxarTests/RexxarTests.m new file mode 100644 index 0000000..007322a --- /dev/null +++ b/RexxarTests/RexxarTests.m @@ -0,0 +1,49 @@ +// +// RexxarTests.m +// RexxarTests +// +// Created by Tony Li on 11/20/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// +#import + +#import "Rexxar.h" +#import "RXRRequestIntercepter.h" +#import "RXRRouteFileManager.h" +#import "RequestDecorator.h" + +@interface RexxarTests : XCTestCase + +@property (nonatomic, readonly) RXRRouteFileManager *routeFileManager; +@property (nonatomic, readonly) id decorater; + +@end + +@implementation RexxarTests + +- (void)setUp +{ + NSURL *routesMapURL = [NSURL URLWithString:@"http://rexxar.douban.com/api/routes"]; + _routeFileManager = [[RXRRouteFileManager alloc] initWithRoutesMapURL:routesMapURL + cacheDirectory:[[NSUUID UUID] UUIDString] + resourceDirectory:[NSBundle bundleForClass:self.class].bundlePath]; +} + +- (void)testInterceptAPI +{ + NSURL *url = [NSURL URLWithString:@"http://frodo.douban.com/jsonp/subject_collection/movie_free_stream/items?os=ios&loc_id=108288&start=0&count=18&_=1448948380006&callback=jsonp1"]; + XCTAssertTrue([RXRRequestIntercepter isRequestInterceptable:[self webResourceRequest:url]]); + + url = [NSURL URLWithString:@"http://frodo.douban.com/api/v2/recommend_feed"]; + XCTAssertTrue([RXRRequestIntercepter isRequestInterceptable:[self webResourceRequest:url]]); +} + +- (NSMutableURLRequest *)webResourceRequest:(NSURL *)url +{ + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request addValue:@"Mozilla" forHTTPHeaderField:@"User-Agent"]; + return request; +} + +@end + diff --git a/RexxarTests/URITests.m b/RexxarTests/URITests.m new file mode 100644 index 0000000..018517d --- /dev/null +++ b/RexxarTests/URITests.m @@ -0,0 +1,74 @@ +// +// URITests.m +// Rexxar +// +// Created by Tony Li on 11/23/15. +// Copyright © 2015 Douban.Inc. All rights reserved. +// + +#import + +#import "RXRConfig.h" +#import "RXRViewController.h" + +@interface UIWebView (URITests) + +@property (nonatomic, readonly) NSString *representedURI; + +@end + +@implementation UIWebView (URITests) + +- (NSString *)representedURI +{ + NSString *result = [self stringByEvaluatingJavaScriptFromString:@"get_uri()"]; + return result; +} + +@end + +@interface URITests : XCTestCase + +@end + +@implementation URITests + +- (void)setUp +{ + [RXRConfig setRoutesResourcePath:[[NSBundle bundleForClass:self.class] pathForResource:@"www" ofType:nil]]; +} + +- (void)testSimply +{ + [self verifyURIString:@"douban://douban.com/note/123"]; +} + +- (void)testQuery +{ + [self verifyURIString:@"douban://douban.com/note/123?key=value"]; + [self verifyURIString:@"douban://douban.com/note/123?key=中文"]; + [self verifyURIString:@"douban://douban.com/note/123?key=中文&a=b"]; +} + +- (void)testComplicate +{ + [self verifyURIString:@"douban://douban.com/note/123?page=10&from=main&title=日记#中文"]; +} + +- (void)verifyURIString:(NSString *)uriString +{ + NSURL *uri = [[[NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil] + matchesInString:uriString options:0 range:NSMakeRange(0, uriString.length)].firstObject URL]; + XCTAssertNotNil(uri); + NSURL *htmlURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"www/uri_test" withExtension:@"html"]; + XCTAssertNotNil(htmlURL); + + RXRViewController *controller = [[RXRViewController alloc] initWithURI:uri htmlFileURL:htmlURL]; + [self expectationForPredicate:[NSPredicate predicateWithFormat:@"representedURI == %@", uriString] + evaluatedWithObject:[[[controller view] subviews] firstObject] + handler:nil]; + + [self waitForExpectationsWithTimeout:1.5 handler:nil]; +} + +@end diff --git a/RexxarTests/www/routes.json b/RexxarTests/www/routes.json new file mode 100644 index 0000000..d8c0aa9 --- /dev/null +++ b/RexxarTests/www/routes.json @@ -0,0 +1,3 @@ +{"count":1,"items":[ + {"remote_file":"subject_collection.html","uri":"douban:\/\/douban.com\/subject_collection\/(\\w+)[\/]?.*"} + ]} \ No newline at end of file diff --git a/RexxarTests/www/uri_test.html b/RexxarTests/www/uri_test.html new file mode 100644 index 0000000..2351fcc --- /dev/null +++ b/RexxarTests/www/uri_test.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/RexxarTests/www/uri_test.js b/RexxarTests/www/uri_test.js new file mode 100644 index 0000000..111c096 --- /dev/null +++ b/RexxarTests/www/uri_test.js @@ -0,0 +1 @@ +!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g1&&(d=c[0]+"@",a=c[1]),a=a.replace(G,".");var e=a.split("."),g=f(e,b).join(".");return d+g}function h(a){for(var b,c,d=[],e=0,f=a.length;f>e;)b=a.charCodeAt(e++),b>=55296&&56319>=b&&f>e?(c=a.charCodeAt(e++),56320==(64512&c)?d.push(((1023&b)<<10)+(1023&c)+65536):(d.push(b),e--)):d.push(b);return d}function i(a){return f(a,function(a){var b="";return a>65535&&(a-=65536,b+=K(a>>>10&1023|55296),a=56320|1023&a),b+=K(a)}).join("")}function j(a){return 10>a-48?a-22:26>a-65?a-65:26>a-97?a-97:w}function k(a,b){return a+22+75*(26>a)-((0!=b)<<5)}function l(a,b,c){var d=0;for(a=c?J(a/A):a>>1,a+=J(a/b);a>I*y>>1;d+=w)a=J(a/I);return J(d+(I+1)*a/(a+z))}function m(a){var b,c,d,f,g,h,k,m,n,o,p=[],q=a.length,r=0,s=C,t=B;for(c=a.lastIndexOf(D),0>c&&(c=0),d=0;c>d;++d)a.charCodeAt(d)>=128&&e("not-basic"),p.push(a.charCodeAt(d));for(f=c>0?c+1:0;q>f;){for(g=r,h=1,k=w;f>=q&&e("invalid-input"),m=j(a.charCodeAt(f++)),(m>=w||m>J((v-r)/h))&&e("overflow"),r+=m*h,n=t>=k?x:k>=t+y?y:k-t,!(n>m);k+=w)o=w-n,h>J(v/o)&&e("overflow"),h*=o;b=p.length+1,t=l(r-g,b,0==g),J(r/b)>v-s&&e("overflow"),s+=J(r/b),r%=b,p.splice(r++,0,s)}return i(p)}function n(a){var b,c,d,f,g,i,j,m,n,o,p,q,r,s,t,u=[];for(a=h(a),q=a.length,b=C,c=0,g=B,i=0;q>i;++i)p=a[i],128>p&&u.push(K(p));for(d=f=u.length,f&&u.push(D);q>d;){for(j=v,i=0;q>i;++i)p=a[i],p>=b&&j>p&&(j=p);for(r=d+1,j-b>J((v-c)/r)&&e("overflow"),c+=(j-b)*r,b=j,i=0;q>i;++i)if(p=a[i],b>p&&++c>v&&e("overflow"),p==b){for(m=c,n=w;o=g>=n?x:n>=g+y?y:n-g,!(o>m);n+=w)t=m-o,s=w-o,u.push(K(k(o+t%s,0))),m=J(t/s);u.push(K(k(m,0))),g=l(c,r,d==f),c=0,++d}++c,++b}return u.join("")}function o(a){return g(a,function(a){return E.test(a)?m(a.slice(4).toLowerCase()):a})}function p(a){return g(a,function(a){return F.test(a)?"xn--"+n(a):a})}var q="object"==typeof d&&d&&!d.nodeType&&d,r="object"==typeof c&&c&&!c.nodeType&&c,s="object"==typeof a&&a;(s.global===s||s.window===s||s.self===s)&&(b=s);var t,u,v=2147483647,w=36,x=1,y=26,z=38,A=700,B=72,C=128,D="-",E=/^xn--/,F=/[^\x20-\x7E]/,G=/[\x2E\u3002\uFF0E\uFF61]/g,H={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},I=w-x,J=Math.floor,K=String.fromCharCode;if(t={version:"1.3.2",ucs2:{decode:h,encode:i},decode:m,encode:n,toASCII:p,toUnicode:o},"function"==typeof define&&"object"==typeof define.amd&&define.amd)define("punycode",function(){return t});else if(q&&r)if(c.exports==q)r.exports=t;else for(u in t)t.hasOwnProperty(u)&&(q[u]=t[u]);else b.punycode=t}(this)}).call(this,"undefined"!=typeof b?b:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],2:[function(a,b,c){"use strict";function d(a,b){return Object.prototype.hasOwnProperty.call(a,b)}b.exports=function(a,b,c,f){b=b||"&",c=c||"=";var g={};if("string"!=typeof a||0===a.length)return g;var h=/\+/g;a=a.split(b);var i=1e3;f&&"number"==typeof f.maxKeys&&(i=f.maxKeys);var j=a.length;i>0&&j>i&&(j=i);for(var k=0;j>k;++k){var l,m,n,o,p=a[k].replace(h,"%20"),q=p.indexOf(c);q>=0?(l=p.substr(0,q),m=p.substr(q+1)):(l=p,m=""),n=decodeURIComponent(l),o=decodeURIComponent(m),d(g,n)?e(g[n])?g[n].push(o):g[n]=[g[n],o]:g[n]=o}return g};var e=Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)}},{}],3:[function(a,b,c){"use strict";function d(a,b){if(a.map)return a.map(b);for(var c=[],d=0;d",'"',"`"," ","\r","\n"," "],q=["{","}","|","\\","^","`"].concat(p),r=["'"].concat(q),s=["%","/","?",";","#"].concat(r),t=["/","?","#"],u=255,v=/^[a-z0-9A-Z_-]{0,63}$/,w=/^([a-z0-9A-Z_-]{0,63})(.*)$/,x={javascript:!0,"javascript:":!0},y={javascript:!0,"javascript:":!0},z={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},A=a("querystring");d.prototype.parse=function(a,b,c){if(!i(a))throw new TypeError("Parameter 'url' must be a string, not "+typeof a);var d=a;d=d.trim();var e=n.exec(d);if(e){e=e[0];var f=e.toLowerCase();this.protocol=f,d=d.substr(e.length)}if(c||e||d.match(/^\/\/[^@\/]+@[^@\/]+/)){var g="//"===d.substr(0,2);!g||e&&y[e]||(d=d.substr(2),this.slashes=!0)}if(!y[e]&&(g||e&&!z[e])){for(var h=-1,j=0;jk)&&(h=k)}var l,o;o=-1===h?d.lastIndexOf("@"):d.lastIndexOf("@",h),-1!==o&&(l=d.slice(0,o),d=d.slice(o+1),this.auth=decodeURIComponent(l)),h=-1;for(var j=0;jk)&&(h=k)}-1===h&&(h=d.length),this.host=d.slice(0,h),d=d.slice(h),this.parseHost(),this.hostname=this.hostname||"";var p="["===this.hostname[0]&&"]"===this.hostname[this.hostname.length-1];if(!p)for(var q=this.hostname.split(/\./),j=0,B=q.length;B>j;j++){var C=q[j];if(C&&!C.match(v)){for(var D="",E=0,F=C.length;F>E;E++)D+=C.charCodeAt(E)>127?"x":C[E];if(!D.match(v)){var G=q.slice(0,j),H=q.slice(j+1),I=C.match(w);I&&(G.push(I[1]),H.unshift(I[2])),H.length&&(d="/"+H.join(".")+d),this.hostname=G.join(".");break}}}if(this.hostname.length>u?this.hostname="":this.hostname=this.hostname.toLowerCase(),!p){for(var J=this.hostname.split("."),K=[],j=0;jj;j++){var O=r[j],P=encodeURIComponent(O);P===O&&(P=escape(O)),d=d.split(O).join(P)}var Q=d.indexOf("#");-1!==Q&&(this.hash=d.substr(Q),d=d.slice(0,Q));var R=d.indexOf("?");if(-1!==R?(this.search=d.substr(R),this.query=d.substr(R+1),b&&(this.query=A.parse(this.query)),d=d.slice(0,R)):b&&(this.search="",this.query={}),d&&(this.pathname=d),z[f]&&this.hostname&&!this.pathname&&(this.pathname="/"),this.pathname||this.search){var M=this.pathname||"",L=this.search||"";this.path=M+L}return this.href=this.format(),this},d.prototype.format=function(){var a=this.auth||"";a&&(a=encodeURIComponent(a),a=a.replace(/%3A/i,":"),a+="@");var b=this.protocol||"",c=this.pathname||"",d=this.hash||"",e=!1,f="";this.host?e=a+this.host:this.hostname&&(e=a+(-1===this.hostname.indexOf(":")?this.hostname:"["+this.hostname+"]"),this.port&&(e+=":"+this.port)),this.query&&j(this.query)&&Object.keys(this.query).length&&(f=A.stringify(this.query));var g=this.search||f&&"?"+f||"";return b&&":"!==b.substr(-1)&&(b+=":"),this.slashes||(!b||z[b])&&e!==!1?(e="//"+(e||""),c&&"/"!==c.charAt(0)&&(c="/"+c)):e||(e=""),d&&"#"!==d.charAt(0)&&(d="#"+d),g&&"?"!==g.charAt(0)&&(g="?"+g),c=c.replace(/[?#]/g,function(a){return encodeURIComponent(a)}),g=g.replace("#","%23"),b+e+c+g+d},d.prototype.resolve=function(a){return this.resolveObject(e(a,!1,!0)).format()},d.prototype.resolveObject=function(a){if(i(a)){var b=new d;b.parse(a,!1,!0),a=b}var c=new d;if(Object.keys(this).forEach(function(a){c[a]=this[a]},this),c.hash=a.hash,""===a.href)return c.href=c.format(),c;if(a.slashes&&!a.protocol)return Object.keys(a).forEach(function(b){"protocol"!==b&&(c[b]=a[b])}),z[c.protocol]&&c.hostname&&!c.pathname&&(c.path=c.pathname="/"),c.href=c.format(),c;if(a.protocol&&a.protocol!==c.protocol){if(!z[a.protocol])return Object.keys(a).forEach(function(b){c[b]=a[b]}),c.href=c.format(),c;if(c.protocol=a.protocol,a.host||y[a.protocol])c.pathname=a.pathname;else{for(var e=(a.pathname||"").split("/");e.length&&!(a.host=e.shift()););a.host||(a.host=""),a.hostname||(a.hostname=""),""!==e[0]&&e.unshift(""),e.length<2&&e.unshift(""),c.pathname=e.join("/")}if(c.search=a.search,c.query=a.query,c.host=a.host||"",c.auth=a.auth,c.hostname=a.hostname||a.host,c.port=a.port,c.pathname||c.search){var f=c.pathname||"",g=c.search||"";c.path=f+g}return c.slashes=c.slashes||a.slashes,c.href=c.format(),c}var h=c.pathname&&"/"===c.pathname.charAt(0),j=a.host||a.pathname&&"/"===a.pathname.charAt(0),m=j||h||c.host&&a.pathname,n=m,o=c.pathname&&c.pathname.split("/")||[],e=a.pathname&&a.pathname.split("/")||[],p=c.protocol&&!z[c.protocol];if(p&&(c.hostname="",c.port=null,c.host&&(""===o[0]?o[0]=c.host:o.unshift(c.host)),c.host="",a.protocol&&(a.hostname=null,a.port=null,a.host&&(""===e[0]?e[0]=a.host:e.unshift(a.host)),a.host=null),m=m&&(""===e[0]||""===o[0])),j)c.host=a.host||""===a.host?a.host:c.host,c.hostname=a.hostname||""===a.hostname?a.hostname:c.hostname,c.search=a.search,c.query=a.query,o=e;else if(e.length)o||(o=[]),o.pop(),o=o.concat(e),c.search=a.search,c.query=a.query;else if(!l(a.search)){if(p){c.hostname=c.host=o.shift();var q=c.host&&c.host.indexOf("@")>0?c.host.split("@"):!1;q&&(c.auth=q.shift(),c.host=c.hostname=q.shift())}return c.search=a.search,c.query=a.query,k(c.pathname)&&k(c.search)||(c.path=(c.pathname?c.pathname:"")+(c.search?c.search:"")),c.href=c.format(),c}if(!o.length)return c.pathname=null,c.search?c.path="/"+c.search:c.path=null,c.href=c.format(),c;for(var r=o.slice(-1)[0],s=(c.host||a.host)&&("."===r||".."===r)||""===r,t=0,u=o.length;u>=0;u--)r=o[u],"."==r?o.splice(u,1):".."===r?(o.splice(u,1),t++):t&&(o.splice(u,1),t--);if(!m&&!n)for(;t--;t)o.unshift("..");!m||""===o[0]||o[0]&&"/"===o[0].charAt(0)||o.unshift(""),s&&"/"!==o.join("/").substr(-1)&&o.push("");var v=""===o[0]||o[0]&&"/"===o[0].charAt(0);if(p){c.hostname=c.host=v?"":o.length?o.shift():"";var q=c.host&&c.host.indexOf("@")>0?c.host.split("@"):!1;q&&(c.auth=q.shift(),c.host=c.hostname=q.shift())}return m=m||c.host&&o.length,m&&!v&&o.unshift(""),o.length?c.pathname=o.join("/"):(c.pathname=null,c.path=null),k(c.pathname)&&k(c.search)||(c.path=(c.pathname?c.pathname:"")+(c.search?c.search:"")),c.auth=a.auth||c.auth,c.slashes=c.slashes||a.slashes,c.href=c.format(),c},d.prototype.parseHost=function(){var a=this.host,b=o.exec(a);b&&(b=b[0],":"!==b&&(this.port=b.substr(1)),a=a.substr(0,a.length-b.length)),a&&(this.hostname=a)}},{punycode:1,querystring:4}],6:[function(a,b,c){url_parser=a("url"),window.get_uri=function(){return url_parser.parse(window.location.href,!0).query.uri}},{url:5}]},{},[6])}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[1]); \ No newline at end of file diff --git a/RexxarTests/www/uri_test.src.js b/RexxarTests/www/uri_test.src.js new file mode 100644 index 0000000..9fac3fb --- /dev/null +++ b/RexxarTests/www/uri_test.src.js @@ -0,0 +1,4 @@ +url_parser = require("url-parse"); +window.get_uri = function() { + return url_parser.parse(window.location.href, true).query.uri; +} diff --git a/docs/INTRODUTION.md b/docs/INTRODUTION.md new file mode 100644 index 0000000..5963460 --- /dev/null +++ b/docs/INTRODUTION.md @@ -0,0 +1,143 @@ +# Rexxar 简介 + +**Rexxar** 是一个针对移动端的混合开发框架。现在支持 Android 和 iOS 平台。 + +团队中喜欢玩魔兽的同学将我们的混合开项目命名为 Rexxar(《魔兽世界》中人物,出生于卡利姆多大陆的菲拉斯,同时具有雷骨兽人和南部菲拉斯野生食人魔血统)。 + +Rexxar 主要由以下三部分组成: + +- Rexxar-Route,我们使用 URL 来标识每一个页面。在 App 中通过指明 URL 跳转到此页面。所以,需要一个路由表,可以根据 URL 找到一个 Rexxar-Web 的对应资源来正确展示相应页面; + +- Rexxar-Web,前端代码库,由 HTML、CSS、JavaScript、Image 等组成,用来提供在移动客户端使用的用户页面; + +- Rexxar-Container,一个前端代码的运行容器。它其实是一个内嵌的浏览器(WebView),我们为内嵌浏览器提供了一些必要的原生端支持,包括 API 的 OAuth 授权、图片缓存等;现在有 Android 和 iOS 两个版本的实现。 + +在项目实践中,Rexxar-Web 和 Rexxar-Route 由一个项目实现,并部署于同一个 Web 项目中。 + +### Rexxar-Route + +Rexxar-Route 比较简单,只需要表达一个路由表即可。我们使用了一个 json 文件来表达路由表。给出一个路由表的例子: + +``` + { + count: 4, + items: [{ + remote_file: "https://img1.doubanio.com/dae/rexxar/files/orders/orders-70dbdbcb1c.html", + uri: "douban://douban.com/orders[/]?.*" + }, { + remote_file: "https://img1.doubanio.com/dae/rexxar/files/related_doulists/related_doulists-1d7d99e1fb.html", + uri: "douban://douban.com/(tag|tv|movie|book|music)/(\w+)/related_doulists[/]?.*" + }, { + remote_file: "https://img1.doubanio.com/dae/rexxar/files/selection/columns-1a4666ac89.html", + uri: "douban://douban.com/selection/columns[/]?.*" + }, { + remote_file: "https://img3.doubanio.com/dae/rexxar/files/seti/category_channel-2974d9257d.html", + uri: "douban://douban.com/seti/category_channel/(.*)[/]?.*" + }], + sig: "api", + deploy_time: "Fri, 04 Mar 2016 11:12:29 GMT + } +``` + +我们发布的每个版本的 App 安装包都会包含最新版本的 routes.json 文件。在 App 启动时,都会尝试下载最新版本的 routes.json。在遇到无法解析的 URL 时,也会去下载新版 routes.json。 + +### Rexxar-Web + +Rexxar-Web 是 Rexxar 前端实现。Rexxar-Web 中,我们使用了 React 作为前端开发框架。 +需要指出的是,虽然 Rexxar-Web 选择了 React,但是 Rexxar-Container 的实现和 Rexxar-Web 的实现是分离的。Rexxar-Container 对 Rexxar-Web 使用何种技术实现并不关心。所以,你可以选择自己的前端技术和 Rexxar-Container 组合。 + +Rexxar-Web 包括了三部分内容: + +#### 工具 + +一套开发 Rexxar-Web 所需的打包,调试,发布工具。 + +#### 公共的前端组件 + +- 通用的错误处理、Loading等效果; +- 相对通用的页面初始数据的支持(不用必须经历空页面->网络加载->页面展示); +- 页面点击反馈效果; +- List 的支持; +- List 上面的操作,Android(长按)与iOS(左划)不同; + +#### 对 Rexxar-Container 实现的 Widget 的调用 + +- ActionBar 的 title 定制 +- ActionBar 的 button 定制 +- Dialog +- 下拉刷新 +- Toast + +有了这些组件,我们日常产品开发的难度就降低了。普通移动开发工程师经过一段时间的学习,也可以像前端工程师一样,以 Rexxar 为工具为 App 做一些产品开发了。这部分可以视为一个纯粹的前端项目。 + +### Rexxar-Container + +我们使用混合开发技术提高开发效率的一个前提是,尽量不损伤 App 的使用体验。基于这个前提,在 Native 和 Web 如何分工方面我们做了一些尝试。首先,为了保证使用体验,我们把 App 里页面切换留给了 Native。这样,每个页面(Controller 或者 Activity)都是一个 Container。Container 内嵌一个浏览器内核。页面内的功能和逻辑在 Native 和 Web 之间如何分工呢?我们尝试过有几种策略: + +- 纯浏览器方案:也就是 Native 除了扔给内嵌浏览器一个 url 地址之外,就没有不做任何事情了,剩余的事情都由 Web 技术完成。这和用 Safari 或 Chrome 等普通浏览器打开一个网页并没有太多区别。只是我们固定了访问的地址。 + +- 前端模板渲染容器方案:这种方案大部分事情由 Native 完成,Web 部分只是负责页面元素的呈现,不参与页面界面之外的其他部分。我们在客户端存储了一个 HTML 作为 UI 模板。Native 代码负责获取数据,向 HTML 文件模板中填入动态数据,得到一个可以在内嵌浏览器渲染的 HTML 文件。这个过程有点类似于 Web 框架里模板渲染库(例如,ninja2)的作用。 + +- Rexxar-Container 方案:Rexxar 采用的方案介于上述两种方案之间。Rexxar-Container 同样提供了一个运行前端代码的容器。它也是一个内嵌的浏览器(WebView)。只是,我们并不是扔给内嵌浏览器一个 url 地址就放手不管了,而是对内嵌浏览器包装了很多功能。 + +Rexxar-Container 方案中,Container 需要实现以下功能: + +- Rexxar-Route 路由表的更新,已经在客户端的保存; +- 为 Rexxar-Web 前端代码发出的 API 请求提供包装。带上必要的 OAuth 参数; +- 缓存 Rexxar-Web 前端代码所需要的静态文件,包括 HTML、CSS、JavaScript、Image(图片素材)等; +- 存 Rexxar-Web 中所需要加载的资源文件,例如图片等; +- 通过协议为 Rexxar-Web 提供一些原生支持的功能。 + +这种实现方案,是基于保证使用体验的前提下,尽量让 Web 技术多做一些事情的考虑。 + +#### Rexxar-Container 和 Rexxar-Web 之间的交互 + +混合开发实践中,一般都会涉及到 Native 和 Web 如何通信的问题。这是因为我们把一件事情交给两种技术完成,那么它们之间便会存在有一些通信和协调。通常会使用 JSBridge(Android: [JsBridge](https://github.com/lzyzsd/JsBridge),iOS:[WebViewJavascriptBridge](https://github.com/marcuswestin/WebViewJavascriptBridge)) 来实现 Native 和 Web 的相互调用。 + +但在 Rexxar 中,我们并没有选择这个方案。这是因为,我们试图尽量缩小 Rexxar-Container 和 Rexxar-Web 所需要的交互。即使有一些交互,我们都事先定义好协议。现在只支持 Rexxar-Web 请求一些定义好的由 Native 实现的功能。而且由于使用场景还未出现这需求,到现在我们仍然不支持 Native 调用 Web 实现的功能。 + +Rexxar 中 Nativie 和 Web 之间协议是由 URL 定义的。Rexxar-Web 访问某个特定的 URL, Rexxar-Container 截获这些 URL 请求,调用 Native 代码完成相应的功能。 + +例如,Rexxar 中 UI 相关的功能的协议如下: + +- 请求 douban://rexxar.douban.com/widget/nav_title,可以定义 Navigation Bar Title。 +- 请求 douban://rexxar.douban.com/widget/nav_menu,可以定义 Navigation Bar Button。 +- 请求 douban://rexxar.douban.com/widget/toast,可以出现一个消息通知 toast。 + +Rexxar-Web 具体前端实现是在 DOM 中加入一个 iframe 来加载此 URL,以来完成对 Rexxar-Container 的通知。 + +将 Native 和 Web 的通信以协议的形式规范起来,是因为我们希望 Native 和 Web 之间的通信是可定义的,可控的。有这种期望的原因是,我们以 Rexxar 完成的页面,不仅仅使用在 App 内,还会使用在移动 Web 页面上。我们的移动站点,特别是分享到外部(如微信,微博)的页面也希望复用 Rexxar 在 App 内的成果。如果,任由开发者自由的定义过多的依赖于原生实现的功能,那么我们就无法顺利地迁移到移动 Web 上去。标准浏览器并不支持 JSBridge 的大部分功能。可以看到我们已经实现的协议,大部分在移动 Web 是被可以忽略的(比如,nav_title, nav_menu),或者我们也可以较容易地以移动 Web 支持的形式再实现一次(比如,toast)。 + +#### Rexxar-Container 的技术实现 + +Rexxar-Container 主要的工作是截获 Rexxar-Web 的数据请求和原生功能请求。Rexxar-Container 截获请求之后,做相应的反应,要么为数据请求加上 OAuth 认证信息,要么按照协议调用某些原生功能。由于原生功能请求也是由 URL 形式定义的,Rexxar-Web 代码在 App 的 Rexxar-Container 内工作方式,就和在普通浏览器里差别不大。代码都是标准 Web 式的,没有为原生移动开发做太多定制。可以顺利移植到 Web 平台,在各种浏览器中都可以正确运行。 + +我们为 iOS 和 Android 各开发了一个 Rexxar-Container。iOS 和 Android 平台截获请求的方式由于平台差异并不完全相同。但本质上都是在 Web 和 Native 之间实现了一个 Proxy。Web 发出的请求会被 Proxy 预先处理。要么是修改后再发出去,要么是由 Rexxar-Container 自己处理。 + +### Rexxar 的工作流 + +![Rexxar 工作流](/docs/images/Rexxar.png) + +例如,客户端接到一个页面请求,要打开一个 URL:douban://douban.com/movie/1292052。Rexxar 的工作流如下: + +1. 根据 URL 查询本机缓存的路由表,看是否能够找到对应的资源记录(一般是一个 HTML 文件)。如果找到不到,请求 Rexxar-Route 服务,获得最新的全量路由表,更新本地缓存,找到对应的资源记录; + +2. 根据路由表指示的 HTML 文件的路径,看本地是否找到对应的文件。如果找不到,请求 Rexxar-Web 资源服务器,更新本地缓存; + +3. 在 Rexxar-Container 里展示该 HTML 文件;如有需要,在 Container 中请求 Image,先检查本地缓存。如不存在,请求 Rexxar-Web 资源服务器; + +4. Rexxar-Web 前端代码在 Container 里继续执行,发出 API 请求,有 Rexxar-Container 代理这些请求,为 API 请求添加 OAuth 验证; + +5. Rexxar-Web 前端代码继续执行,根据 API 返回的结果,展示响应的页面,可能会请求 CDN 的图片等; + +6. Rexxar-Web 前端代码继续执行,如果需要修改 NavigationBar 等原生界面,可能通过定义好的协议向 douban://rexxar.douban.com 发送数据。Rexxar-Container 拦截请求,按定义好的协议作出反应,例如,修改 NavigationBar 上的按钮。 + +### 问题 + +#### 性能 + +混合开发的问题在于,Web 的性能没法和 Native 相比。这种状况可能会长期存在。因为,前端代码运行于内嵌浏览器之上,和直接调用原生系统相比,理论上总会存在性能上的差距。我们现在基本是以规避的方式面对性能问题:即性能问题会明显影响到用户体验时,我们就不使用 Rexxar 来做,而是使用传统 Native 的方式老老实实写两份 Native 代码,一份 iOS,一份 Android。当然,这就限缩了 Rexxar 的使用范围。 + +#### 错误报告 + +在我们上了 Rexxar 之后,在收集到的 Crash Report 中,JavaScript 的相关错误,和浏览器相关的错误开始增加。而对这类错误,由于移动应用的使用环境更为复杂,错误报告经过了 JavaScript 引擎,原生系统两层之后,给出的错误信息并不够明确。我们在这方面的经验也并不多,导致我们还没有很好的办法降低这类错误。这对提高 App 的稳定性带来了问题。 \ No newline at end of file diff --git a/docs/README_EN.md b/docs/README_EN.md new file mode 100644 index 0000000..93c2f85 --- /dev/null +++ b/docs/README_EN.md @@ -0,0 +1,177 @@ +# Rexxar iOS + +[![Build status](http://shields.dapps.douban.com/badge/qa-ci/peteris-rexxar-ios-inHouse)](http://qa-ci.intra.douban.com/job/peteris-rexxar-ios-inHouse) +[![Language](https://img.shields.io/badge/language-ObjC-blue.svg)](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html) +[![iOS](https://img.shields.io/badge/iOS-7.0-green.svg)]() + +**Rexxar** is a Hybrid library for mobile application development. Now it supports iOS and Android platform. `Rexxar iOS` is Rexxar's Container in iOS. + +With Rexxar, you can develop mobile application by traditional web techniques including javascript, html and css. Rexxar Container has not any requirements of web front side. Our demo in the web front side is implemented by [React](https://facebook.github.io/react/). You can use your own front side framework to develop the application in Rexxar Container. + +Rexxar iOS Container supports iOS 7.0 and above systems. + +## Installation + +### Install Cocoapods + +[CocoaPods](http://cocoapods.org) is a dependency manager for Objective-C and Swift. You can install it with the following command: + +```bash +$ gem install cocoapods +``` + +### Podfile + +```ruby +target 'TargetName' do + pod 'Rexxar', '~> 1.2.0' +end +``` + +Then, run the following command: + +```bash +$ pod install +``` + +## Usage + +You can take a look at the example in RexxarDemo folder. + +### Launch local server + +Launch the local server for serving the routes map api. + +```bash +$ python routes.py +``` + +Test by entering the url [http:\\localhost:5000](http:\\localhost:5000) in your browser. You will see the output as json format: + +```json +{ + "items": [{ + "remote_file": "https://img1.doubanio.com/dae/rexxar/files/rexxar/demo-ffb8a4a9fa.html", + "deploy_time": "Thu, 04 Aug 2016 07:43:47 GMT", + "uri": "douban://douban.com/rexxar_demo[/]?.*" + }], + "partial_items": [{ + "remote_file": "https://img1.doubanio.com/dae/rexxar/files/rexxar/demo-ffb8a4a9fa.html", + "deploy_time": "Thu, 04 Aug 2016 07:43:47 GMT", + "uri": "douban://partial.douban.com/rexxar_demo/_.*" + }], + "deploy_time": "Thu, 04 Aug 2016 07:43:47 GMT", +} +``` + +We use the python web framework [Flask](http://flask.pocoo.org/) to serve a local server for this demo. You need implement a real server in your real product serving the routes map api and html, css, javascript resources. You can use any server framework. Rexxar has not any requirement about the server framework. Rexxar offers the configuration interface of the routes map api address in `RXRConfig`. You can find the way to configure the address in next section. + +### Configure with `RXRConfig` + +Configure the routes map url and routes cache path. + +```Swift + RXRConfig.setRoutesMapURL(NSURL(string:"http://rexxar.douban.com/api/routes?edition=pre")!) + RXRConfig.setRoutesCachePath("com.douban.RexxarDemo.rexxar") +``` + +### Use `RXRViewController` + +You can use `RXRViewController` directly as your Hybrid container. Or you can inherit `RXRViewController` to implement your own Rexxar Container. In the RexxarDemo, We use `RXRViewController` directly. + +To Initialize a RXRViewController, you just need a route uri. Your should find this uri in the routes map api served by the local server. Every uri represents a page. Rexxar Container search the page resources (html, css, javascript files) via the route uri. + +```Swift + let controller = RXRViewController(URI: uri) + let titleWidget = RXRNavTitleWidget() + let alertDialogWidget = RXRAlertDialogWidget() + controller.activities = [titleWidget, alertDialogWidget] + navigationController?.pushViewController(controller, animated: true) +``` + +## Customize your own Rexxar Container + +### Create your own RXRWidget + +If you want to implement a native UI feature which can be used by web, for example, displaying a toast and adding pull to refresh ui widget etc, you can inherit `RXRWidget` and implementing three methods: `canPerformWithURL:`, `prepareWithURL:`, `performWithContoller:`. + +You can find an example `RXRNavTitleWidget` in Rexxar. + +```Objective-C +@interface RXRNavTitleWidget () + +@property (nonatomic, copy) NSString *title; + +@end + + +@implementation RXRNavTitleWidget + +- (BOOL)canPerformWithURL:(NSURL *)URL +{ + NSString *path = URL.path; + if (path && [path isEqualToString:@"/widget/nav_title"]) { + return true; + } + return false; +} + +- (void)prepareWithURL:(NSURL *)URL +{ + self.title = [[URL rxr_queryDictionary] rxr_itemForKey:@"title"]; +} + +- (void)performWithController:(RXRViewController *)controller +{ + if (controller) { + controller.title = self.title; + } +} + +@end +``` + +### Create your own RXRContainerAPI + +If you want to offer information computed by native but used by web, for example, getting the device's GPS location information, you can create an Object conforming to `RXRContainerAPI` protocol ant implementing three methods: `shouldInterceptRequest:`, `responseWithRequest:`, `responseData`. + +You can find an example `RXRLocContainerAPI` in RexxarDemo. In this example `RXRLocContainerAPI` returns the city information. Of course, It's a container API offerring false and always the same city information. You can implement your own loc information service on the base of this example. + +### Create your own RXRDecorator + +If you want to modify the request sent by Rexxar Container, for example, adding the authentical information in http header, you can inherit `RXRDecorator` and implementing two methods `shouldInterceptRequest:`, `prepareWithRequest:`. + +You can find an example `RXRAuthDecorator` in RexxarDemo. + +## Architecture + +* Rexxar Container + - `RXRConfig` + - `RXRViewController` + +* Widget + - `RXRWidget` + - `RXRNavTitleWidget` + - `RXRAlertDialogWidget` + +* ContainerAPI + - `RXRNSURLProtocol` + - `RXRContainerIntercepter` + - `RXRContainerAPI` + +* Decorator + - `RXRRequestIntercepter` + - `RXRDecorator` + - `RXRRequestDecorator` + +* Util + - `NSURL+Rexxar` + - `NSDictionary+RXRMultipleItem` + +## Unit Test + +Rexxar iOS includes a suite of unit tests within the RexxarTests subdirectory. These tests can be run simply be executed the test action on the platform framework you would like to test. + +## License + +Rexxar is released under the MIT license. See LICENSE for details. diff --git a/docs/images/Rexxar.png b/docs/images/Rexxar.png new file mode 100644 index 0000000..acc6209 Binary files /dev/null and b/docs/images/Rexxar.png differ diff --git a/peteris.yaml b/peteris.yaml new file mode 100644 index 0000000..55525a3 --- /dev/null +++ b/peteris.yaml @@ -0,0 +1,16 @@ +- job: + name: unittest + description: 'Rexxar iOS Unit Tests' + node: qaci-osx2 + scm: + - git: + branches: + - master + skip-tag: True + url: http://github.intra.douban.com/rexxar/rexxar-ios.git + triggers: + - pollscm: '* * * * *' + builders: + - shell: | + pod lib lint --verbose --allow-warnings --sources=http://code.dapps.douban.com/CocoaPodsSpecs.git + xcodebuild -destination "name=iPhone 6s" -sdk iphonesimulator -scheme Rexxar -project Rexxar.xcodeproj -derivedDataPath build test