diff --git a/.travis.sh b/.travis.sh index 27f1470..dd8559e 100755 --- a/.travis.sh +++ b/.travis.sh @@ -92,6 +92,7 @@ STAGE_MAIN() { pod install XC_TestMac XC_TestAutoIOS "Test-iOS" + XC_Test "Test-tvOS" "platform=tvOS Simulator,name=Apple TV" else logError "Unexpected CI task: $RFCI_TASK" fi diff --git a/.travis.yml b/.travis.yml index 919f999..1a4d741 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: objective-c -sudo: false +os: osx cache: cocoapods env: global: @@ -7,10 +7,9 @@ env: - LANG=en_US.UTF-8 - LANGUAGE=en_US.UTF-8 - RFCI_PRODUCT_NAME="RFAPI" - - RFWorkspace="RFAPI.xcworkspace" -matrix: +jobs: include: - - osx_image: xcode11.3 + - osx_image: xcode11.4 env: RFCI_TASK="POD_LINT" - osx_image: xcode10 env: diff --git a/Documents/Cookbook.md b/Documents/Cookbook.md new file mode 100644 index 0000000..c828994 --- /dev/null +++ b/Documents/Cookbook.md @@ -0,0 +1,66 @@ +# RFAPI Cookbook + +## URL 拼接 + +RFAPIDefine.path 如果是带 scheme 的完整 URL,将直接用这个 URL;否则会先与 pathPrefix 直接进行字符串拼接,并相对 baseURL 生成最终 URL。 + +baseURL 会依次从当前规则、默认规则、RFAPI 属性中取。baseURL host 后面如果有其他 path,应以「/」结尾,如 `http://example.com/base` 实际相当于 `http://example.com`,正确的应是 `http://example.com/base/`。 + +RFAPIDefine.path 支持用花括号定义参数,如 `user/{user_id}/profile`,传参时带入 `user_id: 123`,则最终 URL 会变成 `user/123/profile`。 + +## 传参 + +请求的参数通过 context 的 `parameters` 属性传入: + +```swift +api.request(name: "some api") { c in + c.parameters = ["foo": "bar", "number": 456, "bool": true] +} +``` + +如果整个参数是数组,通过 `RFAPIRequestArrayParameterKey` 传入: + +```swift +api.request(name: "some api") { c in + c.parameters = [RFAPIRequestArrayParameterKey: [1, 2, 3]] +} +``` + +如果 HTTP body 是 `multipart/form-data` 格式的,可通过 context 的 `formData` 构建数据。 + +```swift +api.request(name: "some api") { c in + c.formData = { formData in + try? formData.appendPart(withFileURL: fileUrl, name: "field1") + formData.appendPart(withForm: someData, name: "field2") + } +} +``` + +## Authentication + +接口定义(RFAPIDefine) `needsAuthorization` 为 `true` 的接口会自动附加 `RFAPIDefineManager` 定义的 HTTP 头(authorizationHeader)和参数(authorizationParameters)。 + +设置方法为: + +```swift +let api = ... // RFAPI instance +api.defineManager.authorizationHeader["Authorization"] = "Custom Credential" +api.defineManager.authorizationParameters["Custom Parameter"] = "Custom Value" +``` + +URLCredential 因接口未暴露暂不支持。 + +## 添加 HTTP header + +在请求中附加 HTTP 头有多种方式: + +* 如需集中式的处理,可通过重载 `RFAPI` 的 `preprocessingRequest(parametersRef:httpHeadersRef:parameters:define:context:)` 或 `finalizeSerializedRequest(_:define:context:)` 方法进行修改; +* 通过接口定义(RFAPIDefine)的 `HTTPRequestHeaders` 属性,如果接口定义中设置了该属性,会使用自己的(不会与默认定义合并),否则会使用默认定义(RFAPIDefineManager.defaultDefine)的; +* 通过 context 传入 `HTTPHeaders`,与其他方式设置的头进行合并,这种方式优先级最高,会覆盖接口定义和认证头。 + +## 错误处理 + +创建请求时可以通过 context 设置请求失败的回调(failure),除此之外,重载 `RFAPI` 的 `generalHandlerForError(_:define:task:failure:)` 方法可以集中处理错误。 + +[一个示例](https://github.com/BB9z/iOS-Project-Template/tree/4.1/App/Networking/API.swift#L63):对系统错误进行包装,token 失效登出,创建请求时不定义错误处理默认报错。 diff --git a/Documents/migration_guide_v2.md b/Documents/Migration Guide v2.md similarity index 92% rename from Documents/migration_guide_v2.md rename to Documents/Migration Guide v2.md index 8a6e90f..237a9c2 100644 --- a/Documents/migration_guide_v2.md +++ b/Documents/Migration Guide v2.md @@ -1,6 +1,6 @@ # RFAPI v2 升级指南 -> *Because there should be no non-Chinese developers using this library before, this guide is not available in English at this time.* +> *Because there should be no non-Chinese developers using this library before, this guide is not available in English.* v1 到 v2 几乎全部重写,内部变化很大,但是实际项目需要调整的地方应该不多。 @@ -44,5 +44,4 @@ v1 请求有两个方法,正常请求和表单上传请求,正常请求有 ## 国际化 -// todo - +v1 的错误信息是硬编码在代码中的,且是中文;现在可以在 app 中默认的 Localizable.strings 定义。 diff --git a/Documents/design.md b/Documents/design.md deleted file mode 100644 index cb3dd22..0000000 --- a/Documents/design.md +++ /dev/null @@ -1,4 +0,0 @@ -# RFAPI 背后的设计 - -好的设计应该是不会过时的 - diff --git a/Example/Shared/Models/RFDTestEntity.h b/Example/Shared/Models/RFDTestEntity.h deleted file mode 100644 index b814d39..0000000 --- a/Example/Shared/Models/RFDTestEntity.h +++ /dev/null @@ -1,14 +0,0 @@ -// -// RFDTestEntity.h -// RFDemo -// -// Created by BB9z on 3/29/16. -// Copyright © 2016 RFUI. All rights reserved. -// - -#import "JSONModel.h" - -@interface RFDTestEntity : JSONModel -@property int uid; -@property NSString *name; -@end diff --git a/Example/Shared/Models/RFDTestEntity.m b/Example/Shared/Models/RFDTestEntity.m deleted file mode 100644 index d885443..0000000 --- a/Example/Shared/Models/RFDTestEntity.m +++ /dev/null @@ -1,18 +0,0 @@ - -#import "RFDTestEntity.h" -#import - -@implementation RFDTestEntity - -+ (JSONKeyMapper *)keyMapper { - RFDTestEntity *this; - return [JSONKeyMapper.alloc initWithModelToJSONDictionary:@{ - @"id": @keypath(this, uid), - }]; -} - -+ (BOOL)propertyIsOptional:(NSString *)propertyName { - return YES; -} - -@end diff --git a/Example/Shared/Models/TestEntity.swift b/Example/Shared/Models/TestEntity.swift new file mode 100644 index 0000000..326ef24 --- /dev/null +++ b/Example/Shared/Models/TestEntity.swift @@ -0,0 +1,24 @@ +// +// TestEntity.swift +// Example-iOS +// +// Created by BB9z on 2020/3/29. +// Copyright © 2020 RFUI. All rights reserved. +// + +import Foundation + +@objc(RFDTestEntity) +class RFDTestEntity : JSONModel { + @objc var uid: Int64 = -1 + @objc var name: String? + + override class func keyMapper() -> JSONKeyMapper! { + return JSONKeyMapper(modelToJSONDictionary: [#keyPath(RFDTestEntity.uid) : "id"]) + } + + override class func propertyIsOptional(_ propertyName: String!) -> Bool { + // All property is optional. + return true + } +} diff --git a/Example/Shared/OCBridging-Header.h b/Example/Shared/OCBridging-Header.h index bbc799d..ac331bb 100644 --- a/Example/Shared/OCBridging-Header.h +++ b/Example/Shared/OCBridging-Header.h @@ -4,6 +4,7 @@ #import #import +#import #import #import #import diff --git a/Example/iOS-Swift/Base.lproj/LaunchScreen.storyboard b/Example/iOS-Swift/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index eec14c7..0000000 --- a/Example/iOS-Swift/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/iOS-Swift/AppDelegate.swift b/Example/iOS-Swift/Launching/AppDelegate.swift similarity index 100% rename from Example/iOS-Swift/AppDelegate.swift rename to Example/iOS-Swift/Launching/AppDelegate.swift diff --git a/Example/iOS-Swift/Launching/LaunchScreen.storyboard b/Example/iOS-Swift/Launching/LaunchScreen.storyboard new file mode 100644 index 0000000..c1bef31 --- /dev/null +++ b/Example/iOS-Swift/Launching/LaunchScreen.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/iOS-Swift/Launching/LaunchScreen_zh-Hans.storyboard b/Example/iOS-Swift/Launching/LaunchScreen_zh-Hans.storyboard new file mode 100644 index 0000000..1f56229 --- /dev/null +++ b/Example/iOS-Swift/Launching/LaunchScreen_zh-Hans.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/iOS-Swift/Main.storyboard b/Example/iOS-Swift/Main.storyboard deleted file mode 100644 index bc244a6..0000000 --- a/Example/iOS-Swift/Main.storyboard +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/iOS-Swift/Scenes/Base.lproj/Main.storyboard b/Example/iOS-Swift/Scenes/Base.lproj/Main.storyboard new file mode 100644 index 0000000..3cf16df --- /dev/null +++ b/Example/iOS-Swift/Scenes/Base.lproj/Main.storyboard @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/iOS-Swift/Scenes/ControlBindingVC.swift b/Example/iOS-Swift/Scenes/ControlBindingVC.swift new file mode 100644 index 0000000..a8fee03 --- /dev/null +++ b/Example/iOS-Swift/Scenes/ControlBindingVC.swift @@ -0,0 +1,38 @@ +// +// ControlBindingVC.swift +// Example-iOS +// +// Created by BB9z on 2020/4/11. +// Copyright © 2020 RFUI. All rights reserved. +// + +import UIKit + +class ControlBindingViewController: UIViewController { + @IBOutlet var controls: [Any]! + @IBOutlet weak var barItem: UIBarButtonItem! + @IBOutlet weak var scrollView: UIScrollView! + + override func viewDidLoad() { + super.viewDidLoad() + var newControls = controls! + if #available(iOS 10.0, *) { + let rc = UIRefreshControl() + rc.addTarget(self, action: #selector(refresh(_:)), for: .valueChanged) + scrollView.refreshControl = rc + var newControls = controls + newControls?.append(rc) + } + newControls.append(barItem!) + controls = newControls + } + + @IBAction func refresh(_ sender: Any) { + TestAPI.shared.request(name: "Timeout") { c in + c.timeoutInterval = 3 + c.loadMessage = "" + c.bindControls = controls + c.groupIdentifier = apiGroupIdentifier + } + } +} diff --git a/Example/iOS-Swift/Scenes/NavigationController.swift b/Example/iOS-Swift/Scenes/NavigationController.swift new file mode 100644 index 0000000..21f30a1 --- /dev/null +++ b/Example/iOS-Swift/Scenes/NavigationController.swift @@ -0,0 +1,52 @@ +// +// NavigationController.swift +// Example-iOS +// +// Created by BB9z on 2020/4/12. +// Copyright © 2020 RFUI. All rights reserved. +// + +import UIKit + +class NavigationController: UINavigationController, + UINavigationControllerDelegate { + + override func awakeFromNib() { + super.awakeFromNib() + self.delegate = self + } + + func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { + lastViewControllers = viewControllers + } + + private var lastViewControllers: [UIViewController] = [] { + didSet { + if oldValue == lastViewControllers { return } + + let vcRemoved = oldValue.filter { !lastViewControllers.contains($0) } + let vcAdded = lastViewControllers.filter { !oldValue.contains($0) } + if !vcRemoved.isEmpty { + didRemove(viewControllers: vcRemoved) + } + if !vcAdded.isEmpty { + didAdd(viewControllers: vcAdded) + } + } + } + + func didAdd(viewControllers: [UIViewController]) { + + } + func didRemove(viewControllers: [UIViewController]) { + for vc in viewControllers { + TestAPI.shared.cancelOperations(withGroupIdentifier: vc.apiGroupIdentifier) + } + } +} + +extension UIViewController { + var apiGroupIdentifier: String { + return "\(ObjectIdentifier(self))" + } +} diff --git a/Example/iOS-Swift/TestViewController.swift b/Example/iOS-Swift/Scenes/TestViewController.swift similarity index 65% rename from Example/iOS-Swift/TestViewController.swift rename to Example/iOS-Swift/Scenes/TestViewController.swift index 60adf67..d496186 100644 --- a/Example/iOS-Swift/TestViewController.swift +++ b/Example/iOS-Swift/Scenes/TestViewController.swift @@ -8,11 +8,22 @@ import UIKit +func L(_ value: String, key: String, comment: String = "") -> String { + return NSLocalizedString(key, tableName: nil, bundle: Bundle.main, value: value, comment: comment) +} + class TestRequestObject { var title = "" var APIName = "" var message: String? var modal = false + + convenience init(title: String, api: String, message: String? = "") { + self.init() + self.title = title + self.APIName = api + self.message = message + } } class TestViewController: UIViewController, @@ -34,54 +45,30 @@ class TestViewController: UIViewController, var items = [ListSection]() var uploadRequest: TestRequestObject? func makeListItems() { - let r1 = TestRequestObject() - r1.title = "Null" - r1.APIName = "NullTest" - r1.message = "Request: Null" - - let r2 = TestRequestObject() - r2.title = "An object" - r2.APIName = "ObjSample" - r2.message = "" - - let r3 = TestRequestObject() - r3.title = "Objects" - r3.APIName = "ObjArraySample" - r3.message = "Loadding..." + let r1 = TestRequestObject(title: "Null", api: "NullTest", message: "Request: Null") + let r2 = TestRequestObject(title: "An object", api: "ObjSample", message: "") + let r3 = TestRequestObject(title: "Objects", api: "ObjArraySample", message: L("Loadding...", key: "HUDState.Loadding")) r3.modal = true - - let r4 = TestRequestObject() - r4.title = "Empty object" - r4.APIName = "ObjEmpty" - // r4 no progress - - let r5 = TestRequestObject() - r5.title = "Fail request" - r5.APIName = "NotFound" - - let r6 = TestRequestObject() - r6.title = "big_json" - r6.APIName = "local" - - let r7 = TestRequestObject() - r7.title = "Time out" - r7.APIName = "Timeout" - r7.message = "Waiting..." - - let r8 = TestRequestObject() - r8.title = "Upload" - r8.APIName = "Upload" - r8.message = "Uploading..." + let r4 = TestRequestObject(title: "Empty object", api: "ObjEmpty", message: nil) + let r5 = TestRequestObject(title: "Fail request", api: "NotFound") + let r6 = TestRequestObject(title: "big_json", api: "local") + let r7 = TestRequestObject(title: "Time out", api: "Timeout", message: L("Waiting...", key: "HUDState.Waiting")) + let r8 = TestRequestObject(title: "Upload", api: "Upload", message: L("Uploading...", key: "HUDState.Uploading")) uploadRequest = r8 + let r10 = TestRequestObject(title: "Path not set", api: "NoPath") + let r11 = TestRequestObject(title: "Path invalided", api: "InvaildPath") + let r12 = TestRequestObject(title: "Mismatch object", api: "MismatchObject") + let r13 = TestRequestObject(title: "Mismatch array", api: "MismatchArray") + items = [ - ListSection(title: "Sample Request", objects: [r1, r2, r3, r4, r5]), - ListSection(title: "Local Files", objects: [r6]), - ListSection(title: "HTTPBin", objects: [r7, r8]), + ListSection(title: L("Sample Requests", key: "ListSection.Sample"), objects: [r1, r2, r3, r4, r5]), + ListSection(title: L("Local Files", key: "ListSection.Local", comment: "Load file content."), objects: [r6]), + ListSection(title: L("HTTPBin", key: "ListSection.HTTPBin"), objects: [r7, r8]), + ListSection(title: L("Error", key: "ListSection.Error"), objects: [r10, r11, r12, r13]), ] } - lazy var API = TestAPI() weak var lastTask: RFAPITask? func numberOfSections(in tableView: UITableView) -> Int { @@ -113,7 +100,8 @@ class TestViewController: UIViewController, let define = RFAPIDefine() define.path = Bundle.main.url(forResource: request.title, withExtension: "data")?.absoluteString define.name = RFAPIName(rawValue: request.APIName) - lastTask = API.request(define: define) { c in + lastTask = TestAPI.shared.request(define: define) { c in + c.groupIdentifier = apiGroupIdentifier c.success { [weak self] _, responseObject in self?.display(response: responseObject) } @@ -123,7 +111,8 @@ class TestViewController: UIViewController, } } else { - lastTask = API.request(name: request.APIName) { c in + lastTask = TestAPI.shared.request(name: request.APIName) { c in + c.groupIdentifier = apiGroupIdentifier c.loadMessage = request.message c.loadMessageShownModal = request.modal c.success { [weak self] _, responseObject in @@ -141,13 +130,13 @@ class TestViewController: UIViewController, try! data.appendPart(withFileURL: Bundle.main.executableURL!, name: "eXe") data.throttleBandwidth(withPacketSize: 3000, delay: 0.1) } - c.uploadProgress = { [weak self] task, progress in + c.uploadProgress = { [weak self] _, progress in guard let sf = self else { return } DispatchQueue.main.async { sf.display(response: String(format: "Uploading %.1f%%", progress.fractionCompleted * 100)) } } - c.downloadProgress = { [weak self] task, progress in + c.downloadProgress = { [weak self] _, progress in guard let sf = self else { return } DispatchQueue.main.async { sf.display(response: String(format: "Downloaing %.1f%%", progress.fractionCompleted * 100)) diff --git a/Example/iOS-Swift/Scenes/zh-Hans.lproj/Main.strings b/Example/iOS-Swift/Scenes/zh-Hans.lproj/Main.strings new file mode 100644 index 0000000..38b9703 --- /dev/null +++ b/Example/iOS-Swift/Scenes/zh-Hans.lproj/Main.strings @@ -0,0 +1,33 @@ + +/* Class = "UINavigationItem"; title = "RFAPI"; ObjectID = "AVC-BL-cRR"; */ +"AVC-BL-cRR.title" = "RFAPI"; + +/* Class = "UIBarButtonItem"; title = "Item"; ObjectID = "EPc-r0-xf1"; */ +"EPc-r0-xf1.title" = "Item"; + +/* Class = "UINavigationItem"; title = "Requests"; ObjectID = "J6L-JP-3hQ"; */ +"J6L-JP-3hQ.title" = "请求"; + +/* Class = "UILabel"; text = "Control Binding"; ObjectID = "SoU-6N-unl"; */ +"SoU-6N-unl.text" = "控件绑定"; + +/* Class = "UIButton"; disabledTitle = "Loading"; ObjectID = "TWh-Mz-u8r"; */ +"TWh-Mz-u8r.disabledTitle" = "加载中"; + +/* Class = "UIButton"; normalTitle = "Start"; ObjectID = "TWh-Mz-u8r"; */ +"TWh-Mz-u8r.normalTitle" = "开始"; + +/* Class = "UITextView"; text = "Resopnse"; ObjectID = "VNd-yL-ZI4"; */ +"VNd-yL-ZI4.text" = "请求响应"; + +/* Class = "UILabel"; text = "Title"; ObjectID = "XT4-OY-1f4"; */ +"XT4-OY-1f4.text" = "Title"; + +/* Class = "UIBarButtonItem"; title = "Menu"; ObjectID = "YX8-Sl-RrG"; */ +"YX8-Sl-RrG.title" = "菜单"; + +/* Class = "UINavigationItem"; title = "Control Binding"; ObjectID = "YnI-DD-juW"; */ +"YnI-DD-juW.title" = "控件绑定"; + +/* Class = "UILabel"; text = "Make Requests"; ObjectID = "sGW-gK-m8l"; */ +"sGW-gK-m8l.text" = "创建请求"; diff --git a/Example/iOS-Swift/TestAPI.swift b/Example/iOS-Swift/TestAPI.swift index f662d79..06fda31 100644 --- a/Example/iOS-Swift/TestAPI.swift +++ b/Example/iOS-Swift/TestAPI.swift @@ -7,6 +7,8 @@ // class TestAPI: RFAPI { + static var shared = TestAPI() + override init() { super.init() let config = URLSessionConfiguration.default diff --git a/Example/iOS-Swift/TestAPIDefine.plist b/Example/iOS-Swift/TestAPIDefine.plist index 79b6bb9..49bdfb6 100644 --- a/Example/iOS-Swift/TestAPIDefine.plist +++ b/Example/iOS-Swift/TestAPIDefine.plist @@ -96,5 +96,33 @@ https://httpbin.org/delay/10 + @ Error + + NoPath + + InvaildPath + + Path + null.json?测=❌ + + MismatchObject + + Path + array_sample.json + Response Type + 2 + Response Class + RFDTestEntity + + MismatchArray + + Path + object_sample.json + Response Type + 3 + Response Class + RFDTestEntity + + diff --git a/Example/iOS-Swift/en.lproj/Localizable.strings b/Example/iOS-Swift/en.lproj/Localizable.strings new file mode 100644 index 0000000..29223af --- /dev/null +++ b/Example/iOS-Swift/en.lproj/Localizable.strings @@ -0,0 +1,29 @@ + +"ListSection.Sample" = "Sample Requests"; +"ListSection.Local" = "Local Files"; +"ListSection.HTTPBin" = "HTTPBin"; + +"HUDState.Waiting" = "Waiting..."; +"HUDState.Loadding" = "Loadding..."; +"HUDState.Uploading" = "Uploading..."; + +/// - RFAPI Build-in + +"RFAPI.Error.GeneralFailureReasonApp" = "It seems to be an application bug"; +"RFAPI.Error.GeneralFailureReasonServer" = "It may be the server being upgraded or maintained, or it may be an application bug"; +"RFAPI.Error.GeneralRecoverySuggestion" = "Please try again later. Check for a new version if this error persists"; + +"RFAPI.Debug.CannotCreateRequestError" = "Cannot create request: %@"; +"RFAPI.Error.CannotCreateRequestDescription" = "Internal error, unable to create request"; +"RFAPI.Error.CannotCreateRequestReason" = "It seems to be an application bug"; +"RFAPI.Error.CannotCreateRequestSuggestion" = "Please try again. If it still doesn't work, try restarting the application"; +"RFAPI.Error.DefineNoPath" = "API define path is nil"; +"RFAPI.Debug.CannotJoinPathToBaseURL" = "Unable to join path %1$@ to %2$@, please check the API define"; + +/// RFAPIJSONModelTransformer + +"RFAPI.Error.UnexpectedServerResponse" = "Unexpected server response"; +"RFAPI.Debug.ObjectResponseTypeMismatchClass" = "Server response is %@ other than a dictionary\nPlease check your code first, then contart the server staff if the sever does not return as required"; +"RFAPI.Debug.ObjectResponseConvertToModelError" = "Cannot convert response to model: %@\nPlease check your code first, then contart the server staff if the sever does not return as required"; +"RFAPI.Debug.ArrayResponseTypeMismatchClass" = "Server response is %@ other than an array\nPlease check your code first, then contart the server staff if the sever does not return as required"; +"RFAPI.Debug.ArrayResponseConvertToModelError" = "Cannot convert elements in the array to model: %@\nPlease check your code first, then contart the server staff if the sever does not return as required"; diff --git a/Example/iOS-Swift/zh-Hans.lproj/InfoPlist.strings b/Example/iOS-Swift/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 0000000..579d624 --- /dev/null +++ b/Example/iOS-Swift/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1,5 @@ + +"CFBundleDisplayName" = "RFAPI 演示"; + +// LaunchScreen 是静态的,暂不支持文本的国际化,用加载其他版本曲线实现 +"UILaunchStoryboardName" = "LaunchScreen_zh-Hans"; diff --git a/Example/iOS-Swift/zh-Hans.lproj/Localizable.strings b/Example/iOS-Swift/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000..5d905d7 --- /dev/null +++ b/Example/iOS-Swift/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,29 @@ + +"ListSection.Sample" = "示例请求"; +"ListSection.Local" = "本地文件"; +"ListSection.HTTPBin" = "HTTPBin"; + +"HUDState.Waiting" = "请稍候..."; +"HUDState.Loadding" = "载入中..."; +"HUDState.Uploading" = "上传中..."; + +/// - RFAPI Build-in + +"RFAPI.Error.GeneralFailureReasonApp" = "很可能是应用 bug"; +"RFAPI.Error.GeneralFailureReasonServer" = "可能服务器正在升级或者维护,也可能是应用 bug"; +"RFAPI.Error.GeneralRecoverySuggestion" = "建议稍后重试,如果持续报告这个错误请检查 AppStore 是否有新版本"; + +"RFAPI.Debug.CannotCreateRequestError" = "无法创建请求: %@"; +"RFAPI.Error.CannotCreateRequestDescription" = "内部错误,无法创建请求"; +"RFAPI.Error.CannotCreateRequestReason" = "很可能是应用 bug"; +"RFAPI.Error.CannotCreateRequestSuggestion" = "请再试一次,如果依旧请尝试重启应用。给您带来不便,敬请谅解"; +"RFAPI.Error.DefineNoPath" = "接口定义未设置路径"; +"RFAPI.Debug.CannotJoinPathToBaseURL" = "无法拼接路径 %1$@ 到 %2$@,请检查接口定义"; + +/// RFAPIJSONModelTransformer + +"RFAPI.Error.UnexpectedServerResponse" = "返回数据异常"; +"RFAPI.Debug.ObjectResponseTypeMismatchClass" = "期望的数据类型是字典,而实际是 %@\n请先确认一下代码,如果服务器没按要求返回请联系后台人员"; +"RFAPI.Debug.ObjectResponseConvertToModelError" = "不能将返回内容转换为 model:%@\n请先确认一下代码,如果服务器没按要求返回请联系后台人员"; +"RFAPI.Debug.ArrayResponseTypeMismatchClass" = "期望的数据类型是数组,而实际是 %@\n请先确认一下代码,如果服务器没按要求返回请联系后台人员"; +"RFAPI.Debug.ArrayResponseConvertToModelError" = "不能将数组中的元素转换为 model %@\n请先确认一下代码,如果服务器没按要求返回请联系后台人员"; diff --git a/Podfile b/Podfile index e8f9468..359dda7 100644 --- a/Podfile +++ b/Podfile @@ -20,3 +20,8 @@ target 'Test-macOS' do platform :osx, '10.13' pod 'RFAPI', :path => '.' end + +target 'Test-tvOS' do + platform :tvos, '12.0' + pod 'RFAPI', :path => '.' +end diff --git a/README.md b/README.md index 0f9df6b..24b077e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,46 @@ # RFAPI + + [![Build Status](https://img.shields.io/travis/RFUI/RFAPI.svg?style=flat-square&colorA=333333&colorB=6600cc)](https://travis-ci.com/RFUI/RFAPI) [![Codecov](https://img.shields.io/codecov/c/github/RFUI/RFAPI.svg?style=flat-square&colorA=333333&colorB=6600cc)](https://codecov.io/gh/RFUI/RFAPI) [![CocoaPods](https://img.shields.io/cocoapods/v/RFAPI.svg?style=flat-square&colorA=333333&colorB=6600cc)](https://cocoapods.org/pods/RFAPI) -RFAPI is a full-featured URL session wrapper designed for API requests. It's easy to use and powerfull. +*English* [简体中文 :cn:](README~zh-hans.md) + +RFAPI is a network request library specially designed for API requests. It is a URL session wrapper base on AFNetworking. + +## Feature + +* It uses rules to create requests and process responses instead of stitching URLs in code. No more decode models manually and add additional error handling logic. +* The request process can be bound to the loading progress and UI control status. +* Beautiful request method, more readable than chain call, easy to extend. +* Obtain and cancel requests through the specific and grouped identifiers. You can control a request without passing the request object in different code contexts. +* Automatic model decoding supports different libraries. +* Multiple request completion callbacks, convenient for various usage scenarios, centralized error handling, automatic error handling. + +## Disadvantage + +The biggest problem of RFAPI is being too old. In 2014, the design and 95% of the implementation of v1 were completed. After that, there are only few minor changes in many years. Although good design is not outdated, the related technology stack has been updated. If you still use NSOperation to manage requests, if there is no URL session, no Swift, no Codable, then it's not outdated. -[RFAPI v2 Migration Guide](Documents/migration_guide_v2.md). The `v1` branch contains RFAPI v1 for legacy use, which requires AFNetworking v2. +I start to upgrade it to v2 at the end of 2019. The primary goal is to support AFNetworking v3. The premise is to maintain compatibility with the old version as much as possible. A new interface design has been implemented. The user experience in Swift has been improved. It cannot be completely renovated means: + +* The interface of the main class for overloading is difficult to use in Swift. They are designed for Objective-C. The flexible design of Objective-C becomes strange when transferred into Swfit. +* Codable is not supported, it is troublesome to support a Swift proprietary feature. +* The URL session feature is currently not fully utilized. Currently, only data tasks are implemented. Download and upload tasks are not supported yet. +* The download feature is limited. Please check other libraries if you needs a downloader. + +In the future, encapsulation of Alamofire will open another project. This library will not be rewritten in Swift. + +## Usage ## CocoaPods Install -```ruby +Only integration through CocoaPods is supported due to dependent factors. There is no support plan for SPM and Carthage. +```ruby pod 'RFAPI' ``` @@ -24,3 +51,128 @@ pod 'RFAPI', :git => 'https://github.com/RFUI/RFAPI.git', :branch => 'develop' ``` + +## Define an API + +Unlike most network libraries, you cannot make a request with a url object. Instead, RFAPI uses API define objects to describe not only how to make requests, but also how to handle responses. + +```swift +let define = RFAPIDefine() +define.name = RFAPIName(rawValue: "TopicListRecommended") +define.path = "https://exapmle.com/api/v2/topics/recommended" +define.method = "GET" +define.needsAuthorization = true +define.responseExpectType = .objects +define.responseClass = "TopicEntity" +``` + +Generally, a default define should be created. After that, you only need to provide different parts from the default define when creating other defines. Example of setting default rules: + +```swift +let api = ... // RFAPI instance +let defaultDefine = RFAPIDefine() +defaultDefine.baseURL = URL(string: "https://exapmle.com/") +defaultDefine.pathPrefix = "api/v2/" +defaultDefine.method = "GET" +defaultDefine.needsAuthorization = true +api.defineManager.defaultDefine = defaultDefine +``` + +With the default define, the above API definition can be simplified to: + +```swift +let define = RFAPIDefine() +define.name = RFAPIName(rawValue: "TopicListRecommended") +define.path = "topics/recommended" +define.responseExpectType = .objects +define.responseClass = "TopicEntity" +``` + +The more recommended way is to load the defines from the configuration file. You can load the defines from a local file (such as json, plist), or even get the configuration from the server. eg: + +```json +{ + "DEFAULT": { + "Base": "https://exapmle.com/", + "Path Prefix": "api/v2/", + "Method": "GET", + "Authorization": true + }, + "TopicListRecommended": { + "Path": "topics/recommended", + "Response Type": 3, + "Response Class": "TopicEntity" + }, + "UserLogin": { + "Method": "POST", + "Path": "user/login", + "Authorization": false, + "Response Type": 2, + "Response Class": "LoginResponseEntity" + }, + ... +} +``` + +The file configuration should be a dictionary of type `[String: [String: Any]]`, key is the API name, and `DEFAULT` is for the default define. With a configuration file, it can be loaded into the define manager, and then the request can be make directly with the API name. eg: + +```swift +let rules = ... // Configuration loaded +let api = ... // RFAPI instance +defineManager.setDefinesWithRulesInfo(rules) +``` + +By default, the content format of request and response are both JSON. You can modify it globally by changing the `defaultRequestSerializer` and `defaultResponseSerializer` properties of the define manager. If you need to adjust an individual API, you can specify the serializer type in the API define. eg: + +```json +{ + "FormUpload": { + "Method": "POST", + "Path": "commom/formupload", + "Serializer": "AFHTTPRequestSerializer", + "Response Serializer": "AFPropertyListResponseSerializer", + "Response Type": 1 + } +} +``` + +Other configuration file examples: [Demo Project Configuration](https://github.com/RFUI/RFAPI/blob/develop/Example/iOS-Swift/TestAPIDefine.plist),[iOS Project Template/APIDefine.plist](https://github.com/BB9z/iOS-Project-Template/blob/master/App/Networking/APIDefine.plist) + +### Making requests + +You can directly pass the define object when making a request; if the define has been in the define manager, you can directly pass the API name. Pass all other parameters through the context object. + +```swift +let api = ... // RFAPI instance +api.request(name: "TopicListRecommended") { c in + c.parameters = ["page": 1, "page_size": 20] + c.loadMessage = "List Loading" + c.success { _, rsp in + guard let topics = rsp as? [TopicEntity] else { fatalError() } + ... + } +} +``` + +For more usage, checkout [Cookbook](Documents/Cookbook.md) + +### Differences from the general + +* Cancellation is not considered as failure + + When a request is cancelled, the failure callback will not be called. Also a failure callback will never be called with an `NSURLErrorCancelled` error parameter. + + But you could get an `NSURLErrorCancelled` error from a RFAPITask object in the finished or complation callback. + +* Most parameters are mutable + + Except for properties related to define, most of the parameters passed through the context will not be copied. It's your free to pass mutable array, dictionary, string or any others and change these value after the request has been made. RFAPI allows you to do that and thinks you know what you are doing. + +### Localization + +You can localize RFAPI built-in messages by putting localizable strings into the default table of the main bundle. + +Samples: + +* [en](Example/iOS-Swift/en.lproj/Localizable.strings) +* [zh-Hans](Example/iOS-Swift/zh-Hans.lproj/Localizable.strings) diff --git a/README.zh-hans.md b/README.zh-hans.md new file mode 100644 index 0000000..fcb752e --- /dev/null +++ b/README.zh-hans.md @@ -0,0 +1,180 @@ +# RFAPI + + + +[![Build Status](https://img.shields.io/travis/RFUI/RFAPI.svg?style=flat-square&colorA=333333&colorB=6600cc)](https://travis-ci.com/RFUI/RFAPI) +[![Codecov](https://img.shields.io/codecov/c/github/RFUI/RFAPI.svg?style=flat-square&colorA=333333&colorB=6600cc)](https://codecov.io/gh/RFUI/RFAPI) +[![CocoaPods](https://img.shields.io/cocoapods/v/RFAPI.svg?style=flat-square&colorA=333333&colorB=6600cc)](https://cocoapods.org/pods/RFAPI) + + + +[English :us:](README.md) *简体中文* + +RFAPI 是一个专为 API 请求而设计的网络请求库。它是基于 AFNetworking 的 URL session 封装。 + +[v2 迁移指南](Documents/migration_guide_v2.md)。RFAPI v1 需要 AFNetworking v2 版本,仍可在 `v1` 分支获取。 + +## 特色 + +* 通过规则创建请求和处理响应,不再需要在代码中拼接 URL,不再需要手动解析 model、添加额外的错误处理逻辑; +* 请求进程可与加载进度、UI 控件状态绑定; +* 漂亮的请求方法,比链式调用可读性更好,易于扩展; +* 通过具体和分类两种字符串标识符获取、取消请求,这样在不同的上下文环境中无需传递请求对象即可控制请求; +* Model 自动解析支持不同的库; +* 多种请求完成回调,方便各种使用场景,集中错误处理,自动错误处理。 + +## 劣势 + +RFAPI 最大的问题是太老了。v1 设计和 95% 的实现是在 2014 年完成的,之后有点小修小补谈不上变化,用了多年。虽然好的设计并不过时,但是相关技术栈都更新了。假如还是用 NSOperation 管理请求,没有 URL session,没有 Swift,没有 Codable,它是不过时。 + +2019 年末开始着手 v2 的升级,首要目标是支持 AFNetworking v3,在尽量维持与旧版的兼容的前提下,顺带实现一下新的接口设计,改进 Swift 下的使用体验。无法彻底革新意味着: + +* 主类专为重载的接口在 Swift 下很难用,它们是专为 Objective-C 设计的,Objective-C 下灵活的设计转到 Swfit 里变得很奇怪; +* 不支持 Codable,Swift 专有特性支持起来麻烦; +* URL session 特性目前利用不充分,目前仅实现了数据任务,下载、上传任务还未支持; +* 下载特性未来如果支持也是有限的,选下载器请看其他库。 + +未来对 Alamofire 进行封装会开另一个项目,这个库不会用 Swift 重写。 + +## 使用 + +### CocoaPods 集成 + +因为依赖较多,只支持通过 CocoaPods 集成,SPM、Carthage 无支持计划。 + +```ruby +pod 'RFAPI' +``` + +使用最新版本请指定 develop 分支: + +```ruby +pod 'RFAPI', + :git => 'https://github.com/RFUI/RFAPI.git', + :branch => 'develop' +``` + +### 定义接口 + +和多数网络库不同,你不可以直接用 URL 发起请求,需要先创建 define 对象来描述如何发起请求并处理响应。 + +```swift +let define = RFAPIDefine() +define.name = RFAPIName(rawValue: "TopicListRecommended") +define.path = "https://exapmle.com/api/v2/topics/recommended" +define.method = "GET" +define.needsAuthorization = true +define.responseExpectType = .objects +define.responseClass = "TopicEntity" +``` + +通常应当设置一个默认的规则,这样定义其他规则时只需要写与默认规则不同的。设置默认规则示例: + +```swift +let api = ... // RFAPI 实例 +let defaultDefine = RFAPIDefine() +defaultDefine.baseURL = URL(string: "https://exapmle.com/") +defaultDefine.pathPrefix = "api/v2/" +defaultDefine.method = "GET" +defaultDefine.needsAuthorization = true +api.defineManager.defaultDefine = defaultDefine +``` + +有了默认规则后,上面的接口定义可以简化为: + +```swift +let define = RFAPIDefine() +define.name = RFAPIName(rawValue: "TopicListRecommended") +define.path = "topics/recommended" +define.responseExpectType = .objects +define.responseClass = "TopicEntity" +``` + +更推荐的方式是从配置文件中加载规则,你可以从本地文件中载入规则(如 json、plist),甚至可以从服务器获取配置。示例: + +```json +{ + "DEFAULT": { + "Base": "https://exapmle.com/", + "Path Prefix": "api/v2/", + "Method": "GET", + "Authorization": true + }, + "TopicListRecommended": { + "Path": "topics/recommended", + "Response Type": 3, + "Response Class": "TopicEntity" + }, + "UserLogin": { + "Method": "POST", + "Path": "user/login", + "Authorization": false, + "Response Type": 2, + "Response Class": "LoginResponseEntity" + }, + ... +} +``` + +文件配置应是 `[String : [String : Any]]` 类型的字典,key 是接口名,`DEFAULT` 是默认规则。有了配置文件,可以载入到 define manager 中,之后就可以通过接口名直接发起请求了,示例: + +```swift +let rules = ... // 载入的配置 +let api = ... // RFAPI 实例 +defineManager.setDefinesWithRulesInfo(rules) +``` + +默认请求和响应的内容格式都是 JSON 的,如果需要全局修改可以调整 define manager 的 `defaultRequestSerializer` 和 `defaultResponseSerializer` 属性;如需调整个别接口,可以在接口定义中指定 serializer 的类型。示例: + +```json +{ + "FormUpload": { + "Method": "POST", + "Path": "commom/formupload", + "Serializer": "AFHTTPRequestSerializer", + "Response Serializer": "AFPropertyListResponseSerializer", + "Response Type": 1 + } +} +``` + +其他配置文件的例子:[演示项目的配置](https://github.com/RFUI/RFAPI/blob/develop/Example/iOS-Swift/TestAPIDefine.plist),[iOS Project Template/APIDefine.plist](https://github.com/BB9z/iOS-Project-Template/blob/master/App/Networking/APIDefine.plist) + +### 创建请求 + +发起请求时可以直接传 define 对象;如果规则已在 define manager 中定义,可以直接传接口名。通过 context 对象传递所有其他参数。 + +```swift +let api = ... // RFAPI 实例 +api.request(name: "TopicListRecommended") { c in + c.parameters = ["page": 1, "page_size": 20] + c.loadMessage = "列表加载中" + c.success { _, rsp in + guard let topics = rsp as? [TopicEntity] else { fatalError() } + ... + } +} +``` + +更多用法见 [Cookbook](Documents/Cookbook.md) + +### 特殊设定 + +* 取消不当做错误进行处理 + + 请求被取消时,错误回调不会被调用;错误回调也不会有 `NSURLErrorCancelled` 错误。 + + 但是在 finished 和 complation 回调中你可通过 RFAPITask 对象获取 `NSURLErrorCancelled` 错误。 + +* 多数参数是作为可变量传递的 + + 除了与 define 相关的属性,大部分通过 context 传递的参数都不会被额外拷贝。传递可变数组、字典、字符串等可变量并在请求创建后进行修改是你的自由,RFAPI 允许你这么做并认为你知道自己在做什么。 + +### 国际化 + +RFAPI 支持内部信息的国际化,你需要把本地化 strings 放在主 bundle 默认的 table 中。 + +示例: + +* [en](Example/iOS-Swift/en.lproj/Localizable.strings) +* [zh-Hans](Example/iOS-Swift/zh-Hans.lproj/Localizable.strings) diff --git a/RFAPI.podspec b/RFAPI.podspec index 80dbd53..0b8d3a5 100644 --- a/RFAPI.podspec +++ b/RFAPI.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'RFAPI' - s.version = '2.0.0-beta.1' - s.summary = 'API Manager.' + s.version = '2.0.0-beta.2' + s.summary = 'RFAPI is a network request library specially designed for API requests. It is a URL session wrapper base on AFNetworking.' s.homepage = 'https://github.com/RFUI/RFAPI' s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/RFAPI.xcodeproj/project.pbxproj b/RFAPI.xcodeproj/project.pbxproj index 0254212..6d32d65 100644 --- a/RFAPI.xcodeproj/project.pbxproj +++ b/RFAPI.xcodeproj/project.pbxproj @@ -3,20 +3,29 @@ archiveVersion = 1; classes = { }; - objectVersion = 48; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ 04DDA15841D7E3DB05160199 /* libPods-Example-iOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 219EFF170EACE3662191400C /* libPods-Example-iOS.a */; }; + 09B33A71B410E139F77E6021 /* libPods-Test-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9576327F5DB841FE7835D388 /* libPods-Test-tvOS.a */; }; 2A251C10B17C5D95EAABDBAE /* libPods-Test-macOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 65ED60EEB15D01DD7BDCF30A /* libPods-Test-macOS.a */; }; A805072FFAC70498847A1922 /* libPods-Test-iOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 93B3187890A6DB746F9A8161 /* libPods-Test-iOS.a */; }; D5091B1723BF2E3B00E52FF3 /* TestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5091B1623BF2E3B00E52FF3 /* TestViewController.swift */; }; + D50F619B242EDED20072AB4B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D50F619D242EDED20072AB4B /* Main.storyboard */; }; D54183C3206B685100DD25CE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54183C2206B685100DD25CE /* AppDelegate.swift */; }; D54183CA206B685100DD25CE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D54183C9206B685100DD25CE /* Assets.xcassets */; }; - D54183CD206B685100DD25CE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D54183CB206B685100DD25CE /* LaunchScreen.storyboard */; }; - D5418408206CD45400DD25CE /* RFDTestEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = D54183FE206CD44F00DD25CE /* RFDTestEntity.m */; }; - D541840C206CD45400DD25CE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5418404206CD45100DD25CE /* Main.storyboard */; }; D541840D206CD45400DD25CE /* TestAPIDefine.plist in Resources */ = {isa = PBXBuildFile; fileRef = D5418405206CD45300DD25CE /* TestAPIDefine.plist */; }; + D5518D382444558500C27F69 /* DefineLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ABE7122417348100B9BB98 /* DefineLoad.swift */; }; + D5518D392444558900C27F69 /* archived_define_v1.plist in Resources */ = {isa = PBXBuildFile; fileRef = D5D61FFB23C5E19800B26C33 /* archived_define_v1.plist */; }; + D5518D3A2444558E00C27F69 /* big_json.data in Resources */ = {isa = PBXBuildFile; fileRef = D5F9245823D5BBCA00E9781D /* big_json.data */; }; + D5518D3B2444559600C27F69 /* RTHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D57C354523C7193C000B6A3C /* RTHelper.m */; }; + D5518D3C2444559900C27F69 /* test_defines.plist in Resources */ = {isa = PBXBuildFile; fileRef = D57C353B23C70A56000B6A3C /* test_defines.plist */; }; + D5518D3D2444559C00C27F69 /* TestConvention.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59FF64F23D1E2D800206713 /* TestConvention.swift */; }; + D5518D3E2444559E00C27F69 /* TestDefine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D61FF523C42A5300B26C33 /* TestDefine.swift */; }; + D5518D3F244455A100C27F69 /* TestDefineManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54183FB206CD13000DD25CE /* TestDefineManager.swift */; }; + D5518D40244455A400C27F69 /* TestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C354923C83B89000B6A3C /* TestRequest.swift */; }; + D5518D41244455A700C27F69 /* TestSubclass.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ABE70F24172D6000B9BB98 /* TestSubclass.swift */; }; D55ADAF523BF4AFB00AD6DB2 /* TestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55ADAF423BF4AFB00AD6DB2 /* TestAPI.swift */; }; D55ADB0F23C06C6600AD6DB2 /* TestDefineManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54183FB206CD13000DD25CE /* TestDefineManager.swift */; }; D55ADB1023C06C6700AD6DB2 /* TestDefineManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54183FB206CD13000DD25CE /* TestDefineManager.swift */; }; @@ -28,11 +37,23 @@ D57C354B23C83B89000B6A3C /* TestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C354923C83B89000B6A3C /* TestRequest.swift */; }; D59FF65023D1E2D800206713 /* TestConvention.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59FF64F23D1E2D800206713 /* TestConvention.swift */; }; D59FF65123D1E2D800206713 /* TestConvention.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59FF64F23D1E2D800206713 /* TestConvention.swift */; }; + D5A8C7CF24418E4200CAEF4C /* TestControlBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A8C7CE24418E4200CAEF4C /* TestControlBinding.swift */; }; + D5A8C7D42441B87F00CAEF4C /* ControlBindingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A8C7D22441B87C00CAEF4C /* ControlBindingVC.swift */; }; + D5A8C7D82443487D00CAEF4C /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A8C7D72443487D00CAEF4C /* NavigationController.swift */; }; + D5ABE71024172D6000B9BB98 /* TestSubclass.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ABE70F24172D6000B9BB98 /* TestSubclass.swift */; }; + D5ABE71124172D6000B9BB98 /* TestSubclass.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ABE70F24172D6000B9BB98 /* TestSubclass.swift */; }; + D5ABE7132417348100B9BB98 /* DefineLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ABE7122417348100B9BB98 /* DefineLoad.swift */; }; + D5ABE7142417348100B9BB98 /* DefineLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ABE7122417348100B9BB98 /* DefineLoad.swift */; }; + D5BF665024BEBCA5008BC122 /* big_json.data in Resources */ = {isa = PBXBuildFile; fileRef = D5F9245823D5BBCA00E9781D /* big_json.data */; }; + D5C3AC0F242EE623004D5762 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D5C3AC0C242EE623004D5762 /* Localizable.strings */; }; + D5C3AC1B242EE782004D5762 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5C3AC1A242EE782004D5762 /* LaunchScreen.storyboard */; }; + D5C3AC21242EEADB004D5762 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D5C3AC23242EEADB004D5762 /* InfoPlist.strings */; }; + D5C3AC25242EEC1F004D5762 /* LaunchScreen_zh-Hans.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5C3AC24242EEC1E004D5762 /* LaunchScreen_zh-Hans.storyboard */; }; + D5C3AC2724308E1F004D5762 /* TestEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3AC2624308E1F004D5762 /* TestEntity.swift */; }; D5D61FF623C42A5300B26C33 /* TestDefine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D61FF523C42A5300B26C33 /* TestDefine.swift */; }; D5D61FF723C42A5300B26C33 /* TestDefine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D61FF523C42A5300B26C33 /* TestDefine.swift */; }; D5D61FFC23C5E19800B26C33 /* archived_define_v1.plist in Resources */ = {isa = PBXBuildFile; fileRef = D5D61FFB23C5E19800B26C33 /* archived_define_v1.plist */; }; D5D61FFD23C5E19800B26C33 /* archived_define_v1.plist in Resources */ = {isa = PBXBuildFile; fileRef = D5D61FFB23C5E19800B26C33 /* archived_define_v1.plist */; }; - D5F9245923D5BBCA00E9781D /* big_json.data in Resources */ = {isa = PBXBuildFile; fileRef = D5F9245823D5BBCA00E9781D /* big_json.data */; }; D5F9245A23D5BBCA00E9781D /* big_json.data in Resources */ = {isa = PBXBuildFile; fileRef = D5F9245823D5BBCA00E9781D /* big_json.data */; }; D5F9245B23D5BBCA00E9781D /* big_json.data in Resources */ = {isa = PBXBuildFile; fileRef = D5F9245823D5BBCA00E9781D /* big_json.data */; }; /* End PBXBuildFile section */ @@ -41,27 +62,29 @@ 219EFF170EACE3662191400C /* libPods-Example-iOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Example-iOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 2DB51C46282E6DD0627B6D13 /* Pods-Example-iOS-Test-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example-iOS-Test-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Example-iOS-Test-iOS/Pods-Example-iOS-Test-iOS.release.xcconfig"; sourceTree = ""; }; 387B55A79A2B1A8C07C03E12 /* Pods-Test-macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Test-macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Test-macOS/Pods-Test-macOS.debug.xcconfig"; sourceTree = ""; }; + 507E91E825AB9426BBA59033 /* Pods-Test-tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Test-tvOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Test-tvOS/Pods-Test-tvOS.debug.xcconfig"; sourceTree = ""; }; + 5F59E754E2062B2C005707EE /* Pods-Test-tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Test-tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Test-tvOS/Pods-Test-tvOS.release.xcconfig"; sourceTree = ""; }; 65ED60EEB15D01DD7BDCF30A /* libPods-Test-macOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Test-macOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 735FA53B9EA138BDBFEA4922 /* Pods-Example-iOS-Test-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example-iOS-Test-iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Example-iOS-Test-iOS/Pods-Example-iOS-Test-iOS.debug.xcconfig"; sourceTree = ""; }; 759E83620E24AB315EE017E4 /* Pods-Test-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Test-iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Test-iOS/Pods-Test-iOS.debug.xcconfig"; sourceTree = ""; }; 93B3187890A6DB746F9A8161 /* libPods-Test-iOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Test-iOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9576327F5DB841FE7835D388 /* libPods-Test-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Test-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 97F2581ADA52229A84F7E000 /* Pods-Test-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Test-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Test-iOS/Pods-Test-iOS.release.xcconfig"; sourceTree = ""; }; 9C37EB0BE59A81EC90E0CDCC /* Pods-Example-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Example-iOS/Pods-Example-iOS.release.xcconfig"; sourceTree = ""; }; D5091B1623BF2E3B00E52FF3 /* TestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestViewController.swift; sourceTree = ""; }; + D50F619C242EDED20072AB4B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; D54183BF206B685000DD25CE /* Example-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D54183C2206B685100DD25CE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D54183C9206B685100DD25CE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - D54183CC206B685100DD25CE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; D54183CE206B685100DD25CE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D54183D3206B806200DD25CE /* OCBridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCBridging-Header.h"; sourceTree = ""; }; D54183D4206BB68400DD25CE /* RFUI-Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "RFUI-Config.xcconfig"; sourceTree = ""; }; D54183DD206BBA7B00DD25CE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D54183F2206BDCFF00DD25CE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D54183FB206CD13000DD25CE /* TestDefineManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDefineManager.swift; sourceTree = ""; }; - D54183FE206CD44F00DD25CE /* RFDTestEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RFDTestEntity.m; sourceTree = ""; }; - D5418404206CD45100DD25CE /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; D5418405206CD45300DD25CE /* TestAPIDefine.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestAPIDefine.plist; sourceTree = ""; }; - D5418407206CD45400DD25CE /* RFDTestEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RFDTestEntity.h; sourceTree = ""; }; + D5518D302444555A00C27F69 /* Test-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Test-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D5518D342444555A00C27F69 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D55ADAF423BF4AFB00AD6DB2 /* TestAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAPI.swift; sourceTree = ""; }; D55ADAFB23C06B7E00AD6DB2 /* Test-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Test-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D55ADB0723C06C3B00AD6DB2 /* Test-macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Test-macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -72,6 +95,18 @@ D57C354823C7196B000B6A3C /* TestsBridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestsBridgingHeader.h; sourceTree = ""; }; D57C354923C83B89000B6A3C /* TestRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestRequest.swift; sourceTree = ""; }; D59FF64F23D1E2D800206713 /* TestConvention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConvention.swift; sourceTree = ""; }; + D5A8C7CE24418E4200CAEF4C /* TestControlBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestControlBinding.swift; sourceTree = ""; }; + D5A8C7D22441B87C00CAEF4C /* ControlBindingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlBindingVC.swift; sourceTree = ""; }; + D5A8C7D62441C95600CAEF4C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; + D5A8C7D72443487D00CAEF4C /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; + D5ABE70F24172D6000B9BB98 /* TestSubclass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSubclass.swift; sourceTree = ""; }; + D5ABE7122417348100B9BB98 /* DefineLoad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefineLoad.swift; sourceTree = ""; }; + D5C3AC0D242EE623004D5762 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + D5C3AC0E242EE623004D5762 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + D5C3AC1A242EE782004D5762 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + D5C3AC22242EEADB004D5762 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; + D5C3AC24242EEC1E004D5762 /* LaunchScreen_zh-Hans.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "LaunchScreen_zh-Hans.storyboard"; sourceTree = ""; }; + D5C3AC2624308E1F004D5762 /* TestEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestEntity.swift; sourceTree = ""; }; D5D61FF523C42A5300B26C33 /* TestDefine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDefine.swift; sourceTree = ""; }; D5D61FFB23C5E19800B26C33 /* archived_define_v1.plist */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = archived_define_v1.plist; sourceTree = ""; }; D5F9245823D5BBCA00E9781D /* big_json.data */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = big_json.data; sourceTree = ""; }; @@ -88,6 +123,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D5518D2D2444555A00C27F69 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 09B33A71B410E139F77E6021 /* libPods-Test-tvOS.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D55ADAF823C06B7E00AD6DB2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -113,6 +156,7 @@ 219EFF170EACE3662191400C /* libPods-Example-iOS.a */, 93B3187890A6DB746F9A8161 /* libPods-Test-iOS.a */, 65ED60EEB15D01DD7BDCF30A /* libPods-Test-macOS.a */, + 9576327F5DB841FE7835D388 /* libPods-Test-tvOS.a */, ); name = Frameworks; sourceTree = ""; @@ -128,6 +172,8 @@ 97F2581ADA52229A84F7E000 /* Pods-Test-iOS.release.xcconfig */, 387B55A79A2B1A8C07C03E12 /* Pods-Test-macOS.debug.xcconfig */, E5FA99E5E4196E679EB5925D /* Pods-Test-macOS.release.xcconfig */, + 507E91E825AB9426BBA59033 /* Pods-Test-tvOS.debug.xcconfig */, + 5F59E754E2062B2C005707EE /* Pods-Test-tvOS.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -149,6 +195,7 @@ D54183BF206B685000DD25CE /* Example-iOS.app */, D55ADAFB23C06B7E00AD6DB2 /* Test-iOS.xctest */, D55ADB0723C06C3B00AD6DB2 /* Test-macOS.xctest */, + D5518D302444555A00C27F69 /* Test-tvOS.xctest */, ); name = Products; sourceTree = ""; @@ -156,14 +203,14 @@ D54183C1206B685100DD25CE /* iOS-Swift */ = { isa = PBXGroup; children = ( - D54183C2206B685100DD25CE /* AppDelegate.swift */, + D5A8C7D12441B6B900CAEF4C /* Launching */, + D5A8C7D02441B68200CAEF4C /* Scenes */, D54183C9206B685100DD25CE /* Assets.xcassets */, D54183CE206B685100DD25CE /* Info.plist */, - D54183CB206B685100DD25CE /* LaunchScreen.storyboard */, - D5418404206CD45100DD25CE /* Main.storyboard */, + D5C3AC23242EEADB004D5762 /* InfoPlist.strings */, + D5C3AC0C242EE623004D5762 /* Localizable.strings */, D55ADAF423BF4AFB00AD6DB2 /* TestAPI.swift */, D5418405206CD45300DD25CE /* TestAPIDefine.plist */, - D5091B1623BF2E3B00E52FF3 /* TestViewController.swift */, ); path = "iOS-Swift"; sourceTree = ""; @@ -183,6 +230,7 @@ isa = PBXGroup; children = ( D54183DD206BBA7B00DD25CE /* Info.plist */, + D5A8C7CE24418E4200CAEF4C /* TestControlBinding.swift */, ); path = iOS; sourceTree = ""; @@ -192,6 +240,7 @@ children = ( D5F9245723D5BBB000E9781D /* Data */, D5D61FFB23C5E19800B26C33 /* archived_define_v1.plist */, + D5ABE7122417348100B9BB98 /* DefineLoad.swift */, D57C354423C7193B000B6A3C /* RTHelper.h */, D57C354523C7193C000B6A3C /* RTHelper.m */, D57C353B23C70A56000B6A3C /* test_defines.plist */, @@ -200,6 +249,7 @@ D54183FB206CD13000DD25CE /* TestDefineManager.swift */, D57C354923C83B89000B6A3C /* TestRequest.swift */, D57C354823C7196B000B6A3C /* TestsBridgingHeader.h */, + D5ABE70F24172D6000B9BB98 /* TestSubclass.swift */, ); path = Shared; sourceTree = ""; @@ -210,6 +260,7 @@ D54183DA206BBA7B00DD25CE /* iOS */, D54183EF206BDCFF00DD25CE /* macOS */, D54183E3206BBAA100DD25CE /* Shared */, + D5518D312444555A00C27F69 /* tvOS */, ); path = Tests; sourceTree = ""; @@ -231,15 +282,43 @@ path = macOS; sourceTree = ""; }; + D5518D312444555A00C27F69 /* tvOS */ = { + isa = PBXGroup; + children = ( + D5518D342444555A00C27F69 /* Info.plist */, + ); + path = tvOS; + sourceTree = ""; + }; D55ADAF623BF4D1D00AD6DB2 /* Models */ = { isa = PBXGroup; children = ( - D5418407206CD45400DD25CE /* RFDTestEntity.h */, - D54183FE206CD44F00DD25CE /* RFDTestEntity.m */, + D5C3AC2624308E1F004D5762 /* TestEntity.swift */, ); path = Models; sourceTree = ""; }; + D5A8C7D02441B68200CAEF4C /* Scenes */ = { + isa = PBXGroup; + children = ( + D5A8C7D22441B87C00CAEF4C /* ControlBindingVC.swift */, + D50F619D242EDED20072AB4B /* Main.storyboard */, + D5A8C7D72443487D00CAEF4C /* NavigationController.swift */, + D5091B1623BF2E3B00E52FF3 /* TestViewController.swift */, + ); + path = Scenes; + sourceTree = ""; + }; + D5A8C7D12441B6B900CAEF4C /* Launching */ = { + isa = PBXGroup; + children = ( + D54183C2206B685100DD25CE /* AppDelegate.swift */, + D5C3AC1A242EE782004D5762 /* LaunchScreen.storyboard */, + D5C3AC24242EEC1E004D5762 /* LaunchScreen_zh-Hans.storyboard */, + ); + path = Launching; + sourceTree = ""; + }; D5F9245723D5BBB000E9781D /* Data */ = { isa = PBXGroup; children = ( @@ -270,6 +349,24 @@ productReference = D54183BF206B685000DD25CE /* Example-iOS.app */; productType = "com.apple.product-type.application"; }; + D5518D2F2444555A00C27F69 /* Test-tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D5518D372444555A00C27F69 /* Build configuration list for PBXNativeTarget "Test-tvOS" */; + buildPhases = ( + 5E52A1B3D9307FA063D09FD4 /* [CP] Check Pods Manifest.lock */, + D5518D2C2444555A00C27F69 /* Sources */, + D5518D2D2444555A00C27F69 /* Frameworks */, + D5518D2E2444555A00C27F69 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Test-tvOS"; + productName = "Test-tvOS"; + productReference = D5518D302444555A00C27F69 /* Test-tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; D55ADAFA23C06B7E00AD6DB2 /* Test-iOS */ = { isa = PBXNativeTarget; buildConfigurationList = D55ADB0023C06B7E00AD6DB2 /* Build configuration list for PBXNativeTarget "Test-iOS" */; @@ -313,7 +410,7 @@ isa = PBXProject; attributes = { CLASSPREFIX = DM; - LastSwiftUpdateCheck = 1130; + LastSwiftUpdateCheck = 1140; LastUpgradeCheck = 1130; ORGANIZATIONNAME = RFUI; TargetAttributes = { @@ -321,6 +418,9 @@ CreatedOnToolsVersion = 9.2; LastSwiftMigration = 0920; }; + D5518D2F2444555A00C27F69 = { + CreatedOnToolsVersion = 11.4; + }; D55ADAFA23C06B7E00AD6DB2 = { CreatedOnToolsVersion = 11.3; LastSwiftMigration = 1130; @@ -334,12 +434,13 @@ }; }; buildConfigurationList = D54183B8206B66FA00DD25CE /* Build configuration list for PBXProject "RFAPI" */; - compatibilityVersion = "Xcode 8.0"; + compatibilityVersion = "Xcode 10.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, + "zh-Hans", ); mainGroup = D54183B4206B66FA00DD25CE; productRefGroup = D54183C0206B685000DD25CE /* Products */; @@ -349,6 +450,7 @@ D54183BE206B685000DD25CE /* Example-iOS */, D55ADAFA23C06B7E00AD6DB2 /* Test-iOS */, D55ADB0623C06C3B00AD6DB2 /* Test-macOS */, + D5518D2F2444555A00C27F69 /* Test-tvOS */, ); }; /* End PBXProject section */ @@ -358,14 +460,27 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D5BF665024BEBCA5008BC122 /* big_json.data in Resources */, D54183CA206B685100DD25CE /* Assets.xcassets in Resources */, - D5F9245923D5BBCA00E9781D /* big_json.data in Resources */, - D54183CD206B685100DD25CE /* LaunchScreen.storyboard in Resources */, - D541840C206CD45400DD25CE /* Main.storyboard in Resources */, + D5C3AC21242EEADB004D5762 /* InfoPlist.strings in Resources */, + D5C3AC1B242EE782004D5762 /* LaunchScreen.storyboard in Resources */, + D5C3AC25242EEC1F004D5762 /* LaunchScreen_zh-Hans.storyboard in Resources */, + D5C3AC0F242EE623004D5762 /* Localizable.strings in Resources */, + D50F619B242EDED20072AB4B /* Main.storyboard in Resources */, D541840D206CD45400DD25CE /* TestAPIDefine.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; + D5518D2E2444555A00C27F69 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D5518D392444558900C27F69 /* archived_define_v1.plist in Resources */, + D5518D3A2444558E00C27F69 /* big_json.data in Resources */, + D5518D3C2444559900C27F69 /* test_defines.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D55ADAF923C06B7E00AD6DB2 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -416,19 +531,40 @@ buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Example-iOS/Pods-Example-iOS-resources.sh", - "${PODS_ROOT}/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example-iOS/Pods-Example-iOS-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SVProgressHUD.bundle", + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example-iOS/Pods-Example-iOS-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example-iOS/Pods-Example-iOS-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 5E52A1B3D9307FA063D09FD4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Test-tvOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; A2A2AF6793561F6333AD17C3 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -477,21 +613,40 @@ buildActionMask = 2147483647; files = ( D54183C3206B685100DD25CE /* AppDelegate.swift in Sources */, - D5418408206CD45400DD25CE /* RFDTestEntity.m in Sources */, + D5A8C7D42441B87F00CAEF4C /* ControlBindingVC.swift in Sources */, + D5A8C7D82443487D00CAEF4C /* NavigationController.swift in Sources */, D55ADAF523BF4AFB00AD6DB2 /* TestAPI.swift in Sources */, + D5C3AC2724308E1F004D5762 /* TestEntity.swift in Sources */, D5091B1723BF2E3B00E52FF3 /* TestViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + D5518D2C2444555A00C27F69 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D5518D382444558500C27F69 /* DefineLoad.swift in Sources */, + D5518D3B2444559600C27F69 /* RTHelper.m in Sources */, + D5518D3D2444559C00C27F69 /* TestConvention.swift in Sources */, + D5518D3E2444559E00C27F69 /* TestDefine.swift in Sources */, + D5518D3F244455A100C27F69 /* TestDefineManager.swift in Sources */, + D5518D40244455A400C27F69 /* TestRequest.swift in Sources */, + D5518D41244455A700C27F69 /* TestSubclass.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D55ADAF723C06B7E00AD6DB2 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D5ABE7132417348100B9BB98 /* DefineLoad.swift in Sources */, D57C354623C7193C000B6A3C /* RTHelper.m in Sources */, + D5A8C7CF24418E4200CAEF4C /* TestControlBinding.swift in Sources */, D59FF65023D1E2D800206713 /* TestConvention.swift in Sources */, D5D61FF623C42A5300B26C33 /* TestDefine.swift in Sources */, D55ADB0F23C06C6600AD6DB2 /* TestDefineManager.swift in Sources */, D57C354A23C83B89000B6A3C /* TestRequest.swift in Sources */, + D5ABE71024172D6000B9BB98 /* TestSubclass.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -499,23 +654,43 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D5ABE7142417348100B9BB98 /* DefineLoad.swift in Sources */, D57C354723C7193C000B6A3C /* RTHelper.m in Sources */, D59FF65123D1E2D800206713 /* TestConvention.swift in Sources */, D5D61FF723C42A5300B26C33 /* TestDefine.swift in Sources */, D55ADB1023C06C6700AD6DB2 /* TestDefineManager.swift in Sources */, D57C354B23C83B89000B6A3C /* TestRequest.swift in Sources */, + D5ABE71124172D6000B9BB98 /* TestSubclass.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - D54183CB206B685100DD25CE /* LaunchScreen.storyboard */ = { + D50F619D242EDED20072AB4B /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D50F619C242EDED20072AB4B /* Base */, + D5A8C7D62441C95600CAEF4C /* zh-Hans */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + D5C3AC0C242EE623004D5762 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( - D54183CC206B685100DD25CE /* Base */, + D5C3AC0D242EE623004D5762 /* en */, + D5C3AC0E242EE623004D5762 /* zh-Hans */, ); - name = LaunchScreen.storyboard; + name = Localizable.strings; + sourceTree = ""; + }; + D5C3AC23242EEADB004D5762 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D5C3AC22242EEADB004D5762 /* zh-Hans */, + ); + name = InfoPlist.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ @@ -525,6 +700,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = D54183D4206BB68400DD25CE /* RFUI-Config.xcconfig */; buildSettings = { + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; @@ -552,6 +728,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "Example/Shared/OCBridging-Header.h"; }; name = Debug; @@ -560,6 +737,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = D54183D4206BB68400DD25CE /* RFUI-Config.xcconfig */; buildSettings = { + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; @@ -643,15 +821,17 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Example/iOS-Swift/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.github.RFUI.RFAPI.Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -706,20 +886,115 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Example/iOS-Swift/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.github.RFUI.RFAPI.Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; + D5518D352444555A00C27F69 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 507E91E825AB9426BBA59033 /* Pods-Test-tvOS.debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = Tests/tvOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.github.RFUI.RFAPI.Test-tvOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_OBJC_BRIDGING_HEADER = Tests/Shared/TestsBridgingHeader.h; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 13.4; + }; + name = Debug; + }; + D5518D362444555A00C27F69 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5F59E754E2062B2C005707EE /* Pods-Test-tvOS.release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = Tests/tvOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.github.RFUI.RFAPI.Test-tvOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OBJC_BRIDGING_HEADER = Tests/Shared/TestsBridgingHeader.h; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 13.4; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; D55ADB0123C06B7E00AD6DB2 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 759E83620E24AB315EE017E4 /* Pods-Test-iOS.debug.xcconfig */; @@ -775,14 +1050,17 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Tests/iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.github.RFUI.RFAPI.Test-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = Tests/Shared/TestsBridgingHeader.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.2; @@ -839,14 +1117,19 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Tests/iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.github.RFUI.RFAPI.Test-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = Tests/Shared/TestsBridgingHeader.h; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -907,7 +1190,11 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Tests/macOS/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -915,7 +1202,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.github.RFUI.RFAPI.Test-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = Tests/Shared/TestsBridgingHeader.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.2; @@ -970,15 +1256,20 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Tests/macOS/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.github.RFUI.RFAPI.Test-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = Tests/Shared/TestsBridgingHeader.h; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 4.2; }; name = Release; @@ -1004,6 +1295,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D5518D372444555A00C27F69 /* Build configuration list for PBXNativeTarget "Test-tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D5518D352444555A00C27F69 /* Debug */, + D5518D362444555A00C27F69 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D55ADB0023C06B7E00AD6DB2 /* Build configuration list for PBXNativeTarget "Test-iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/RFAPI.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme b/RFAPI.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme index 291e105..8c41417 100644 --- a/RFAPI.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme +++ b/RFAPI.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme @@ -34,6 +34,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "zh-Hans" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/RFAPI.xcworkspace/contents.xcworkspacedata b/RFAPI.xcworkspace/contents.xcworkspacedata index 33a9067..6d1926d 100644 --- a/RFAPI.xcworkspace/contents.xcworkspacedata +++ b/RFAPI.xcworkspace/contents.xcworkspacedata @@ -1,6 +1,12 @@ + + + + diff --git a/Sources/RFAPI/Define/RFAPIDefine.h b/Sources/RFAPI/Define/RFAPIDefine.h index 5c38b4a..3dc2d64 100644 --- a/Sources/RFAPI/Define/RFAPIDefine.h +++ b/Sources/RFAPI/Define/RFAPIDefine.h @@ -12,8 +12,6 @@ typedef NSString * RFAPIName NS_EXTENSIBLE_STRING_ENUM; -// todo: Default define - /** A define object is used to describe all aspects of an API: what the URL is, how to send the request sent, how to handle the response, and so on. */ diff --git a/Sources/RFAPI/Define/RFAPIDefineConfigFile.m b/Sources/RFAPI/Define/RFAPIDefineConfigFile.m index 07303b4..a18f6fb 100644 --- a/Sources/RFAPI/Define/RFAPIDefineConfigFile.m +++ b/Sources/RFAPI/Define/RFAPIDefineConfigFile.m @@ -118,7 +118,7 @@ - (void)setDefinesWithRulesInfo:(NSDictionary *defines = [NSMutableArray.alloc initWithCapacity:prules.count]; RFAPIDefineRawConfig defaultRule = prules[RFAPIDefineDefaultKey]; diff --git a/Sources/RFAPI/Define/RFAPIDefineManager.m b/Sources/RFAPI/Define/RFAPIDefineManager.m index 25eb387..781566f 100644 --- a/Sources/RFAPI/Define/RFAPIDefineManager.m +++ b/Sources/RFAPI/Define/RFAPIDefineManager.m @@ -67,9 +67,7 @@ - (NSURL *)requestURLForDefine:(RFAPIDefine *)define parameters:(NSMutableDictio NSMutableString *path = define.path.mutableCopy; if (!path) { if (error) { - *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:@{ - NSLocalizedDescriptionKey : @"API define path is nil." - }]; + *error = [RFAPI localizedErrorWithDoomain:NSURLErrorDomain code:NSURLErrorBadURL underlyingError:nil descriptionKey:@"RFAPI.Error.DefineNoPath" descriptionValue:@"API define path is nil" reasonKey:@"RFAPI.Error.GeneralFailureReasonApp" reasonValue:nil suggestionKey:@"RFAPI.Error.GeneralRecoverySuggestion" suggestionValue:nil url:nil]; } return nil; } @@ -101,13 +99,12 @@ - (NSURL *)requestURLForDefine:(RFAPIDefine *)define parameters:(NSMutableDictio } if (!url) { - RFAPILogError_(@"无法拼接路径 %@ 到 %@\n请检查接口定义", path, define.baseURL); +#if RFDEBUG + NSString *debugFormat = [RFAPI localizedStringForKey:@"RFAPI.Debug.CannotJoinPathToBaseURL" value:@"Unable to join path %1$@ to %2$@, please check the API define"]; + RFAPILogError_(debugFormat, path, define.baseURL) +#endif if (error) { - *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:@{ - NSLocalizedDescriptionKey : @"内部错误,无法创建请求", - NSLocalizedFailureReasonErrorKey : @"很可能是应用 bug", - NSLocalizedRecoverySuggestionErrorKey : @"请再试一次,如果依旧请尝试重启应用。给您带来不便,敬请谅解" - }]; + *error = [RFAPI localizedErrorWithDoomain:NSURLErrorDomain code:NSURLErrorBadURL underlyingError:nil descriptionKey:@"RFAPI.Error.CannotCreateRequestDescription" descriptionValue:@"Internal error, unable to create request" reasonKey:@"RFAPI.Error.CannotCreateRequestReason" reasonValue:@"It seems to be an application bug" suggestionKey:@"RFAPI.Error.CannotCreateRequestSuggestion" suggestionValue:@"Please try again. If it still doesn't work, try restarting the application" url:nil]; } return nil; } diff --git a/Sources/RFAPI/ModelTransformer/RFAPIJSONModelTransformer.m b/Sources/RFAPI/ModelTransformer/RFAPIJSONModelTransformer.m index 70e5a4e..2c65264 100644 --- a/Sources/RFAPI/ModelTransformer/RFAPIJSONModelTransformer.m +++ b/Sources/RFAPI/ModelTransformer/RFAPIJSONModelTransformer.m @@ -11,54 +11,49 @@ - (id)transformResponse:(id)response toType:(RFAPIDefineResponseExpectType)type case RFAPIDefineResponseExpectObject: { NSDictionary *responseObject = response; if (![responseObject isKindOfClass:NSDictionary.class]) { - RFAPILogError_(@"期望的数据类型是字典,而实际是 %@\n请先确认一下代码,如果服务器没按要求返回请联系后台人员", responseObject.class) - *error = [NSError errorWithDomain:RFAPIErrorDomain code:0 userInfo:@{ - NSLocalizedDescriptionKey: @"返回数据异常", - NSLocalizedFailureReasonErrorKey: @"可能服务器正在升级或者维护,也可能是应用 bug", - NSLocalizedRecoverySuggestionErrorKey: @"建议稍后重试,如果持续报告这个错误请检查 AppStore 是否有新版本" - }]; +#if RFDEBUG + NSString *debugFormat = [RFAPI localizedStringForKey:@"RFAPI.Debug.ObjectResponseTypeMismatchClass" value:@"Server response is %@ other than a dictionary\nPlease check your code first, then contart the server staff if the sever does not return as required"]; + RFAPILogError_(debugFormat, responseObject.class) +#endif + *error = [self badResponseError:nil]; return nil; } NSError *e = nil; id JSONModelObject = [(JSONModel *)[modelClass alloc] initWithDictionary:responseObject error:&e]; if (!JSONModelObject) { - RFAPILogError_(@"不能将返回内容转换为Model:%@\n请先确认一下代码,如果服务器没按要求返回请联系后台人员", e) - - *error = [NSError errorWithDomain:RFAPIErrorDomain code:0 userInfo:@{ - NSLocalizedDescriptionKey: @"返回数据异常", - NSLocalizedFailureReasonErrorKey: @"可能服务器正在升级或者维护,也可能是应用 bug", - NSLocalizedRecoverySuggestionErrorKey: @"建议稍后重试,如果持续报告这个错误请检查 AppStore 是否有新版本" - }]; +#if RFDEBUG + NSString *debugFormat = [RFAPI localizedStringForKey:@"RFAPI.Debug.ObjectResponseConvertToModelError" value:@"Cannot convert response to model: %@\nPlease check your code first, then contart the server staff if the sever does not return as required"]; + RFAPILogError_(debugFormat, e) +#endif + *error = [self badResponseError:e]; } return JSONModelObject; } case RFAPIDefineResponseExpectObjects: { NSArray *responseObject = response; if (![responseObject isKindOfClass:NSArray.class]) { - RFAPILogError_(@"期望的数据类型是数组,而实际是 %@\n请先确认一下代码,如果服务器没按要求返回请联系后台人员", responseObject.class) - *error = [NSError errorWithDomain:RFAPIErrorDomain code:0 userInfo:@{ - NSLocalizedDescriptionKey: @"返回数据异常", - NSLocalizedFailureReasonErrorKey: @"可能服务器正在升级或者维护,也可能是应用 bug", - NSLocalizedRecoverySuggestionErrorKey: @"建议稍后重试,如果持续报告这个错误请检查 AppStore 是否有新版本" - }]; +#if RFDEBUG + NSString *debugFormat = [RFAPI localizedStringForKey:@"RFAPI.Debug.ArrayResponseTypeMismatchClass" value:@"Server response is %@ other than an array\nPlease check your code first, then contart the server staff if the sever does not return as required"]; + RFAPILogError_(debugFormat, responseObject.class) +#endif + *error = [self badResponseError:nil]; return nil; } NSMutableArray *objects = [NSMutableArray.alloc initWithCapacity:responseObject.count]; for (NSDictionary *info in responseObject) { - id obj = [(JSONModel *)[modelClass alloc] initWithDictionary:info error:error]; + NSError *e = nil; + id obj = [(JSONModel *)[modelClass alloc] initWithDictionary:info error:&e]; if (obj) { [objects addObject:obj]; continue; } - - RFAPILogError_(@"不能将数组中的元素转换为Model %@\n请先确认一下代码,如果服务器没按要求返回请联系后台人员", *error) - *error = [NSError errorWithDomain:RFAPIErrorDomain code:0 userInfo:@{ - NSLocalizedDescriptionKey: @"返回数据异常", - NSLocalizedFailureReasonErrorKey: @"可能服务器正在升级或者维护,也可能是应用 bug", - NSLocalizedRecoverySuggestionErrorKey: @"建议稍后重试,如果持续报告这个错误请检查 AppStore 是否有新版本" - }]; +#if RFDEBUG + NSString *debugFormat = [RFAPI localizedStringForKey:@"RFAPI.Debug.ObjectResponseConvertToModelError" value:@"Cannot convert elements in the array to model: %@\nPlease check your code first, then contart the server staff if the sever does not return as required"]; + RFAPILogError_(debugFormat, e) +#endif + *error = [self badResponseError:e]; return nil; } return objects; @@ -70,4 +65,8 @@ - (id)transformResponse:(id)response toType:(RFAPIDefineResponseExpectType)type } } +- (NSError *)badResponseError:(NSError *)underlyingError { + return [RFAPI localizedErrorWithDoomain:RFAPIErrorDomain code:0 underlyingError:underlyingError descriptionKey:@"RFAPI.Error.UnexpectedServerResponse" descriptionValue:@"Unexpected server response" reasonKey:@"RFAPI.Error.GeneralFailureReasonServer" reasonValue:@"It may be the server being upgraded or maintained, or it may be an application bug" suggestionKey:@"RFAPI.Error.GeneralRecoverySuggestion" suggestionValue:@"Please try again later. Check for a new version if this error persists" url:nil]; +} + @end diff --git a/Sources/RFAPI/RFAPI.h b/Sources/RFAPI/RFAPI.h index 3940333..570ac70 100644 --- a/Sources/RFAPI/RFAPI.h +++ b/Sources/RFAPI/RFAPI.h @@ -46,9 +46,12 @@ /// Serialized response object from server response. @property (nullable) id responseObject; -/// An error object that indicates why the task failed. +/// An error object that indicates why the task is unsuccessful. @property (nullable) NSError *error; +/// Whether the request completed successfully. +@property (readonly) BOOL isSuccess; + /// This property is the dictionary pass through the request context. @property (nullable) NSDictionary *userInfo; @@ -159,7 +162,7 @@ typedef void(^RFAPIRequestCombinedCompletionCallback)(id __nullable t Default implementation just return YES. - This method is called on the processingQueue. + This method is called on the completionQueue. @return Returning YES will continue error processing and continue with the callback processing of the request, if NO processing ends immediately. */ @@ -177,6 +180,22 @@ typedef void(^RFAPIRequestCombinedCompletionCallback)(id __nullable t */ - (BOOL)isSuccessResponse:(id __nullable __strong *__nonnull)responseObjectRef error:(NSError *__nullable __autoreleasing *__nonnull)error NS_SWIFT_NOTHROW; +#pragma mark - Localization + +/** + Make localized error object. + + Localized version of strings reads from the default table in the main bundle. + */ ++ (nonnull NSError *)localizedErrorWithDoomain:(nonnull NSErrorDomain)domain code:(NSInteger)code underlyingError:(nullable NSError *)error descriptionKey:(nonnull NSString *)descriptionKey descriptionValue:(nonnull NSString *)descriptionValue reasonKey:(nullable NSString *)reasonKey reasonValue:(nullable NSString *)reasonValue suggestionKey:(nullable NSString *)suggestionKey suggestionValue:(nullable NSString *)suggestionValue url:(nullable NSURL *)url; + +/** + The localized string loaded from main bundle's default table. + + key and value must not be nil at the same time. + */ ++ (nonnull NSString *)localizedStringForKey:(nullable NSString *)key value:(nullable NSString *)value; + @end /// Send array parameters @@ -233,16 +252,27 @@ FOUNDATION_EXTERN NSErrorDomain __nonnull const RFAPIErrorDomain; /// Note this block is called on the session queue, not the main queue. @property (nullable) RFAPIRequestProgressBlock downloadProgress; +/** + You could pass some UI elements here. + + For UIControl or any object response to `setEnabled:`, it will set enabled to NO when the request starts and restore to YES after request is finished. + For UIRefreshControl, it will try call `beginRefreshing` when the request starts and `endRefreshing` after request is finished. + For UIActivityIndicatorView, it will call `startAnimating` when the request starts and `stopAnimating` after request is finished. + */ +@property (nullable) NSArray *bindControls; + /// A block object to be executed when the request finishes successfully. @property (nullable) RFAPIRequestSuccessCallback success NS_SWIFT_NAME(successCallback); /// A block object to be executed when the request finishes unsuccessfully. +/// It will not be called if the request is cancelled. @property (nullable) RFAPIRequestFailureCallback failure NS_SWIFT_NAME(failureCallback); /// A block object to be executed when the request is complated. @property (nullable) RFAPIRequestFinishedCallback finished NS_SWIFT_NAME(finishedCallback); /// A block object to be executed when the request is complated. +/// Error will be nil if the request is cancelled. At this time, you could get the error object on the task object. @property (nullable) RFAPIRequestCombinedCompletionCallback combinedComplation NS_SWIFT_NAME(complationCallback); /// For debugging purposes, delaying the sending of network requests. diff --git a/Sources/RFAPI/RFAPI.m b/Sources/RFAPI/RFAPI.m index 7fdaade..7e5e4a9 100644 --- a/Sources/RFAPI/RFAPI.m +++ b/Sources/RFAPI/RFAPI.m @@ -10,10 +10,6 @@ NSString *const RFAPIRequestArrayParameterKey = @"_RFParmArray_"; NSString *const RFAPIRequestForceQuryStringParametersKey = @"_RFParmForceQuryString_"; -NSString *RFAPILocalizedString(NSString *key, NSString *value) { - return [NSBundle.mainBundle localizedStringForKey:key value:value table:nil]; -} - // Avoid create many concurrent GCD queue. static dispatch_queue_t api_default_processing_queue() { static dispatch_queue_t queue; @@ -109,14 +105,12 @@ - (AFSecurityPolicy *)securityPolicy { - (void)cancelOperationWithIdentifier:(nullable NSString *)identifier { for (idop in [self operationsWithIdentifier:identifier]) { - _dout_debug(@"Cancel HTTP request operation(%p) with identifier: %@", (void *)op, identifier); [op cancel]; } } - (void)cancelOperationsWithGroupIdentifier:(nullable NSString *)identifier { for (idop in [self operationsWithGroupIdentifier:identifier]) { - _dout_debug(@"Cancel HTTP request operation(%p) with group identifier: %@", (void *)op, identifier); [op cancel]; } } @@ -145,7 +139,7 @@ - (void)cancelOperationsWithGroupIdentifier:(nullable NSString *)identifier { if (!define.name) { define.name = APIName; } - RFAssert(define, @"Can not find an API with name: %@.", APIName) + NSAssert(define, @"Can not find an API with name: %@.", APIName); if (!define) return nil; return [self requestWithDefine:define context:contextBlock]; } @@ -158,7 +152,7 @@ - (void)cancelOperationsWithGroupIdentifier:(nullable NSString *)identifier { NSString *identifier = context.identifier; if (!identifier) { identifier = APIDefine.name; - RFAssert(identifier, @"Context identifier and define name both are nil.") + NSAssert(identifier, @"Context identifier and define name both are nil."); context.identifier = identifier; } if (!context.activityMessage && context.loadMessage) { @@ -176,15 +170,11 @@ - (void)cancelOperationsWithGroupIdentifier:(nullable NSString *)identifier { NSError *e = nil; NSMutableURLRequest *request = [self _RFAPI_makeURLRequestWithDefine:define context:context error:&e]; if (!request) { - RFAPILogError_(@"无法创建请求: %@", e) - NSMutableDictionary *eInfo = [NSMutableDictionary.alloc initWithCapacity:4]; - eInfo[NSLocalizedDescriptionKey] = @"内部错误,无法创建请求"; - eInfo[NSLocalizedFailureReasonErrorKey] = @"很可能是应用 bug"; - eInfo[NSLocalizedRecoverySuggestionErrorKey] = @"请再试一次,如果依旧请尝试重启应用。给您带来不便,敬请谅解"; - if (e) { - eInfo[NSUnderlyingErrorKey] = e; - } - NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:eInfo]; +#if RFDEBUG + NSString *debugFormat = [self.class localizedStringForKey:@"RFAPI.Debug.CannotCreateRequestError" value:@"Cannot create request: %@"]; + RFAPILogError_(debugFormat, e) +#endif + NSError *error = [self.class localizedErrorWithDoomain:NSURLErrorDomain code:NSURLErrorCancelled underlyingError:e descriptionKey:@"RFAPI.Error.CannotCreateRequest" descriptionValue:@"Internal error, unable to create request" reasonKey:@"RFAPI.Error.CannotCreateRequestReason" reasonValue:@"It seems to be an application bug" suggestionKey:@"RFAPI.Error.CannotCreateRequestSuggestion" suggestionValue:@"Please try again. If it still doesn't work, try restarting the application" url:request.URL]; [self _RFAPI_executeContext:context failure:error]; return nil; } @@ -194,6 +184,7 @@ - (void)cancelOperationsWithGroupIdentifier:(nullable NSString *)identifier { task.manager = self; task.define = define; [self transferContext:context toTask:task]; + [task updateBindControlsEnabled:NO]; // Start request RFNetworkActivityMessage *message = task.activityMessage; @@ -222,12 +213,13 @@ - (void)transferContext:(RFAPIRequestConext *)context toTask:(_RFAPISessionTask task.activityMessage = context.activityMessage; task.uploadProgressBlock = context.uploadProgress; task.downloadProgressBlock = context.downloadProgress; + task.bindControls = context.bindControls; task.success = context.success; task.failure = context.failure; task.complation = context.finished; task.combinedComplation = context.combinedComplation; - task.userInfo = context.userInfo; task.debugDelayRequestSend = context.debugDelayRequestSend; + task.userInfo = context.userInfo; } #pragma mark - Build Request @@ -362,6 +354,10 @@ - (void)_RFAPI_handleTaskComplete:(_RFAPISessionTask *)task response:(NSURLRespo case RFAPIDefineResponseExpectObjects: { NSError *error = nil; id modelObject = [self.modelTransformer transformResponse:(id)responseObject toType:type kind:task.define.responseClass error:&error]; + if (!self.modelTransformer) { + NSLog(@"⚠️ Response except object, but modelTransformer has not been set."); + modelObject = responseObject; + } if (error) { [self _RFAPI_executeTaskCallback:task failure:error]; } @@ -379,19 +375,22 @@ - (void)_RFAPI_handleTaskComplete:(_RFAPISessionTask *)task response:(NSURLRespo - (void)_RFAPI_executeTaskCallback:(nonnull _RFAPISessionTask *)task success:(nullable id)responseObject { task.responseObject = responseObject; + task.isSuccess = YES; dispatch_group_async(self.completionGroup, self.completionQueue, ^{ - task.failure = nil; - RFAPIRequestSuccessCallback scb = task.success; - if (scb) { - task.success = nil; - scb(task, responseObject); - } + [task updateBindControlsEnabled:YES]; RFNetworkActivityMessage *message = task.activityMessage; if (message) { dispatch_sync_on_main(^{ [self.networkActivityIndicatorManager hideMessage:message]; }); } + + RFAPIRequestSuccessCallback scb = task.success; + if (scb) { + task.success = nil; + scb(task, responseObject); + } + task.failure = nil; RFAPIRequestFinishedCallback ccb = task.complation; if (ccb) { task.complation = nil; @@ -407,19 +406,24 @@ - (void)_RFAPI_executeTaskCallback:(nonnull _RFAPISessionTask *)task success:(nu - (void)_RFAPI_executeTaskCallback:(nonnull _RFAPISessionTask *)task failure:(nonnull NSError *)error { task.error = error; - BOOL shouldContinue = [self generalHandlerForError:error withDefine:task.define task:task failureCallback:task.failure]; - + task.isSuccess = NO; + BOOL isCancel = (error.code == NSURLErrorCancelled && [error.domain isEqualToString:NSURLErrorDomain]); dispatch_group_async(self.completionGroup, self.completionQueue, ^{ - task.success = nil; + [task updateBindControlsEnabled:YES]; RFMessageManager *messageManager = self.networkActivityIndicatorManager; - if (shouldContinue) { - BOOL isCancel = (error.code == NSURLErrorCancelled && [error.domain isEqualToString:NSURLErrorDomain]); + RFNetworkActivityMessage *message = task.activityMessage; + if (message && messageManager) { + dispatch_sync_on_main(^{ + [messageManager hideMessage:message]; + }); + } + task.success = nil; + BOOL shouldContinueErrorHandling = [self generalHandlerForError:error withDefine:task.define task:task failureCallback:task.failure]; + if (shouldContinueErrorHandling && !isCancel) { RFAPIRequestFailureCallback fcb = task.failure; if (fcb) { - if (!isCancel) { - fcb(task, error); - } + fcb(task, error); } else { if (messageManager) { @@ -430,13 +434,6 @@ - (void)_RFAPI_executeTaskCallback:(nonnull _RFAPISessionTask *)task failure:(no } } task.failure = nil; - - RFNetworkActivityMessage *message = task.activityMessage; - if (message && messageManager) { - dispatch_sync_on_main(^{ - [messageManager hideMessage:message]; - }); - } RFAPIRequestFinishedCallback ccb = task.complation; if (ccb) { task.complation = nil; @@ -445,7 +442,7 @@ - (void)_RFAPI_executeTaskCallback:(nonnull _RFAPISessionTask *)task failure:(no RFAPIRequestCombinedCompletionCallback cbcb = task.combinedComplation; if (cbcb) { task.combinedComplation = nil; - cbcb(task, nil, error); + cbcb(task, nil, isCancel ? nil : error); } }); } @@ -476,6 +473,37 @@ - (BOOL)isSuccessResponse:(id _Nullable __strong *)responseObjectRef error:(NSE return YES; } +#pragma mark - + ++ (NSError *)localizedErrorWithDoomain:(NSErrorDomain)domain code:(NSInteger)code underlyingError:(NSError *)error descriptionKey:(NSString *)descriptionKey descriptionValue:(NSString *)descriptionValue reasonKey:(NSString *)reasonKey reasonValue:(NSString *)reasonValue suggestionKey:(NSString *)suggestionKey suggestionValue:(NSString *)suggestionValue url:(NSURL *)url { + NSMutableDictionary *eInfo = [NSMutableDictionary.alloc initWithCapacity:5]; + eInfo[NSLocalizedDescriptionKey] = [self localizedStringForKey:descriptionKey value:descriptionValue]; + if (reasonKey || reasonValue) { + NSString *reason = [self localizedStringForKey:reasonKey value:reasonValue]; + if (reason.length) { + eInfo[NSLocalizedFailureReasonErrorKey] = reason; + } + } + if (suggestionKey || suggestionValue) { + NSString *suggestion = [self localizedStringForKey:suggestionKey value:suggestionValue]; + if (suggestion.length) { + eInfo[NSLocalizedRecoverySuggestionErrorKey] = suggestion; + } + } + if (error) { + eInfo[NSUnderlyingErrorKey] = error; + } + if (url) { + eInfo[NSURLErrorKey] = url; + } + return [NSError errorWithDomain:domain code:code userInfo:eInfo]; +} + ++ (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value { + NSParameterAssert(key || value); + return [NSBundle.mainBundle localizedStringForKey:key value:value table:nil]; +} + @end diff --git a/Sources/RFAPI/RFAPIPrivate.h b/Sources/RFAPI/RFAPIPrivate.h index 2a09b1b..c43e099 100644 --- a/Sources/RFAPI/RFAPIPrivate.h +++ b/Sources/RFAPI/RFAPIPrivate.h @@ -14,11 +14,12 @@ #import #import -/// The localized string loaded from main bundle's default table. -extern NSString *__nonnull RFAPILocalizedString(NSString *__nonnull key, NSString *__nonnull value); - #if RFDEBUG -# define RFAPILogError_(DEBUG_ERROR, ...) dout_error(DEBUG_ERROR, __VA_ARGS__) +# define RFAPILogError_(DEBUG_ERROR, ...) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wformat-nonliteral\"") \ + dout_error(DEBUG_ERROR, __VA_ARGS__) \ + _Pragma("clang diagnostic pop") #else # define RFAPILogError_(DEBUG_ERROR, ...) #endif diff --git a/Sources/RFAPI/URLSession/RFAPISessionManager.h b/Sources/RFAPI/URLSession/RFAPISessionManager.h index 7982822..13a9f90 100644 --- a/Sources/RFAPI/URLSession/RFAPISessionManager.h +++ b/Sources/RFAPI/URLSession/RFAPISessionManager.h @@ -2,7 +2,7 @@ RFAPISessionManager RFAPI -Copyright © 2019 BB9z +Copyright © 2019-2020 BB9z https://github.com/RFUI/RFAPI The MIT License (MIT) diff --git a/Sources/RFAPI/URLSession/RFAPISessionTask.h b/Sources/RFAPI/URLSession/RFAPISessionTask.h index 8950b0f..0f06541 100644 --- a/Sources/RFAPI/URLSession/RFAPISessionTask.h +++ b/Sources/RFAPI/URLSession/RFAPISessionTask.h @@ -39,6 +39,10 @@ typedef void(^RFAPITaskComplation)(id __nullable responseObject, NSURLResponse * @property (nullable) NSString *groupIdentifier; @property (nullable) RFNetworkActivityMessage *activityMessage; +/// Set nil when task finished. +@property (nullable) NSArray *bindControls; +- (void)updateBindControlsEnabled:(BOOL)enabled; + /// From request context. @property (nullable) NSDictionary *userInfo; @property NSTimeInterval debugDelayRequestSend; @@ -50,6 +54,7 @@ typedef void(^RFAPITaskComplation)(id __nullable responseObject, NSURLResponse * @property (readonly, copy, nullable, nonatomic) NSURLResponse *response; @property (nullable) id responseObject; @property (nullable) NSError *error; +@property BOOL isSuccess; /// @property (readonly, nonatomic) BOOL isEnd; diff --git a/Sources/RFAPI/URLSession/RFAPISessionTask.m b/Sources/RFAPI/URLSession/RFAPISessionTask.m index 2abef1f..9ef71d5 100644 --- a/Sources/RFAPI/URLSession/RFAPISessionTask.m +++ b/Sources/RFAPI/URLSession/RFAPISessionTask.m @@ -28,6 +28,46 @@ - (void)dealloc { [self.uploadProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))]; } +- (void)updateBindControlsEnabled:(BOOL)enabled { + if (!self.bindControls.count) return; + @weakify(self) + dispatch_block_t work = ^{ + @strongify(self) + if (!self) return; + NSArray *controls = self.bindControls.copy; + for (id anyControl in controls) { +#if TARGET_OS_OSX + if ([(NSObject *)anyControl respondsToSelector:@selector(setEnabled:)]) { + [(NSControl *)anyControl setEnabled:enabled]; + } +#else + if ([(NSObject *)anyControl isKindOfClass:UIActivityIndicatorView.class]) { + if (enabled) [(UIActivityIndicatorView *)anyControl stopAnimating]; + else [(UIActivityIndicatorView *)anyControl startAnimating]; + } +#if !TARGET_OS_TV + else if ([(NSObject *)anyControl isKindOfClass:UIRefreshControl.class]) { + if (enabled) [(UIRefreshControl *)anyControl endRefreshing]; + else [(UIRefreshControl *)anyControl beginRefreshing]; + } +#endif + else if ([(NSObject *)anyControl respondsToSelector:@selector(setEnabled:)]) { + [(UIControl *)anyControl setEnabled:enabled]; + } +#endif + } + if (enabled) { + self.bindControls = nil; + } + }; + if (NSThread.isMainThread) { + work(); + } + else { + dispatch_async(dispatch_get_main_queue(), work); + } +} + #pragma mark - - (NSURLRequest *)currentRequest { diff --git a/Tests/Shared/DefineLoad.swift b/Tests/Shared/DefineLoad.swift new file mode 100644 index 0000000..ded807c --- /dev/null +++ b/Tests/Shared/DefineLoad.swift @@ -0,0 +1,16 @@ +// +// DefineLoad.swift +// RFAPI +// +// Created by BB9z on 2020/3/10. +// Copyright © 2020 RFUI. All rights reserved. +// + +extension RFAPI { + /// Load api defines in test_defines.plist + func loadTestDefines() { + let defineConfigURL = Bundle(for: type(of: self)).url(forResource: "test_defines", withExtension: "plist")! + let defineConfig = NSDictionary(contentsOf: defineConfigURL) as! [String: [String: Any]] + defineManager.setDefinesWithRulesInfo(defineConfig) + } +} diff --git a/Tests/Shared/TestConvention.swift b/Tests/Shared/TestConvention.swift index c265cc0..b07ccdd 100644 --- a/Tests/Shared/TestConvention.swift +++ b/Tests/Shared/TestConvention.swift @@ -8,15 +8,13 @@ import XCTest -class TestConvention: XCTestCase { +private class TestConvention: XCTestCase { // Has default base url lazy var api: TestAPI = { let api = TestAPI() api.baseURL = URL(string: "https://httpbin.org") - let defineConfigURL = Bundle(for: type(of: self)).url(forResource: "test_defines", withExtension: "plist")! - let defineConfig = NSDictionary(contentsOf: defineConfigURL) as! [String: [String: Any]] - api.defineManager.setDefinesWithRulesInfo(defineConfig) + api.loadTestDefines() return api }() @@ -122,5 +120,6 @@ class TestConvention: XCTestCase { } XCTAssertNotNil(apiInstance) wait(for: [requestComplateExpectation, managerDeallocExpectation], timeout: 10, enforceOrder: true) + XCTAssertNil(apiInstance) } } diff --git a/Tests/Shared/TestDefine.swift b/Tests/Shared/TestDefine.swift index 5316c30..80c7530 100644 --- a/Tests/Shared/TestDefine.swift +++ b/Tests/Shared/TestDefine.swift @@ -8,7 +8,7 @@ import XCTest -class TestDefine: XCTestCase { +private class TestDefine: XCTestCase { func testMerge() { let r1 = RFAPIDefine() diff --git a/Tests/Shared/TestDefineManager.swift b/Tests/Shared/TestDefineManager.swift index fbcf059..622599d 100644 --- a/Tests/Shared/TestDefineManager.swift +++ b/Tests/Shared/TestDefineManager.swift @@ -14,7 +14,7 @@ class TestRequestSerializer: AFHTTPRequestSerializer {} @objc(TestResponseSerializer) class TestResponseSerializer: AFHTTPResponseSerializer {} -class TestDefineManager: XCTestCase { +private class TestDefineManager: XCTestCase { lazy var manager = RFAPIDefineManager() lazy var rawConfig: [String: [String: Any]] = { diff --git a/Tests/Shared/TestRequest.swift b/Tests/Shared/TestRequest.swift index 30ef1b0..42bf151 100644 --- a/Tests/Shared/TestRequest.swift +++ b/Tests/Shared/TestRequest.swift @@ -16,7 +16,7 @@ class TestAPI: RFAPI { } } -class TestRequest: XCTestCase { +private class TestRequest: XCTestCase { // No default base url lazy var api: TestAPI = { @@ -24,9 +24,7 @@ class TestRequest: XCTestCase { let uc = URLSessionConfiguration.default uc.timeoutIntervalForRequest = 5 api.sessionConfiguration = uc - let defineConfigURL = Bundle(for: type(of: self)).url(forResource: "test_defines", withExtension: "plist")! - let defineConfig = NSDictionary(contentsOf: defineConfigURL) as! [String: [String: Any]] - api.defineManager.setDefinesWithRulesInfo(defineConfig) + api.loadTestDefines() return api }() @@ -50,6 +48,33 @@ class TestRequest: XCTestCase { wait(for: [successExpectation, completeExpectation], timeout: 10, enforceOrder: true) } + func testSuccssAndFailure() { + let successRequestFinishExpectation = expectation(description: "successRequest finish") + let failureRequsetFinishExpectation = expectation(description: "failureRequset finish") + let successRequest = api.request(name: "IsSuccess") { c in + c.complation { task, _, _ in + XCTAssertTrue(task!.isSuccess) + successRequestFinishExpectation.fulfill() + } + } + let failureRequset = api.request(name: "IsFailure") { c in + c.complation { task, _, _ in + XCTAssertFalse(task!.isSuccess) + failureRequsetFinishExpectation.fulfill() + } + } + guard let sRequest = successRequest, + let fRequset = failureRequset else { + assertionFailure() + return + } + XCTAssertFalse(sRequest.isSuccess) + XCTAssertFalse(fRequset.isSuccess) + wait(for: [successRequestFinishExpectation, failureRequsetFinishExpectation], timeout: 10, enforceOrder: false) + XCTAssertTrue(sRequest.isSuccess) + XCTAssertFalse(fRequset.isSuccess) + } + func testIdentifierControl() { } @@ -58,8 +83,69 @@ class TestRequest: XCTestCase { } - func testTaskCancel() { + func testTaskCancelImmediately() { + let completeExpectation = expectation(description: "") + let request = api.request(name: "Delay") { c in + c.success { _, _ in + XCTAssert(false, "This request should fail.") + } + c.failure { _, _ in + XCTAssert(false, "Do not call the failure callback when canceling.") + } + c.finished { task, success in + XCTAssertFalse(success) + guard let task = task else { + XCTAssert(false, "Task should have") + return + } + XCTAssertNil(task.response) + XCTAssertNil(task.responseObject) + } + c.complation { task, rsp, error in + XCTAssertNotNil(task) + XCTAssertNil(rsp) + XCTAssertNil(error) + XCTAssertNotNil(task?.error) + completeExpectation.fulfill() + } + } + request?.cancel() + XCTAssertNotNil(request) + wait(for: [completeExpectation], timeout: 1) + } + func testTaskCancelAfterAWhile() { + let completeExpectation = expectation(description: "") + let request = api.request(name: "Delay") { c in + c.parameters = ["time": 10] + c.success { _, _ in + XCTAssert(false, "This request should fail.") + } + c.failure { _, _ in + XCTAssert(false, "Do not call the failure callback when canceling.") + } + c.finished { task, success in + XCTAssertFalse(success) + guard let task = task else { + XCTAssert(false, "Task should have") + return + } + XCTAssertNil(task.response) + XCTAssertNil(task.responseObject) + } + c.complation { task, rsp, error in + XCTAssertNotNil(task) + XCTAssertNil(rsp) + XCTAssertNil(error) + XCTAssertNotNil(task?.error) + completeExpectation.fulfill() + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + request?.cancel() + } + XCTAssertNotNil(request) + wait(for: [completeExpectation], timeout: 2) } func testTimeout() { @@ -82,10 +168,10 @@ class TestRequest: XCTestCase { XCTAssertFalse(s) } c.complation { task, rsp, error in - debugPrint(error!) XCTAssertNotNil(task) XCTAssertNil(rsp) XCTAssertNotNil(error) + debugPrint(error!) completeExpectation.fulfill() } } diff --git a/Tests/Shared/TestSubclass.swift b/Tests/Shared/TestSubclass.swift new file mode 100644 index 0000000..f54396a --- /dev/null +++ b/Tests/Shared/TestSubclass.swift @@ -0,0 +1,67 @@ +// +// TestSubclass.swift +// RFAPI +// +// Created by BB9z on 2020/3/10. +// Copyright © 2020 RFUI. All rights reserved. +// + +import XCTest + +extension DispatchQueue { + class var currentQueueLabel: String? { + let name = __dispatch_queue_get_label(nil) + let label = String(cString: name, encoding: .utf8) + return label ?? OperationQueue.current?.underlyingQueue?.label ?? Thread.current.name + } +} + +fileprivate class Sub1API: RFAPI { + + override func preprocessingRequest(parametersRef: UnsafeMutablePointer, httpHeadersRef: UnsafeMutablePointer, parameters: [AnyHashable : Any]?, define: RFAPIDefine, context: RFAPIRequestConext) { + XCTAssertEqual(DispatchQueue.currentQueueLabel, context.userInfo?["QueueName"] as? String) + super.preprocessingRequest(parametersRef: parametersRef, httpHeadersRef: httpHeadersRef, parameters: parameters, define: define, context: context) + } + + override func finalizeSerializedRequest(_ request: NSMutableURLRequest, define: RFAPIDefine, context: RFAPIRequestConext) -> NSMutableURLRequest { + XCTAssertEqual(DispatchQueue.currentQueueLabel, context.userInfo?["QueueName"] as? String) + return super.finalizeSerializedRequest(request, define: define, context: context) + } + + override func generalHandlerForError(_ error: Error, define: RFAPIDefine, task: RFAPITask, failure: RFAPIRequestFailureCallback? = nil) -> Bool { + XCTAssertEqual(DispatchQueue.currentQueueLabel, completionQueue.label) + return super.generalHandlerForError(error, define: define, task: task, failure: failure) + } + + override func isSuccessResponse(_ responseObjectRef: UnsafeMutablePointer, error: NSErrorPointer) -> Bool { + XCTAssertEqual(DispatchQueue.currentQueueLabel, processingQueue.label) + return super.isSuccessResponse(responseObjectRef, error: error) + } +} + +private class TestSubclass: XCTestCase { + func testFunctionThread() { + let api = Sub1API() + api.loadTestDefines() + + let sendQueue = DispatchQueue(label: "SendQueue") + + let completeExpectation = expectation(description: "Success Completed") + let completeExpectation2 = expectation(description: "Fail Completed") + sendQueue.async { + api.request(name: "IsSuccess") { c in + c.userInfo = ["QueueName": sendQueue.label] + c.complation { _, _, _ in + completeExpectation.fulfill() + } + } + api.request(name: "404") { c in + c.userInfo = ["QueueName": sendQueue.label] + c.complation { _, _, _ in + completeExpectation2.fulfill() + } + } + } + wait(for: [completeExpectation, completeExpectation2], timeout: 10, enforceOrder: false) + } +} diff --git a/Tests/Shared/test_defines.plist b/Tests/Shared/test_defines.plist index 4d5a3a4..a73a902 100644 --- a/Tests/Shared/test_defines.plist +++ b/Tests/Shared/test_defines.plist @@ -86,6 +86,22 @@ Path https://httpbin.org/delay/{time} + IsSuccess + + Method + DELETE + Path + https://httpbin.org/delete + Response Type + 1 + + IsFailure + + Path + https://httpbin.org/status/400 + Response Type + 1 + diff --git a/Tests/iOS/TestControlBinding.swift b/Tests/iOS/TestControlBinding.swift new file mode 100644 index 0000000..0e82c91 --- /dev/null +++ b/Tests/iOS/TestControlBinding.swift @@ -0,0 +1,71 @@ +// +// TestControlBinding.swift +// Test-iOS +// +// Created by BB9z on 2020/4/11. +// Copyright © 2020 RFUI. All rights reserved. +// + +import XCTest + +@objc class CustomControl: NSObject { + @objc var enabled: Bool = true +} + +private class TestControlBinding: XCTestCase { + // No default base url + lazy var api: TestAPI = { + let api = TestAPI() + let uc = URLSessionConfiguration.default + uc.timeoutIntervalForRequest = 5 + api.sessionConfiguration = uc + api.loadTestDefines() + return api + }() + + func testTimeout() { + let completeExpectation = expectation(description: "") + let button = UIButton() + let barItem = UIBarButtonItem() + let activityIndicator = UIActivityIndicatorView() + let refreshControl = UIRefreshControl() + let customControl = CustomControl() + XCTAssertTrue(Thread.isMainThread) + + XCTAssertTrue(button.isEnabled) + XCTAssertTrue(barItem.isEnabled) + XCTAssertFalse(activityIndicator.isAnimating) + XCTAssertFalse(refreshControl.isRefreshing) + XCTAssertTrue(refreshControl.isEnabled) + XCTAssertTrue(customControl.enabled) + let _ = api.request(name: "Delay") { c in + c.timeoutInterval = 1 + c.parameters = ["time": 10] + c.bindControls = [button, barItem, activityIndicator, refreshControl, customControl] + c.complation { _, _, _ in + XCTAssertTrue(button.isEnabled) + XCTAssertTrue(barItem.isEnabled) + XCTAssertFalse(activityIndicator.isAnimating) + XCTAssertFalse(refreshControl.isRefreshing) + XCTAssertTrue(refreshControl.isEnabled) + XCTAssertTrue(customControl.enabled) + completeExpectation.fulfill() + } + } + XCTAssertFalse(button.isEnabled) + XCTAssertFalse(barItem.isEnabled) + XCTAssertTrue(activityIndicator.isAnimating) + XCTAssertTrue(refreshControl.isRefreshing) + XCTAssertTrue(refreshControl.isEnabled) + XCTAssertFalse(customControl.enabled) + + wait(for: [completeExpectation], timeout: 10) + + XCTAssertTrue(button.isEnabled) + XCTAssertTrue(barItem.isEnabled) + XCTAssertFalse(activityIndicator.isAnimating) + XCTAssertFalse(refreshControl.isRefreshing) + XCTAssertTrue(refreshControl.isEnabled) + XCTAssertTrue(customControl.enabled) + } +} diff --git a/Tests/tvOS/Info.plist b/Tests/tvOS/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/Tests/tvOS/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + +